diff --git a/internal/ui/app.go b/internal/ui/app.go
index 3117ed2..d835b18 100644
--- a/internal/ui/app.go
+++ b/internal/ui/app.go
@@ -458,9 +458,9 @@ func (a *App) onAppActivate(ctx context.Context) {
gutil.Connect(a.win.MainWindow, a.win.MainWindow.ConnectCloseRequest, func(gtk.Window) bool {
win := a.win
a.win = nil
- // The window is destroyed after this returns; release the
- // window's Go-side references once the main loop is idle.
- gutil.IdleAdd(win.Destroy)
+ // The window is destroyed after this returns; drop ours once
+ // the main loop is idle.
+ gutil.IdleAdd(win.Widget().Unref)
return false
})
diff --git a/internal/ui/builder_test.go b/internal/ui/builder_test.go
index 72f9a1f..5b7abd5 100644
--- a/internal/ui/builder_test.go
+++ b/internal/ui/builder_test.go
@@ -3,19 +3,20 @@ package ui
import (
"testing"
- "deedles.dev/trayscale/internal/gutil"
+ "codeberg.org/puregotk/puregotk/v4/adw"
+ "codeberg.org/puregotk/puregotk/v4/gio"
"deedles.dev/trayscale/internal/tsutil"
"tailscale.com/ipn"
"tailscale.com/tailcfg"
)
-// TestFillFromBuilder builds every .ui file into its Go struct so that
-// a type missing from gutil's registry fails here instead of panicking
-// at runtime.
-func TestFillFromBuilder(t *testing.T) {
+// TestTemplates builds every template class through its constructor,
+// so a template child whose type doesn't match its field fails here
+// instead of panicking at runtime.
+func TestTemplates(t *testing.T) {
requireGTK(t)
- fill := func(t *testing.T, build func()) {
+ build := func(t *testing.T, build func()) {
t.Helper()
defer func() {
if r := recover(); r != nil {
@@ -25,29 +26,33 @@ func TestFillFromBuilder(t *testing.T) {
build()
}
+ running := &tsutil.IPNStatus{State: ipn.Running, Prefs: ipn.NewPrefs().View()}
+
t.Run("MainWindow", func(t *testing.T) {
- var win MainWindow
- fill(t, func() { gutil.FillFromUI(&win, menuXML, mainWindowXML); gutil.UnrefFields(&win) })
+ app := &App{app: adw.NewApplication("dev.deedles.Trayscale.test.templates", gio.GApplicationNonUniqueValue)}
+ defer app.app.Unref()
+ build(t, func() {
+ win := NewMainWindow(app)
+ win.MainWindow.Destroy()
+ win.Widget().Unref()
+ })
})
t.Run("OfflinePage", func(t *testing.T) {
- fill(t, func() { offlinePageClass.New().Destroy() })
+ build(t, func() { offlinePageClass.New().Widget().Unref() })
})
t.Run("PeerPage", func(t *testing.T) {
peer := layoutPeer(t, "peer", "peer", true, false, false)
status := layoutStatus([]tailcfg.NodeView{peer})
status.Prefs = ipn.NewPrefs().View()
- fill(t, func() { NewPeerPage(&App{}, status, peer).Destroy() })
+ build(t, func() { NewPeerPage(&App{}, status, peer).Widget().Unref() })
})
t.Run("SelfPage", func(t *testing.T) {
- status := &tsutil.IPNStatus{State: ipn.Running, Prefs: ipn.NewPrefs().View()}
- fill(t, func() { NewSelfPage(&App{}, status).Destroy() })
+ build(t, func() { NewSelfPage(&App{}, running).Widget().Unref() })
})
t.Run("MullvadPage", func(t *testing.T) {
- status := &tsutil.IPNStatus{State: ipn.Running, Prefs: ipn.NewPrefs().View()}
- fill(t, func() { NewMullvadPage(&App{}, status).Destroy() })
+ build(t, func() { NewMullvadPage(&App{}, running).Widget().Unref() })
})
t.Run("PreferencesDialog", func(t *testing.T) {
- var dialog PreferencesDialog
- fill(t, func() { gutil.FillFromUI(&dialog, preferencesXML); gutil.UnrefFields(&dialog) })
+ build(t, func() { NewPreferencesDialog().Widget().Unref() })
})
}
diff --git a/internal/ui/leak_test.go b/internal/ui/leak_test.go
index 7fb0644..09ada42 100644
--- a/internal/ui/leak_test.go
+++ b/internal/ui/leak_test.go
@@ -59,7 +59,7 @@ func TestPeerPageLeaks(t *testing.T) {
}
stack.Remove(page.Widget())
vp.Unref()
- page.Destroy()
+ page.Widget().Unref()
}
const warmup, n, objs = 5, 200, 6
@@ -102,7 +102,7 @@ func TestSelfPageLeaks(t *testing.T) {
}
stack.Remove(page.Widget())
vp.Unref()
- page.Destroy()
+ page.Widget().Unref()
}
const warmup, n, objs = 5, 200, 5
@@ -171,7 +171,7 @@ func TestMullvadPageLeaks(t *testing.T) {
stack.Remove(page.Widget())
vp.Unref()
- page.Destroy()
+ page.Widget().Unref()
}
const warmup, n = 5, 100
diff --git a/internal/ui/mainwindow.go b/internal/ui/mainwindow.go
index 7cd5658..5f5ae21 100644
--- a/internal/ui/mainwindow.go
+++ b/internal/ui/mainwindow.go
@@ -15,6 +15,7 @@ import (
"codeberg.org/puregotk/puregotk/v4/gio"
"codeberg.org/puregotk/puregotk/v4/glib"
"codeberg.org/puregotk/puregotk/v4/gtk"
+ "deedles.dev/trayscale/internal/gobj"
"deedles.dev/trayscale/internal/gutil"
"deedles.dev/trayscale/internal/listmodels"
"deedles.dev/trayscale/internal/locale"
@@ -33,10 +34,20 @@ var (
menuXML string
)
+// The main window's template carries menu.ui's menus, which its menu
+// buttons refer to by id.
+var mainWindowClass = gobj.Widget[MainWindow](gobj.WidgetSpec{
+ Name: "TrayscaleMainWindow",
+ Parent: adw.ApplicationWindowGLibType,
+ Template: strings.Replace(mainWindowXML, "", menuXML[strings.Index(menuXML, "