apprt/gtk: implement context menu

Implements context menu for GTK with:
- copy
- paste
- split right
- split down
- terminal inspector
This commit is contained in:
karei
2024-07-20 20:56:40 +03:00
parent e6a4bb99f7
commit 57db35036e
3 changed files with 136 additions and 4 deletions

View File

@ -53,6 +53,9 @@ cursor_none: ?*c.GdkCursor,
/// The shared application menu. /// The shared application menu.
menu: ?*c.GMenu = null, menu: ?*c.GMenu = null,
/// The shared context menu.
context_menu: ?*c.GMenu = null,
/// The configuration errors window, if it is currently open. /// The configuration errors window, if it is currently open.
config_errors_window: ?*ConfigErrorsWindow = null, 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.cursor_none) |cursor| c.g_object_unref(cursor);
if (self.menu) |menu| c.g_object_unref(menu); 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); if (self.transient_cgroup_base) |path| self.core_app.alloc.free(path);
self.config.deinit(); self.config.deinit();
@ -364,6 +368,8 @@ fn syncActionAccelerators(self: *App) !void {
try self.syncActionAccelerator("win.new_tab", .{ .new_tab = {} }); try self.syncActionAccelerator("win.new_tab", .{ .new_tab = {} });
try self.syncActionAccelerator("win.split_right", .{ .new_split = .right }); try self.syncActionAccelerator("win.split_right", .{ .new_split = .right });
try self.syncActionAccelerator("win.split_down", .{ .new_split = .down }); 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( fn syncActionAccelerator(
@ -460,6 +466,7 @@ pub fn run(self: *App) !void {
// Setup our menu items // Setup our menu items
self.initActions(); self.initActions();
self.initMenu(); self.initMenu();
self.initContextMenu();
// On startup, we want to check for configuration errors right away // 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 // 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; 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 { fn isValidAppId(app_id: [:0]const u8) bool {
if (app_id.len > 255 or app_id.len == 0) return false; if (app_id.len > 255 or app_id.len == 0) return false;
if (app_id[0] == '.') return false; if (app_id[0] == '.') return false;

View File

@ -1168,6 +1168,40 @@ pub fn showDesktopNotification(
c.g_application_send_notification(g_app, body.ptr, notif); 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 { fn gtkRealize(area: *c.GtkGLArea, ud: ?*anyopaque) callconv(.C) void {
log.debug("gl surface realized", .{}); log.debug("gl surface realized", .{});
@ -1303,8 +1337,8 @@ fn scaledCoordinates(
fn gtkMouseDown( fn gtkMouseDown(
gesture: *c.GtkGestureClick, gesture: *c.GtkGestureClick,
_: c.gint, _: c.gint,
_: c.gdouble, x: c.gdouble,
_: c.gdouble, y: c.gdouble,
ud: ?*anyopaque, ud: ?*anyopaque,
) callconv(.C) void { ) callconv(.C) void {
const self = userdataSelf(ud.?); const self = userdataSelf(ud.?);
@ -1320,10 +1354,22 @@ fn gtkMouseDown(
self.grabFocus(); 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}); log.err("error in key callback err={}", .{err});
return; 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( fn gtkMouseUp(

View File

@ -31,6 +31,8 @@ window: *c.GtkWindow,
/// The notebook (tab grouping) for this window. /// The notebook (tab grouping) for this window.
notebook: *c.GtkNotebook, notebook: *c.GtkNotebook,
context_menu: *c.GtkWidget,
pub fn create(alloc: Allocator, app: *App) !*Window { pub fn create(alloc: Allocator, app: *App) !*Window {
// Allocate a fixed pointer for our window. We try to minimize // Allocate a fixed pointer for our window. We try to minimize
// allocations but windows and other GUI requirements are so minimal // allocations but windows and other GUI requirements are so minimal
@ -51,6 +53,7 @@ pub fn init(self: *Window, app: *App) !void {
.app = app, .app = app,
.window = undefined, .window = undefined,
.notebook = undefined, .notebook = undefined,
.context_menu = undefined,
}; };
// Create the window // Create the window
@ -140,10 +143,16 @@ pub fn init(self: *Window, app: *App) !void {
} }
c.gtk_box_append(@ptrCast(box), notebook_widget); 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 we are in fullscreen mode, new windows start fullscreen.
if (app.config.fullscreen) c.gtk_window_fullscreen(self.window); if (app.config.fullscreen) c.gtk_window_fullscreen(self.window);
// All of our events // All of our events
_ = c.g_signal_connect_data(self.context_menu, "closed", c.G_CALLBACK(&gtkRefocusTerm), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(window, "close-request", c.G_CALLBACK(&gtkCloseRequest), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(window, "close-request", c.G_CALLBACK(&gtkCloseRequest), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(&gtkDestroy), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(&gtkDestroy), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(notebook, "page-added", c.G_CALLBACK(&gtkPageAdded), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(notebook, "page-added", c.G_CALLBACK(&gtkPageAdded), self, null, c.G_CONNECT_DEFAULT);
@ -173,6 +182,8 @@ fn initActions(self: *Window) void {
.{ "split_right", &gtkActionSplitRight }, .{ "split_right", &gtkActionSplitRight },
.{ "split_down", &gtkActionSplitDown }, .{ "split_down", &gtkActionSplitDown },
.{ "toggle_inspector", &gtkActionToggleInspector }, .{ "toggle_inspector", &gtkActionToggleInspector },
.{ "copy", &gtkActionCopy },
.{ "paste", &gtkActionPaste },
}; };
inline for (actions) |entry| { 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. /// Add a new tab to this window.
pub fn newTab(self: *Window, parent: ?*CoreSurface) !void { pub fn newTab(self: *Window, parent: ?*CoreSurface) !void {
@ -399,6 +412,16 @@ fn gtkNotebookCreateWindow(
return window.notebook; 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 { fn gtkCloseRequest(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool {
_ = v; _ = v;
log.debug("window close request", .{}); 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. /// Returns the surface to use for an action.
fn actionSurface(self: *Window) ?*CoreSurface { fn actionSurface(self: *Window) ?*CoreSurface {
const page_idx = c.gtk_notebook_get_current_page(self.notebook); const page_idx = c.gtk_notebook_get_current_page(self.notebook);