Compare commits
4 Commits
8b81180c92
...
f53447d3e8
| Author | SHA1 | Date | |
|---|---|---|---|
| f53447d3e8 | |||
| 24a8140e26 | |||
| 9ecb8b61f8 | |||
| 43809e5076 |
@@ -2,6 +2,36 @@
|
||||
|
||||
All notable GoSentry changes are recorded in this file.
|
||||
|
||||
## 0.11.0 - 2026-06-25
|
||||
|
||||
**Manual runs while paused, two-column Settings/details, and a more compact job list.**
|
||||
|
||||
**Scheduler:**
|
||||
- "Run now" is now allowed while the scheduler is globally paused. The global
|
||||
pause stops only automatic scheduled runs; an explicit manual run is the user's
|
||||
own one-off action and is no longer blocked (the already-running and
|
||||
sequential-mode guards still apply).
|
||||
|
||||
**Jobs details panel:**
|
||||
- Metadata captions (Folder, Command, Run mode, …) are pinned to a fixed width
|
||||
instead of an even split, so widening the window now grows the value column
|
||||
rather than the short caption.
|
||||
- Fixed a bug where the "Selected job activity" panel kept showing the previous
|
||||
job's entries when a different job was selected; the list now refreshes on
|
||||
every selection change.
|
||||
|
||||
**Jobs list:**
|
||||
- List rows (name, schedule/command, status) are condensed with a tight,
|
||||
negative-gap layout so more jobs are visible without scrolling.
|
||||
|
||||
**Settings tab:**
|
||||
- The form is reorganized into two columns — Application and Queue on the left,
|
||||
Storage and About on the right — with the Save button spanning the full width
|
||||
below. The Autostart status moved onto its own line so the section fits a
|
||||
half-width column.
|
||||
- Removed the blank row that sat between the Save button and the following
|
||||
separator.
|
||||
|
||||
## 0.10.2 - 2026-06-25
|
||||
|
||||
**Condensed details/settings panels and a window that shrinks to 720p.**
|
||||
|
||||
+29
-14
@@ -335,21 +335,27 @@ func TestRunNowRefusedWhileAlreadyRunning(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunNowRefusedWhilePaused(t *testing.T) {
|
||||
func TestRunNowAllowedWhilePaused(t *testing.T) {
|
||||
svc := newTempService(t, []domain.Job{{ID: 1, Name: "A", Schedule: "@every 1m", Command: "echo", Enabled: true}})
|
||||
var ran bool
|
||||
done := make(chan struct{}, 1)
|
||||
svc.runJob = func(context.Context, *domain.Job, string, string) domain.RunRecord {
|
||||
ran = true
|
||||
return domain.RunRecord{}
|
||||
select {
|
||||
case done <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
return domain.RunRecord{State: "Success"}
|
||||
}
|
||||
if err := svc.SetGlobalPause(true); err != nil {
|
||||
t.Fatalf("SetGlobalPause: %v", err)
|
||||
}
|
||||
if err := svc.RunNow(1); err == nil {
|
||||
t.Error("expected RunNow to be refused while paused")
|
||||
// Pause stops only scheduled runs; an explicit manual run is still allowed.
|
||||
if err := svc.RunNow(1); err != nil {
|
||||
t.Fatalf("RunNow should be allowed while paused: %v", err)
|
||||
}
|
||||
if ran {
|
||||
t.Error("runner must not be invoked while paused")
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Error("runner was not invoked for a manual run while paused")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -586,23 +592,32 @@ func TestServiceRebuiltFromPausedStoreStartsPaused(t *testing.T) {
|
||||
svc2 := NewService(svc.store, svc.Jobs())
|
||||
|
||||
var ran int32
|
||||
runStarted := make(chan struct{}, 1)
|
||||
svc2.runJob = func(context.Context, *domain.Job, string, string) domain.RunRecord {
|
||||
atomic.AddInt32(&ran, 1)
|
||||
select {
|
||||
case runStarted <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
return domain.RunRecord{}
|
||||
}
|
||||
|
||||
// RunDue must not start any job while paused.
|
||||
// RunDue must not start any job while paused: the scheduler stays paused after
|
||||
// a restart that rebuilt the service from a paused store.
|
||||
svc2.RunDue(time.Now().Add(2 * time.Minute))
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
if atomic.LoadInt32(&ran) != 0 {
|
||||
t.Error("RunDue ran a job on a service rebuilt from a paused store")
|
||||
}
|
||||
|
||||
// RunNow must be refused.
|
||||
if err := svc2.RunNow(1); err == nil {
|
||||
t.Error("RunNow should be refused on a service rebuilt from a paused store")
|
||||
// A manual RunNow is still allowed while paused — pause only stops the
|
||||
// scheduler, not the user's explicit action.
|
||||
if err := svc2.RunNow(1); err != nil {
|
||||
t.Errorf("RunNow should be allowed while paused: %v", err)
|
||||
}
|
||||
if atomic.LoadInt32(&ran) != 0 {
|
||||
t.Error("runner was invoked despite global pause")
|
||||
select {
|
||||
case <-runStarted:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Error("manual run was not started on a service rebuilt from a paused store")
|
||||
}
|
||||
}
|
||||
|
||||
+3
-6
@@ -11,8 +11,9 @@ import (
|
||||
"gitea.mixdep.ru/mix/gosentry/src/runner"
|
||||
)
|
||||
|
||||
// RunNow starts a manual run of a job. It refuses to run while globally paused —
|
||||
// the pause is an emergency stop for all execution — and will not start a job
|
||||
// RunNow starts a manual run of a job. Global pause stops only the scheduler's
|
||||
// automatic runs (see RunDue), so a manual "Run now" is allowed even while
|
||||
// paused — it is the user's explicit, one-off action. It will not start a job
|
||||
// that is already running. In sequential execution mode it also refuses while
|
||||
// any other job is running, so a manual run never breaks the one-at-a-time
|
||||
// guarantee. The run itself happens on a background goroutine that records the
|
||||
@@ -21,10 +22,6 @@ import (
|
||||
// "Running" status), not the run's own outcome.
|
||||
func (s *Service) RunNow(id int) error {
|
||||
s.mu.Lock()
|
||||
if s.paused {
|
||||
s.mu.Unlock()
|
||||
return errors.New("scheduler is paused")
|
||||
}
|
||||
job := s.findByIDLocked(id)
|
||||
if job == nil {
|
||||
s.mu.Unlock()
|
||||
|
||||
+1
-1
@@ -3,4 +3,4 @@ package app
|
||||
// Version is the application version shown in the GUI and used by build
|
||||
// scripts in artifact names. It is a var rather than a const so release builds
|
||||
// can override it with Go ldflags when CI tags a build.
|
||||
var Version = "0.10.2"
|
||||
var Version = "0.11.0"
|
||||
|
||||
+11
-9
@@ -28,6 +28,12 @@ const maxJobActivityRows = 3
|
||||
// padding, tightening the block so it fits comfortably on 720p screens.
|
||||
const detailRowSpacing float32 = -8
|
||||
|
||||
// jobRowSpacing is the (negative) gap between the name, metadata, and status
|
||||
// lines within each job list row. Like the details panel, it overlaps the
|
||||
// labels' built-in vertical padding so each row reads as one compact block and
|
||||
// more jobs are visible without scrolling.
|
||||
const jobRowSpacing float32 = -8
|
||||
|
||||
// newJobsView builds the Jobs tab: list sidebar, details panel, and toolbar.
|
||||
// It returns the assembled panel and a refresh function the caller invokes
|
||||
// whenever the service state may have changed (e.g., from the event subscriber
|
||||
@@ -97,7 +103,7 @@ func newJobsView(w fyne.Window, svc *app.Service) (fyne.CanvasObject, func()) {
|
||||
name := widget.NewLabelWithStyle("Job name", fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
|
||||
meta := widget.NewLabel("schedule")
|
||||
status := widget.NewLabel("status")
|
||||
return container.NewVBox(name, meta, status)
|
||||
return container.New(compactVBoxLayout{spacing: jobRowSpacing}, name, meta, status)
|
||||
},
|
||||
func(id widget.ListItemID, item fyne.CanvasObject) {
|
||||
row := item.(*fyne.Container)
|
||||
@@ -185,14 +191,10 @@ func newJobsView(w fyne.Window, svc *app.Service) (fyne.CanvasObject, func()) {
|
||||
if selected < 0 || selected >= len(jobs) {
|
||||
return
|
||||
}
|
||||
if schedulerPaused {
|
||||
// The global pause is treated as an emergency stop for all execution,
|
||||
// including manual "Run now", so the user has one reliable switch.
|
||||
dialog.ShowInformation("Scheduler paused", "Global pause is active. Resume the scheduler before running jobs.", w)
|
||||
return
|
||||
}
|
||||
// RunNow refuses an already-running job (it returns an error); the UI has
|
||||
// always ignored that case silently, so the run simply does not start.
|
||||
// A manual run is allowed even while the scheduler is paused: pause only
|
||||
// stops automatic scheduled runs, not the user's explicit "Run now".
|
||||
// RunNow still refuses an already-running job (it returns an error); the UI
|
||||
// has always ignored that case silently, so the run simply does not start.
|
||||
if err := svc.RunNow(jobs[selected].ID); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
+37
-11
@@ -87,6 +87,10 @@ func (d *detailsPanel) update(j job, rt *domain.JobRuntime, globalOverlapPolicy
|
||||
d.stats.SetText(app.DisplayStats(rt))
|
||||
d.commandOutput.SetText(rt.Output)
|
||||
d.selectedLogs = lastJobLogs(rt.Logs)
|
||||
// The activity list renders d.selectedLogs but, unlike the labels above whose
|
||||
// SetText refreshes them, only its backing slice changed. Refresh it here so
|
||||
// switching jobs immediately redraws the panel instead of keeping stale rows.
|
||||
d.logs.Refresh()
|
||||
}
|
||||
|
||||
func (d *detailsPanel) clear() {
|
||||
@@ -103,6 +107,7 @@ func (d *detailsPanel) clear() {
|
||||
d.stats.SetText("")
|
||||
d.commandOutput.SetText("")
|
||||
d.selectedLogs = nil
|
||||
d.logs.Refresh()
|
||||
}
|
||||
|
||||
// container assembles the details pane layout: metadata rows pin to the top,
|
||||
@@ -111,12 +116,13 @@ func (d *detailsPanel) container() fyne.CanvasObject {
|
||||
// Metadata is laid out in two columns so the block stays half as tall,
|
||||
// keeping the details pane usable on 720p screens where a single column of
|
||||
// ten rows pushes the minimum window height past the available space.
|
||||
capW := detailCaptionWidth()
|
||||
rows := container.New(compactVBoxLayout{spacing: detailRowSpacing},
|
||||
detailRowPair("Folder", d.folder, "Schedule", d.schedule),
|
||||
detailRowPair("Command", d.command, "Arguments", d.arguments),
|
||||
detailRowPair("Run mode", d.runMode, "Overlap policy", d.overlapPolicy),
|
||||
detailRowPair("Last run", d.lastRun, "Next run", d.nextRun),
|
||||
detailRowPair("State", d.state, "Statistics", d.stats),
|
||||
detailRowPair(capW, "Folder", d.folder, "Schedule", d.schedule),
|
||||
detailRowPair(capW, "Command", d.command, "Arguments", d.arguments),
|
||||
detailRowPair(capW, "Run mode", d.runMode, "Overlap policy", d.overlapPolicy),
|
||||
detailRowPair(capW, "Last run", d.lastRun, "Next run", d.nextRun),
|
||||
detailRowPair(capW, "State", d.state, "Statistics", d.stats),
|
||||
)
|
||||
top := container.NewVBox(
|
||||
d.title,
|
||||
@@ -147,16 +153,36 @@ func activityRowsHeight(rows int) float32 {
|
||||
return (itemHeight+padding)*float32(rows) - padding + 1
|
||||
}
|
||||
|
||||
// detailRowPair places two label/value pairs side by side, producing the
|
||||
// four-column caption|value|caption|value rows the compact metadata grid uses.
|
||||
func detailRowPair(l1 string, v1 fyne.CanvasObject, l2 string, v2 fyne.CanvasObject) fyne.CanvasObject {
|
||||
return container.NewGridWithColumns(2, detailRow(l1, v1), detailRow(l2, v2))
|
||||
// detailCaptionWidth returns the width reserved for every metadata caption,
|
||||
// derived from the widest caption label so the value columns all start at the
|
||||
// same x and no caption truncates. Measuring a real label keeps it DPI- and
|
||||
// theme-aware instead of relying on a hand-tuned constant.
|
||||
func detailCaptionWidth() float32 {
|
||||
captions := []string{
|
||||
"Folder", "Schedule", "Command", "Arguments", "Run mode",
|
||||
"Overlap policy", "Last run", "Next run", "State", "Statistics",
|
||||
}
|
||||
var width float32
|
||||
for _, c := range captions {
|
||||
if w := widget.NewLabelWithStyle(c, fyne.TextAlignLeading, fyne.TextStyle{Bold: true}).MinSize().Width; w > width {
|
||||
width = w
|
||||
}
|
||||
}
|
||||
return width
|
||||
}
|
||||
|
||||
func detailRow(label string, value fyne.CanvasObject) fyne.CanvasObject {
|
||||
// detailRowPair places two label/value pairs side by side, producing the
|
||||
// four-column caption|value|caption|value rows the compact metadata grid uses.
|
||||
func detailRowPair(captionWidth float32, l1 string, v1 fyne.CanvasObject, l2 string, v2 fyne.CanvasObject) fyne.CanvasObject {
|
||||
return container.NewGridWithColumns(2, detailRow(captionWidth, l1, v1), detailRow(captionWidth, l2, v2))
|
||||
}
|
||||
|
||||
func detailRow(captionWidth float32, label string, value fyne.CanvasObject) fyne.CanvasObject {
|
||||
caption := widget.NewLabelWithStyle(label, fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
|
||||
caption.Wrapping = fyne.TextTruncate
|
||||
return container.NewGridWithColumns(2, caption, value)
|
||||
// A fixed caption width (rather than an even split) means widening the window
|
||||
// feeds the extra space to the value, not the short caption.
|
||||
return container.New(captionValueLayout{captionWidth: captionWidth}, caption, value)
|
||||
}
|
||||
|
||||
func newJobDetailLabel(text string) *widget.Label {
|
||||
|
||||
@@ -2,6 +2,7 @@ package ui
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
)
|
||||
|
||||
type minWidthLayout struct {
|
||||
@@ -107,3 +108,45 @@ func (l fixedHeightLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) {
|
||||
object.Resize(fyne.NewSize(size.Width, l.height))
|
||||
}
|
||||
}
|
||||
|
||||
// captionValueLayout places a fixed-width caption on the left and lets the value
|
||||
// fill the remaining width, separated by one theme padding. Capping the caption
|
||||
// stops it from growing with the window (as an even two-column grid would), so
|
||||
// the extra space a wider window provides goes entirely to the value column. It
|
||||
// expects exactly two children: caption first, value second.
|
||||
type captionValueLayout struct {
|
||||
captionWidth float32
|
||||
}
|
||||
|
||||
func (l captionValueLayout) MinSize(objects []fyne.CanvasObject) fyne.Size {
|
||||
if len(objects) != 2 {
|
||||
return fyne.Size{}
|
||||
}
|
||||
captionMin, valueMin := objects[0].MinSize(), objects[1].MinSize()
|
||||
height := captionMin.Height
|
||||
if valueMin.Height > height {
|
||||
height = valueMin.Height
|
||||
}
|
||||
return fyne.NewSize(l.captionWidth+theme.Padding()+valueMin.Width, height)
|
||||
}
|
||||
|
||||
func (l captionValueLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) {
|
||||
if len(objects) != 2 {
|
||||
return
|
||||
}
|
||||
caption, value := objects[0], objects[1]
|
||||
captionWidth := l.captionWidth
|
||||
if captionWidth > size.Width {
|
||||
captionWidth = size.Width
|
||||
}
|
||||
caption.Move(fyne.NewPos(0, 0))
|
||||
caption.Resize(fyne.NewSize(captionWidth, size.Height))
|
||||
|
||||
valueX := captionWidth + theme.Padding()
|
||||
valueWidth := size.Width - valueX
|
||||
if valueWidth < 0 {
|
||||
valueWidth = 0
|
||||
}
|
||||
value.Move(fyne.NewPos(valueX, 0))
|
||||
value.Resize(fyne.NewSize(valueWidth, size.Height))
|
||||
}
|
||||
|
||||
+77
-22
@@ -17,9 +17,11 @@ import (
|
||||
"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 settingsStatusWidth float32 = 280
|
||||
const projectRepositoryURL = "https://gitea.mixdep.ru/mix/gosentry"
|
||||
|
||||
// settingsRowSpacing is the (negative) gap between rows of the settings form,
|
||||
@@ -29,6 +31,10 @@ const settingsRowSpacing float32 = -6
|
||||
|
||||
func settingsView(w fyne.Window, svc *app.Service) fyne.CanvasObject {
|
||||
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.SetChecked(store.Config.StartOnLogin)
|
||||
autostartStatus := widget.NewLabel("")
|
||||
@@ -43,39 +49,52 @@ func settingsView(w fyne.Window, svc *app.Service) fyne.CanvasObject {
|
||||
startOnLogin.OnChanged = func(bool) {
|
||||
if startOnLogin.Checked != store.Config.StartOnLogin {
|
||||
autostartStatus.SetText("Pending: save settings to apply")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
refreshAutostartStatus()
|
||||
}
|
||||
updateSaveState()
|
||||
}
|
||||
refreshAutostartStatus()
|
||||
minimizeToTray := widget.NewCheck("Keep running in the system tray", nil)
|
||||
minimizeToTray.SetChecked(store.Config.KeepRunningInTray)
|
||||
minimizeToTray.OnChanged = func(bool) { updateSaveState() }
|
||||
notifications := widget.NewCheck("Show desktop notifications for failed jobs", nil)
|
||||
notifications.SetChecked(store.Config.NotifyOnFailure)
|
||||
notifications.OnChanged = func(bool) { updateSaveState() }
|
||||
executionModeSelect := widget.NewSelect(
|
||||
[]string{string(domain.ExecutionModeParallel), string(domain.ExecutionModeSequential)},
|
||||
nil,
|
||||
)
|
||||
executionModeSelect.SetSelected(string(store.Config.ExecutionMode))
|
||||
executionModeSelect.OnChanged = func(string) { updateSaveState() }
|
||||
overlapPolicySelect := widget.NewSelect(
|
||||
[]string{string(domain.OverlapPolicySkip), string(domain.OverlapPolicyQueue)},
|
||||
nil,
|
||||
)
|
||||
overlapPolicySelect.SetSelected(string(store.Config.OverlapPolicy))
|
||||
overlapPolicySelect.OnChanged = func(string) { updateSaveState() }
|
||||
jobsDir := widget.NewEntry()
|
||||
jobsDir.SetText(store.Config.JobsDir)
|
||||
jobsDir.OnChanged = func(string) { updateSaveState() }
|
||||
jobsDirBrowse := widget.NewButtonWithIcon("Browse", theme.FolderOpenIcon(), func() {
|
||||
chooseFolder(w, jobsDir)
|
||||
})
|
||||
logsDir := widget.NewEntry()
|
||||
logsDir.SetText(store.Config.LogsDir)
|
||||
logsDir.OnChanged = func(string) { updateSaveState() }
|
||||
logsDirBrowse := widget.NewButtonWithIcon("Browse", theme.FolderOpenIcon(), func() {
|
||||
chooseFolder(w, logsDir)
|
||||
})
|
||||
maxLogFiles := widget.NewEntry()
|
||||
maxLogFiles.SetText(strconv.Itoa(store.Config.MaxLogFiles))
|
||||
maxLogFiles.OnChanged = func(string) { updateSaveState() }
|
||||
maxLogAgeDays := widget.NewEntry()
|
||||
maxLogAgeDays.SetText(strconv.Itoa(store.Config.MaxLogAgeDays))
|
||||
maxLogAgeDays.OnChanged = func(string) { updateSaveState() }
|
||||
// Autostart status sits on its own row beneath the checkbox (rather than
|
||||
// beside it) so the Application section fits within a half-width column.
|
||||
// Truncating keeps a long status message from forcing the column wider.
|
||||
autostartStatus.Wrapping = fyne.TextTruncate
|
||||
settingsStatus := widget.NewLabel("")
|
||||
|
||||
saveSettings := widget.NewButtonWithIcon("Save settings", theme.DocumentSaveIcon(), func() {
|
||||
@@ -121,27 +140,57 @@ func settingsView(w fyne.Window, svc *app.Service) fyne.CanvasObject {
|
||||
}
|
||||
refreshAutostartStatus()
|
||||
settingsStatus.SetText("Saved")
|
||||
// The form now matches the persisted config, so disable Save again.
|
||||
updateSaveState()
|
||||
})
|
||||
|
||||
// The form is grouped into sections in an outer VBox. The outer box keeps the
|
||||
// theme's normal padding, so the separators and the editable Storage fields
|
||||
// get proper breathing room; only the label-only sections are condensed with
|
||||
// the tight settingsSection spacing. 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.
|
||||
return container.NewVScroll(container.NewPadded(container.NewVBox(
|
||||
// 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
|
||||
// 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",
|
||||
settingsRowWithStatus("Autostart", startOnLogin, autostartStatus),
|
||||
settingsRow("Autostart", container.New(minWidthLayout{width: settingsControlWidth}, 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("", autostartStatus),
|
||||
settingsRow("Tray", container.New(minWidthLayout{width: settingsControlWidth}, minimizeToTray)),
|
||||
settingsRow("Notifications", container.New(minWidthLayout{width: settingsControlWidth}, notifications)),
|
||||
),
|
||||
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("Default overlap policy", container.New(minWidthLayout{width: settingsControlWidth}, overlapPolicySelect)),
|
||||
),
|
||||
widget.NewSeparator(),
|
||||
)
|
||||
rightColumn := container.NewVBox(
|
||||
// Storage holds editable entry fields. It uses the default VBox spacing
|
||||
// (not the condensed section layout) so the entry boxes keep a visible
|
||||
// gap between them instead of merging into one block.
|
||||
@@ -153,8 +202,6 @@ func settingsView(w fyne.Window, svc *app.Service) fyne.CanvasObject {
|
||||
settingsRow("Max log files", maxLogFiles),
|
||||
settingsRow("Max log age days", maxLogAgeDays),
|
||||
),
|
||||
saveSettings,
|
||||
settingsStatus,
|
||||
widget.NewSeparator(),
|
||||
settingsSection("About",
|
||||
settingsRow("GoSentry", widget.NewLabel(app.Version)),
|
||||
@@ -162,6 +209,19 @@ func settingsView(w fyne.Window, svc *app.Service) fyne.CanvasObject {
|
||||
settingsRow("Fyne", widget.NewLabel(fyneVersion())),
|
||||
settingsRow("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.
|
||||
return container.NewVScroll(container.NewPadded(container.NewVBox(
|
||||
container.NewGridWithColumns(2, leftColumn, rightColumn),
|
||||
widget.NewSeparator(),
|
||||
// Save button and its status share one row so an empty status (the common
|
||||
// case) does not leave a blank line above the separator. The status appears
|
||||
// beside the button once a save reports a result.
|
||||
container.NewHBox(saveSettings, settingsStatus),
|
||||
)))
|
||||
}
|
||||
|
||||
@@ -234,8 +294,3 @@ func settingsRow(label string, value fyne.CanvasObject) fyne.CanvasObject {
|
||||
return container.NewBorder(nil, nil, captionBox, nil, value)
|
||||
}
|
||||
|
||||
func settingsRowWithStatus(label string, value fyne.CanvasObject, status fyne.CanvasObject) fyne.CanvasObject {
|
||||
valueBox := container.New(minWidthLayout{width: settingsControlWidth}, value)
|
||||
statusBox := container.New(minWidthLayout{width: settingsStatusWidth}, status)
|
||||
return settingsRow(label, container.NewBorder(nil, nil, valueBox, nil, statusBox))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user