diff options
| -rw-r--r-- | docs/gui-checklist.md | 3 | ||||
| -rw-r--r-- | gui/internal/model/prefs.go | 26 | ||||
| -rw-r--r-- | gui/internal/ui/plan.go | 92 | ||||
| -rw-r--r-- | gui/internal/ui/window.go | 29 | ||||
| -rw-r--r-- | man/krino-gui.1 | 7 |
5 files changed, 126 insertions, 31 deletions
diff --git a/docs/gui-checklist.md b/docs/gui-checklist.md index e8b7fed..5cc5734 100644 --- a/docs/gui-checklist.md +++ b/docs/gui-checklist.md @@ -124,3 +124,6 @@ A dialog is its own window: take it by its own id, not the main window's. plan - an applied plan is history, not a list to keep working from. 39. In Settings, the window half needs no saving and says so; Save krino.conf is lit only when the defaults above it have changed. +40. Both dividers stay where they are dragged, per layout, across a + restart, and a list narrower than its columns still starts at the file + names rather than scrolled past them. 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. diff --git a/man/krino-gui.1 b/man/krino-gui.1 index 1cfb76d..cfd6ff4 100644 --- a/man/krino-gui.1 +++ b/man/krino-gui.1 @@ -63,9 +63,10 @@ the reason and cannot be selected. Selecting a row explains it: every step with its rule and reason, and any warning, and shows the file itself underneath: a picture for an image, the first page for a PDF, the first lines for anything that is text. Nothing -else is opened, and the file is only read. The divider above the preview -sets how tall it is - drag it, and the next window opens the same way; a -PDF page is rendered to suit that size, and +else is opened, and the file is only read. Every divider is kept where it is put: the one between the file list and +the explanation, and the one above the preview, each remembered for the +layout it belongs to, so the next window opens as this one was left. A PDF +page is rendered to suit the preview's size, and .Cm Settings has the number for setting it exactly. The right button offers .Cm Trash instead |
