From 57db35036e4be645592d5bdfc46d2b9f2b1e86a3 Mon Sep 17 00:00:00 2001 From: karei Date: Sat, 20 Jul 2024 20:56:40 +0300 Subject: [PATCH] apprt/gtk: implement context menu Implements context menu for GTK with: - copy - paste - split right - split down - terminal inspector --- src/apprt/gtk/App.zig | 37 ++++++++++++++++++++++++++++ src/apprt/gtk/Surface.zig | 52 ++++++++++++++++++++++++++++++++++++--- src/apprt/gtk/Window.zig | 51 +++++++++++++++++++++++++++++++++++++- 3 files changed, 136 insertions(+), 4 deletions(-) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index b679d1b5f..cc7af2758 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -53,6 +53,9 @@ 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, @@ -300,6 +303,7 @@ pub fn terminate(self: *App) void { 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); self.config.deinit(); @@ -364,6 +368,8 @@ fn syncActionAccelerators(self: *App) !void { 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.copy", .{ .copy_to_clipboard = {} }); + try self.syncActionAccelerator("win.paste", .{ .paste_from_clipboard = {} }); } fn syncActionAccelerator( @@ -460,6 +466,7 @@ pub fn run(self: *App) !void { // Setup our menu items 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 @@ -786,6 +793,36 @@ fn initMenu(self: *App) void { 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, "Terminal Inspector", "win.toggle_inspector"); + } + + self.context_menu = menu; +} + 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 84a9d44b5..4d7ff688c 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -1168,6 +1168,40 @@ pub fn showDesktopNotification( c.g_application_send_notification(g_app, body.ptr, notif); } +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), + ) == c.False) { + 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); + c.gtk_popover_popup(@ptrCast(@alignCast(window.context_menu))); +} + fn gtkRealize(area: *c.GtkGLArea, ud: ?*anyopaque) callconv(.C) void { log.debug("gl surface realized", .{}); @@ -1303,8 +1337,8 @@ fn scaledCoordinates( fn gtkMouseDown( gesture: *c.GtkGestureClick, _: c.gint, - _: c.gdouble, - _: c.gdouble, + x: c.gdouble, + y: c.gdouble, ud: ?*anyopaque, ) callconv(.C) void { const self = userdataSelf(ud.?); @@ -1320,10 +1354,22 @@ fn gtkMouseDown( self.grabFocus(); } - _ = self.core_surface.mouseButtonCallback(.press, button, mods) catch |err| { + // Allow forcing context menu to open with ctrl in apps that would normally consume the click + if (button == .right and mods.ctrl) { + self.showContextMenu(@floatCast(x), @floatCast(y)); + return; + } + + const consumed = self.core_surface.mouseButtonCallback(.press, button, mods) catch |err| { log.err("error in key callback err={}", .{err}); return; }; + + // 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) { + self.showContextMenu(@floatCast(x), @floatCast(y)); + } } fn gtkMouseUp( diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 4dbe2a979..384145332 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -31,6 +31,8 @@ window: *c.GtkWindow, /// The notebook (tab grouping) for this window. notebook: *c.GtkNotebook, +context_menu: *c.GtkWidget, + pub fn create(alloc: Allocator, app: *App) !*Window { // Allocate a fixed pointer for our window. We try to minimize // allocations but windows and other GUI requirements are so minimal @@ -51,6 +53,7 @@ pub fn init(self: *Window, app: *App) !void { .app = app, .window = undefined, .notebook = undefined, + .context_menu = undefined, }; // Create the window @@ -140,10 +143,16 @@ pub fn init(self: *Window, app: *App) !void { } c.gtk_box_append(@ptrCast(box), notebook_widget); + self.context_menu = c.gtk_popover_menu_new_from_model(@ptrCast(@alignCast(self.app.context_menu))); + c.gtk_widget_set_parent(self.context_menu, window); + c.gtk_popover_set_has_arrow(@ptrCast(@alignCast(self.context_menu)), c.False); + c.gtk_widget_set_halign(self.context_menu, c.GTK_ALIGN_START); + // If we are in fullscreen mode, new windows start fullscreen. if (app.config.fullscreen) c.gtk_window_fullscreen(self.window); // 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, "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); _ = c.g_signal_connect_data(notebook, "page-added", c.G_CALLBACK(>kPageAdded), self, null, c.G_CONNECT_DEFAULT); @@ -173,6 +182,8 @@ fn initActions(self: *Window) void { .{ "split_right", >kActionSplitRight }, .{ "split_down", >kActionSplitDown }, .{ "toggle_inspector", >kActionToggleInspector }, + .{ "copy", >kActionCopy }, + .{ "paste", >kActionPaste }, }; inline for (actions) |entry| { @@ -190,7 +201,9 @@ fn initActions(self: *Window) void { } } -pub fn deinit(_: *Window) void {} +pub fn deinit(self: *Window) void { + c.gtk_widget_unparent(@ptrCast(self.context_menu)); +} /// Add a new tab to this window. pub fn newTab(self: *Window, parent: ?*CoreSurface) !void { @@ -399,6 +412,16 @@ fn gtkNotebookCreateWindow( return window.notebook; } +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", .{}); @@ -580,6 +603,32 @@ fn gtkActionToggleInspector( }; } +fn gtkActionCopy( + _: *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(.{ .copy_to_clipboard = {} }) catch |err| { + log.warn("error performing binding action error={}", .{err}); + return; + }; +} + +fn gtkActionPaste( + _: *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(.{ .paste_from_clipboard = {} }) catch |err| { + log.warn("error performing binding action error={}", .{err}); + return; + }; +} + /// Returns the surface to use for an action. fn actionSurface(self: *Window) ?*CoreSurface { const page_idx = c.gtk_notebook_get_current_page(self.notebook);