From 22f6eb41edff2e51cea7ad04804ee95829102351 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 11 Jan 2025 13:12:59 -0600 Subject: [PATCH] gtk: there can be only one: menu Unify the main and context menus. Right now they are identical when displayed as a main menu and when displayed as a context menu. Both the main menu and context menu also now use GtkPopoverMenu to display the menu. Each surface has a separate context menu to eliminate bugs with positioning the context menus when they pop up. The menus are built using GtkBuilder XML files, which reduces the amount of code necessary to build the menus and enables translation in the future as well as making changes easier. --- src/apprt/gtk/App.zig | 125 +++++-------------------------- src/apprt/gtk/Surface.zig | 49 ++++-------- src/apprt/gtk/Window.zig | 101 ++++++++++++++++--------- src/apprt/gtk/gresource.zig | 37 ++++++--- src/apprt/gtk/menu.zig | 110 +++++++++++++++++++++++++++ src/apprt/gtk/ui/menu-surface.ui | 93 +++++++++++++++++++++++ src/apprt/gtk/ui/menu-window.ui | 93 +++++++++++++++++++++++ 7 files changed, 422 insertions(+), 186 deletions(-) create mode 100644 src/apprt/gtk/menu.zig create mode 100644 src/apprt/gtk/ui/menu-surface.ui create mode 100644 src/apprt/gtk/ui/menu-window.ui diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 96275684e5..6b434940e4 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -58,12 +58,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, @@ -480,8 +474,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| { @@ -1030,20 +1022,28 @@ fn updateConfigErrors(self: *App) !void { } 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.copy", .{ .copy_to_clipboard = {} }); try self.syncActionAccelerator("win.paste", .{ .paste_from_clipboard = {} }); + + try self.syncActionAccelerator("win.new-window", .{ .new_window = {} }); + try self.syncActionAccelerator("win.close", .{ .close_window = {} }); + + try self.syncActionAccelerator("win.new-tab", .{ .new_tab = {} }); + try self.syncActionAccelerator("win.close-tab", .{ .close_tab = {} }); + + try self.syncActionAccelerator("win.split-up", .{ .new_split = .up }); + try self.syncActionAccelerator("win.split-down", .{ .new_split = .down }); + try self.syncActionAccelerator("win.split-left", .{ .new_split = .left }); + try self.syncActionAccelerator("win.split-right", .{ .new_split = .right }); + + try self.syncActionAccelerator("win.clear", .{ .clear_screen = {} }); try self.syncActionAccelerator("win.reset", .{ .reset = {} }); + + try self.syncActionAccelerator("win.toggle-inspector", .{ .inspector = .toggle }); + try self.syncActionAccelerator("app.open-config", .{ .open_config = {} }); + try self.syncActionAccelerator("app.reload-config", .{ .reload_config = {} }); + + try self.syncActionAccelerator("app.quit", .{ .quit = {} }); } fn syncActionAccelerator( @@ -1274,10 +1274,8 @@ pub fn run(self: *App) !void { // Setup our D-Bus connection for listening to settings changes. self.initDbus(); - // Setup our menu items + // Setup our actions self.initActions(); - self.initMenu(); - self.initContextMenu(); // Setup our initial color scheme self.colorSchemeEvent(self.getColorScheme()); @@ -1817,89 +1815,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"); - } - - if (!self.config.@"window-decoration".isCSD()) { - 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/Surface.zig b/src/apprt/gtk/Surface.zig index 61866dcec6..51c09bfc5e 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"); @@ -379,6 +380,9 @@ im_len: u7 = 0, /// details on what this is. cgroup_path: ?[]const u8 = null, +/// Our context menu. +menu: Menu(Surface), + /// Configuration used for initializing the surface. We have to copy some /// data since initialization is delayed with GTK (on realize). pub const InitConfig = struct { @@ -563,9 +567,14 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void { .cursor_pos = .{ .x = 0, .y = 0 }, .im_context = im_context, .cgroup_path = cgroup_path, + .menu = undefined, }; errdefer self.* = undefined; + // initialize the context menu + self.menu.init(); + self.menu.setParent(overlay); + // Set our default mouse shape try self.setMouseShape(.text); @@ -1214,6 +1223,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; } @@ -1251,38 +1261,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; - }; - - var point: c.graphene_point_t = .{ .x = x, .y = y }; - if (c.gtk_widget_compute_point( - self.primaryWidget(), - @ptrCast(window.window), - &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", .{}); @@ -1453,7 +1431,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.menu.popupAt(x, y); } } @@ -1999,15 +1977,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.menu.isVisible()) return; if (self.unfocused_widget != null) return; self.unfocused_widget = c.gtk_drawing_area_new(); diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 3e972ca021..7b3f405720 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"); @@ -47,7 +48,8 @@ tab_overview: ?*c.GtkWidget, /// can be either c.GtkNotebook or c.AdwTabView. notebook: Notebook, -context_menu: *c.GtkWidget, +/// The "main" menu that is attached to a button in the headerbar. +menu: Menu(Window), /// The libadwaita widget for receiving toast send requests. If libadwaita is /// not used, this is null and unused. @@ -81,7 +83,7 @@ pub fn init(self: *Window, app: *App) !void { .headerbar = undefined, .tab_overview = null, .notebook = undefined, - .context_menu = undefined, + .menu = undefined, .toast_overlay = undefined, .winproto = .none, }; @@ -123,6 +125,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 menu + self.menu.init(); + // Setup our notebook self.notebook.init(); @@ -160,7 +165,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), self.menu.asWidget()); + _ = c.g_signal_connect_data( + btn, + "notify::active", + c.G_CALLBACK(>kMenuActivate), + self, + null, + c.G_CONNECT_DEFAULT, + ); self.headerbar.packEnd(btn); } @@ -257,11 +270,6 @@ pub fn init(self: *Window, app: *App) !void { c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.adw.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); - // If we want the window to be maximized, we do that here. if (app.config.maximize) c.gtk_window_maximize(self.window); @@ -276,7 +284,6 @@ pub fn init(self: *Window, app: *App) !void { c.gtk_widget_add_controller(window, 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(window, "realize", c.G_CALLBACK(>kRealize), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(window, "close-request", c.G_CALLBACK(>kCloseRequest), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT); @@ -445,16 +452,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| { @@ -473,8 +482,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| { @@ -742,16 +749,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", .{}); @@ -912,11 +909,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( @@ -941,6 +934,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, @@ -1045,8 +1051,21 @@ 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; @@ -1055,3 +1074,17 @@ fn actionSurface(self: *Window) ?*CoreSurface { fn userdataSelf(ud: *anyopaque) *Window { return @ptrCast(@alignCast(ud)); } + +fn gtkMenuActivate( + 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.menu.refresh(); + } else { + self.focusCurrentTab(); + } +} diff --git a/src/apprt/gtk/gresource.zig b/src/apprt/gtk/gresource.zig index 327680993d..6935619b86 100644 --- a/src/apprt/gtk/gresource.zig +++ b/src/apprt/gtk/gresource.zig @@ -53,6 +53,11 @@ const icons = [_]struct { }, }; +const builder_files = [_][]const u8{ + "menu-window.ui", + "menu-surface.ui", +}; + pub const gresource_xml = comptimeGenerateGResourceXML(); fn comptimeGenerateGResourceXML() []const u8 { @@ -73,9 +78,6 @@ fn writeGResourceXML(writer: anytype) !void { try writer.writeAll( \\ \\ - \\ - ); - try writer.writeAll( \\ \\ ); @@ -87,9 +89,6 @@ fn writeGResourceXML(writer: anytype) !void { } try writer.writeAll( \\ - \\ - ); - try writer.writeAll( \\ \\ ); @@ -99,6 +98,14 @@ fn writeGResourceXML(writer: anytype) !void { .{ icon.alias, icon.source }, ); } + try writer.writeAll( + \\ + \\ + \\ + ); + for (builder_files) |builder_file| { + try writer.print(" src/apprt/gtk/ui/{s}\n", .{ builder_file, builder_file }); + } try writer.writeAll( \\ \\ @@ -107,12 +114,20 @@ fn writeGResourceXML(writer: anytype) !void { } pub const dependencies = deps: { - var deps: [css_files.len + icons.len][]const u8 = undefined; - for (css_files, 0..) |css_file, i| { - deps[i] = std.fmt.comptimePrint("src/apprt/gtk/{s}", .{css_file}); + const total = css_files.len + icons.len + builder_files.len; + var deps: [total][]const u8 = undefined; + var index: usize = 0; + for (css_files) |css_file| { + deps[index] = std.fmt.comptimePrint("src/apprt/gtk/{s}", .{css_file}); + index += 1; + } + for (icons) |icon| { + deps[index] = std.fmt.comptimePrint("images/icons/icon_{s}.png", .{icon.source}); + index += 1; } - for (icons, css_files.len..) |icon, i| { - deps[i] = std.fmt.comptimePrint("images/icons/icon_{s}.png", .{icon.source}); + for (builder_files) |builder_file| { + deps[index] = std.fmt.comptimePrint("src/apprt/gtk/ui/{s}", .{builder_file}); + index += 1; } break :deps deps; }; diff --git a/src/apprt/gtk/menu.zig b/src/apprt/gtk/menu.zig new file mode 100644 index 0000000000..f4293e29d9 --- /dev/null +++ b/src/apprt/gtk/menu.zig @@ -0,0 +1,110 @@ +const std = @import("std"); + +const c = @import("c.zig").c; +const apprt = @import("../../apprt.zig"); +const App = @import("App.zig"); +const Window = @import("Window.zig"); +const Surface = @import("Surface.zig"); + +const log = std.log.scoped(.gtk_menu); + +pub fn Menu(comptime T: type) type { + return struct { + parent: *T, + popover: *c.GtkPopover, + + pub fn init(self: *Menu(T)) void { + const name = switch (T) { + Window => "window", + Surface => "surface", + else => unreachable, + }; + const parent: *T = @alignCast(@fieldParentPtr("menu", self)); + + const builder = c.gtk_builder_new_from_resource("/com/mitchellh/ghostty/ui/menu-" ++ name ++ ".ui"); + defer c.g_object_unref(@ptrCast(builder)); + + const menu: *c.GMenuModel = @ptrCast(@alignCast(c.gtk_builder_get_object(builder, "menu"))); + const popover: *c.GtkPopover = @ptrCast(@alignCast(c.gtk_popover_menu_new_from_model(menu))); + c.gtk_popover_menu_set_flags(@ptrCast(@alignCast(popover)), c.GTK_POPOVER_MENU_NESTED); + + _ = c.g_signal_connect_data( + popover, + "closed", + c.G_CALLBACK(>kRefocusTerm), + self, + null, + c.G_CONNECT_DEFAULT, + ); + + self.* = .{ + .parent = parent, + .popover = popover, + }; + } + + pub fn setParent(self: *const Menu(T), widget: *c.GtkWidget) void { + c.gtk_widget_set_parent(self.asWidget(), widget); + } + + pub fn asPopover(self: *const Menu(T)) *c.GtkPopover { + return self.popover; + } + + pub fn asWidget(self: *const Menu(T)) *c.GtkWidget { + return @ptrCast(@alignCast(self.popover)); + } + + pub fn isVisible(self: *const Menu(T)) bool { + return c.gtk_widget_get_visible(self.asWidget()) != 0; + } + + pub fn refresh(self: *const Menu(T)) void { + const window: *Window, const has_selection: bool = switch (T) { + Window => window: { + const core_surface = self.parent.actionSurface() orelse break :window .{ self.parent, false }; + const has_selection = core_surface.hasSelection(); + break :window .{ self.parent, has_selection }; + }, + Surface => surface: { + const window = self.parent.container.window() orelse return; + const has_selection = self.parent.core_surface.hasSelection(); + break :surface .{ window, has_selection }; + }, + else => unreachable, + }; + + const action: ?*c.GSimpleAction = @ptrCast(c.g_action_map_lookup_action( + @ptrCast(@alignCast(window.window)), + "copy", + )); + c.g_simple_action_set_enabled(action, @intFromBool(has_selection)); + } + + pub fn popupAt(self: *const Menu(T), x: f64, y: f64) void { + const rect: c.GdkRectangle = .{ + .x = @intFromFloat(x), + .y = @intFromFloat(y), + .width = 1, + .height = 1, + }; + c.gtk_popover_set_pointing_to(self.popover, &rect); + self.refresh(); + c.gtk_popover_popup(self.popover); + } + + fn gtkRefocusTerm(_: *c.GtkPopover, _: *c.GVariant, ud: ?*anyopaque) callconv(.C) bool { + const self: *Menu(T) = @ptrCast(@alignCast(ud orelse return false)); + + const window: *Window = switch (T) { + Window => self.parent, + Surface => self.parent.container.window() orelse return false, + else => unreachable, + }; + + window.focusCurrentTab(); + + return true; + } + }; +} diff --git a/src/apprt/gtk/ui/menu-surface.ui b/src/apprt/gtk/ui/menu-surface.ui new file mode 100644 index 0000000000..9345e0aea2 --- /dev/null +++ b/src/apprt/gtk/ui/menu-surface.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 + +
+
+
diff --git a/src/apprt/gtk/ui/menu-window.ui b/src/apprt/gtk/ui/menu-window.ui new file mode 100644 index 0000000000..9345e0aea2 --- /dev/null +++ b/src/apprt/gtk/ui/menu-window.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 + +
+
+