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 10af251010..a89381cc53 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 + +
+
+