Skip to content

Commit

Permalink
gtk: update menus to use popovers and builder ui files (#5781)
Browse files Browse the repository at this point in the history
Menus (context menus and the window hamburger menu) now use popovers and
are defined using GTK builder UI files. This is a bit more "modern" and
reduces the amount of code to define menus.
  • Loading branch information
jcollie authored Feb 22, 2025
2 parents e307f1a + 2d5a07c commit 3f715c2
Show file tree
Hide file tree
Showing 11 changed files with 423 additions and 181 deletions.
112 changes: 11 additions & 101 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 @@ -448,8 +442,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 @@ -478,7 +470,6 @@ pub fn performAction(
}),
.toggle_maximize => self.toggleMaximize(target),
.toggle_fullscreen => self.toggleFullscreen(target, value),

.new_tab => try self.newTab(target),
.close_tab => try self.closeTab(target),
.goto_tab => return self.gotoTab(target, value),
Expand Down Expand Up @@ -1012,17 +1003,19 @@ 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.toggle-inspector", .{ .inspector = .toggle });
try self.syncActionAccelerator("win.close", .{ .close_window = {} });
try self.syncActionAccelerator("win.new-window", .{ .new_window = {} });
try self.syncActionAccelerator("win.new-tab", .{ .new_tab = {} });
try self.syncActionAccelerator("win.close-tab", .{ .close_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.reset", .{ .reset = {} });
try self.syncActionAccelerator("win.clear", .{ .clear_screen = {} });
}

fn syncActionAccelerator(
Expand Down Expand Up @@ -1254,10 +1247,8 @@ pub fn run(self: *App) !void {
// and asynchronously request the initial color scheme
self.initDbus();

// Setup our menu items
// Setup our actions
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
Expand Down Expand Up @@ -1775,87 +1766,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");
}

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
4 changes: 2 additions & 2 deletions src/apprt/gtk/Builder.zig
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,15 @@ pub fn setWidgetClassTemplate(self: *const Builder, class: *gtk.WidgetClass) voi
class.setTemplateFromResource(self.resource_name);
}

pub fn getObject(self: *Builder, name: [:0]const u8) ?*gobject.Object {
pub fn getObject(self: *Builder, comptime T: type, name: [:0]const u8) ?*T {
const builder = builder: {
if (self.builder) |builder| break :builder builder;
const builder = gtk.Builder.newFromResource(self.resource_name);
self.builder = builder;
break :builder builder;
};

return builder.getObject(name);
return gobject.ext.cast(T, builder.getObject(name) orelse return null);
}

pub fn deinit(self: *const Builder) void {
Expand Down
53 changes: 14 additions & 39 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 @@ -378,6 +379,9 @@ im_len: u7 = 0,
/// details on what this is.
cgroup_path: ?[]const u8 = null,

/// Our context menu.
context_menu: Menu(Surface, "context_menu", false),

/// The state of the key event while we're doing IM composition.
/// See gtkKeyPressed for detailed descriptions.
pub const IMKeyEvent = enum {
Expand Down Expand Up @@ -576,9 +580,14 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
.cursor_pos = .{ .x = -1, .y = -1 },
.im_context = im_context,
.cgroup_path = cgroup_path,
.context_menu = undefined,
};
errdefer self.* = undefined;

// initialize the context menu
self.context_menu.init(self);
self.context_menu.setParent(@ptrCast(@alignCast(overlay)));

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

Expand Down Expand Up @@ -913,7 +922,7 @@ fn updateTitleLabels(self: *Surface) void {

// If we have a tab and are the focused child, then we have to update the tab
if (self.container.tab()) |tab| {
if (tab.focus_child == self) tab.setLabelText(title);
if (tab.focus_child == self) tab.setTitleText(title);
}

// If we have a window and are focused, then we have to update the window title.
Expand Down Expand Up @@ -1224,6 +1233,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 @@ -1261,40 +1271,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;
};

// Convert surface coordinate into coordinate space of the
// context menu's parent
var point: c.graphene_point_t = .{ .x = x, .y = y };
if (c.gtk_widget_compute_point(
self.primaryWidget(),
c.gtk_widget_get_parent(@ptrCast(window.context_menu)),
&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 @@ -1465,7 +1441,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.context_menu.popupAt(@intFromFloat(x), @intFromFloat(y));
}
}

Expand Down Expand Up @@ -2073,15 +2049,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.context_menu.isVisible()) return;

if (self.unfocused_widget != null) return;
self.unfocused_widget = c.gtk_drawing_area_new();
Expand Down
4 changes: 2 additions & 2 deletions src/apprt/gtk/Tab.zig
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,8 @@ pub fn replaceElem(self: *Tab, elem: Surface.Container.Elem) void {
self.elem = elem;
}

pub fn setLabelText(self: *Tab, title: [:0]const u8) void {
self.window.notebook.setTabLabel(self, title);
pub fn setTitleText(self: *Tab, title: [:0]const u8) void {
self.window.notebook.setTabTitle(self, title);
}

pub fn setTooltipText(self: *Tab, tooltip: [:0]const u8) void {
Expand Down
4 changes: 2 additions & 2 deletions src/apprt/gtk/TabView.zig
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ pub fn reorderPage(self: *TabView, tab: *Tab, position: c_int) void {
_ = self.tab_view.reorderPage(page, position);
}

pub fn setTabLabel(self: *TabView, tab: *Tab, title: [:0]const u8) void {
pub fn setTabTitle(self: *TabView, tab: *Tab, title: [:0]const u8) void {
const page = self.tab_view.getPage(@ptrCast(tab.box));
page.setTitle(title.ptr);
}
Expand All @@ -188,7 +188,7 @@ pub fn addTab(self: *TabView, tab: *Tab, title: [:0]const u8) void {
const position = self.newTabInsertPosition(tab);
const box_widget: *gtk.Widget = @ptrCast(tab.box);
const page = self.tab_view.insert(box_widget, position);
self.setTabLabel(tab, title);
self.setTabTitle(tab, title);
self.tab_view.setSelectedPage(page);
}

Expand Down
Loading

0 comments on commit 3f715c2

Please sign in to comment.