Skip to content

Commit

Permalink
gtk: there can be only one: menu
Browse files Browse the repository at this point in the history
Unify the main and context menus. Right now they are identical when
displayed as a main menu and when displayed as a context menu.

Both the main menu and context menu also now use GtkPopoverMenu to
display the menu. Each surface has a separate context menu to eliminate
bugs with positioning the context menus when they pop up.

The menus are built using GtkBuilder XML files, which reduces the amount
of code necessary to build the menus and enables translation in the
future as well as making changes easier.
  • Loading branch information
jcollie committed Jan 14, 2025
1 parent 9a47cda commit 0de2748
Show file tree
Hide file tree
Showing 7 changed files with 422 additions and 186 deletions.
125 changes: 20 additions & 105 deletions src/apprt/gtk/App.zig
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,6 @@ single_instance: bool,
/// The "none" cursor. We use one that is shared across the entire app.
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,

Expand Down Expand Up @@ -480,8 +474,6 @@ pub fn terminate(self: *App) void {
c.g_object_unref(self.app);

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);

for (self.custom_css_providers.items) |provider| {
Expand Down Expand Up @@ -1030,20 +1022,28 @@ fn updateConfigErrors(self: *App) !void {
}

fn syncActionAccelerators(self: *App) !void {
try self.syncActionAccelerator("app.quit", .{ .quit = {} });
try self.syncActionAccelerator("app.open-config", .{ .open_config = {} });
try self.syncActionAccelerator("app.reload-config", .{ .reload_config = {} });
try self.syncActionAccelerator("win.toggle_inspector", .{ .inspector = .toggle });
try self.syncActionAccelerator("win.close", .{ .close_surface = {} });
try self.syncActionAccelerator("win.new_window", .{ .new_window = {} });
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.split_left", .{ .new_split = .left });
try self.syncActionAccelerator("win.split_up", .{ .new_split = .up });
try self.syncActionAccelerator("win.copy", .{ .copy_to_clipboard = {} });
try self.syncActionAccelerator("win.paste", .{ .paste_from_clipboard = {} });

try self.syncActionAccelerator("win.new-window", .{ .new_window = {} });
try self.syncActionAccelerator("win.close", .{ .close_window = {} });

try self.syncActionAccelerator("win.new-tab", .{ .new_tab = {} });
try self.syncActionAccelerator("win.close-tab", .{ .close_tab = {} });

try self.syncActionAccelerator("win.split-up", .{ .new_split = .up });
try self.syncActionAccelerator("win.split-down", .{ .new_split = .down });
try self.syncActionAccelerator("win.split-left", .{ .new_split = .left });
try self.syncActionAccelerator("win.split-right", .{ .new_split = .right });

try self.syncActionAccelerator("win.clear", .{ .clear_screen = {} });
try self.syncActionAccelerator("win.reset", .{ .reset = {} });

try self.syncActionAccelerator("win.toggle-inspector", .{ .inspector = .toggle });
try self.syncActionAccelerator("app.open-config", .{ .open_config = {} });
try self.syncActionAccelerator("app.reload-config", .{ .reload_config = {} });

try self.syncActionAccelerator("app.quit", .{ .quit = {} });
}

fn syncActionAccelerator(
Expand Down Expand Up @@ -1274,10 +1274,8 @@ pub fn run(self: *App) !void {
// Setup our D-Bus connection for listening to settings changes.
self.initDbus();

// Setup our menu items
// Setup our actions
self.initActions();
self.initMenu();
self.initContextMenu();

// Setup our initial color scheme
self.colorSchemeEvent(self.getColorScheme());
Expand Down Expand Up @@ -1817,89 +1815,6 @@ fn initActions(self: *App) void {
}
}

/// Initializes and populates the provided GMenu with sections and actions.
/// This function is used to set up the application's menu structure, either for
/// the main menu button or as a context menu when window decorations are disabled.
fn initMenuContent(menu: *c.GMenu) void {
{
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, "New Window", "win.new_window");
c.g_menu_append(section, "New Tab", "win.new_tab");
c.g_menu_append(section, "Close Tab", "win.close_tab");
c.g_menu_append(section, "Split Right", "win.split_right");
c.g_menu_append(section, "Split Down", "win.split_down");
c.g_menu_append(section, "Close Window", "win.close");
}

{
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");
c.g_menu_append(section, "Open Configuration", "app.open-config");
c.g_menu_append(section, "Reload Configuration", "app.reload-config");
c.g_menu_append(section, "About Ghostty", "win.about");
}
}

/// This sets the self.menu property to the application menu that can be
/// shared by all application windows.
fn initMenu(self: *App) void {
const menu = c.g_menu_new();
errdefer c.g_object_unref(menu);
initMenuContent(@ptrCast(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, "Reset", "win.reset");
c.g_menu_append(section, "Terminal Inspector", "win.toggle_inspector");
}

if (!self.config.@"window-decoration".isCSD()) {
const section = c.g_menu_new();
defer c.g_object_unref(section);
const submenu = c.g_menu_new();
defer c.g_object_unref(submenu);

initMenuContent(@ptrCast(submenu));
c.g_menu_append_submenu(section, "Menu", @ptrCast(@alignCast(submenu)));
c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
}

self.context_menu = menu;
}

pub fn refreshContextMenu(_: *App, window: ?*c.GtkWindow, has_selection: bool) void {
const action: ?*c.GSimpleAction = @ptrCast(c.g_action_map_lookup_action(@ptrCast(window), "copy"));
c.g_simple_action_set_enabled(action, if (has_selection) 1 else 0);
}

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;
Expand Down
49 changes: 13 additions & 36 deletions src/apprt/gtk/Surface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const App = @import("App.zig");
const Split = @import("Split.zig");
const Tab = @import("Tab.zig");
const Window = @import("Window.zig");
const Menu = @import("menu.zig").Menu;
const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig");
const ResizeOverlay = @import("ResizeOverlay.zig");
const inspector = @import("inspector.zig");
Expand Down Expand Up @@ -379,6 +380,9 @@ im_len: u7 = 0,
/// details on what this is.
cgroup_path: ?[]const u8 = null,

/// Our context menu.
menu: Menu(Surface),

/// Configuration used for initializing the surface. We have to copy some
/// data since initialization is delayed with GTK (on realize).
pub const InitConfig = struct {
Expand Down Expand Up @@ -563,9 +567,14 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
.cursor_pos = .{ .x = 0, .y = 0 },
.im_context = im_context,
.cgroup_path = cgroup_path,
.menu = undefined,
};
errdefer self.* = undefined;

// initialize the context menu
self.menu.init();
self.menu.setParent(overlay);

// Set our default mouse shape
try self.setMouseShape(.text);

Expand Down Expand Up @@ -1214,6 +1223,7 @@ fn getClipboard(widget: *c.GtkWidget, clipboard: apprt.Clipboard) ?*c.GdkClipboa
.selection, .primary => c.gtk_widget_get_primary_clipboard(widget),
};
}

pub fn getCursorPos(self: *const Surface) !apprt.CursorPos {
return self.cursor_pos;
}
Expand Down Expand Up @@ -1251,38 +1261,6 @@ pub fn showDesktopNotification(
c.g_application_send_notification(g_app, body.ptr, notification);
}

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),
) == 0) {
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(window.window, 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", .{});

Expand Down Expand Up @@ -1453,7 +1431,7 @@ fn gtkMouseDown(
// 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));
self.menu.popupAt(x, y);
}
}

Expand Down Expand Up @@ -1999,15 +1977,14 @@ 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 {
_ = 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 == 1) return;
if (self.menu.isVisible()) return;

if (self.unfocused_widget != null) return;
self.unfocused_widget = c.gtk_drawing_area_new();
Expand Down
Loading

0 comments on commit 0de2748

Please sign in to comment.