5b0e6fe51b
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>
145 lines
6.9 KiB
Go
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)
|
|
}
|