summaryrefslogtreecommitdiff
path: root/gui/internal/ui/plan.go
diff options
context:
space:
mode:
Diffstat (limited to 'gui/internal/ui/plan.go')
-rw-r--r--gui/internal/ui/plan.go159
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 = ""