mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 08:46:08 +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.
|
/// 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;
|
||||||
|
@ -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(
|
||||||
|
@ -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(>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, "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(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);
|
_ = 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_right", >kActionSplitRight },
|
||||||
.{ "split_down", >kActionSplitDown },
|
.{ "split_down", >kActionSplitDown },
|
||||||
.{ "toggle_inspector", >kActionToggleInspector },
|
.{ "toggle_inspector", >kActionToggleInspector },
|
||||||
|
.{ "copy", >kActionCopy },
|
||||||
|
.{ "paste", >kActionPaste },
|
||||||
};
|
};
|
||||||
|
|
||||||
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);
|
||||||
|
Reference in New Issue
Block a user