From 57db35036e4be645592d5bdfc46d2b9f2b1e86a3 Mon Sep 17 00:00:00 2001 From: karei Date: Sat, 20 Jul 2024 20:56:40 +0300 Subject: [PATCH 1/5] 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); From 6e5bc62726bf9c5520682a2b4698535ab7694f17 Mon Sep 17 00:00:00 2001 From: karei Date: Sat, 20 Jul 2024 23:02:17 +0300 Subject: [PATCH 2/5] apprt/gtk: disable copy in context menu while without selection Left a FIXME where the "Copy" button action is disabled. Though very hackish this was the best way I found to do this currently. Disable sensitivity on the button didn't do anything and trying to remove the button altogether like on macOS, causes the menu to become really buggy. Either by the context menu turning into a scrollable list or by it becoming really janky and showing the user pre-update UI. --- src/apprt/gtk/App.zig | 22 +++++++++++++++------- src/apprt/gtk/Surface.zig | 3 +++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index cc7af2758..60fefc6de 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -797,13 +797,7 @@ 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"); - } + createContextMenuCopyPasteSection(menu, false); { const section = c.g_menu_new(); @@ -823,6 +817,20 @@ fn initContextMenu(self: *App) void { self.context_menu = menu; } +fn createContextMenuCopyPasteSection(menu: ?*c.GMenu, has_selection: bool) void { + const section = c.g_menu_new(); + defer c.g_object_unref(section); + c.g_menu_prepend_section(menu, null, @ptrCast(@alignCast(section))); + // FIXME: Feels really hackish, but disabling sensitivity on this doesn't seems to work(?) + c.g_menu_append(section, "Copy", if (has_selection) "win.copy" else "noop"); + c.g_menu_append(section, "Paste", "win.paste"); +} + +pub fn refreshContextMenu(self: *App, has_selection: bool) void { + c.g_menu_remove(self.context_menu, 0); + createContextMenuCopyPasteSection(self.context_menu, has_selection); +} + 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 4d7ff688c..390cf8c90 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -1199,6 +1199,9 @@ fn showContextMenu(self: *Surface, x: f32, y: f32) void { }; c.gtk_popover_set_pointing_to(@ptrCast(@alignCast(window.context_menu)), &rect); + + self.app.refreshContextMenu(self.core_surface.hasSelection()); + c.gtk_popover_popup(@ptrCast(@alignCast(window.context_menu))); } From cc3b04057165478f41fbd5448dfb156e3aedf4ad Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 22 Jul 2024 10:21:18 -0700 Subject: [PATCH 3/5] apprt/gtk: get rid of forcing context menu for now We have escapes (shift) so lets see how that goes and compare to some other GTK apps first. --- src/apprt/gtk/Surface.zig | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 390cf8c90..e6ec1e44a 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -1357,19 +1357,14 @@ fn gtkMouseDown( self.grabFocus(); } - // 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 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)); } From 53e942abae16635f10e53eb40f65f1823e7d8a25 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 22 Jul 2024 10:29:59 -0700 Subject: [PATCH 4/5] apprt/gtk: some stylistic changes --- src/apprt/gtk/Surface.zig | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index e6ec1e44a..9cd3d62f4 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -1177,10 +1177,7 @@ fn showContextMenu(self: *Surface, x: f32, y: f32) void { return; }; - var point: c.graphene_point_t = .{ - .x = x, - .y = y, - }; + var point: c.graphene_point_t = .{ .x = x, .y = y }; if (c.gtk_widget_compute_point( self.primaryWidget(), @ptrCast(window.window), @@ -1199,9 +1196,7 @@ fn showContextMenu(self: *Surface, x: f32, y: f32) void { }; c.gtk_popover_set_pointing_to(@ptrCast(@alignCast(window.context_menu)), &rect); - self.app.refreshContextMenu(self.core_surface.hasSelection()); - c.gtk_popover_popup(@ptrCast(@alignCast(window.context_menu))); } From 5eb7925446dd3a351afc0d7c206accdfc0817338 Mon Sep 17 00:00:00 2001 From: karei Date: Mon, 22 Jul 2024 21:03:49 +0300 Subject: [PATCH 5/5] apprt/gtk: don't dim surface when opening context menu --- src/apprt/gtk/Surface.zig | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 9cd3d62f4..ee6c6869e 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -1887,7 +1887,7 @@ fn gtkFocusLeave(_: *c.GtkEventControllerFocus, ud: ?*anyopaque) callconv(.C) vo // Notify our IM context c.gtk_im_context_focus_out(self.im_context); - // We only dim the surface if we are a split + // We only try dimming the surface if we are a split switch (self.container) { .split_br, .split_tl, @@ -1904,6 +1904,16 @@ 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 { + 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 == c.True) return; + if (self.unfocused_widget != null) return; self.unfocused_widget = c.gtk_drawing_area_new(); c.gtk_widget_add_css_class(self.unfocused_widget.?, "unfocused-split");