summaryrefslogtreecommitdiff
path: root/gui
diff options
context:
space:
mode:
Diffstat (limited to 'gui')
-rw-r--r--gui/internal/model/prefs.go26
-rw-r--r--gui/internal/ui/plan.go92
-rw-r--r--gui/internal/ui/window.go29
3 files changed, 119 insertions, 28 deletions
diff --git a/gui/internal/model/prefs.go b/gui/internal/model/prefs.go
index 3c249a3..77aa784 100644
--- a/gui/internal/model/prefs.go
+++ b/gui/internal/model/prefs.go
@@ -23,6 +23,14 @@ type Prefs struct {
// PreviewHeight is how tall the preview under the explanation is, in
// pixels; the divider between them sets it, and so does Settings.
PreviewHeight int `json:"preview_height"`
+ // PreviewWidth is the same measurement in the stacked layout, where
+ // the preview sits beside the explanation rather than under it.
+ PreviewWidth int `json:"preview_width"`
+ // ListWidth and ListHeight are where the divider between the file list
+ // and the explanation was left, in each layout. Every divider a hand
+ // moves is kept (his request, 2026-09-17).
+ ListWidth int `json:"list_width"`
+ ListHeight int `json:"list_height"`
// Layout is how the Plan tab is arranged: "side" puts the file list
// beside the explanation, "stacked" puts it above, with the preview
// beside the explanation underneath.
@@ -35,14 +43,21 @@ const (
LayoutStacked = "stacked"
)
-// DefaultPreviewHeight is the preview's height until one is chosen, and the
-// floor a smaller one is raised to.
-const DefaultPreviewHeight = 280
+// The sizes a window opens with until a divider is moved, and the floor a
+// smaller one is raised to.
+const (
+ DefaultPreviewHeight = 280
+ DefaultPreviewWidth = 360
+ DefaultListWidth = 780
+ DefaultListHeight = 420
+)
// DefaultPrefs is what a window does before anything is chosen.
func DefaultPrefs() Prefs {
return Prefs{Colours: true, Preview: true, SelectAll: true,
- PreviewHeight: DefaultPreviewHeight, Layout: LayoutSide}
+ PreviewHeight: DefaultPreviewHeight, PreviewWidth: DefaultPreviewWidth,
+ ListWidth: DefaultListWidth, ListHeight: DefaultListHeight,
+ Layout: LayoutSide}
}
// PrefsFile is where they are kept.
@@ -64,6 +79,9 @@ func LoadPrefs() Prefs {
if p.PreviewHeight < 80 {
p.PreviewHeight = DefaultPreviewHeight
}
+ if p.PreviewWidth < 80 {
+ p.PreviewWidth = DefaultPreviewWidth
+ }
if p.Layout != LayoutSide && p.Layout != LayoutStacked {
p.Layout = LayoutSide
}
diff --git a/gui/internal/ui/plan.go b/gui/internal/ui/plan.go
index 38412c2..4fa9c39 100644
--- a/gui/internal/ui/plan.go
+++ b/gui/internal/ui/plan.go
@@ -36,12 +36,13 @@ type planView struct {
selNone *gtk.Button
checked *gtk.MenuButton
- groups [6]*gtk.SizeGroup
- filter *gtk.SearchEntry
- shown []int
- list *gtk.ListBox
- headerBox *gtk.Box
- details *gtk.TextView
+ groups [6]*gtk.SizeGroup
+ listScroll *gtk.ScrolledWindow
+ filter *gtk.SearchEntry
+ shown []int
+ list *gtk.ListBox
+ headerBox *gtk.Box
+ details *gtk.TextView
previewNote *gtk.Label
picture *gtk.Picture
@@ -124,6 +125,7 @@ func newPlanView(w *Window) *planView {
listScroll.SetChild(p.list)
listScroll.SetHExpand(true)
listScroll.SetVExpand(true)
+ p.listScroll = listScroll
for i := range p.groups {
p.groups[i] = gtk.NewSizeGroup(gtk.SizeGroupHorizontal)
}
@@ -567,6 +569,12 @@ func (p *planView) fillList() {
p.selAll.SetSensitive(!p.tab.Applied)
p.selNone.SetSensitive(!p.tab.Applied)
p.sayWhatIsShown()
+ // A fresh list starts at its left edge: without this the view can open
+ // scrolled sideways, with the file names out of sight (his report,
+ // 2026-09-17).
+ if adj := p.listScroll.HAdjustment(); adj != nil {
+ adj.SetValue(0)
+ }
}
// widths is how wide each column has to be for this plan: enough for the
@@ -602,6 +610,10 @@ func (p *planView) showOutcome() bool {
return p.tab != nil && p.tab.Applied
}
+// headerFloors are how narrow each heading may become, matching the cells
+// under it.
+var headerFloors = [5]int{12, 6, 16, 10, 8}
+
// columnTitles name the columns of the plan.
var columnTitles = [5]string{"file", "action", "where it would go", "rule", "outcome"}
@@ -625,7 +637,7 @@ func (p *planView) header(w [5]int) {
if i == 4 && !p.showOutcome() {
continue
}
- l := columnMin(title, w[i], yieldChars, i == 0 || i == 2)
+ l := columnMin(title, w[i], headerFloors[i], i == 0 || i == 2)
l.AddCSSClass("heading")
l.SetTooltipText("")
p.groups[i+1].AddWidget(l)
@@ -658,16 +670,19 @@ func (p *planView) rowWidget(i int, r model.Row, w [5]int) *gtk.ListBoxRow {
// shrink first; the action and the rule keep their width, so neither is
// ever the column cut to an ellipsis.
action, colour := rowAction(r)
+ // Every column can shrink: a pane narrower than their natural widths
+ // used to push the whole row out of view to the left (his report,
+ // 2026-09-17).
cells := []gtk.Widgetter{
- column(escape(r.Rel), w[0], true),
+ columnMin(escape(r.Rel), w[0], 12, true),
colouredColumn(action, w[1], colour),
- columnMin(escape(rowWhere(r, p.dirRoot())), w[2], 18, true),
- column(escape(r.Rule), w[3], false),
+ columnMin(escape(rowWhere(r, p.dirRoot())), w[2], 16, true),
+ columnMin(escape(r.Rule), w[3], 10, 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))
+ cells = append(cells, columnMin(escape(r.Outcome), w[4], 8, false))
}
for i, cell := range cells {
p.groups[i+1].AddWidget(cell)
@@ -864,17 +879,52 @@ func (p *planView) setLayout(which string) {
p.arrangement.SetResizeEndChild(false)
p.arrangement.SetShrinkEndChild(false)
p.arrangement.SetVExpand(true)
+ prefs := p.w.prefs
if which == model.LayoutSide {
p.detailPane.SetSizeRequest(340, -1)
- p.arrangement.SetPosition(780)
+ p.arrangement.SetPosition(orDefault(prefs.ListWidth, model.DefaultListWidth))
+ p.previewHeight = orDefault(prefs.PreviewHeight, model.DefaultPreviewHeight)
} else {
p.detailPane.SetSizeRequest(-1, 240)
- p.arrangement.SetPosition(420)
+ p.arrangement.SetPosition(orDefault(prefs.ListHeight, model.DefaultListHeight))
+ p.previewHeight = orDefault(prefs.PreviewWidth, model.DefaultPreviewWidth)
}
+ p.arrangement.Connect("notify::position", p.rememberListSize)
p.root.Append(p.arrangement)
p.setPreviewHeight(p.previewHeight)
}
+// orDefault is n, or the default when nothing has been chosen yet.
+func orDefault(n, def int) int {
+ if n < 80 {
+ return def
+ }
+ return n
+}
+
+// rememberListSize keeps where the divider between the file list and the
+// explanation was left, for this layout.
+func (p *planView) rememberListSize() {
+ at := p.arrangement.Position()
+ if at < 80 {
+ return
+ }
+ prefs := p.w.prefs
+ if p.layout == model.LayoutStacked {
+ if prefs.ListHeight == at {
+ return
+ }
+ prefs.ListHeight = at
+ } else {
+ if prefs.ListWidth == at {
+ return
+ }
+ prefs.ListWidth = at
+ }
+ p.w.prefs = prefs
+ p.w.savePrefsSoon()
+}
+
// rememberPreviewHeight keeps what a drag of the divider chose, so the next
// window opens the same way.
func (p *planView) rememberPreviewHeight() {
@@ -888,11 +938,13 @@ func (p *planView) rememberPreviewHeight() {
}
p.previewHeight = height
prefs := p.w.prefs
- prefs.PreviewHeight = height
- p.w.prefs = prefs
- if err := prefs.Save(); err != nil {
- p.w.setStatus("settings: %v", err)
+ if p.layout == model.LayoutStacked {
+ prefs.PreviewWidth = height
+ } else {
+ prefs.PreviewHeight = height
}
+ p.w.prefs = prefs
+ p.w.savePrefsSoon()
}
// setPreviewHeight puts the divider where a height asks for.
@@ -926,7 +978,11 @@ func (p *planView) applyPrefs(prefs model.Prefs) {
p.previewOff = !prefs.Preview
p.startSelected = prefs.SelectAll
p.setLayout(prefs.Layout)
- p.setPreviewHeight(prefs.PreviewHeight)
+ if prefs.Layout == model.LayoutStacked {
+ p.setPreviewHeight(orDefault(prefs.PreviewWidth, model.DefaultPreviewWidth))
+ } else {
+ p.setPreviewHeight(orDefault(prefs.PreviewHeight, model.DefaultPreviewHeight))
+ }
if p.previewOff {
p.previewFor = ""
p.picture.SetVisible(false)
diff --git a/gui/internal/ui/window.go b/gui/internal/ui/window.go
index 61fe157..06bce05 100644
--- a/gui/internal/ui/window.go
+++ b/gui/internal/ui/window.go
@@ -11,6 +11,7 @@ import (
"os"
"strings"
+ "github.com/diamondburned/gotk4/pkg/glib/v2"
"github.com/diamondburned/gotk4/pkg/gtk/v4"
"krino/gui/internal/model"
@@ -26,12 +27,13 @@ type Window struct {
engine *engine.Engine
- plan *planView
- history *historyView
- rules *rulesView
- status *gtk.Label
- prefs model.Prefs
- leaving bool
+ plan *planView
+ history *historyView
+ rules *rulesView
+ status *gtk.Label
+ prefs model.Prefs
+ leaving bool
+ saveTimer glib.SourceHandle
}
// NewWindow builds the window for e. Each plan and each undo is its own
@@ -147,6 +149,21 @@ func (w *Window) confirmLeaving() {
d.Show()
}
+// savePrefsSoon writes the preferences a moment after the last change, so
+// dragging a divider does not write the file on every pixel.
+func (w *Window) savePrefsSoon() {
+ if w.saveTimer != 0 {
+ glib.SourceRemove(w.saveTimer)
+ }
+ w.saveTimer = glib.TimeoutAdd(500, func() bool {
+ w.saveTimer = 0
+ if err := w.prefs.Save(); err != nil {
+ w.setStatus("settings: %v", err)
+ }
+ return false
+ })
+}
+
// applyPrefs takes a change from the settings window: the font of the
// editor, whether the configuration is coloured, and whether the file
// behind a row is shown. What is already on screen changes at once.