mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
apprt/gtk: implement context menu
Implements context menu for GTK with: - copy - paste - split right - split down - terminal inspector
This commit is contained in:
@ -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;
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
|
Reference in New Issue
Block a user