Keeping the editor records run("data.pivot", answer={...}) for a new
pivot and run("data.pivot_edit", answer={...}) for a changed one, with
the definition as a line of the file; scripts answer both commands.
Dialog answers no longer record the selection after them, which their
command recorded before it ran: a pivot's new sheet is the command's
own doing. The filter button and chart moves, which no command opens,
record the selection first.- commit
- 0c96387a2b14f4b98b8c002f086628e491a68387
- parent
- dffc28fa05af0aaf38c649fbd30b1f9584105c98
- author
- Dami <dami.etoile@gmail.com>
- kind
- feat risk score 2.0 of 3, Jev's estimate
| M | internal/ | 7 | +++-- |
| M | internal/ | 11 | +++++-- |
| M | internal/ | 2 | +- |
| M | internal/ | 33 | ++++++++++++++++++++++ |
| M | internal/ | 5 | ++- |
| M | internal/ | 1 | + |
| M | internal/ | 18 | +++++++++++- |
| M | internal/ | 46 | +++++++++++++++++++++++++----- |
@@ -51,8 +51,9 @@ type chartHost interface {
5151 passKey(k tea.KeyPressMsg) tea.Cmd5252 passMouse(e overlay.MouseEvent) tea.Cmd5353 // recordChart records a chart inserted, edited, moved or resized, as54- // the command that does it answered with the chart (chartmacro.go).55- recordChart(id string, i int, before, after sheet.Chart)54+ // the command that does it answered with the chart; placed is set55+ // for a move or resize without a command (chartmacro.go).56+ recordChart(id string, i int, before, after sheet.Chart, placed bool)5657 }5758 5859 // chartEditor is the chart editor: a bar on the context line, in the@@ -106,7 +107,7 @@ func (e *chartEditor) keep() {
106107 id = "insert.chart"107108 m.say("Inserted a chart of " + e.chart().Data.String())108109 }109- m.recordChart(id, e.i, e.before, e.chart())110+ m.recordChart(id, e.i, e.before, e.chart(), false)110111 }111112 112113 func (e *chartEditor) Key(k tea.KeyPressMsg) tea.Cmd {@@ -122,12 +122,17 @@ func chartWith(c sheet.Chart, answer []jsonField) (sheet.Chart, error) {
122122 }123123 124124 // recordChart records chart i inserted or changed from before to after,125-// as the command id that does it answered with the chart.126-func (m *Model) recordChart(id string, i int, before, after sheet.Chart) {125+// as the command id that does it answered with the chart. A chart placed126+// (moved or resized while selected, not by a command) comes after the127+// selection it follows.128+func (m *Model) recordChart(id string, i int, before, after sheet.Chart, placed bool) {127129 r := m.rec128130 if r == nil || r.depth > 0 {129131 return130132 }133+ if placed {134+ r.flush(m)135+ }131136 if id == "insert.chart" {132137 m.recordDialog(id, after.JSON())133138 return@@ -137,7 +142,7 @@ func (m *Model) recordChart(id string, i int, before, after sheet.Chart) {
137142 return138143 }139144 n := json.RawMessage(strconv.Itoa(i + 1))140- if last, ok := r.lastPlacement(i + 1); ok && r.base == m.selState() && placement(changes) {145+ if last, ok := r.lastPlacement(i + 1); ok && placed && placement(changes) {141146 for _, f := range changes {142147 if k := fieldIndex(last, f.key); k >= 0 {143148 last[k].value = f.value@@ -97,7 +97,7 @@ func (s *chartSel) place(from, to sheet.Chart, label string) {
9797 sh.SetChart(s.i, to, label)9898 s.m.syncChanged()9999 if now, ok := s.chart(); ok {100- s.m.recordChart("chart.edit", s.i, from, now)100+ s.m.recordChart("chart.edit", s.i, from, now, true)101101 }102102 }103103 @@ -210,3 +210,36 @@ run("chart.edit", answer={"chart": 1, "at": "H2", "legend": "none"})`)
210210 t.Fatalf("warn %q overlay %T", m.warn, m.overlay)211211 }212212 }213+214+func TestRecordPivotEditor(t *testing.T) {215+ m := wideSales()216+ got := recordDo(t, m, "Pivot", func() {217+ run(m, m.runCommand("data.pivot"))218+ press(t, m, "<space>", "reg", "<enter>")219+ press(t, m, "<down>", "<down>", "<space>", "units", "<enter>", "<enter>")220+ // Edited again: the grand total row off. Unchanged, it records221+ // nothing.222+ run(m, m.runCommand("data.pivot_edit"))223+ press(t, m, "<enter>")224+ run(m, m.runCommand("data.pivot_edit"))225+ press(t, m, "<end>", "<up>", "<space>", "<enter>")226+ })227+ want := `run("data.pivot", answer={"source": "Sheet1!A1:C5", "rows": [{"column": "A"}], "values": [{"column": "B", "summarize": "sum"}], "rowTotals": True, "columnTotals": True})228+run("data.pivot_edit", answer={"source": "Sheet1!A1:C5", "rows": [{"column": "A"}], "values": [{"column": "B", "summarize": "sum"}], "rowTotals": False, "columnTotals": True})`229+ if got != want {230+ t.Fatalf("recorded:\n%s\nwant:\n%s", got, want)231+ }232+ replays(t, m, "Pivot", wideSales)233+}234+235+func TestScriptAnswersPivotCommands(t *testing.T) {236+ m := wideSales()237+ script(t, m, `run("data.pivot_edit", answer={"source": "Sheet1!A1:C5"})`)238+ if !strings.Contains(m.warn, "Sheet1 has no pivot table") {239+ t.Fatalf("warn %q", m.warn)240+ }241+ script(t, m, `run("data.pivot", answer={"source": "Sheet1!A1:C5", "rows": [{"column": "A"}], "values": [{"column": "C", "summarize": "max"}]})`)242+ if m.sheet.Name() != "Pivot Table 1" || shows(m, "B1") != "MAX of Price" || m.overlay != nil {243+ t.Fatalf("on %s, B1 %q, overlay %T, warn %q", m.sheet.Name(), shows(m, "B1"), m.overlay, m.warn)244+ }245+}@@ -163,13 +163,14 @@ func (m *Model) recordAnswer(answer string, cancelled bool) {
163163 164164 // recordDialog records what a dialog did once it has, as its command165165 // answered with the dialog's choices (see command.answer), which a166-// replay makes again.166+// replay makes again. The selection it acted on was recorded when its167+// command ran, or by recordFlush when something else opened it: what the168+// command did to the selection since (showing a new sheet) is its own.167169 func (m *Model) recordDialog(id, answer string) {168170 r := m.rec169171 if r == nil || r.depth > 0 || m.mode == modeError {170172 return171173 }172- r.flush(m)173174 r.add(m, macro.Call("run", id).With("answer", macro.JSON(answer)))174175 }175176 @@ -226,6 +226,7 @@ func (m *Model) readyPress(h hit, mouse tea.Mouse, double bool) tea.Cmd {
226226 case hitFillHandle:227227 m.startFill()228228 case hitFilterButton:229+ m.recordFlush() // opened without a command, which records the selection229230 m.openFilterPicker(h.addr.Col)230231 case hitCheckbox, hitDropdown:231232 m.cur = h.addr@@ -1,6 +1,8 @@
11 package ui22 33 import (4+ "cmp"5+ "errors"46 "log/slog"57 "strconv"68 @@ -21,12 +23,26 @@ import (
2123 func init() {2224 register(2325 &command{id: "data.pivot", title: "Pivot table", desc: "Summarize the selection, or the table around the active cell, on a new sheet",24- run: (*Model).createPivot},26+ run: (*Model).createPivot,27+ answer: func(m *Model, text string) (tea.Cmd, error) {28+ m.createPivot()29+ e, ok := m.overlay.(*pivotEditor)30+ if !ok {31+ return nil, errors.New(cmp.Or(m.errMsg, m.note))32+ }33+ return nil, e.answer(m, text)34+ }},2535 &command{id: "data.pivot_edit", title: "Edit pivot table", desc: "Change the pivot table's rows, columns, values and filters",2636 enabled: func(m *Model) bool { _, ok := m.sheet.Pivot(); return ok },2737 run: func(m *Model) tea.Cmd {2838 m.openPivotEditor(m.sheet.StateID(), nil)2939 return nil40+ },41+ answer: func(m *Model, text string) (tea.Cmd, error) {42+ if _, ok := m.sheet.Pivot(); !ok {43+ return nil, errors.New(m.sheet.Name() + " has no pivot table")44+ }45+ return nil, m.openPivotEditor(m.sheet.StateID(), nil).answer(m, text)3046 }},3147 &command{id: "data.frequency", title: "Frequency table (column stats)", desc: "Count each value of the active column, most frequent first, on a new sheet, as Sheets' Column stats do",3248 run: (*Model).frequency},@@ -23,10 +23,11 @@ import (
2323 // it. Enter keeps the changes; Esc undoes them, removing a pivot just2424 // created.2525 type pivotEditor struct {26- m pivotHost // the model, through what the editor needs of it27- start int // the workbook's state before editing, to undo back to28- back *sheet.Sheet // for a new pivot, the sheet it summarizes, shown again on Esc29- msg string // why the last change was refused, for the status line26+ m pivotHost // the model, through what the editor needs of it27+ start int // the workbook's state before editing, to undo back to28+ back *sheet.Sheet // for a new pivot, the sheet it summarizes, shown again on Esc29+ before string // the pivot when the editor opened, as a macro answers it30+ msg string // why the last change was refused, for the status line3031 overlay.List3132 }3233 @@ -55,6 +56,9 @@ type pivotHost interface {
5556 pointRange(label string, done func(sheet.Rect), cancel func())5657 // askText asks for text, such as a value's name.5758 askText(label, initial string, done func(string), cancel func())59+ // recordDialog records the pivot made, as the command id answered60+ // with its definition.61+ recordDialog(id, answer string)5862 }5963 6064 // Sections of the editor, in order.@@ -97,11 +101,13 @@ const pivotEditorID = "pivot"
97101 98102 // openPivotEditor edits the shown sheet's pivot. start is the state Esc99103 // returns to; back, for a pivot just created, the sheet to show then.100-func (m *Model) openPivotEditor(start int, back *sheet.Sheet) {104+func (m *Model) openPivotEditor(start int, back *sheet.Sheet) *pivotEditor {101105 e := &pivotEditor{m: m, start: start, back: back}106+ e.before = e.pivot(m).JSON()102107 e.Sel = 2 // the Rows section103108 m.clearSelection()104109 m.openOverlay(e)110+ return e105111 }106112 107113 func (e *pivotEditor) Indicator() string { return "PIVOT" }@@ -183,7 +189,7 @@ func (e *pivotEditor) Key(k tea.KeyPressMsg) tea.Cmd {
183189 it := e.current(m)184190 switch key := k.String(); key {185191 case "enter":186- m.closeOverlay()192+ e.keep(m)187193 case "esc":188194 e.cancel(m)189195 case "up", "ctrl+p", "shift+tab":@@ -225,6 +231,32 @@ func (e *pivotEditor) cancel(m pivotHost) {
225231 m.syncChanged()226232 }227233 234+// keep closes the editor keeping the changes, and records the pivot235+// made: a new one as Data > Pivot table, a changed one as Edit pivot236+// table, answered with its definition.237+func (e *pivotEditor) keep(m pivotHost) {238+ m.closeOverlay()239+ id, now := "data.pivot_edit", e.pivot(m).JSON()240+ if e.back != nil {241+ id = "data.pivot"242+ } else if now == e.before {243+ return244+ }245+ m.recordDialog(id, now)246+}247+248+// answer makes the pivot a macro's answer defines, as a line of the file249+// writes it, and closes the editor.250+func (e *pivotEditor) answer(m pivotHost, text string) error {251+ p, err := sheet.ParsePivot(text)252+ if err == nil {253+ err = m.sheetShown().SetPivot(p, "")254+ }255+ m.syncChanged()256+ m.closeOverlay()257+ return err258+}259+228260 // reopen returns to the editor after a picker or prompt.229261 func (e *pivotEditor) reopen() { e.m.openOverlay(e) }230262 @@ -232,7 +264,7 @@ func (e *pivotEditor) Mouse(ev overlay.MouseEvent) tea.Cmd {
232264 m := e.m233265 if ev.Box != pivotEditorID {234266 if ev.Kind == overlay.MousePress {235- m.closeOverlay() // a click elsewhere keeps the changes267+ e.keep(m) // a click elsewhere keeps the changes236268 }237269 return nil238270 }