mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-01-01 03:02:15 +00:00
Merge pull request #1979 from kareigu/gtk-context-menu
apprt/gtk: implement context menu
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,44 @@ 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);
|
||||
|
||||
createContextMenuCopyPasteSection(menu, false);
|
||||
|
||||
{
|
||||
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 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;
|
||||
|
||||
@@ -1168,6 +1168,38 @@ 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);
|
||||
self.app.refreshContextMenu(self.core_surface.hasSelection());
|
||||
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 +1335,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 +1352,17 @@ fn gtkMouseDown(
|
||||
self.grabFocus();
|
||||
}
|
||||
|
||||
_ = self.core_surface.mouseButtonCallback(.press, button, mods) catch |err| {
|
||||
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(
|
||||
@@ -1848,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,
|
||||
@@ -1865,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");
|
||||
|
||||
@@ -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