From 167f7935741e1c11c9f416805789771820fa32ad Mon Sep 17 00:00:00 2001 From: Cedric Lewe <0skillallluck@pm.me> Date: Fri, 8 May 2026 19:37:16 +0200 Subject: [PATCH] Add native macOS menu bar --- app/dialogs/shortcuts/shortcuts.go | 20 +++++--- app/dialogs/shortcuts/shortcuts_darwin.go | 9 ++++ app/dialogs/shortcuts/shortcuts_other.go | 13 ++++++ app/windows/main.go | 1 + app/windows/main_actions.go | 32 ++++++++++++- app/windows/main_content.go | 33 +++---------- app/windows/menubar_darwin.go | 56 +++++++++++++++++++++++ app/windows/menubar_other.go | 39 ++++++++++++++++ main.go | 3 ++ 9 files changed, 171 insertions(+), 35 deletions(-) create mode 100644 app/dialogs/shortcuts/shortcuts_darwin.go create mode 100644 app/dialogs/shortcuts/shortcuts_other.go create mode 100644 app/windows/menubar_darwin.go create mode 100644 app/windows/menubar_other.go diff --git a/app/dialogs/shortcuts/shortcuts.go b/app/dialogs/shortcuts/shortcuts.go index 2141a13..0b5a070 100644 --- a/app/dialogs/shortcuts/shortcuts.go +++ b/app/dialogs/shortcuts/shortcuts.go @@ -8,14 +8,20 @@ import ( // NewShortcutsDialog creates and returns a new keyboard shortcuts dialog. func NewShortcutsDialog() schwifty.ShortcutsDialog { + basic := []any{ + ShortcutsItemFromAction(gettext.Get("Close"), "win.close"), + ShortcutsItemFromAction(gettext.Get("Quit"), "app.quit"), + } + if item := mainMenuShortcut(); item != nil { + basic = append(basic, item) + } + basic = append(basic, + ShortcutsItemFromAction(gettext.Get("Keyboard Shortcuts"), "app.shortcuts"), + ShortcutsItemFromAction(gettext.Get("Preferences"), "app.preferences"), + ) + return ShortcutsDialog( - ShortcutsSection( - ShortcutsItemFromAction(gettext.Get("Close"), "win.close"), - ShortcutsItemFromAction(gettext.Get("Quit"), "app.quit"), - ShortcutsItemFromAction(gettext.Get("Main Menu"), "win.main-menu"), - ShortcutsItemFromAction(gettext.Get("Keyboard Shortcuts"), "app.shortcuts"), - ShortcutsItemFromAction(gettext.Get("Preferences"), "app.preferences"), - ).Title(gettext.Get("Basic Shortcuts")), + ShortcutsSection(basic...).Title(gettext.Get("Basic Shortcuts")), ShortcutsSection( ShortcutsItemFromAction(gettext.Get("Back"), "win.navigate-back"), ShortcutsItemFromAction(gettext.Get("Search"), "win.search"), diff --git a/app/dialogs/shortcuts/shortcuts_darwin.go b/app/dialogs/shortcuts/shortcuts_darwin.go new file mode 100644 index 0000000..16fea1e --- /dev/null +++ b/app/dialogs/shortcuts/shortcuts_darwin.go @@ -0,0 +1,9 @@ +//go:build darwin + +package shortcuts + +import adwbindings "codeberg.org/dergs/tonearm/pkg/schwifty/bindings/adw" + +// mainMenuShortcut returns nil on macOS — the in-app hamburger menu (and its +// F10 shortcut) doesn't exist there; the menu lives in NSMainMenu instead. +func mainMenuShortcut() adwbindings.ShortcutsItem { return nil } diff --git a/app/dialogs/shortcuts/shortcuts_other.go b/app/dialogs/shortcuts/shortcuts_other.go new file mode 100644 index 0000000..9163dfc --- /dev/null +++ b/app/dialogs/shortcuts/shortcuts_other.go @@ -0,0 +1,13 @@ +//go:build !darwin + +package shortcuts + +import ( + adwbindings "codeberg.org/dergs/tonearm/pkg/schwifty/bindings/adw" + . "codeberg.org/dergs/tonearm/pkg/schwifty/syntax" + "github.com/0skillallluck/scanline/internal/gettext" +) + +func mainMenuShortcut() adwbindings.ShortcutsItem { + return ShortcutsItemFromAction(gettext.Get("Main Menu"), "win.main-menu") +} diff --git a/app/windows/main.go b/app/windows/main.go index cca1de3..a9b4e1a 100644 --- a/app/windows/main.go +++ b/app/windows/main.go @@ -72,6 +72,7 @@ func NewWindow(app *adw.Application, appCtx *appctx.AppContext) *Window { } window.installAppActions() + window.installNativeMenubar() if appCtx.Manager.HasAccounts() { window.showMainContent() diff --git a/app/windows/main_actions.go b/app/windows/main_actions.go index 5c07a4f..5484868 100644 --- a/app/windows/main_actions.go +++ b/app/windows/main_actions.go @@ -54,6 +54,37 @@ func (w *Window) installAppActions() { })) w.AddAction(closeAction) w.GetApplication().SetAccelsForAction("win.close", []string{accels.PrimaryMod + "w"}) + + minimizeAction := gio.NewSimpleAction("minimize", nil) + minimizeAction.ConnectActivate(new(func(action gio.SimpleAction, parameter uintptr) { + w.Minimize() + })) + w.AddAction(minimizeAction) + w.GetApplication().SetAccelsForAction("win.minimize", []string{accels.PrimaryMod + "m"}) + + zoomAction := gio.NewSimpleAction("zoom", nil) + zoomAction.ConnectActivate(new(func(action gio.SimpleAction, parameter uintptr) { + if w.IsMaximized() { + w.Unmaximize() + } else { + w.Maximize() + } + })) + w.AddAction(zoomAction) + + bringAllAction := gio.NewSimpleAction("bring-all-to-front", nil) + bringAllAction.ConnectActivate(new(func(action gio.SimpleAction, parameter uintptr) { + for node := w.GetApplication().GetWindows(); node != nil; node = node.Next { + gtk.WindowNewFromInternalPtr(node.Data).Present() + } + })) + w.GetApplication().Application.AddAction(bringAllAction) + + // Bind accels for window actions referenced by the macOS menubar so the + // shortcut labels render — installWindowActions runs after the menubar + // is built, which is too late on macOS where NSMenu bakes in accels at + // conversion time. + w.GetApplication().SetAccelsForAction("win.search", []string{accels.PrimaryMod + "f"}) } // installWindowActions installs actions that only make sense when main content is shown: @@ -81,7 +112,6 @@ func (w *Window) installWindowActions() { } })) w.AddAction(searchAction) - w.GetApplication().SetAccelsForAction("win.search", []string{accels.PrimaryMod + "f"}) routeMovieAction := gio.NewSimpleAction("route.movie", glib.NewVariantType("s")) routeMovieAction.ConnectActivate(new(func(action gio.SimpleAction, parameter uintptr) { diff --git a/app/windows/main_content.go b/app/windows/main_content.go index 8d7ee08..3a19e4d 100644 --- a/app/windows/main_content.go +++ b/app/windows/main_content.go @@ -8,7 +8,6 @@ import ( "codeberg.org/dergs/tonearm/pkg/schwifty/state" . "codeberg.org/dergs/tonearm/pkg/schwifty/syntax" "codeberg.org/puregotk/puregotk/v4/adw" - "codeberg.org/puregotk/puregotk/v4/gio" "codeberg.org/puregotk/puregotk/v4/gtk" "github.com/0skillallluck/scanline/app/components" "github.com/0skillallluck/scanline/app/preference" @@ -41,15 +40,6 @@ func updateDecorationLayout() { } } -func (w *Window) buildMainMenu() *gio.Menu { - mainMenu := gio.NewMenu() - mainMenu.Append(gettext.Get("Select Sources"), "win.select-sources") - mainMenu.Append(gettext.Get("Preferences"), "app.preferences") - mainMenu.Append(gettext.Get("Keyboard Shortcuts"), "app.shortcuts") - mainMenu.Append(gettext.Get("About Scanline"), "app.about") - return mainMenu -} - func iconForSectionType(sectionType string) string { switch sectionType { case "movie": @@ -181,8 +171,6 @@ func (w *Window) buildContentHeader() *gtk.Widget { }) }) - mainMenu := w.buildMainMenu() - gtkSettings := gtk.SettingsGetDefault() gtkSettings.ConnectSignal("notify::gtk-decoration-layout", new(func() { updateDecorationLayout() @@ -191,7 +179,7 @@ func (w *Window) buildContentHeader() *gtk.Widget { hasSources := len(mgr.EnabledSources()) > 0 - headerbar := HeaderBar(). + headerbarBuilder := HeaderBar(). BindDecorationLayout(decorationLayoutState). CenteringPolicy(adw.CenteringPolicyStrictValue). PackStart( @@ -251,20 +239,11 @@ func (w *Window) buildContentHeader() *gtk.Widget { })) }), ). - TitleWidget(defaultToolbar). - PackEnd( - MenuButton(). - IconName("open-menu-symbolic"). - MenuModel(&mainMenu.MenuModel). - TooltipText(gettext.Get("Main Menu")).ConnectConstruct(func(mb *gtk.MenuButton) { - menuAction := gio.NewSimpleAction("main-menu", nil) - menuAction.ConnectActivate(new(func(action gio.SimpleAction, parameter uintptr) { - mb.Popup() - })) - w.AddAction(menuAction) - w.GetApplication().SetAccelsForAction("win.main-menu", []string{"F10"}) - }), - ). + TitleWidget(defaultToolbar) + + headerbarBuilder = w.packMainMenuButton(headerbarBuilder) + + headerbar := headerbarBuilder. ConnectDestroy(func(w gtk.Widget) { gtkSettings.Unref() })() diff --git a/app/windows/menubar_darwin.go b/app/windows/menubar_darwin.go new file mode 100644 index 0000000..e2bf8b8 --- /dev/null +++ b/app/windows/menubar_darwin.go @@ -0,0 +1,56 @@ +//go:build darwin + +package windows + +import ( + adwbindings "codeberg.org/dergs/tonearm/pkg/schwifty/bindings/adw" + "codeberg.org/puregotk/puregotk/v4/gio" + "codeberg.org/puregotk/puregotk/v4/glib" + "github.com/0skillallluck/scanline/internal/gettext" +) + +// installNativeMenubar wires the application's menu model into NSMainMenu via +// gtk_application_set_menubar. GTK4's macOS backend auto-generates the +// application menu (About / Settings / Hide / Quit) from the standard Cocoa +// items, so we only contribute File / Edit / Help here — adding a "Scanline" +// submenu would render as a duplicate after the auto-generated app menu. +func (w *Window) installNativeMenubar() { + menubar := gio.NewMenu() + + fileMenu := gio.NewMenu() + fileMenu.Append(gettext.Get("Select Sources…"), "win.select-sources") + appendSection(fileMenu, gettext.Get("Close Window"), "win.close") + menubar.AppendSubmenu(gettext.Get("File"), &fileMenu.MenuModel) + + editMenu := gio.NewMenu() + editMenu.Append(gettext.Get("Find"), "win.search") + menubar.AppendSubmenu(gettext.Get("Edit"), &editMenu.MenuModel) + + windowMenu := gio.NewMenu() + windowMenu.Append(gettext.Get("Minimize"), "win.minimize") + windowMenu.Append(gettext.Get("Zoom"), "win.zoom") + appendSection(windowMenu, gettext.Get("Bring All to Front"), "app.bring-all-to-front") + appendSection(windowMenu, gettext.Get("Close Window"), "win.close") + // gtk-macos-special=window-submenu opts the submenu into AppKit's native + // Window menu management (open-window list, standard window items). + windowItem := gio.NewMenuItemSubmenu(gettext.Get("Window"), &windowMenu.MenuModel) + windowItem.SetAttributeValue("gtk-macos-special", glib.NewVariantString("window-submenu")) + menubar.AppendItem(windowItem) + + helpMenu := gio.NewMenu() + helpMenu.Append(gettext.Get("About Scanline"), "app.about") + helpMenu.Append(gettext.Get("Keyboard Shortcuts"), "app.shortcuts") + menubar.AppendSubmenu(gettext.Get("Help"), &helpMenu.MenuModel) + + w.GetApplication().SetMenubar(&menubar.MenuModel) +} + +func (w *Window) packMainMenuButton(b adwbindings.HeaderBar) adwbindings.HeaderBar { + return b +} + +func appendSection(parent *gio.Menu, label, action string) { + section := gio.NewMenu() + section.Append(label, action) + parent.AppendSection("", §ion.MenuModel) +} diff --git a/app/windows/menubar_other.go b/app/windows/menubar_other.go new file mode 100644 index 0000000..7176996 --- /dev/null +++ b/app/windows/menubar_other.go @@ -0,0 +1,39 @@ +//go:build !darwin + +package windows + +import ( + adwbindings "codeberg.org/dergs/tonearm/pkg/schwifty/bindings/adw" + . "codeberg.org/dergs/tonearm/pkg/schwifty/syntax" + "codeberg.org/puregotk/puregotk/v4/gio" + "codeberg.org/puregotk/puregotk/v4/gtk" + "github.com/0skillallluck/scanline/internal/gettext" +) + +func (w *Window) installNativeMenubar() {} + +func (w *Window) buildMainMenu() *gio.Menu { + mainMenu := gio.NewMenu() + mainMenu.Append(gettext.Get("Select Sources"), "win.select-sources") + mainMenu.Append(gettext.Get("Preferences"), "app.preferences") + mainMenu.Append(gettext.Get("Keyboard Shortcuts"), "app.shortcuts") + mainMenu.Append(gettext.Get("About Scanline"), "app.about") + return mainMenu +} + +func (w *Window) packMainMenuButton(b adwbindings.HeaderBar) adwbindings.HeaderBar { + mainMenu := w.buildMainMenu() + return b.PackEnd( + MenuButton(). + IconName("open-menu-symbolic"). + MenuModel(&mainMenu.MenuModel). + TooltipText(gettext.Get("Main Menu")).ConnectConstruct(func(mb *gtk.MenuButton) { + menuAction := gio.NewSimpleAction("main-menu", nil) + menuAction.ConnectActivate(new(func(action gio.SimpleAction, parameter uintptr) { + mb.Popup() + })) + w.AddAction(menuAction) + w.GetApplication().SetAccelsForAction("win.main-menu", []string{"F10"}) + }), + ) +} diff --git a/main.go b/main.go index 7458db4..0ff933f 100644 --- a/main.go +++ b/main.go @@ -49,6 +49,9 @@ func init() { } func main() { + glib.SetApplicationName("Scanline") + glib.SetPrgname("Scanline") + application := adw.NewApplication("dev.skillless.Scanline", gio.GApplicationDefaultFlagsValue) defer application.Unref() application.ConnectActivate(new(app.OnActivate(application)))