adw tab headers should have their own menu

This commit is contained in:
Jeffrey C. Ollie
2024-11-02 10:02:36 -05:00
parent c64bcabb6e
commit 229342d508
2 changed files with 124 additions and 46 deletions

View File

@ -42,8 +42,7 @@ header: ?*c.GtkWidget,
tab_overview: ?*c.GtkWidget, tab_overview: ?*c.GtkWidget,
/// The notebook (tab grouping) for this window. /// The notebook (tab grouping) for this window.
/// can be either c.GtkNotebook or c.AdwTabView. notebook: *Notebook,
notebook: Notebook,
context_menu: *c.GtkWidget, context_menu: *c.GtkWidget,
@ -54,7 +53,7 @@ toast_overlay: ?*c.GtkWidget,
/// See adwTabOverviewOpen for why we have this. /// See adwTabOverviewOpen for why we have this.
adw_tab_overview_focus_timer: ?c.guint = null, adw_tab_overview_focus_timer: ?c.guint = null,
pub fn create(alloc: Allocator, app: *App) !*Window { pub fn create(alloc: Allocator, app: *App) std.mem.Allocator.Error!*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
// compared to the steady-state terminal operation so we use heap // compared to the steady-state terminal operation so we use heap
@ -64,11 +63,11 @@ pub fn create(alloc: Allocator, app: *App) !*Window {
// freed when the window is closed. // freed when the window is closed.
var window = try alloc.create(Window); var window = try alloc.create(Window);
errdefer alloc.destroy(window); errdefer alloc.destroy(window);
try window.init(app); try window.init(alloc, app);
return window; return window;
} }
pub fn init(self: *Window, app: *App) !void { pub fn init(self: *Window, alloc: Allocator, app: *App) std.mem.Allocator.Error!void {
// Set up our own state // Set up our own state
self.* = .{ self.* = .{
.app = app, .app = app,
@ -226,7 +225,7 @@ pub fn init(self: *Window, app: *App) !void {
} }
// Setup our notebook // Setup our notebook
self.notebook = Notebook.create(self); self.notebook = try Notebook.create(alloc, self);
// Setup our toast overlay if we have one // Setup our toast overlay if we have one
self.toast_overlay = if (adwaita.enabled(&self.app.config)) toast: { self.toast_overlay = if (adwaita.enabled(&self.app.config)) toast: {
@ -245,8 +244,8 @@ pub fn init(self: *Window, app: *App) !void {
// If we have a tab overview then we can set it on our notebook. // If we have a tab overview then we can set it on our notebook.
if (self.tab_overview) |tab_overview| { if (self.tab_overview) |tab_overview| {
if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable; if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable;
assert(self.notebook == .adw_tab_view); assert(self.notebook.* == .adw_tab_view);
c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.adw_tab_view); c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.adw_tab_view.tab_view);
} }
self.context_menu = c.gtk_popover_menu_new_from_model(@ptrCast(@alignCast(self.app.context_menu))); self.context_menu = c.gtk_popover_menu_new_from_model(@ptrCast(@alignCast(self.app.context_menu)));
@ -280,7 +279,7 @@ pub fn init(self: *Window, app: *App) !void {
const header_widget: *c.GtkWidget = @ptrCast(@alignCast(self.header.?)); const header_widget: *c.GtkWidget = @ptrCast(@alignCast(self.header.?));
c.adw_toolbar_view_add_top_bar(toolbar_view, header_widget); c.adw_toolbar_view_add_top_bar(toolbar_view, header_widget);
const tab_bar = c.adw_tab_bar_new(); const tab_bar = c.adw_tab_bar_new();
c.adw_tab_bar_set_view(tab_bar, self.notebook.adw_tab_view); c.adw_tab_bar_set_view(tab_bar, self.notebook.adw_tab_view.tab_view);
if (!app.config.@"gtk-wide-tabs") c.adw_tab_bar_set_expand_tabs(tab_bar, 0); if (!app.config.@"gtk-wide-tabs") c.adw_tab_bar_set_expand_tabs(tab_bar, 0);
@ -323,7 +322,7 @@ pub fn init(self: *Window, app: *App) !void {
); );
} }
} else { } else {
switch (self.notebook) { switch (self.notebook.*) {
.adw_tab_view => |tab_view| if (comptime adwaita.versionAtLeast(0, 0, 0)) { .adw_tab_view => |tab_view| if (comptime adwaita.versionAtLeast(0, 0, 0)) {
// In earlier adwaita versions, we need to add the tabbar manually since we do not use // In earlier adwaita versions, we need to add the tabbar manually since we do not use
// an AdwToolbarView. // an AdwToolbarView.
@ -344,7 +343,7 @@ pub fn init(self: *Window, app: *App) !void {
@ptrCast(@alignCast(tab_bar)), @ptrCast(@alignCast(tab_bar)),
), ),
} }
c.adw_tab_bar_set_view(tab_bar, tab_view); c.adw_tab_bar_set_view(tab_bar, tab_view.tab_view);
if (!app.config.@"gtk-wide-tabs") { if (!app.config.@"gtk-wide-tabs") {
c.adw_tab_bar_set_expand_tabs(tab_bar, 0); c.adw_tab_bar_set_expand_tabs(tab_bar, 0);
@ -403,6 +402,8 @@ pub fn deinit(self: *Window) void {
if (self.adw_tab_overview_focus_timer) |timer| { if (self.adw_tab_overview_focus_timer) |timer| {
_ = c.g_source_remove(timer); _ = c.g_source_remove(timer);
} }
self.app.core_app.alloc.destroy(self.notebook);
} }
/// Returns true if this window should use an Adwaita window. /// Returns true if this window should use an Adwaita window.
@ -552,14 +553,14 @@ fn gtkNewTabFromOverview(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) ?*c.AdwT
const alloc = self.app.core_app.alloc; const alloc = self.app.core_app.alloc;
const surface = self.actionSurface(); const surface = self.actionSurface();
const tab = Tab.create(alloc, self, surface) catch return null; const tab = Tab.create(alloc, self, surface) catch return null;
return c.adw_tab_view_get_page(self.notebook.adw_tab_view, @ptrCast(@alignCast(tab.box))); return c.adw_tab_view_get_page(self.notebook.adw_tab_view.tab_view, @ptrCast(@alignCast(tab.box)));
} }
fn adwTabOverviewOpen( fn adwTabOverviewOpen(
object: *c.GObject, object: *c.GObject,
_: *c.GParamSpec, _: *c.GParamSpec,
ud: ?*anyopaque, ud: ?*anyopaque,
) void { ) callconv(.C) void {
const tab_overview: *c.AdwTabOverview = @ptrCast(@alignCast(object)); const tab_overview: *c.AdwTabOverview = @ptrCast(@alignCast(object));
// We only care about when the tab overview is closed. // We only care about when the tab overview is closed.

View File

@ -14,16 +14,36 @@ const AdwTabPage = if (adwaita.versionAtLeast(0, 0, 0)) c.AdwTabPage else anyopa
/// An abstraction over the GTK notebook and Adwaita tab view to manage /// An abstraction over the GTK notebook and Adwaita tab view to manage
/// all the terminal tabs in a window. /// all the terminal tabs in a window.
pub const Notebook = union(enum) { pub const Notebook = union(enum) {
adw_tab_view: *AdwTabView, adw_tab_view: struct {
/// the tab view
tab_view: *AdwTabView,
/// the last tab to have the context menu shown
last_tab: ?*Tab,
},
gtk_notebook: *c.GtkNotebook, gtk_notebook: *c.GtkNotebook,
pub fn create(window: *Window) Notebook { pub fn create(alloc: std.mem.Allocator, window: *Window) std.mem.Allocator.Error!*Notebook {
// Allocate a fixed pointer for our notebook. We try to minimize
// allocations but windows and other GUI requirements are so minimal
// compared to the steady-state terminal operation so we use heap
// allocation for this.
//
// The allocation is owned by the GtkWindow created. It will be
// freed when the window is closed.
var notebook = try alloc.create(Notebook);
errdefer alloc.destroy(notebook);
const app = window.app; const app = window.app;
if (adwaita.enabled(&app.config)) return initAdw(window); if (adwaita.enabled(&app.config)) {
return initGtk(window); notebook.initAdw(window);
return notebook;
}
notebook.initGtk(window);
return notebook;
} }
fn initGtk(window: *Window) Notebook { fn initGtk(self: *Notebook, window: *Window) void {
const app = window.app; const app = window.app;
// Create a notebook to hold our tabs. // Create a notebook to hold our tabs.
@ -58,10 +78,10 @@ pub const Notebook = union(enum) {
_ = c.g_signal_connect_data(notebook, "switch-page", c.G_CALLBACK(&gtkSwitchPage), window, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(notebook, "switch-page", c.G_CALLBACK(&gtkSwitchPage), window, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(notebook, "create-window", c.G_CALLBACK(&gtkNotebookCreateWindow), window, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(notebook, "create-window", c.G_CALLBACK(&gtkNotebookCreateWindow), window, null, c.G_CONNECT_DEFAULT);
return .{ .gtk_notebook = notebook }; self.* = .{ .gtk_notebook = notebook };
} }
fn initAdw(window: *Window) Notebook { fn initAdw(self: *Notebook, window: *Window) void {
const app = window.app; const app = window.app;
assert(adwaita.enabled(&app.config)); assert(adwaita.enabled(&app.config));
@ -73,19 +93,24 @@ pub const Notebook = union(enum) {
c.adw_tab_view_remove_shortcuts(tab_view, c.ADW_TAB_VIEW_SHORTCUT_ALL_SHORTCUTS); c.adw_tab_view_remove_shortcuts(tab_view, c.ADW_TAB_VIEW_SHORTCUT_ALL_SHORTCUTS);
} }
c.adw_tab_view_set_menu_model(tab_view, @ptrCast(@alignCast(app.context_menu))); self.* = .{
.adw_tab_view = .{
.tab_view = tab_view,
.last_tab = null,
},
};
self.initContextMenu(window);
_ = c.g_signal_connect_data(tab_view, "page-attached", c.G_CALLBACK(&adwPageAttached), window, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(tab_view, "page-attached", c.G_CALLBACK(&adwPageAttached), window, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(tab_view, "create-window", c.G_CALLBACK(&adwTabViewCreateWindow), window, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(tab_view, "create-window", c.G_CALLBACK(&adwTabViewCreateWindow), window, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(tab_view, "setup-menu", c.G_CALLBACK(&adwTabViewSetupMenu), window, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(tab_view, "notify::selected-page", c.G_CALLBACK(&adwSelectPage), window, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(tab_view, "notify::selected-page", c.G_CALLBACK(&adwSelectPage), window, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(tab_view, "setup-menu", c.G_CALLBACK(&adwTabViewSetupMenu), self, null, c.G_CONNECT_DEFAULT);
return .{ .adw_tab_view = tab_view };
} }
pub fn asWidget(self: Notebook) *c.GtkWidget { pub fn asWidget(self: Notebook) *c.GtkWidget {
return switch (self) { return switch (self) {
.adw_tab_view => |tab_view| @ptrCast(@alignCast(tab_view)), .adw_tab_view => |tab_view| @ptrCast(@alignCast(tab_view.tab_view)),
.gtk_notebook => |notebook| @ptrCast(@alignCast(notebook)), .gtk_notebook => |notebook| @ptrCast(@alignCast(notebook)),
}; };
} }
@ -94,7 +119,7 @@ pub const Notebook = union(enum) {
return switch (self) { return switch (self) {
.gtk_notebook => |notebook| c.gtk_notebook_get_n_pages(notebook), .gtk_notebook => |notebook| c.gtk_notebook_get_n_pages(notebook),
.adw_tab_view => |tab_view| if (comptime adwaita.versionAtLeast(0, 0, 0)) .adw_tab_view => |tab_view| if (comptime adwaita.versionAtLeast(0, 0, 0))
c.adw_tab_view_get_n_pages(tab_view) c.adw_tab_view_get_n_pages(tab_view.tab_view)
else else
unreachable, unreachable,
}; };
@ -106,8 +131,8 @@ pub const Notebook = union(enum) {
switch (self) { switch (self) {
.adw_tab_view => |tab_view| { .adw_tab_view => |tab_view| {
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable; if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
const page = c.adw_tab_view_get_selected_page(tab_view) orelse return null; const page = c.adw_tab_view_get_selected_page(tab_view.tab_view) orelse return null;
return c.adw_tab_view_get_page_position(tab_view, page); return c.adw_tab_view_get_page_position(tab_view.tab_view, page);
}, },
.gtk_notebook => |notebook| { .gtk_notebook => |notebook| {
@ -122,7 +147,7 @@ pub const Notebook = union(enum) {
const child = switch (self) { const child = switch (self) {
.adw_tab_view => |tab_view| child: { .adw_tab_view => |tab_view| child: {
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable; if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
const page = c.adw_tab_view_get_selected_page(tab_view) orelse return null; const page = c.adw_tab_view_get_selected_page(tab_view.tab_view) orelse return null;
const child = c.adw_tab_page_get_child(page); const child = c.adw_tab_page_get_child(page);
break :child child; break :child child;
}, },
@ -141,8 +166,8 @@ pub const Notebook = union(enum) {
switch (self) { switch (self) {
.adw_tab_view => |tab_view| { .adw_tab_view => |tab_view| {
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable; if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
const page_to_select = c.adw_tab_view_get_nth_page(tab_view, position); const page_to_select = c.adw_tab_view_get_nth_page(tab_view.tab_view, position);
c.adw_tab_view_set_selected_page(tab_view, page_to_select); c.adw_tab_view_set_selected_page(tab_view.tab_view, page_to_select);
}, },
.gtk_notebook => |notebook| c.gtk_notebook_set_current_page(notebook, position), .gtk_notebook => |notebook| c.gtk_notebook_set_current_page(notebook, position),
} }
@ -152,8 +177,8 @@ pub const Notebook = union(enum) {
return switch (self) { return switch (self) {
.adw_tab_view => |tab_view| page_idx: { .adw_tab_view => |tab_view| page_idx: {
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable; if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
const page = c.adw_tab_view_get_page(tab_view, @ptrCast(tab.box)) orelse return null; const page = c.adw_tab_view_get_page(tab_view.tab_view, @ptrCast(tab.box)) orelse return null;
break :page_idx c.adw_tab_view_get_page_position(tab_view, page); break :page_idx c.adw_tab_view_get_page_position(tab_view.tab_view, page);
}, },
.gtk_notebook => |notebook| page_idx: { .gtk_notebook => |notebook| page_idx: {
const page = c.gtk_notebook_get_page(notebook, @ptrCast(tab.box)) orelse return null; const page = c.gtk_notebook_get_page(notebook, @ptrCast(tab.box)) orelse return null;
@ -210,8 +235,8 @@ pub const Notebook = union(enum) {
}, },
.adw_tab_view => |tab_view| { .adw_tab_view => |tab_view| {
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable; if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
const page = c.adw_tab_view_get_page(tab_view, @ptrCast(tab.box)); const page = c.adw_tab_view_get_page(tab_view.tab_view, @ptrCast(tab.box));
_ = c.adw_tab_view_reorder_page(tab_view, page, position); _ = c.adw_tab_view_reorder_page(tab_view.tab_view, page, position);
}, },
} }
} }
@ -220,7 +245,7 @@ pub const Notebook = union(enum) {
switch (self) { switch (self) {
.adw_tab_view => |tab_view| { .adw_tab_view => |tab_view| {
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable; if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
const page = c.adw_tab_view_get_page(tab_view, @ptrCast(tab.box)); const page = c.adw_tab_view_get_page(tab_view.tab_view, @ptrCast(tab.box));
c.adw_tab_page_set_title(page, title.ptr); c.adw_tab_page_set_title(page, title.ptr);
}, },
.gtk_notebook => c.gtk_label_set_text(tab.label_text, title.ptr), .gtk_notebook => c.gtk_label_set_text(tab.label_text, title.ptr),
@ -253,11 +278,11 @@ pub const Notebook = union(enum) {
.adw_tab_view => |tab_view| { .adw_tab_view => |tab_view| {
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable; if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
const page = c.adw_tab_view_insert(tab_view, box_widget, self.newTabInsertPosition(tab)); const page = c.adw_tab_view_insert(tab_view.tab_view, box_widget, self.newTabInsertPosition(tab));
c.adw_tab_page_set_title(page, title.ptr); c.adw_tab_page_set_title(page, title.ptr);
// Switch to the new tab // Switch to the new tab
c.adw_tab_view_set_selected_page(tab_view, page); c.adw_tab_view_set_selected_page(tab_view.tab_view, page);
}, },
.gtk_notebook => |notebook| { .gtk_notebook => |notebook| {
// Build the tab label // Build the tab label
@ -327,8 +352,8 @@ pub const Notebook = union(enum) {
.adw_tab_view => |tab_view| { .adw_tab_view => |tab_view| {
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable; if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
const page = c.adw_tab_view_get_page(tab_view, @ptrCast(tab.box)) orelse return; const page = c.adw_tab_view_get_page(tab_view.tab_view, @ptrCast(tab.box)) orelse return;
c.adw_tab_view_close_page(tab_view, page); c.adw_tab_view_close_page(tab_view.tab_view, page);
// If we have no more tabs we close the window // If we have no more tabs we close the window
if (self.nPages() == 0) { if (self.nPages() == 0) {
@ -385,6 +410,53 @@ pub const Notebook = union(enum) {
return c.g_value_get_int(&value); return c.g_value_get_int(&value);
} }
pub fn initContextMenu(self: *Notebook, window: *Window) void {
switch (self.*) {
.adw_tab_view => |tab_view| {
{
var buf: [32]u8 = undefined;
const action_name = std.fmt.bufPrintZ(
&buf,
"close-tab-{x:8>0}",
.{@intFromPtr(self)},
) catch unreachable;
const action = c.g_simple_action_new(action_name, null);
defer c.g_object_unref(action);
_ = c.g_signal_connect_data(
action,
"activate",
c.G_CALLBACK(&adwTabViewCloseTab),
self,
null,
c.G_CONNECT_DEFAULT,
);
c.g_action_map_add_action(@ptrCast(window.window), @ptrCast(action));
}
const menu = c.g_menu_new();
errdefer c.g_object_unref(menu);
{
var buf: [32]u8 = undefined;
const action_name = std.fmt.bufPrintZ(
&buf,
"win.close-tab-{x:8>0}",
.{@intFromPtr(self)},
) catch unreachable;
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, "Close Tab", action_name);
}
c.adw_tab_view_set_menu_model(tab_view.tab_view, @ptrCast(@alignCast(menu)));
},
.gtk_notebook => {},
}
}
}; };
fn gtkPageRemoved( fn gtkPageRemoved(
@ -442,7 +514,7 @@ fn gtkPageAdded(
fn adwSelectPage(_: *c.GObject, _: *c.GParamSpec, ud: ?*anyopaque) void { fn adwSelectPage(_: *c.GObject, _: *c.GParamSpec, ud: ?*anyopaque) void {
const window: *Window = @ptrCast(@alignCast(ud.?)); const window: *Window = @ptrCast(@alignCast(ud.?));
const page = c.adw_tab_view_get_selected_page(window.notebook.adw_tab_view) orelse return; const page = c.adw_tab_view_get_selected_page(window.notebook.adw_tab_view.tab_view) orelse return;
const title = c.adw_tab_page_get_title(page); const title = c.adw_tab_page_get_title(page);
c.gtk_window_set_title(window.window, title); c.gtk_window_set_title(window.window, title);
} }
@ -464,7 +536,7 @@ fn adwTabViewCreateWindow(
log.warn("error creating new window error={}", .{err}); log.warn("error creating new window error={}", .{err});
return null; return null;
}; };
return window.notebook.adw_tab_view; return window.notebook.adw_tab_view.tab_view;
} }
fn gtkNotebookCreateWindow( fn gtkNotebookCreateWindow(
@ -497,14 +569,19 @@ fn createWindow(currentWindow: *Window) !*Window {
return Window.create(alloc, app); return Window.create(alloc, app);
} }
fn adwTabViewSetupMenu(tab_view: *AdwTabView, page: *AdwTabPage, ud: ?*anyopaque) callconv(.C) void { fn adwTabViewSetupMenu(_: *AdwTabView, page: *AdwTabPage, ud: ?*anyopaque) callconv(.C) void {
const window: *Window = @ptrCast(@alignCast(ud.?)); const self: *Notebook = @ptrCast(@alignCast(ud.?));
self.adw_tab_view.last_tab = null;
const child = c.adw_tab_page_get_child(page); const child = c.adw_tab_page_get_child(page);
const tab: *Tab = @ptrCast(@alignCast( const tab: *Tab = @ptrCast(@alignCast(
c.g_object_get_data(@ptrCast(child), Tab.GHOSTTY_TAB) orelse return, c.g_object_get_data(@ptrCast(child), Tab.GHOSTTY_TAB) orelse return,
)); ));
window.app.refreshContextMenu(window.window, if (tab.focus_child) |focus_child| focus_child.core_surface.hasSelection() else false);
c.adw_tab_view_set_menu_model(tab_view, @ptrCast(@alignCast(window.app.context_menu))); self.adw_tab_view.last_tab = tab;
}
fn adwTabViewCloseTab(_: *c.GSimpleAction, _: *c.GVariant, ud: ?*anyopaque) callconv(.C) void {
const self: *Notebook = @ptrCast(@alignCast(ud.?));
self.closeTab(self.adw_tab_view.last_tab orelse return);
} }