Files
gosentry/src/ui/settings_view_layout.go
T
mix 5b0e6fe51b Wire KeepRunningInTray to runtime so tray, close, and autostart follow the saved setting.
Autostart entries pass --start-in-tray only when the tray is enabled; Settings warns that the notification icon needs a restart (Fyne limitation).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 22:44:02 +03:00

145 lines
6.9 KiB
Go

package ui
import (
"runtime"
"gitea.mixdep.ru/mix/gosentry/src/app"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
// settingsFormFields groups every widget the Settings tab's two-column layout
// arranges. It exists so newSettingsLayout takes one argument instead of
// twenty, and so a widget added in settingsView is added to exactly one
// struct literal rather than threaded through a long parameter list.
type settingsFormFields struct {
startOnLogin *widget.Check
autostartStatus *widget.Label
minimizeToTray *widget.Check
trayRestartHint *widget.Label
notifications *widget.Check
themeSelect *widget.Select
executionModeSelect *widget.Select
overlapPolicySelect *widget.Select
defaultTimeout *widget.Entry
configPath string
jobsFile *widget.Entry
jobsFileBrowse *widget.Button
logsDir *widget.Entry
logsDirOpen *widget.Button
logsDirBrowse *widget.Button
maxLogFiles *widget.Entry
maxLogAgeDays *widget.Entry
saveSettings *widget.Button
cancelSettings *widget.Button
restoreDefaults *widget.Button
settingsStatus *widget.Label
}
// newSettingsLayout assembles the Settings tab from its fields: a two-column
// grid (Application/Queue on the left, Storage/About on the right) with the
// Save/Cancel/Defaults row below.
func newSettingsLayout(f settingsFormFields) fyne.CanvasObject {
// capW is measured once from every caption in the tab so all rows —
// Application, Queue, Storage, About — share one value column start,
// instead of each settingsRow call re-measuring its own single caption.
capW := captionColumnWidth(settingsCaptions...)
// The form is split into two columns so a wide window uses its horizontal
// space instead of stretching into one tall strip. The left column holds the
// toggles (Application, Queue); the right holds the editable Storage fields and
// the read-only About block. Save spans the full width below both columns.
leftColumn := container.NewVBox(
settingsSection("Application", rowOverlap(),
settingsRow(capW, "Autostart", f.startOnLogin),
// Autostart status sits on its own row, aligned under the checkbox via an
// empty caption, so the Application section fits in a half-width column.
settingsRow(capW, "", f.autostartStatus),
settingsRow(capW, "Tray", f.minimizeToTray),
settingsRow(capW, "", f.trayRestartHint),
settingsRow(capW, "Notifications", f.notifications),
// Theme is the one row here whose value is not text: the Select paints
// a box out to the row's edge, so the section's overlap would leave it
// flush against the Notifications checkbox. Cancelling the overlap for
// this row alone restores the gap the checkbox rows have.
cancelRowOverlap(settingsRow(capW, "Theme", f.themeSelect)),
),
widget.NewSeparator(),
// Queue used to inline its own container.NewVBox at the theme's default
// spacing; settingsSection now takes that spacing explicitly so both
// idioms for "a titled block of rows" collapse into one constructor.
settingsSection("Queue", theme.Padding(),
settingsRow(capW, "Execution mode", f.executionModeSelect),
settingsRow(capW, "Default overlap policy", f.overlapPolicySelect),
settingsRow(capW, "Default timeout (s)", f.defaultTimeout),
),
)
// Truncating keeps a long config path from forcing the Settings tab's
// minimum width to track the path length instead of the layout itself.
configPathLabel := widget.NewLabel(f.configPath)
configPathLabel.Truncation = fyne.TextTruncateClip
rightColumn := container.NewVBox(
settingsSection("Storage", theme.Padding(),
settingsRow(capW, "Config JSON", configPathLabel),
settingsRow(capW, "Jobs file", container.NewBorder(nil, nil, nil, f.jobsFileBrowse, f.jobsFile)),
// Browse stays rightmost so it lines up with the Jobs file row
// above it; Open sits between it and the path it opens.
settingsRow(capW, "Logs directory", container.NewBorder(nil, nil, nil, container.NewHBox(f.logsDirOpen, f.logsDirBrowse), f.logsDir)),
settingsRow(capW, "Max log files", f.maxLogFiles),
settingsRow(capW, "Max log age days", f.maxLogAgeDays),
),
widget.NewSeparator(),
settingsSection("About", rowOverlap(),
settingsRow(capW, "GoSentry", widget.NewLabel(app.Version)),
settingsRow(capW, "Go", widget.NewLabel(runtime.Version())),
settingsRow(capW, "Fyne", widget.NewLabel(fyneVersion())),
settingsRow(capW, "Repository", widget.NewHyperlink(projectRepositoryURL, mustParseURL(projectRepositoryURL))),
),
)
// The two columns sit in a top-aligned grid; Save spans the full width below.
// Wrapping the whole thing in a vertical scroll keeps its minimum height small
// so it does not dictate the window's minimum height (AppTabs sizes to the
// tallest tab) and it scrolls on short 720p screens.
// The button row sits right below the separator's hairline, which reads as
// tighter than the other vertical gaps in the tab (those separate whole
// sections, not a single thin line from a row of buttons). A top pad the
// height of the default padding closes that gap up to match, and a left pad
// indents the buttons 4px from the edge the layout promises.
return container.NewVScroll(container.NewPadded(container.NewVBox(
container.NewGridWithColumns(2, leftColumn, rightColumn),
widget.NewSeparator(),
container.New(
layout.NewCustomPaddedLayout(2*theme.Padding(), 0, theme.Padding(), 0),
// Save/Cancel/Defaults share one row with the status so an empty status
// (the common case) does not leave a blank line above the separator. The
// status appears beside the buttons once a save reports a result.
container.NewHBox(f.saveSettings, f.cancelSettings, f.restoreDefaults, f.settingsStatus),
),
)))
}
// settingsSection groups a bold header above its rows, using the given
// vertical spacing between them. Application and About pass rowOverlap() so
// the block reads as one compact unit; Queue and Storage pass theme.Padding()
// (the same spacing container.NewVBox would use) so their entry-heavy rows
// keep a visible gap. One constructor for "a titled block of rows" rather
// than two spellings of it.
func settingsSection(title string, spacing float32, rows ...fyne.CanvasObject) fyne.CanvasObject {
children := make([]fyne.CanvasObject, 0, len(rows)+1)
children = append(children, widget.NewLabelWithStyle(title, fyne.TextAlignLeading, fyne.TextStyle{Bold: true}))
children = append(children, rows...)
return container.New(layout.NewCustomPaddedVBoxLayout(spacing), children...)
}
func settingsRow(captionWidth float32, label string, value fyne.CanvasObject) fyne.CanvasObject {
caption := widget.NewLabelWithStyle(label, fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
caption.Truncation = fyne.TextTruncateClip
captionBox := container.New(minWidthLayout{width: captionWidth}, caption)
return container.NewBorder(nil, nil, captionBox, nil, value)
}