diff --git a/src/apprt/gtk-ng/class/surface.zig b/src/apprt/gtk-ng/class/surface.zig index 3242337c2..ecddb6e79 100644 --- a/src/apprt/gtk-ng/class/surface.zig +++ b/src/apprt/gtk-ng/class/surface.zig @@ -327,6 +327,18 @@ pub const Surface = extern struct { ); }; + /// Emitted just prior to the context menu appearing. + pub const menu = struct { + pub const name = "menu"; + pub const connect = impl.connect; + const impl = gobject.ext.defineSignal( + name, + Self, + &.{}, + void, + ); + }; + /// Emitted when the focus wants to be brought to the top and /// focused. pub const @"present-request" = struct { @@ -462,6 +474,7 @@ pub const Surface = extern struct { // Template binds child_exited_overlay: *ChildExited, + context_menu: *gtk.PopoverMenu, drop_target: *gtk.DropTarget, progress_bar_overlay: *gtk.ProgressBar, @@ -1473,6 +1486,16 @@ pub const Surface = extern struct { self.close(.{ .surface = false }); } + fn contextMenuClosed( + _: *gtk.PopoverMenu, + self: *Self, + ) callconv(.c) void { + // When the context menu closes, it moves focus back to the tab + // bar if there are tabs. That's not correct. We need to grab it + // on the surface. + self.grabFocus(); + } + fn dtDrop( _: *gtk.DropTarget, value: *gobject.Value, @@ -1647,9 +1670,9 @@ pub const Surface = extern struct { } // Report the event + const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton()); const consumed = if (priv.core_surface) |surface| consumed: { const gtk_mods = event.getModifierState(); - const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton()); const mods = gtk_key.translateMods(gtk_mods); break :consumed surface.mouseButtonCallback( .press, @@ -1661,10 +1684,28 @@ pub const Surface = extern struct { }; } else false; - // TODO: context menu - _ = consumed; - _ = x; - _ = y; + // If a right click isn't consumed, mouseButtonCallback selects the hovered + // word and returns false. We can use this to handle the context menu + // opening under normal scenarios. + if (!consumed and button == .right) { + signals.menu.impl.emit( + self, + null, + .{}, + null, + ); + + const rect: gdk.Rectangle = .{ + .f_x = @intFromFloat(x), + .f_y = @intFromFloat(y), + .f_width = 1, + .f_height = 1, + }; + + const popover = priv.context_menu.as(gtk.Popover); + popover.setPointingTo(&rect); + popover.popup(); + } } fn gcMouseUp( @@ -2259,6 +2300,7 @@ pub const Surface = extern struct { class.bindTemplateChildPrivate("url_left", .{}); class.bindTemplateChildPrivate("url_right", .{}); class.bindTemplateChildPrivate("child_exited_overlay", .{}); + class.bindTemplateChildPrivate("context_menu", .{}); class.bindTemplateChildPrivate("progress_bar_overlay", .{}); class.bindTemplateChildPrivate("resize_overlay", .{}); class.bindTemplateChildPrivate("drop_target", .{}); @@ -2288,6 +2330,7 @@ pub const Surface = extern struct { class.bindTemplateCallback("url_mouse_enter", &ecUrlMouseEnter); class.bindTemplateCallback("url_mouse_leave", &ecUrlMouseLeave); class.bindTemplateCallback("child_exited_close", &childExitedClose); + class.bindTemplateCallback("context_menu_closed", &contextMenuClosed); class.bindTemplateCallback("notify_config", &propConfig); class.bindTemplateCallback("notify_mouse_hover_url", &propMouseHoverUrl); class.bindTemplateCallback("notify_mouse_hidden", &propMouseHidden); @@ -2315,6 +2358,7 @@ pub const Surface = extern struct { signals.@"clipboard-read".impl.register(.{}); signals.@"clipboard-write".impl.register(.{}); signals.init.impl.register(.{}); + signals.menu.impl.register(.{}); signals.@"present-request".impl.register(.{}); signals.@"toggle-fullscreen".impl.register(.{}); signals.@"toggle-maximize".impl.register(.{}); diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 127aff1e1..009a815b5 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -590,6 +590,27 @@ pub const Window = extern struct { }; } + /// Sync the state of any actions on this window. + fn syncActions(self: *Self) void { + const has_selection = selection: { + const surface = self.getActiveSurface() orelse + break :selection false; + const core_surface = surface.core() orelse + break :selection false; + break :selection core_surface.hasSelection(); + }; + + const action_map: *gio.ActionMap = gobject.ext.cast( + gio.ActionMap, + self, + ) orelse return; + const action: *gio.SimpleAction = gobject.ext.cast( + gio.SimpleAction, + action_map.lookupAction("copy") orelse return, + ) orelse return; + action.setEnabled(@intFromBool(has_selection)); + } + fn toggleCssClass(self: *Self, class: [:0]const u8, value: bool) void { const widget = self.as(gtk.Widget); if (value) @@ -845,23 +866,7 @@ pub const Window = extern struct { const active = button.getActive() != 0; if (!active) return; - const has_selection = selection: { - const surface = self.getActiveSurface() orelse - break :selection false; - const core_surface = surface.core() orelse - break :selection false; - break :selection core_surface.hasSelection(); - }; - - const action_map: *gio.ActionMap = gobject.ext.cast( - gio.ActionMap, - self, - ) orelse return; - const action: *gio.SimpleAction = gobject.ext.cast( - gio.SimpleAction, - action_map.lookupAction("copy") orelse return, - ) orelse return; - action.setEnabled(@intFromBool(has_selection)); + self.syncActions(); } fn propQuickTerminal( @@ -1210,6 +1215,13 @@ pub const Window = extern struct { self, .{}, ); + _ = Surface.signals.menu.connect( + surface, + *Self, + surfaceMenu, + self, + .{}, + ); _ = Surface.signals.@"toggle-fullscreen".connect( surface, *Self, @@ -1355,6 +1367,13 @@ pub const Window = extern struct { } } + fn surfaceMenu( + _: *Surface, + self: *Self, + ) callconv(.c) void { + self.syncActions(); + } + fn surfacePresentRequest( surface: *Surface, self: *Self, diff --git a/src/apprt/gtk-ng/ui/1.2/surface.blp b/src/apprt/gtk-ng/ui/1.2/surface.blp index 4cbbef097..ab34cadac 100644 --- a/src/apprt/gtk-ng/ui/1.2/surface.blp +++ b/src/apprt/gtk-ng/ui/1.2/surface.blp @@ -15,19 +15,32 @@ template $GhosttySurface: Adw.Bin { focusable: false; focus-on-click: false; - GLArea gl_area { - realize => $gl_realize(); - unrealize => $gl_unrealize(); - render => $gl_render(); - resize => $gl_resize(); + child: Box { hexpand: true; vexpand: true; - focusable: true; - focus-on-click: true; - has-stencil-buffer: false; - has-depth-buffer: false; - use-es: false; - } + + GLArea gl_area { + realize => $gl_realize(); + unrealize => $gl_unrealize(); + render => $gl_render(); + resize => $gl_resize(); + hexpand: true; + vexpand: true; + focusable: true; + focus-on-click: true; + has-stencil-buffer: false; + has-depth-buffer: false; + use-es: false; + } + + PopoverMenu context_menu { + closed => $context_menu_closed(); + menu-model: context_menu_model; + flags: nested; + halign: start; + has-arrow: false; + } + }; [overlay] ProgressBar progress_bar_overlay { @@ -122,3 +135,104 @@ IMMulticontext im_context { preedit-end => $im_preedit_end(); commit => $im_commit(); } + +menu context_menu_model { + section { + item { + label: _("Copy"); + action: "win.copy"; + } + + item { + label: _("Paste"); + action: "win.paste"; + } + } + + section { + item { + label: _("Clear"); + action: "win.clear"; + } + + item { + label: _("Reset"); + action: "win.reset"; + } + } + + section { + submenu { + label: _("Split"); + + item { + label: _("Change Title…"); + action: "win.prompt-title"; + } + + item { + label: _("Split Up"); + action: "win.split-up"; + } + + item { + label: _("Split Down"); + action: "win.split-down"; + } + + item { + label: _("Split Left"); + action: "win.split-left"; + } + + item { + label: _("Split Right"); + action: "win.split-right"; + } + } + + submenu { + label: _("Tab"); + + item { + label: _("New Tab"); + action: "win.new-tab"; + } + + item { + label: _("Close Tab"); + action: "win.close-tab"; + } + } + + submenu { + label: _("Window"); + + item { + label: _("New Window"); + action: "win.new-window"; + } + + item { + label: _("Close Window"); + action: "win.close"; + } + } + } + + section { + submenu { + label: _("Config"); + + item { + label: _("Open Configuration"); + action: "app.open-config"; + } + + item { + label: _("Reload Configuration"); + action: "app.reload-config"; + } + } + } +} diff --git a/src/apprt/gtk/menu.zig b/src/apprt/gtk/menu.zig index d9d0083d0..50d0d1227 100644 --- a/src/apprt/gtk/menu.zig +++ b/src/apprt/gtk/menu.zig @@ -82,10 +82,6 @@ pub fn Menu( 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.