From b3f994a9d2e30fd9f5f3c0c12038d55551b13a6b Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 12 Feb 2025 22:19:03 -0600 Subject: [PATCH] gtk: use builder ui files and popovers for menus --- src/apprt/gtk/App.zig | 112 ++------------- src/apprt/gtk/Builder.zig | 4 +- src/apprt/gtk/Surface.zig | 53 ++----- src/apprt/gtk/Tab.zig | 4 +- src/apprt/gtk/TabView.zig | 4 +- src/apprt/gtk/Window.zig | 101 ++++++++----- src/apprt/gtk/gresource.zig | 5 +- src/apprt/gtk/menu.zig | 135 ++++++++++++++++++ src/apprt/gtk/ui/menu-surface-context_menu.ui | 90 ++++++++++++ src/apprt/gtk/ui/menu-window-titlebar_menu.ui | 93 ++++++++++++ 10 files changed, 420 insertions(+), 181 deletions(-) create mode 100644 src/apprt/gtk/menu.zig create mode 100644 src/apprt/gtk/ui/menu-surface-context_menu.ui create mode 100644 src/apprt/gtk/ui/menu-window-titlebar_menu.ui diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 227c36ec4..2e820a957 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -57,12 +57,6 @@ single_instance: bool, /// The "none" cursor. We use one that is shared across the entire app. cursor_none: ?*c.GdkCursor, -/// The shared application menu. -menu: ?*c.GMenu = null, - -/// The shared context menu. -context_menu: ?*c.GMenu = null, - /// The configuration errors window, if it is currently open. config_errors_window: ?*ConfigErrorsWindow = null, @@ -426,8 +420,6 @@ pub fn terminate(self: *App) void { c.g_object_unref(self.app); if (self.cursor_none) |cursor| c.g_object_unref(cursor); - if (self.menu) |menu| c.g_object_unref(menu); - if (self.context_menu) |context_menu| c.g_object_unref(context_menu); if (self.transient_cgroup_base) |path| self.core_app.alloc.free(path); for (self.custom_css_providers.items) |provider| { @@ -456,7 +448,6 @@ pub fn performAction( }), .toggle_maximize => self.toggleMaximize(target), .toggle_fullscreen => self.toggleFullscreen(target, value), - .new_tab => try self.newTab(target), .close_tab => try self.closeTab(target), .goto_tab => return self.gotoTab(target, value), @@ -990,17 +981,19 @@ fn syncActionAccelerators(self: *App) !void { try self.syncActionAccelerator("app.quit", .{ .quit = {} }); try self.syncActionAccelerator("app.open-config", .{ .open_config = {} }); try self.syncActionAccelerator("app.reload-config", .{ .reload_config = {} }); - try self.syncActionAccelerator("win.toggle_inspector", .{ .inspector = .toggle }); - try self.syncActionAccelerator("win.close", .{ .close_surface = {} }); - try self.syncActionAccelerator("win.new_window", .{ .new_window = {} }); - try self.syncActionAccelerator("win.new_tab", .{ .new_tab = {} }); - try self.syncActionAccelerator("win.split_right", .{ .new_split = .right }); - try self.syncActionAccelerator("win.split_down", .{ .new_split = .down }); - try self.syncActionAccelerator("win.split_left", .{ .new_split = .left }); - try self.syncActionAccelerator("win.split_up", .{ .new_split = .up }); + try self.syncActionAccelerator("win.toggle-inspector", .{ .inspector = .toggle }); + try self.syncActionAccelerator("win.close", .{ .close_window = {} }); + try self.syncActionAccelerator("win.new-window", .{ .new_window = {} }); + try self.syncActionAccelerator("win.new-tab", .{ .new_tab = {} }); + try self.syncActionAccelerator("win.close-tab", .{ .close_tab = {} }); + try self.syncActionAccelerator("win.split-right", .{ .new_split = .right }); + try self.syncActionAccelerator("win.split-down", .{ .new_split = .down }); + try self.syncActionAccelerator("win.split-left", .{ .new_split = .left }); + try self.syncActionAccelerator("win.split-up", .{ .new_split = .up }); try self.syncActionAccelerator("win.copy", .{ .copy_to_clipboard = {} }); try self.syncActionAccelerator("win.paste", .{ .paste_from_clipboard = {} }); try self.syncActionAccelerator("win.reset", .{ .reset = {} }); + try self.syncActionAccelerator("win.clear", .{ .clear_screen = {} }); } fn syncActionAccelerator( @@ -1232,10 +1225,8 @@ pub fn run(self: *App) !void { // and asynchronously request the initial color scheme self.initDbus(); - // Setup our menu items + // Setup our actions self.initActions(); - self.initMenu(); - self.initContextMenu(); // On startup, we want to check for configuration errors right away // so we can show our error window. We also need to setup other initial @@ -1753,87 +1744,6 @@ fn initActions(self: *App) void { } } -/// Initializes and populates the provided GMenu with sections and actions. -/// This function is used to set up the application's menu structure, either for -/// the main menu button or as a context menu when window decorations are disabled. -fn initMenuContent(menu: *c.GMenu) void { - { - const section = c.g_menu_new(); - defer c.g_object_unref(section); - c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section))); - c.g_menu_append(section, "New Window", "win.new_window"); - c.g_menu_append(section, "New Tab", "win.new_tab"); - c.g_menu_append(section, "Close Tab", "win.close_tab"); - c.g_menu_append(section, "Split Right", "win.split_right"); - c.g_menu_append(section, "Split Down", "win.split_down"); - c.g_menu_append(section, "Close Window", "win.close"); - } - - { - const section = c.g_menu_new(); - defer c.g_object_unref(section); - c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section))); - c.g_menu_append(section, "Terminal Inspector", "win.toggle_inspector"); - c.g_menu_append(section, "Open Configuration", "app.open-config"); - c.g_menu_append(section, "Reload Configuration", "app.reload-config"); - c.g_menu_append(section, "About Ghostty", "win.about"); - } -} - -/// This sets the self.menu property to the application menu that can be -/// shared by all application windows. -fn initMenu(self: *App) void { - const menu = c.g_menu_new(); - errdefer c.g_object_unref(menu); - initMenuContent(@ptrCast(menu)); - self.menu = menu; -} - -fn initContextMenu(self: *App) void { - const menu = c.g_menu_new(); - errdefer c.g_object_unref(menu); - - { - const section = c.g_menu_new(); - defer c.g_object_unref(section); - c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section))); - c.g_menu_append(section, "Copy", "win.copy"); - c.g_menu_append(section, "Paste", "win.paste"); - } - - { - const section = c.g_menu_new(); - defer c.g_object_unref(section); - c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section))); - c.g_menu_append(section, "Split Right", "win.split_right"); - c.g_menu_append(section, "Split Down", "win.split_down"); - } - - { - const section = c.g_menu_new(); - defer c.g_object_unref(section); - c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section))); - c.g_menu_append(section, "Reset", "win.reset"); - c.g_menu_append(section, "Terminal Inspector", "win.toggle_inspector"); - } - - const section = c.g_menu_new(); - defer c.g_object_unref(section); - const submenu = c.g_menu_new(); - defer c.g_object_unref(submenu); - - initMenuContent(@ptrCast(submenu)); - c.g_menu_append_submenu(section, "Menu", @ptrCast(@alignCast(submenu))); - c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section))); - - self.context_menu = menu; -} - -pub fn refreshContextMenu(_: *App, window: ?*c.GtkWindow, has_selection: bool) void { - const action: ?*c.GSimpleAction = @ptrCast(c.g_action_map_lookup_action(@ptrCast(window), "copy")); - c.g_simple_action_set_enabled(action, if (has_selection) 1 else 0); -} - fn isValidAppId(app_id: [:0]const u8) bool { if (app_id.len > 255 or app_id.len == 0) return false; if (app_id[0] == '.') return false; diff --git a/src/apprt/gtk/Builder.zig b/src/apprt/gtk/Builder.zig index ffacd3adf..f9b0c226a 100644 --- a/src/apprt/gtk/Builder.zig +++ b/src/apprt/gtk/Builder.zig @@ -56,7 +56,7 @@ pub fn setWidgetClassTemplate(self: *const Builder, class: *gtk.WidgetClass) voi class.setTemplateFromResource(self.resource_name); } -pub fn getObject(self: *Builder, name: [:0]const u8) ?*gobject.Object { +pub fn getObject(self: *Builder, comptime T: type, name: [:0]const u8) ?*T { const builder = builder: { if (self.builder) |builder| break :builder builder; const builder = gtk.Builder.newFromResource(self.resource_name); @@ -64,7 +64,7 @@ pub fn getObject(self: *Builder, name: [:0]const u8) ?*gobject.Object { break :builder builder; }; - return builder.getObject(name); + return gobject.ext.cast(T, builder.getObject(name) orelse return null); } pub fn deinit(self: *const Builder) void { diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 9a8c4513d..9835b5b77 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -20,6 +20,7 @@ const App = @import("App.zig"); const Split = @import("Split.zig"); const Tab = @import("Tab.zig"); const Window = @import("Window.zig"); +const Menu = @import("menu.zig").Menu; const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig"); const ResizeOverlay = @import("ResizeOverlay.zig"); const inspector = @import("inspector.zig"); @@ -378,6 +379,9 @@ im_len: u7 = 0, /// details on what this is. cgroup_path: ?[]const u8 = null, +/// Our context menu. +context_menu: Menu(Surface, "context_menu", false), + /// The state of the key event while we're doing IM composition. /// See gtkKeyPressed for detailed descriptions. pub const IMKeyEvent = enum { @@ -576,9 +580,14 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void { .cursor_pos = .{ .x = -1, .y = -1 }, .im_context = im_context, .cgroup_path = cgroup_path, + .context_menu = undefined, }; errdefer self.* = undefined; + // initialize the context menu + self.context_menu.init(self); + self.context_menu.setParent(@ptrCast(@alignCast(overlay))); + // Set our default mouse shape try self.setMouseShape(.text); @@ -913,7 +922,7 @@ fn updateTitleLabels(self: *Surface) void { // If we have a tab and are the focused child, then we have to update the tab if (self.container.tab()) |tab| { - if (tab.focus_child == self) tab.setLabelText(title); + if (tab.focus_child == self) tab.setTitleText(title); } // If we have a window and are focused, then we have to update the window title. @@ -1224,6 +1233,7 @@ fn getClipboard(widget: *c.GtkWidget, clipboard: apprt.Clipboard) ?*c.GdkClipboa .selection, .primary => c.gtk_widget_get_primary_clipboard(widget), }; } + pub fn getCursorPos(self: *const Surface) !apprt.CursorPos { return self.cursor_pos; } @@ -1261,40 +1271,6 @@ pub fn showDesktopNotification( c.g_application_send_notification(g_app, body.ptr, notification); } -fn showContextMenu(self: *Surface, x: f32, y: f32) void { - const window: *Window = self.container.window() orelse { - log.info( - "showContextMenu invalid for container={s}", - .{@tagName(self.container)}, - ); - return; - }; - - // Convert surface coordinate into coordinate space of the - // context menu's parent - var point: c.graphene_point_t = .{ .x = x, .y = y }; - if (c.gtk_widget_compute_point( - self.primaryWidget(), - c.gtk_widget_get_parent(@ptrCast(window.context_menu)), - &c.GRAPHENE_POINT_INIT(point.x, point.y), - @ptrCast(&point), - ) == 0) { - log.warn("failed computing point for context menu", .{}); - return; - } - - const rect: c.GdkRectangle = .{ - .x = @intFromFloat(point.x), - .y = @intFromFloat(point.y), - .width = 1, - .height = 1, - }; - - c.gtk_popover_set_pointing_to(@ptrCast(@alignCast(window.context_menu)), &rect); - self.app.refreshContextMenu(window.window, self.core_surface.hasSelection()); - c.gtk_popover_popup(@ptrCast(@alignCast(window.context_menu))); -} - fn gtkRealize(area: *c.GtkGLArea, ud: ?*anyopaque) callconv(.C) void { log.debug("gl surface realized", .{}); @@ -1465,7 +1441,7 @@ fn gtkMouseDown( // word and returns false. We can use this to handle the context menu // opening under normal scenarios. if (!consumed and button == .right) { - self.showContextMenu(@floatCast(x), @floatCast(y)); + self.context_menu.popupAt(@intFromFloat(x), @intFromFloat(y)); } } @@ -2073,15 +2049,14 @@ fn gtkFocusLeave(_: *c.GtkEventControllerFocus, ud: ?*anyopaque) callconv(.C) vo /// Adds the unfocused_widget to the overlay. If the unfocused_widget has already been added, this /// is a no-op pub fn dimSurface(self: *Surface) void { - const window = self.container.window() orelse { + _ = self.container.window() orelse { log.warn("dimSurface invalid for container={}", .{self.container}); return; }; // Don't dim surface if context menu is open. // This means we got unfocused due to it opening. - const context_menu_open = c.gtk_widget_get_visible(window.context_menu); - if (context_menu_open == 1) return; + if (self.context_menu.isVisible()) return; if (self.unfocused_widget != null) return; self.unfocused_widget = c.gtk_drawing_area_new(); diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index d320daa7c..214928790 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -108,8 +108,8 @@ pub fn replaceElem(self: *Tab, elem: Surface.Container.Elem) void { self.elem = elem; } -pub fn setLabelText(self: *Tab, title: [:0]const u8) void { - self.window.notebook.setTabLabel(self, title); +pub fn setTitleText(self: *Tab, title: [:0]const u8) void { + self.window.notebook.setTabTitle(self, title); } pub fn setTooltipText(self: *Tab, tooltip: [:0]const u8) void { diff --git a/src/apprt/gtk/TabView.zig b/src/apprt/gtk/TabView.zig index 156fd0b93..efef3d621 100644 --- a/src/apprt/gtk/TabView.zig +++ b/src/apprt/gtk/TabView.zig @@ -165,7 +165,7 @@ pub fn reorderPage(self: *TabView, tab: *Tab, position: c_int) void { _ = self.tab_view.reorderPage(page, position); } -pub fn setTabLabel(self: *TabView, tab: *Tab, title: [:0]const u8) void { +pub fn setTabTitle(self: *TabView, tab: *Tab, title: [:0]const u8) void { const page = self.tab_view.getPage(@ptrCast(tab.box)); page.setTitle(title.ptr); } @@ -188,7 +188,7 @@ pub fn addTab(self: *TabView, tab: *Tab, title: [:0]const u8) void { const position = self.newTabInsertPosition(tab); const box_widget: *gtk.Widget = @ptrCast(tab.box); const page = self.tab_view.insert(box_widget, position); - self.setTabLabel(tab, title); + self.setTabTitle(tab, title); self.tab_view.setSelectedPage(page); } diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 3b415435a..cfd340d6c 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -18,6 +18,7 @@ const CoreSurface = @import("../../Surface.zig"); const App = @import("App.zig"); const Color = configpkg.Config.Color; const Surface = @import("Surface.zig"); +const Menu = @import("menu.zig").Menu; const Tab = @import("Tab.zig"); const c = @import("c.zig").c; const adwaita = @import("adwaita.zig"); @@ -46,7 +47,8 @@ tab_overview: ?*c.GtkWidget, /// The notebook (tab grouping) for this window. notebook: TabView, -context_menu: *c.GtkWidget, +/// The "main" menu that is attached to a button in the headerbar. +titlebar_menu: Menu(Window, "titlebar_menu", true), /// The libadwaita widget for receiving toast send requests. toast_overlay: *c.GtkWidget, @@ -112,7 +114,7 @@ pub fn init(self: *Window, app: *App) !void { .headerbar = undefined, .tab_overview = null, .notebook = undefined, - .context_menu = undefined, + .titlebar_menu = undefined, .toast_overlay = undefined, .winproto = .none, }; @@ -137,6 +139,9 @@ pub fn init(self: *Window, app: *App) !void { // Create our box which will hold our widgets in the main content area. const box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0); + // Set up the menus + self.titlebar_menu.init(self); + // Setup our notebook self.notebook.init(self); @@ -174,7 +179,15 @@ pub fn init(self: *Window, app: *App) !void { const btn = c.gtk_menu_button_new(); c.gtk_widget_set_tooltip_text(btn, "Main Menu"); c.gtk_menu_button_set_icon_name(@ptrCast(btn), "open-menu-symbolic"); - c.gtk_menu_button_set_menu_model(@ptrCast(btn), @ptrCast(@alignCast(app.menu))); + c.gtk_menu_button_set_popover(@ptrCast(btn), @ptrCast(@alignCast(self.titlebar_menu.asWidget()))); + _ = c.g_signal_connect_data( + btn, + "notify::active", + c.G_CALLBACK(>kTitlebarMenuActivate), + self, + null, + c.G_CONNECT_DEFAULT, + ); self.headerbar.packEnd(btn); } @@ -259,11 +272,6 @@ pub fn init(self: *Window, app: *App) !void { c.adw_tab_overview_set_view(@ptrCast(tab_overview), @ptrCast(@alignCast(self.notebook.tab_view))); } - self.context_menu = c.gtk_popover_menu_new_from_model(@ptrCast(@alignCast(self.app.context_menu))); - c.gtk_widget_set_parent(self.context_menu, box); - c.gtk_popover_set_has_arrow(@ptrCast(@alignCast(self.context_menu)), 0); - c.gtk_widget_set_halign(self.context_menu, c.GTK_ALIGN_START); - // We register a key event controller with the window so // we can catch key events when our surface may not be // focused (i.e. when the libadw tab overview is shown). @@ -272,7 +280,6 @@ pub fn init(self: *Window, app: *App) !void { c.gtk_widget_add_controller(gtk_widget, ec_key_press); // All of our events - _ = c.g_signal_connect_data(self.context_menu, "closed", c.G_CALLBACK(>kRefocusTerm), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(self.window, "realize", c.G_CALLBACK(>kRealize), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(self.window, "close-request", c.G_CALLBACK(>kCloseRequest), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(self.window, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT); @@ -459,16 +466,18 @@ fn initActions(self: *Window) void { const actions = .{ .{ "about", >kActionAbout }, .{ "close", >kActionClose }, - .{ "new_window", >kActionNewWindow }, - .{ "new_tab", >kActionNewTab }, - .{ "split_right", >kActionSplitRight }, - .{ "split_down", >kActionSplitDown }, - .{ "split_left", >kActionSplitLeft }, - .{ "split_up", >kActionSplitUp }, - .{ "toggle_inspector", >kActionToggleInspector }, + .{ "new-window", >kActionNewWindow }, + .{ "new-tab", >kActionNewTab }, + .{ "close-tab", >kActionCloseTab }, + .{ "split-right", >kActionSplitRight }, + .{ "split-down", >kActionSplitDown }, + .{ "split-left", >kActionSplitLeft }, + .{ "split-up", >kActionSplitUp }, + .{ "toggle-inspector", >kActionToggleInspector }, .{ "copy", >kActionCopy }, .{ "paste", >kActionPaste }, .{ "reset", >kActionReset }, + .{ "clear", >kActionClear }, }; inline for (actions) |entry| { @@ -487,8 +496,6 @@ fn initActions(self: *Window) void { } pub fn deinit(self: *Window) void { - c.gtk_widget_unparent(@ptrCast(self.context_menu)); - self.winproto.deinit(self.app.core_app.alloc); if (self.adw_tab_overview_focus_timer) |timer| { @@ -752,16 +759,6 @@ fn adwTabOverviewFocusTimer( return 0; } -fn gtkRefocusTerm(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool { - _ = v; - log.debug("refocus term request", .{}); - const self = userdataSelf(ud.?); - - self.focusCurrentTab(); - - return true; -} - fn gtkCloseRequest(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool { _ = v; log.debug("window close request", .{}); @@ -919,11 +916,7 @@ fn gtkActionClose( ud: ?*anyopaque, ) callconv(.C) void { const self: *Window = @ptrCast(@alignCast(ud orelse return)); - const surface = self.actionSurface() orelse return; - _ = surface.performBindingAction(.{ .close_surface = {} }) catch |err| { - log.warn("error performing binding action error={}", .{err}); - return; - }; + c.gtk_window_destroy(self.window); } fn gtkActionNewWindow( @@ -948,6 +941,19 @@ fn gtkActionNewTab( gtkTabNewClick(undefined, ud); } +fn gtkActionCloseTab( + _: *c.GSimpleAction, + _: *c.GVariant, + ud: ?*anyopaque, +) callconv(.C) void { + const self: *Window = @ptrCast(@alignCast(ud orelse return)); + const surface = self.actionSurface() orelse return; + _ = surface.performBindingAction(.{ .close_tab = {} }) catch |err| { + log.warn("error performing binding action error={}", .{err}); + return; + }; +} + fn gtkActionSplitRight( _: *c.GSimpleAction, _: *c.GVariant, @@ -1052,13 +1058,40 @@ fn gtkActionReset( }; } +fn gtkActionClear( + _: *c.GSimpleAction, + _: *c.GVariant, + ud: ?*anyopaque, +) callconv(.C) void { + const self: *Window = @ptrCast(@alignCast(ud orelse return)); + const surface = self.actionSurface() orelse return; + _ = surface.performBindingAction(.{ .clear_screen = {} }) catch |err| { + log.warn("error performing binding action error={}", .{err}); + return; + }; +} + /// Returns the surface to use for an action. -fn actionSurface(self: *Window) ?*CoreSurface { +pub fn actionSurface(self: *Window) ?*CoreSurface { const tab = self.notebook.currentTab() orelse return null; const surface = tab.focus_child orelse return null; return &surface.core_surface; } +fn gtkTitlebarMenuActivate( + btn: *c.GtkMenuButton, + _: *c.GParamSpec, + ud: ?*anyopaque, +) callconv(.C) void { + const active = c.gtk_menu_button_get_active(btn) != 0; + const self = userdataSelf(ud orelse return); + if (active) { + self.titlebar_menu.refresh(); + } else { + self.focusCurrentTab(); + } +} + fn userdataSelf(ud: *anyopaque) *Window { return @ptrCast(@alignCast(ud)); } diff --git a/src/apprt/gtk/gresource.zig b/src/apprt/gtk/gresource.zig index 050605b00..d45997d6c 100644 --- a/src/apprt/gtk/gresource.zig +++ b/src/apprt/gtk/gresource.zig @@ -53,7 +53,10 @@ const icons = [_]struct { }, }; -pub const ui_files = [_][]const u8{}; +pub const ui_files = [_][]const u8{ + "menu-window-titlebar_menu", + "menu-surface-context_menu", +}; pub const blueprint_files = [_][]const u8{}; pub fn main() !void { diff --git a/src/apprt/gtk/menu.zig b/src/apprt/gtk/menu.zig new file mode 100644 index 000000000..c36ace646 --- /dev/null +++ b/src/apprt/gtk/menu.zig @@ -0,0 +1,135 @@ +const std = @import("std"); + +const gtk = @import("gtk"); +const gdk = @import("gdk"); +const gio = @import("gio"); +const gobject = @import("gobject"); + +const apprt = @import("../../apprt.zig"); +const App = @import("App.zig"); +const Window = @import("Window.zig"); +const Surface = @import("Surface.zig"); +const Builder = @import("Builder.zig"); + +/// Abstract GTK menus to take advantage of machinery for buildtime/comptime +/// error checking. +pub fn Menu( + /// GTK apprt type that the menu is "for". Window and Surface are supported + /// right now. + comptime T: type, + /// Name of the menu. Along with the apprt type, this is used to look up the + /// builder ui definitions of the menu. + comptime menu_name: []const u8, + /// Should the popup have a pointer pointing to the location that it's + /// attached to. + comptime arrow: bool, +) type { + return struct { + const Self = @This(); + + /// parent apprt object + parent: *T, + + /// our widget + menu_widget: *gtk.PopoverMenu, + + /// initialize the menu + pub fn init(self: *Self, parent: *T) void { + const object_type = switch (T) { + Window => "window", + Surface => "surface", + else => unreachable, + }; + + var builder = Builder.init("menu-" ++ object_type ++ "-" ++ menu_name, .ui); + defer builder.deinit(); + + const menu_model = builder.getObject(gio.MenuModel, "menu").?; + + const menu_widget = gtk.PopoverMenu.newFromModelFull(menu_model, .{ .nested = true }); + menu_widget.as(gtk.Popover).setHasArrow(@intFromBool(arrow)); + _ = gtk.Popover.signals.closed.connect( + menu_widget, + *Self, + gtkRefocusTerm, + self, + .{}, + ); + + self.* = .{ + .parent = parent, + .menu_widget = menu_widget, + }; + } + + pub fn setParent(self: *const Self, widget: *gtk.Widget) void { + self.menu_widget.as(gtk.Widget).setParent(widget); + } + + pub fn asWidget(self: *const Self) *gtk.Widget { + return self.menu_widget.as(gtk.Widget); + } + + pub fn isVisible(self: *const Self) bool { + return self.menu_widget.as(gtk.Widget).getVisible() != 0; + } + + pub fn setVisible(self: *const Self, visible: bool) void { + self.menu_widget.as(gtk.Widget).setVisible(@intFromBool(visible)); + } + + /// Refresh the menu. Right now that means enabling/disabling the "Copy" + /// menu item based on whether there is an active selection or not, but + /// that may change in the future. + pub fn refresh(self: *const Self) void { + const window: *gtk.Window, const has_selection: bool = switch (T) { + Window => window: { + const core_surface = self.parent.actionSurface() orelse break :window .{ + @ptrCast(@alignCast(self.parent.window)), + false, + }; + const has_selection = core_surface.hasSelection(); + break :window .{ @ptrCast(@alignCast(self.parent.window)), has_selection }; + }, + Surface => surface: { + const window = self.parent.container.window() orelse return; + const has_selection = self.parent.core_surface.hasSelection(); + break :surface .{ @ptrCast(@alignCast(window.window)), has_selection }; + }, + else => unreachable, + }; + + const action_map: *gio.ActionMap = gobject.ext.cast(gio.ActionMap, window) orelse return; + const action: *gio.SimpleAction = gobject.ext.cast( + gio.SimpleAction, + action_map.lookupAction("copy") orelse return, + ) orelse return; + action.setEnabled(@intFromBool(has_selection)); + } + + /// Pop up the menu at the given coordinates + pub fn popupAt(self: *const Self, x: c_int, y: c_int) void { + const rect: gdk.Rectangle = .{ + .f_x = x, + .f_y = y, + .f_width = 1, + .f_height = 1, + }; + const popover = self.menu_widget.as(gtk.Popover); + popover.setPointingTo(&rect); + self.refresh(); + popover.popup(); + } + + /// Refocus tab that lost focus because of the popover menu + fn gtkRefocusTerm(_: *gtk.PopoverMenu, self: *Self) callconv(.C) void { + const window: *Window = switch (T) { + Window => self.parent, + Surface => self.parent.container.window() orelse return, + else => unreachable, + }; + + window.focusCurrentTab(); + } + }; +} diff --git a/src/apprt/gtk/ui/menu-surface-context_menu.ui b/src/apprt/gtk/ui/menu-surface-context_menu.ui new file mode 100644 index 000000000..b5bf55cef --- /dev/null +++ b/src/apprt/gtk/ui/menu-surface-context_menu.ui @@ -0,0 +1,90 @@ + + + + +
+ + Copy + win.copy + + + Paste + win.paste + +
+
+ + Clear + win.clear + + + Reset + win.reset + +
+
+ + Split +
+ + Split Up + win.split-up + + + Split Down + win.split-down + + + Split Left + win.split-left + + + Split Right + win.split-right + +
+
+ + Tab +
+ + New Tab + win.new-tab + + + Close Tab + win.close-tab + +
+
+ + Window +
+ + New Window + win.new-window + + + Close Window + win.close + +
+
+
+
+ + Config +
+ + Open Configuration + app.open-config + + + Reload Configuration + app.reload-config + +
+
+
+
+
diff --git a/src/apprt/gtk/ui/menu-window-titlebar_menu.ui b/src/apprt/gtk/ui/menu-window-titlebar_menu.ui new file mode 100644 index 000000000..6359b993c --- /dev/null +++ b/src/apprt/gtk/ui/menu-window-titlebar_menu.ui @@ -0,0 +1,93 @@ + + + + +
+ + Copy + win.copy + + + Paste + win.paste + +
+
+ + New Window + win.new-window + + + Close Window + win.close + +
+
+ + New Tab + win.new-tab + + + Close Tab + win.close-tab + +
+
+ + Split +
+ + Split Up + win.split-up + + + Split Down + win.split-down + + + Split Left + win.split-left + + + Split Right + win.split-right + +
+
+
+
+ + Clear + win.clear + + + Reset + win.reset + +
+
+ + Terminal Inspector + win.toggle-inspector + + + Open Configuration + app.open-config + + + Reload Configuration + app.reload-config + +
+
+ + About Ghostty + win.about + + + Quit + app.quit + +
+
+