feat(ui): align settings spacing, fix label clipping, gate Save button
- Give the Queue selects the same default spacing as the Storage fields - Widen the settings caption column so "Default overlap policy" fits - Disable Save until a field differs from the saved config, re-enabling on change and re-disabling after a successful save Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+49
-4
@@ -17,7 +17,10 @@ import (
|
|||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
)
|
)
|
||||||
|
|
||||||
const settingsLabelWidth float32 = 140
|
// settingsLabelWidth is wide enough to show the longest caption ("Default
|
||||||
|
// overlap policy") in full; the captions truncate, so a narrower width would
|
||||||
|
// clip it. All rows share this width so their value controls stay aligned.
|
||||||
|
const settingsLabelWidth float32 = 180
|
||||||
const settingsControlWidth float32 = 330
|
const settingsControlWidth float32 = 330
|
||||||
const projectRepositoryURL = "https://gitea.mixdep.ru/mix/gosentry"
|
const projectRepositoryURL = "https://gitea.mixdep.ru/mix/gosentry"
|
||||||
|
|
||||||
@@ -28,6 +31,10 @@ const settingsRowSpacing float32 = -6
|
|||||||
|
|
||||||
func settingsView(w fyne.Window, svc *app.Service) fyne.CanvasObject {
|
func settingsView(w fyne.Window, svc *app.Service) fyne.CanvasObject {
|
||||||
store := svc.Store()
|
store := svc.Store()
|
||||||
|
// updateSaveState compares the form to the saved config and enables Save only
|
||||||
|
// when something differs. It is defined below (once Save and every field
|
||||||
|
// exist) but declared here so the field change handlers can reference it.
|
||||||
|
var updateSaveState func()
|
||||||
startOnLogin := widget.NewCheck("Start on login", nil)
|
startOnLogin := widget.NewCheck("Start on login", nil)
|
||||||
startOnLogin.SetChecked(store.Config.StartOnLogin)
|
startOnLogin.SetChecked(store.Config.StartOnLogin)
|
||||||
autostartStatus := widget.NewLabel("")
|
autostartStatus := widget.NewLabel("")
|
||||||
@@ -42,39 +49,48 @@ func settingsView(w fyne.Window, svc *app.Service) fyne.CanvasObject {
|
|||||||
startOnLogin.OnChanged = func(bool) {
|
startOnLogin.OnChanged = func(bool) {
|
||||||
if startOnLogin.Checked != store.Config.StartOnLogin {
|
if startOnLogin.Checked != store.Config.StartOnLogin {
|
||||||
autostartStatus.SetText("Pending: save settings to apply")
|
autostartStatus.SetText("Pending: save settings to apply")
|
||||||
return
|
} else {
|
||||||
|
refreshAutostartStatus()
|
||||||
}
|
}
|
||||||
refreshAutostartStatus()
|
updateSaveState()
|
||||||
}
|
}
|
||||||
refreshAutostartStatus()
|
refreshAutostartStatus()
|
||||||
minimizeToTray := widget.NewCheck("Keep running in the system tray", nil)
|
minimizeToTray := widget.NewCheck("Keep running in the system tray", nil)
|
||||||
minimizeToTray.SetChecked(store.Config.KeepRunningInTray)
|
minimizeToTray.SetChecked(store.Config.KeepRunningInTray)
|
||||||
|
minimizeToTray.OnChanged = func(bool) { updateSaveState() }
|
||||||
notifications := widget.NewCheck("Show desktop notifications for failed jobs", nil)
|
notifications := widget.NewCheck("Show desktop notifications for failed jobs", nil)
|
||||||
notifications.SetChecked(store.Config.NotifyOnFailure)
|
notifications.SetChecked(store.Config.NotifyOnFailure)
|
||||||
|
notifications.OnChanged = func(bool) { updateSaveState() }
|
||||||
executionModeSelect := widget.NewSelect(
|
executionModeSelect := widget.NewSelect(
|
||||||
[]string{string(domain.ExecutionModeParallel), string(domain.ExecutionModeSequential)},
|
[]string{string(domain.ExecutionModeParallel), string(domain.ExecutionModeSequential)},
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
executionModeSelect.SetSelected(string(store.Config.ExecutionMode))
|
executionModeSelect.SetSelected(string(store.Config.ExecutionMode))
|
||||||
|
executionModeSelect.OnChanged = func(string) { updateSaveState() }
|
||||||
overlapPolicySelect := widget.NewSelect(
|
overlapPolicySelect := widget.NewSelect(
|
||||||
[]string{string(domain.OverlapPolicySkip), string(domain.OverlapPolicyQueue)},
|
[]string{string(domain.OverlapPolicySkip), string(domain.OverlapPolicyQueue)},
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
overlapPolicySelect.SetSelected(string(store.Config.OverlapPolicy))
|
overlapPolicySelect.SetSelected(string(store.Config.OverlapPolicy))
|
||||||
|
overlapPolicySelect.OnChanged = func(string) { updateSaveState() }
|
||||||
jobsDir := widget.NewEntry()
|
jobsDir := widget.NewEntry()
|
||||||
jobsDir.SetText(store.Config.JobsDir)
|
jobsDir.SetText(store.Config.JobsDir)
|
||||||
|
jobsDir.OnChanged = func(string) { updateSaveState() }
|
||||||
jobsDirBrowse := widget.NewButtonWithIcon("Browse", theme.FolderOpenIcon(), func() {
|
jobsDirBrowse := widget.NewButtonWithIcon("Browse", theme.FolderOpenIcon(), func() {
|
||||||
chooseFolder(w, jobsDir)
|
chooseFolder(w, jobsDir)
|
||||||
})
|
})
|
||||||
logsDir := widget.NewEntry()
|
logsDir := widget.NewEntry()
|
||||||
logsDir.SetText(store.Config.LogsDir)
|
logsDir.SetText(store.Config.LogsDir)
|
||||||
|
logsDir.OnChanged = func(string) { updateSaveState() }
|
||||||
logsDirBrowse := widget.NewButtonWithIcon("Browse", theme.FolderOpenIcon(), func() {
|
logsDirBrowse := widget.NewButtonWithIcon("Browse", theme.FolderOpenIcon(), func() {
|
||||||
chooseFolder(w, logsDir)
|
chooseFolder(w, logsDir)
|
||||||
})
|
})
|
||||||
maxLogFiles := widget.NewEntry()
|
maxLogFiles := widget.NewEntry()
|
||||||
maxLogFiles.SetText(strconv.Itoa(store.Config.MaxLogFiles))
|
maxLogFiles.SetText(strconv.Itoa(store.Config.MaxLogFiles))
|
||||||
|
maxLogFiles.OnChanged = func(string) { updateSaveState() }
|
||||||
maxLogAgeDays := widget.NewEntry()
|
maxLogAgeDays := widget.NewEntry()
|
||||||
maxLogAgeDays.SetText(strconv.Itoa(store.Config.MaxLogAgeDays))
|
maxLogAgeDays.SetText(strconv.Itoa(store.Config.MaxLogAgeDays))
|
||||||
|
maxLogAgeDays.OnChanged = func(string) { updateSaveState() }
|
||||||
// Autostart status sits on its own row beneath the checkbox (rather than
|
// Autostart status sits on its own row beneath the checkbox (rather than
|
||||||
// beside it) so the Application section fits within a half-width column.
|
// beside it) so the Application section fits within a half-width column.
|
||||||
// Truncating keeps a long status message from forcing the column wider.
|
// Truncating keeps a long status message from forcing the column wider.
|
||||||
@@ -124,8 +140,33 @@ func settingsView(w fyne.Window, svc *app.Service) fyne.CanvasObject {
|
|||||||
}
|
}
|
||||||
refreshAutostartStatus()
|
refreshAutostartStatus()
|
||||||
settingsStatus.SetText("Saved")
|
settingsStatus.SetText("Saved")
|
||||||
|
// The form now matches the persisted config, so disable Save again.
|
||||||
|
updateSaveState()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Save stays disabled until a field differs from the saved config, so the
|
||||||
|
// button only invites a click when there is something to persist. The numeric
|
||||||
|
// fields compare against their canonical string form; any unparsable text
|
||||||
|
// counts as a change so the user can click Save and see the validation error.
|
||||||
|
updateSaveState = func() {
|
||||||
|
c := store.Config
|
||||||
|
changed := startOnLogin.Checked != c.StartOnLogin ||
|
||||||
|
minimizeToTray.Checked != c.KeepRunningInTray ||
|
||||||
|
notifications.Checked != c.NotifyOnFailure ||
|
||||||
|
executionModeSelect.Selected != string(c.ExecutionMode) ||
|
||||||
|
overlapPolicySelect.Selected != string(c.OverlapPolicy) ||
|
||||||
|
strings.TrimSpace(jobsDir.Text) != c.JobsDir ||
|
||||||
|
strings.TrimSpace(logsDir.Text) != c.LogsDir ||
|
||||||
|
strings.TrimSpace(maxLogFiles.Text) != strconv.Itoa(c.MaxLogFiles) ||
|
||||||
|
strings.TrimSpace(maxLogAgeDays.Text) != strconv.Itoa(c.MaxLogAgeDays)
|
||||||
|
if changed {
|
||||||
|
saveSettings.Enable()
|
||||||
|
} else {
|
||||||
|
saveSettings.Disable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateSaveState()
|
||||||
|
|
||||||
// The form is split into two columns so a wide window uses its horizontal
|
// 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
|
// space instead of stretching into one tall strip. The left column holds the
|
||||||
// toggles (Application, Queue); the right holds the editable Storage fields and
|
// toggles (Application, Queue); the right holds the editable Storage fields and
|
||||||
@@ -140,7 +181,11 @@ func settingsView(w fyne.Window, svc *app.Service) fyne.CanvasObject {
|
|||||||
settingsRow("Notifications", container.New(minWidthLayout{width: settingsControlWidth}, notifications)),
|
settingsRow("Notifications", container.New(minWidthLayout{width: settingsControlWidth}, notifications)),
|
||||||
),
|
),
|
||||||
widget.NewSeparator(),
|
widget.NewSeparator(),
|
||||||
settingsSection("Queue",
|
// Queue holds the execution mode and overlap policy comboboxes. Like
|
||||||
|
// Storage, it uses the default VBox spacing (not the condensed section
|
||||||
|
// layout) so the comboboxes keep a visible gap between them.
|
||||||
|
container.NewVBox(
|
||||||
|
widget.NewLabelWithStyle("Queue", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||||
settingsRow("Execution mode", container.New(minWidthLayout{width: settingsControlWidth}, executionModeSelect)),
|
settingsRow("Execution mode", container.New(minWidthLayout{width: settingsControlWidth}, executionModeSelect)),
|
||||||
settingsRow("Default overlap policy", container.New(minWidthLayout{width: settingsControlWidth}, overlapPolicySelect)),
|
settingsRow("Default overlap policy", container.New(minWidthLayout{width: settingsControlWidth}, overlapPolicySelect)),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user