1package ui23import (4 "slices"5 "strconv"6 "strings"78 tea "charm.land/bubbletea/v2"910 "src.rest/cat/012/internal/sheet"11 "src.rest/cat/012/internal/ui/overlay"12 "src.rest/cat/012/internal/ui/picker"13 "src.rest/cat/012/internal/ui/sortbar"14)1516// What the pivot editor's keys do to the pivot, and how its fields read.1718// fieldName is the source field a field line is about.19func (e *pivotEditor) fieldName(m pivotHost, p sheet.Pivot, it pivotItem) string {20 return m.book().FieldName(p, fieldCol(p, it))21}2223func fieldCol(p sheet.Pivot, it pivotItem) int {24 switch it.sect {25 case sectRows:26 return p.Rows[it.i].Col27 case sectColumns:28 return p.Columns[it.i].Col29 case sectValues:30 return p.Values[it.i].Col31 }32 return p.Filters[it.i].Col33}3435// groups returns the rows or the columns of p, for editing.36func groups(p *sheet.Pivot, sect int) *[]sheet.PivotGroup {37 if sect == sectColumns {38 return &p.Columns39 }40 return &p.Rows41}4243// fieldDetail is the right side of a field line: a group's order, a44// value's summary as a chip to change, a filter's criteria.45func (e *pivotEditor) fieldDetail(m pivotHost, p sheet.Pivot, it pivotItem, sel bool) string {46 dim, chip := m.styles().Muted, m.styles().KeyChip47 if sel {48 dim, chip = m.styles().MenuSelected, m.styles().MenuSelected49 }50 switch it.sect {51 case sectValues:52 v := p.Values[it.i]53 text := chip.Render("‹ " + v.Summarize.Title() + " ›")54 if v.ShowAs != sheet.ShowValue {55 text += dim.Render(" " + v.ShowAs.Title())56 }57 return text58 case sectFilters:59 return dim.Render(criteriaText(p.Filters[it.i].Criteria))60 }61 g := (*groups(&p, it.sect))[it.i]62 return dim.Render(orderText(m, p, g))63}6465// orderText says how a group is ordered: A→Z, or by a value's total.66func orderText(m pivotHost, p sheet.Pivot, g sheet.PivotGroup) string {67 if i := g.SortBy - 1; i >= 0 && i < len(p.Values) {68 arrow := "↑"69 if g.Desc {70 arrow = "↓"71 }72 return m.book().ValueTitle(p, p.Values[i]) + " " + arrow73 }74 return sortbar.OrderName(g.Desc)75}7677// criteriaText sums up a filter: the values it hides, or its condition.78func criteriaText(cr sheet.Criteria) string {79 switch {80 case cr.Cond.Op != sheet.CondNone && len(cr.Hidden) > 0:81 return cr.Cond.Op.Title() + ", " + strconv.Itoa(len(cr.Hidden)) + " hidden"82 case cr.Cond.Op != sheet.CondNone:83 return cr.Cond.Op.Title() + " " + cr.Cond.Arg84 case len(cr.Hidden) > 0:85 return strconv.Itoa(len(cr.Hidden)) + " hidden"86 }87 return "showing all"88}8990// act does what Space does on a line.91func (e *pivotEditor) act(m pivotHost, it pivotItem) tea.Cmd {92 switch {93 case it.kind == itemSource:94 e.editSource(m)95 case it.kind == itemSection:96 e.addField(m, it.sect)97 case it.kind == itemToggle:98 e.set(m, "change grand totals", func(p *sheet.Pivot) {99 if it.i == 0 {100 p.RowTotals = !p.RowTotals101 } else {102 p.ColumnTotals = !p.ColumnTotals103 }104 })105 case it.kind == itemField && it.sect == sectFilters:106 e.editFilter(m, it.i)107 case it.kind == itemField:108 e.adjust(m, it, 1)109 }110 return nil111}112113// adjust steps a field's setting d places: a group's order (A→Z, Z→A,114// then by each value, smallest and largest first), or a value's summary.115func (e *pivotEditor) adjust(m pivotHost, it pivotItem, d int) {116 switch {117 case it.kind == itemToggle:118 e.act(m, it)119 case it.kind != itemField || it.sect == sectFilters:120 case it.sect == sectValues:121 e.set(m, "summarize by", func(p *sheet.Pivot) {122 fs := sheet.Summaries()123 v := &p.Values[it.i]124 v.Summarize = fs[wrap(slices.Index(fs, v.Summarize)+d, len(fs))]125 })126 default:127 e.set(m, "order "+sectNames[it.sect], func(p *sheet.Pivot) {128 g := &(*groups(p, it.sect))[it.i]129 n := 2 * (len(p.Values) + 1)130 k := wrap(2*g.SortBy+boolInt(g.Desc)+d, n)131 if g.SortBy > len(p.Values) {132 k = 0133 }134 g.SortBy, g.Desc = k/2, k%2 == 1135 })136 }137}138139func wrap(i, n int) int { return ((i % n) + n) % n }140141func boolInt(b bool) int {142 if b {143 return 1144 }145 return 0146}147148// cycleShowAs steps a value through Sheets' "Show as" choices.149func (e *pivotEditor) cycleShowAs(m pivotHost, it pivotItem) {150 if it.kind != itemField || it.sect != sectValues {151 return152 }153 e.set(m, "show as", func(p *sheet.Pivot) {154 all := sheet.ShowAsList()155 v := &p.Values[it.i]156 v.ShowAs = all[wrap(slices.Index(all, v.ShowAs)+1, len(all))]157 })158}159160// rename asks for a value's name, the header of its columns; an empty161// name goes back to Sheets' "SUM of Sales".162func (e *pivotEditor) rename(m pivotHost, it pivotItem) {163 if it.kind != itemField || it.sect != sectValues {164 return165 }166 p := e.pivot(m)167 m.closeOverlay()168 m.askText("Value name:", m.book().ValueTitle(p, p.Values[it.i]), func(text string) {169 e.set(m, "rename "+e.fieldName(m, e.pivot(m), it), func(p *sheet.Pivot) {170 v := &p.Values[it.i]171 v.Name = strings.TrimSpace(text)172 if v.Name == m.book().ValueTitle(*p, sheet.PivotValue{Col: v.Col, Summarize: v.Summarize}) {173 v.Name = "" // the default, which follows the summary174 }175 })176 e.reopen()177 }, e.reopen)178}179180// lineName is what a field's line calls it: a value's own name, or the181// source field's.182func (e *pivotEditor) lineName(m pivotHost, p sheet.Pivot, it pivotItem) string {183 if it.sect == sectValues && p.Values[it.i].Name != "" {184 return p.Values[it.i].Name185 }186 return e.fieldName(m, p, it)187}188189// remove takes a field out of its section.190func (e *pivotEditor) remove(m pivotHost, it pivotItem) {191 if it.kind != itemField {192 return193 }194 e.set(m, "remove "+e.fieldName(m, e.pivot(m), it)+" from "+sectNames[it.sect], func(p *sheet.Pivot) {195 switch it.sect {196 case sectValues:197 p.Values = slices.Delete(p.Values, it.i, it.i+1)198 for _, gs := range [][]sheet.PivotGroup{p.Rows, p.Columns} {199 for k := range gs {200 switch {201 case gs[k].SortBy == it.i+1:202 gs[k].SortBy, gs[k].Desc = 0, false203 case gs[k].SortBy > it.i+1:204 gs[k].SortBy--205 }206 }207 }208 case sectFilters:209 p.Filters = slices.Delete(p.Filters, it.i, it.i+1)210 default:211 gs := groups(p, it.sect)212 *gs = slices.Delete(*gs, it.i, it.i+1)213 }214 })215 e.current(m)216}217218// reorder moves a row or column field up or down within its section,219// which nests the groups differently.220func (e *pivotEditor) reorder(m pivotHost, it pivotItem, down bool) {221 if it.kind != itemField || it.sect == sectFilters {222 return223 }224 p := e.pivot(m)225 j := it.i - 1226 if down {227 j = it.i + 1228 }229 if j < 0 || j >= sectLen(p, it.sect) {230 return231 }232 e.set(m, "move "+e.fieldName(m, p, it), func(p *sheet.Pivot) {233 swap := func(s []sheet.PivotGroup) { s[it.i], s[j] = s[j], s[it.i] }234 if it.sect == sectValues {235 p.Values[it.i], p.Values[j] = p.Values[j], p.Values[it.i]236 return237 }238 swap(*groups(p, it.sect))239 })240 e.selectItem(m, pivotItem{kind: itemField, sect: it.sect, i: j})241}242243// addField picks a field of the source to add to a section.244func (e *pivotEditor) addField(m pivotHost, sect int) {245 p := e.pivot(m)246 used := map[int]bool{}247 switch sect {248 case sectRows, sectColumns:249 for _, g := range slices.Concat(p.Rows, p.Columns) {250 used[g.Col] = true251 }252 case sectFilters:253 for _, f := range p.Filters {254 used[f.Col] = true255 }256 }257 var items []picker.Item258 for col := p.Range.From.Col; col <= p.Range.To.Col && !p.Lost; col++ {259 name := m.book().FieldName(p, col)260 items = append(items, picker.Item{261 Title: name, Name: len(name), Detail: "column " + sheet.ColName(col), Off: used[col],262 Desc: "Add " + name + " to " + sectNames[sect],263 Pick: func() tea.Cmd {264 m.closeOverlay()265 e.add(m, sect, col)266 e.reopen()267 return nil268 },269 })270 }271 fp := m.newPicker("Add to "+sectNames[sect], "Type a field name", 50, items)272 fp.Action = "add"273 m.openOverlay(&fieldPicker{m: m, Picker: fp, back: e})274}275276// add puts the field in column col at the end of a section.277func (e *pivotEditor) add(m pivotHost, sect, col int) {278 name := m.book().FieldName(e.pivot(m), col)279 e.set(m, "add "+name+" to "+sectNames[sect], func(p *sheet.Pivot) {280 switch sect {281 case sectValues:282 p.Values = append(p.Values, sheet.PivotValue{Col: col, Summarize: m.book().DefaultSummarize(*p, col)})283 case sectFilters:284 p.Filters = append(p.Filters, sheet.PivotFilter{Col: col})285 default:286 gs := groups(p, sect)287 *gs = append(*gs, sheet.PivotGroup{Col: col})288 }289 })290 e.selectItem(m, pivotItem{kind: itemField, sect: sect, i: sectLen(e.pivot(m), sect) - 1})291}292293// fieldPicker is the picker of fields to add; Esc returns to the editor.294type fieldPicker struct {295 m pivotHost // the model, through what the editor needs of it296 *picker.Picker297 back *pivotEditor298}299300func (f *fieldPicker) Key(k tea.KeyPressMsg) tea.Cmd {301 m := f.m302 if k.String() == "esc" {303 m.closeOverlay()304 f.back.reopen()305 return nil306 }307 return f.Picker.Key(k)308}309310func (f *fieldPicker) Mouse(e overlay.MouseEvent) tea.Cmd {311 m := f.m312 if e.Box != picker.ID && e.Kind == overlay.MousePress {313 m.closeOverlay()314 f.back.reopen()315 return nil316 }317 return f.Picker.Mouse(e)318}319320// editFilter opens the values list of filter i, in the manner of a321// filter's column.322func (e *pivotEditor) editFilter(m pivotHost, i int) {323 p := e.pivot(m)324 f := p.Filters[i]325 name := m.book().FieldName(p, f.Col)326 x, _, _ := e.box(m)327 m.pickValues("Filter "+name, x, m.book().PivotFilterValues(p, f.Col), f.Criteria.Cond,328 func(cr sheet.Criteria) {329 e.set(m, "filter "+name, func(p *sheet.Pivot) { p.Filters[i].Criteria = cr })330 e.reopen()331 }, e.reopen)332}333334// editSource points at the data on its sheet, starting from the current335// range; fields outside the new range are dropped.336func (e *pivotEditor) editSource(m pivotHost) {337 p := e.pivot(m)338 src, home := m.book().Lookup(p.Source), m.sheetShown()339 if src == nil || src == home {340 e.msg = "The source sheet " + p.Source + " doesn't exist"341 return342 }343 if src.Hidden() {344 e.msg = hiddenMsg(src)345 return346 }347 m.closeOverlay()348 m.showSheet(src)349 if !p.Lost {350 m.selectRect(p.Range)351 }352 back := func() {353 m.clearSelection()354 m.showSheet(home)355 e.reopen()356 }357 m.pointRange("Pivot data:", func(r sheet.Rect) {358 back()359 e.set(m, "change the pivot's data to "+r.String(), func(p *sheet.Pivot) {360 p.Source, p.Range, p.Lost = src.Name(), r, false361 in := func(c int) bool { return c >= r.From.Col && c <= r.To.Col }362 keep := func(g sheet.PivotGroup) bool { return !in(g.Col) }363 p.Rows = slices.DeleteFunc(p.Rows, keep)364 p.Columns = slices.DeleteFunc(p.Columns, keep)365 p.Values = slices.DeleteFunc(p.Values, func(v sheet.PivotValue) bool { return !in(v.Col) })366 p.Filters = slices.DeleteFunc(p.Filters, func(f sheet.PivotFilter) bool { return !in(f.Col) })367 })368 }, back)369}