diff options
Diffstat (limited to 'gui/internal/ui/plan.go')
| -rw-r--r-- | gui/internal/ui/plan.go | 159 |
1 files changed, 120 insertions, 39 deletions
diff --git a/gui/internal/ui/plan.go b/gui/internal/ui/plan.go index 740341c..472350a 100644 --- a/gui/internal/ui/plan.go +++ b/gui/internal/ui/plan.go @@ -36,6 +36,7 @@ type planView struct { selNone *gtk.Button checked *gtk.MenuButton + groups [6]*gtk.SizeGroup filter *gtk.SearchEntry shown []int list *gtk.ListBox @@ -49,6 +50,11 @@ type planView struct { previewTmp string previewFor string detailPane *gtk.Paned + arrangement *gtk.Paned + listSide *gtk.Box + detailScroll *gtk.ScrolledWindow + previewBox *gtk.Box + layout string renderedDir string previewHeight int previewOff bool @@ -96,16 +102,16 @@ func newPlanView(w *Window) *planView { bar.Append(gtk.NewLabel("Directory")) bar.Append(p.dirs) bar.Append(p.scan) - bar.Append(p.path) - // A filter over the plan: type a few letters of a name, as fzf does, - // and act on what is left (his request, 2026-09-17). + // and act on what is left. It sits by the Scan button, where the eye + // starts (his request, 2026-09-17). p.filter = gtk.NewSearchEntry() p.filter.SetPlaceholderText("filter") p.filter.SetTooltipText("show only the files whose name or rule has these letters, in order; Select all then checks those") - p.filter.SetSizeRequest(160, -1) + p.filter.SetSizeRequest(200, -1) bar.Append(p.filter) + bar.Append(p.path) bar.Append(p.selAll) bar.Append(p.selNone) bar.Append(p.checkedMenu()) @@ -118,6 +124,9 @@ func newPlanView(w *Window) *planView { listScroll.SetChild(p.list) listScroll.SetHExpand(true) listScroll.SetVExpand(true) + for i := range p.groups { + p.groups[i] = gtk.NewSizeGroup(gtk.SizeGroupHorizontal) + } p.headerBox = gtk.NewBox(gtk.OrientationHorizontal, 8) p.headerBox.SetMarginTop(4) p.headerBox.SetMarginBottom(4) @@ -176,34 +185,13 @@ func newPlanView(w *Window) *planView { previewBox.Append(p.picture) previewBox.Append(previewScroll) - // The divider between the explanation and the preview is the size - // control: drag it, and the preview stays that tall (his request, - // 2026-09-17). - p.detailPane = gtk.NewPaned(gtk.OrientationVertical) - p.detailPane.SetStartChild(detailScroll) - p.detailPane.SetEndChild(previewBox) - p.detailPane.SetResizeStartChild(true) - p.detailPane.SetResizeEndChild(false) - p.detailPane.SetShrinkEndChild(false) - p.detailPane.SetVExpand(true) - p.detailPane.SetSizeRequest(340, -1) - p.detailPane.Connect("notify::position", p.rememberPreviewHeight) - detailBox := p.detailPane - - // A pane the user can drag: on a narrow window the list needs the room, - // on a wide one the explanation does. - panes := gtk.NewPaned(gtk.OrientationHorizontal) - panes.SetStartChild(listSide) - panes.SetEndChild(detailBox) - panes.SetResizeStartChild(true) - panes.SetResizeEndChild(false) - panes.SetShrinkEndChild(false) - panes.SetPosition(780) - panes.SetVExpand(true) + p.listSide = listSide + p.detailScroll = detailScroll + p.previewBox = previewBox p.root.Append(bar) p.root.Append(gtk.NewSeparator(gtk.OrientationHorizontal)) - p.root.Append(panes) + p.setLayout(model.LayoutSide) p.scan.ConnectClicked(p.onScan) p.apply.ConnectClicked(p.onApply) @@ -547,6 +535,9 @@ func (p *planView) sayWhatIsShown() { // rule, and the outcome once applied. func (p *planView) fillList() { clearList(p.list) + for i := range p.groups { + p.groups[i] = gtk.NewSizeGroup(gtk.SizeGroupHorizontal) + } if p.tab == nil { return } @@ -584,9 +575,22 @@ func (p *planView) widths() [5]int { for i, cap := range [5]int{40, 16, 52, 34, 24} { w[i] = min(w[i], cap) } + // A column is never narrower than its own heading, or the heading is + // the thing that ends in an ellipsis (his report, 2026-09-17). + for i, title := range columnTitles { + w[i] = max(w[i], len([]rune(title))) + } return w } +// showOutcome reports whether there is anything to put in the last column. +func (p *planView) showOutcome() bool { + return p.tab != nil && p.tab.Applied +} + +// columnTitles name the columns of the plan. +var columnTitles = [5]string{"file", "action", "where it would go", "rule", "outcome"} + // header is the line above the list saying what each column is. func (p *planView) header(w [5]int) { if child := p.headerBox.FirstChild(); child != nil { @@ -601,11 +605,16 @@ func (p *planView) header(w [5]int) { // The checkbox has no title, but its width has to be accounted for. spacer := gtk.NewLabel("") spacer.SetSizeRequest(24, -1) + p.groups[0].AddWidget(spacer) p.headerBox.Append(spacer) - for i, title := range []string{"file", "action", "where it would go", "rule", "outcome"} { + for i, title := range columnTitles { + if i == 4 && !p.showOutcome() { + continue + } l := columnMin(title, w[i], yieldChars, i == 0 || i == 2) l.AddCSSClass("heading") l.SetTooltipText("") + p.groups[i+1].AddWidget(l) p.headerBox.Append(l) } } @@ -628,18 +637,27 @@ func (p *planView) rowWidget(i int, r model.Row, w [5]int) *gtk.ListBoxRow { p.apply.SetSensitive(!p.tab.Applied && p.tab.SelectedCount() > 0) } }) + p.groups[0].AddWidget(check) box.Append(check) // The name and destination columns share whatever space is left and // shrink first; the action and the rule keep their width, so neither is // ever the column cut to an ellipsis. action, colour := rowAction(r) - box.Append(column(escape(r.Rel), w[0], true)) - box.Append(colouredColumn(action, w[1], colour)) - box.Append(columnMin(escape(rowWhere(r, p.dirRoot())), w[2], 18, true)) - box.Append(column(escape(r.Rule), w[3], false)) - if r.Outcome != "" { - box.Append(column(escape(r.Outcome), w[4], false)) + cells := []gtk.Widgetter{ + column(escape(r.Rel), w[0], true), + colouredColumn(action, w[1], colour), + columnMin(escape(rowWhere(r, p.dirRoot())), w[2], 18, true), + column(escape(r.Rule), w[3], false), + } + // The outcome column appears once a plan has been applied: before that + // it would be an empty column with a heading over it. + if p.showOutcome() { + cells = append(cells, column(escape(r.Outcome), w[4], false)) + } + for i, cell := range cells { + p.groups[i+1].AddWidget(cell) + box.Append(cell) } row := gtk.NewListBoxRow() row.SetChild(box) @@ -793,10 +811,64 @@ func (p *planView) showPreview(rel string) { }) } +// setLayout arranges the tab the way the settings ask: the file list beside +// the explanation, or above it with the preview to its side (his sketch, +// 2026-09-17). The widgets are the same either way; only the panes holding +// them change. +func (p *planView) setLayout(which string) { + if p.layout == which && p.arrangement != nil { + return + } + p.layout = which + if p.arrangement != nil { + p.detailPane.SetStartChild(nil) + p.detailPane.SetEndChild(nil) + p.arrangement.SetStartChild(nil) + p.arrangement.SetEndChild(nil) + p.root.Remove(p.arrangement) + } + + // The divider between the explanation and the preview is the size + // control: drag it, and the preview stays that big. + detailOrientation, outerOrientation := gtk.OrientationVertical, gtk.OrientationHorizontal + if which == model.LayoutStacked { + detailOrientation, outerOrientation = gtk.OrientationHorizontal, gtk.OrientationVertical + } + p.detailPane = gtk.NewPaned(detailOrientation) + p.detailPane.SetStartChild(p.detailScroll) + p.detailPane.SetEndChild(p.previewBox) + p.detailPane.SetResizeStartChild(true) + p.detailPane.SetResizeEndChild(false) + p.detailPane.SetShrinkEndChild(false) + p.detailPane.SetVExpand(true) + p.detailPane.Connect("notify::position", p.rememberPreviewHeight) + + p.arrangement = gtk.NewPaned(outerOrientation) + p.arrangement.SetStartChild(p.listSide) + p.arrangement.SetEndChild(p.detailPane) + p.arrangement.SetResizeStartChild(true) + p.arrangement.SetResizeEndChild(false) + p.arrangement.SetShrinkEndChild(false) + p.arrangement.SetVExpand(true) + if which == model.LayoutSide { + p.detailPane.SetSizeRequest(340, -1) + p.arrangement.SetPosition(780) + } else { + p.detailPane.SetSizeRequest(-1, 240) + p.arrangement.SetPosition(420) + } + p.root.Append(p.arrangement) + p.setPreviewHeight(p.previewHeight) +} + // rememberPreviewHeight keeps what a drag of the divider chose, so the next // window opens the same way. func (p *planView) rememberPreviewHeight() { - height := p.detailPane.Height() - p.detailPane.Position() + total := p.detailPane.Height() + if p.layout == model.LayoutStacked { + total = p.detailPane.Width() + } + height := total - p.detailPane.Position() if height < 80 || height == p.previewHeight { return } @@ -815,14 +887,22 @@ func (p *planView) setPreviewHeight(height int) { height = model.DefaultPreviewHeight } p.previewHeight = height - if total := p.detailPane.Height(); total > height+80 { + total := p.detailPane.Height() + if p.layout == model.LayoutStacked { + total = p.detailPane.Width() + } + if total > height+80 { p.detailPane.SetPosition(total - height) return } // Before the window is drawn there is no height to subtract from, so // the end child's own request puts the divider in the right place. if child := p.detailPane.EndChild(); child != nil { - gtk.BaseWidget(child).SetSizeRequest(-1, height) + if p.layout == model.LayoutStacked { + gtk.BaseWidget(child).SetSizeRequest(height, -1) + } else { + gtk.BaseWidget(child).SetSizeRequest(-1, height) + } } } @@ -831,6 +911,7 @@ func (p *planView) setPreviewHeight(height int) { func (p *planView) applyPrefs(prefs model.Prefs) { p.previewOff = !prefs.Preview p.startSelected = prefs.SelectAll + p.setLayout(prefs.Layout) p.setPreviewHeight(prefs.PreviewHeight) if p.previewOff { p.previewFor = "" |
