mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
gtk: rename notebook to TabView and switch to gobject (#5795)
This commit is contained in:
269
src/apprt/gtk/TabView.zig
Normal file
269
src/apprt/gtk/TabView.zig
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
/// An abstraction over the Adwaita tab view to manage all the terminal tabs in
|
||||||
|
/// a window.
|
||||||
|
const TabView = @This();
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const gtk = @import("gtk");
|
||||||
|
const adw = @import("adw");
|
||||||
|
const gobject = @import("gobject");
|
||||||
|
|
||||||
|
const Window = @import("Window.zig");
|
||||||
|
const Tab = @import("Tab.zig");
|
||||||
|
const adwaita = @import("adwaita.zig");
|
||||||
|
|
||||||
|
const log = std.log.scoped(.gtk);
|
||||||
|
|
||||||
|
/// our window
|
||||||
|
window: *Window,
|
||||||
|
|
||||||
|
/// the tab view
|
||||||
|
tab_view: *adw.TabView,
|
||||||
|
|
||||||
|
/// Set to true so that the adw close-page handler knows we're forcing
|
||||||
|
/// and to allow a close to happen with no confirm. This is a bit of a hack
|
||||||
|
/// because we currently use GTK alerts to confirm tab close and they
|
||||||
|
/// don't carry with them the ADW state that we are confirming or not.
|
||||||
|
/// Long term we should move to ADW alerts so we can know if we are
|
||||||
|
/// confirming or not.
|
||||||
|
forcing_close: bool = false,
|
||||||
|
|
||||||
|
pub fn init(self: *TabView, window: *Window) void {
|
||||||
|
self.* = .{
|
||||||
|
.window = window,
|
||||||
|
.tab_view = adw.TabView.new(),
|
||||||
|
};
|
||||||
|
self.tab_view.as(gtk.Widget).addCssClass("notebook");
|
||||||
|
|
||||||
|
if (adwaita.versionAtLeast(1, 2, 0)) {
|
||||||
|
// Adwaita enables all of the shortcuts by default.
|
||||||
|
// We want to manage keybindings ourselves.
|
||||||
|
self.tab_view.removeShortcuts(.{
|
||||||
|
.alt_digits = true,
|
||||||
|
.alt_zero = true,
|
||||||
|
.control_end = true,
|
||||||
|
.control_home = true,
|
||||||
|
.control_page_down = true,
|
||||||
|
.control_page_up = true,
|
||||||
|
.control_shift_end = true,
|
||||||
|
.control_shift_home = true,
|
||||||
|
.control_shift_page_down = true,
|
||||||
|
.control_shift_page_up = true,
|
||||||
|
.control_shift_tab = true,
|
||||||
|
.control_tab = true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = adw.TabView.signals.page_attached.connect(
|
||||||
|
self.tab_view,
|
||||||
|
*TabView,
|
||||||
|
adwPageAttached,
|
||||||
|
self,
|
||||||
|
.{},
|
||||||
|
);
|
||||||
|
_ = adw.TabView.signals.close_page.connect(
|
||||||
|
self.tab_view,
|
||||||
|
*TabView,
|
||||||
|
adwClosePage,
|
||||||
|
self,
|
||||||
|
.{},
|
||||||
|
);
|
||||||
|
_ = adw.TabView.signals.create_window.connect(
|
||||||
|
self.tab_view,
|
||||||
|
*TabView,
|
||||||
|
adwTabViewCreateWindow,
|
||||||
|
self,
|
||||||
|
.{},
|
||||||
|
);
|
||||||
|
_ = gobject.Object.signals.notify.connect(
|
||||||
|
self.tab_view,
|
||||||
|
*TabView,
|
||||||
|
adwSelectPage,
|
||||||
|
self,
|
||||||
|
.{
|
||||||
|
.detail = "selected-page",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn asWidget(self: *TabView) *gtk.Widget {
|
||||||
|
return self.tab_view.as(gtk.Widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn nPages(self: *TabView) c_int {
|
||||||
|
return self.tab_view.getNPages();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the index of the currently selected page.
|
||||||
|
/// Returns null if the notebook has no pages.
|
||||||
|
fn currentPage(self: *TabView) ?c_int {
|
||||||
|
const page = self.tab_view.getSelectedPage() orelse return null;
|
||||||
|
return self.tab_view.getPagePosition(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the currently selected tab or null if there are none.
|
||||||
|
pub fn currentTab(self: *TabView) ?*Tab {
|
||||||
|
const page = self.tab_view.getSelectedPage() orelse return null;
|
||||||
|
const child = page.getChild().as(gobject.Object);
|
||||||
|
return @ptrCast(@alignCast(child.getData(Tab.GHOSTTY_TAB) orelse return null));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gotoNthTab(self: *TabView, position: c_int) bool {
|
||||||
|
const page_to_select = self.tab_view.getNthPage(position);
|
||||||
|
self.tab_view.setSelectedPage(page_to_select);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getTabPosition(self: *TabView, tab: *Tab) ?c_int {
|
||||||
|
const page = self.tab_view.getPage(@ptrCast(tab.box));
|
||||||
|
return self.tab_view.getPagePosition(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gotoPreviousTab(self: *TabView, tab: *Tab) bool {
|
||||||
|
const page_idx = self.getTabPosition(tab) orelse return false;
|
||||||
|
|
||||||
|
// The next index is the previous or we wrap around.
|
||||||
|
const next_idx = if (page_idx > 0) page_idx - 1 else next_idx: {
|
||||||
|
const max = self.nPages();
|
||||||
|
break :next_idx max -| 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Do nothing if we have one tab
|
||||||
|
if (next_idx == page_idx) return false;
|
||||||
|
|
||||||
|
return self.gotoNthTab(next_idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gotoNextTab(self: *TabView, tab: *Tab) bool {
|
||||||
|
const page_idx = self.getTabPosition(tab) orelse return false;
|
||||||
|
|
||||||
|
const max = self.nPages() -| 1;
|
||||||
|
const next_idx = if (page_idx < max) page_idx + 1 else 0;
|
||||||
|
if (next_idx == page_idx) return false;
|
||||||
|
|
||||||
|
return self.gotoNthTab(next_idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn moveTab(self: *TabView, tab: *Tab, position: c_int) void {
|
||||||
|
const page_idx = self.getTabPosition(tab) orelse return;
|
||||||
|
|
||||||
|
const max = self.nPages() -| 1;
|
||||||
|
var new_position: c_int = page_idx + position;
|
||||||
|
|
||||||
|
if (new_position < 0) {
|
||||||
|
new_position = max + new_position + 1;
|
||||||
|
} else if (new_position > max) {
|
||||||
|
new_position = new_position - max - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new_position == page_idx) return;
|
||||||
|
self.reorderPage(tab, new_position);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reorderPage(self: *TabView, tab: *Tab, position: c_int) void {
|
||||||
|
const page = self.tab_view.getPage(@ptrCast(tab.box));
|
||||||
|
_ = self.tab_view.reorderPage(page, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setTabLabel(self: *TabView, tab: *Tab, title: [:0]const u8) void {
|
||||||
|
const page = self.tab_view.getPage(@ptrCast(tab.box));
|
||||||
|
page.setTitle(title.ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setTabTooltip(self: *TabView, tab: *Tab, tooltip: [:0]const u8) void {
|
||||||
|
const page = self.tab_view.getPage(@ptrCast(tab.box));
|
||||||
|
page.setTooltip(tooltip.ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn newTabInsertPosition(self: *TabView, tab: *Tab) c_int {
|
||||||
|
const numPages = self.nPages();
|
||||||
|
return switch (tab.window.app.config.@"window-new-tab-position") {
|
||||||
|
.current => if (self.currentPage()) |page| page + 1 else numPages,
|
||||||
|
.end => numPages,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a new tab with the given title to the notebook.
|
||||||
|
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.tab_view.setSelectedPage(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn closeTab(self: *TabView, tab: *Tab) void {
|
||||||
|
// closeTab always expects to close unconditionally so we mark this
|
||||||
|
// as true so that the close_page call below doesn't request
|
||||||
|
// confirmation.
|
||||||
|
self.forcing_close = true;
|
||||||
|
const n = self.nPages();
|
||||||
|
defer {
|
||||||
|
// self becomes invalid if we close the last page because we close
|
||||||
|
// the whole window
|
||||||
|
if (n > 1) self.forcing_close = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = self.tab_view.getPage(@ptrCast(tab.box));
|
||||||
|
self.tab_view.closePage(page);
|
||||||
|
|
||||||
|
// If we have no more tabs we close the window
|
||||||
|
if (self.nPages() == 0) {
|
||||||
|
// libadw versions <= 1.3.x leak the final page view
|
||||||
|
// which causes our surface to not properly cleanup. We
|
||||||
|
// unref to force the cleanup. This will trigger a critical
|
||||||
|
// warning from GTK, but I don't know any other workaround.
|
||||||
|
// Note: I'm not actually sure if 1.4.0 contains the fix,
|
||||||
|
// I just know that 1.3.x is broken and 1.5.1 is fixed.
|
||||||
|
// If we know that 1.4.0 is fixed, we can change this.
|
||||||
|
if (!adwaita.versionAtLeast(1, 4, 0)) {
|
||||||
|
const box: *gtk.Box = @ptrCast(@alignCast(tab.box));
|
||||||
|
box.as(gobject.Object).unref();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn createWindow(currentWindow: *Window) !*Window {
|
||||||
|
const alloc = currentWindow.app.core_app.alloc;
|
||||||
|
const app = currentWindow.app;
|
||||||
|
|
||||||
|
// Create a new window
|
||||||
|
return Window.create(alloc, app);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn adwPageAttached(_: *adw.TabView, page: *adw.TabPage, _: c_int, self: *TabView) callconv(.C) void {
|
||||||
|
const child = page.getChild().as(gobject.Object);
|
||||||
|
const tab: *Tab = @ptrCast(@alignCast(child.getData(Tab.GHOSTTY_TAB) orelse return));
|
||||||
|
tab.window = self.window;
|
||||||
|
|
||||||
|
self.window.focusCurrentTab();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn adwClosePage(
|
||||||
|
_: *adw.TabView,
|
||||||
|
page: *adw.TabPage,
|
||||||
|
self: *TabView,
|
||||||
|
) callconv(.C) c_int {
|
||||||
|
const child = page.getChild().as(gobject.Object);
|
||||||
|
const tab: *Tab = @ptrCast(@alignCast(child.getData(Tab.GHOSTTY_TAB) orelse return 0));
|
||||||
|
self.tab_view.closePageFinish(page, @intFromBool(self.forcing_close));
|
||||||
|
if (!self.forcing_close) tab.closeWithConfirmation();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn adwTabViewCreateWindow(
|
||||||
|
_: *adw.TabView,
|
||||||
|
self: *TabView,
|
||||||
|
) callconv(.C) ?*adw.TabView {
|
||||||
|
const window = createWindow(self.window) catch |err| {
|
||||||
|
log.warn("error creating new window error={}", .{err});
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
return window.notebook.tab_view;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn adwSelectPage(_: *adw.TabView, _: *gobject.ParamSpec, self: *TabView) callconv(.C) void {
|
||||||
|
const page = self.tab_view.getSelectedPage() orelse return;
|
||||||
|
const title = page.getTitle();
|
||||||
|
self.window.setTitle(std.mem.span(title));
|
||||||
|
}
|
@ -22,7 +22,7 @@ const Tab = @import("Tab.zig");
|
|||||||
const c = @import("c.zig").c;
|
const c = @import("c.zig").c;
|
||||||
const adwaita = @import("adwaita.zig");
|
const adwaita = @import("adwaita.zig");
|
||||||
const gtk_key = @import("key.zig");
|
const gtk_key = @import("key.zig");
|
||||||
const Notebook = @import("notebook.zig");
|
const TabView = @import("TabView.zig");
|
||||||
const HeaderBar = @import("headerbar.zig");
|
const HeaderBar = @import("headerbar.zig");
|
||||||
const version = @import("version.zig");
|
const version = @import("version.zig");
|
||||||
const winproto = @import("winproto.zig");
|
const winproto = @import("winproto.zig");
|
||||||
@ -42,7 +42,7 @@ headerbar: HeaderBar,
|
|||||||
tab_overview: ?*c.GtkWidget,
|
tab_overview: ?*c.GtkWidget,
|
||||||
|
|
||||||
/// The notebook (tab grouping) for this window.
|
/// The notebook (tab grouping) for this window.
|
||||||
notebook: Notebook,
|
notebook: TabView,
|
||||||
|
|
||||||
context_menu: *c.GtkWidget,
|
context_menu: *c.GtkWidget,
|
||||||
|
|
||||||
@ -110,12 +110,12 @@ pub fn init(self: *Window, app: *App) !void {
|
|||||||
const box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0);
|
const box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0);
|
||||||
|
|
||||||
// Setup our notebook
|
// Setup our notebook
|
||||||
self.notebook.init();
|
self.notebook.init(self);
|
||||||
|
|
||||||
// If we are using Adwaita, then we can support the tab overview.
|
// If we are using Adwaita, then we can support the tab overview.
|
||||||
self.tab_overview = if (adwaita.versionAtLeast(1, 4, 0)) overview: {
|
self.tab_overview = if (adwaita.versionAtLeast(1, 4, 0)) overview: {
|
||||||
const tab_overview = c.adw_tab_overview_new();
|
const tab_overview = c.adw_tab_overview_new();
|
||||||
c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.tab_view);
|
c.adw_tab_overview_set_view(@ptrCast(tab_overview), @ptrCast(@alignCast(self.notebook.tab_view)));
|
||||||
c.adw_tab_overview_set_enable_new_tab(@ptrCast(tab_overview), 1);
|
c.adw_tab_overview_set_enable_new_tab(@ptrCast(tab_overview), 1);
|
||||||
_ = c.g_signal_connect_data(
|
_ = c.g_signal_connect_data(
|
||||||
tab_overview,
|
tab_overview,
|
||||||
@ -171,7 +171,7 @@ pub fn init(self: *Window, app: *App) !void {
|
|||||||
|
|
||||||
.hidden => btn: {
|
.hidden => btn: {
|
||||||
const btn = c.adw_tab_button_new();
|
const btn = c.adw_tab_button_new();
|
||||||
c.adw_tab_button_set_view(@ptrCast(btn), self.notebook.tab_view);
|
c.adw_tab_button_set_view(@ptrCast(btn), @ptrCast(@alignCast(self.notebook.tab_view)));
|
||||||
c.gtk_actionable_set_action_name(@ptrCast(btn), "overview.open");
|
c.gtk_actionable_set_action_name(@ptrCast(btn), "overview.open");
|
||||||
break :btn btn;
|
break :btn btn;
|
||||||
},
|
},
|
||||||
@ -229,7 +229,7 @@ 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 (!adwaita.versionAtLeast(1, 4, 0)) unreachable;
|
if (!adwaita.versionAtLeast(1, 4, 0)) unreachable;
|
||||||
c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.tab_view);
|
c.adw_tab_overview_set_view(@ptrCast(tab_overview), @ptrCast(@alignCast(self.notebook.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)));
|
||||||
@ -267,7 +267,7 @@ pub fn init(self: *Window, app: *App) !void {
|
|||||||
|
|
||||||
if (self.app.config.@"gtk-tabs-location" != .hidden) {
|
if (self.app.config.@"gtk-tabs-location" != .hidden) {
|
||||||
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.tab_view);
|
c.adw_tab_bar_set_view(tab_bar, @ptrCast(@alignCast(self.notebook.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);
|
||||||
|
|
||||||
@ -315,7 +315,7 @@ pub fn init(self: *Window, app: *App) !void {
|
|||||||
),
|
),
|
||||||
.hidden => unreachable,
|
.hidden => unreachable,
|
||||||
}
|
}
|
||||||
c.adw_tab_bar_set_view(tab_bar, self.notebook.tab_view);
|
c.adw_tab_bar_set_view(tab_bar, @ptrCast(@alignCast(self.notebook.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);
|
||||||
}
|
}
|
||||||
@ -662,13 +662,13 @@ fn gtkTabNewClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void {
|
|||||||
/// Create a new tab from the AdwTabOverview. We can't copy gtkTabNewClick
|
/// Create a new tab from the AdwTabOverview. We can't copy gtkTabNewClick
|
||||||
/// because we need to return an AdwTabPage from this function.
|
/// because we need to return an AdwTabPage from this function.
|
||||||
fn gtkNewTabFromOverview(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) ?*c.AdwTabPage {
|
fn gtkNewTabFromOverview(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) ?*c.AdwTabPage {
|
||||||
|
if (!adwaita.versionAtLeast(1, 4, 0)) unreachable;
|
||||||
const self: *Window = userdataSelf(ud.?);
|
const self: *Window = userdataSelf(ud.?);
|
||||||
assert(adwaita.versionAtLeast(1, 4, 0));
|
|
||||||
|
|
||||||
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.tab_view, @ptrCast(@alignCast(tab.box)));
|
return c.adw_tab_view_get_page(@ptrCast(@alignCast(self.notebook.tab_view)), @ptrCast(@alignCast(tab.box)));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn adwTabOverviewOpen(
|
fn adwTabOverviewOpen(
|
||||||
|
@ -1,249 +0,0 @@
|
|||||||
/// An abstraction over the GTK notebook and Adwaita tab view to manage
|
|
||||||
/// all the terminal tabs in a window.
|
|
||||||
const Notebook = @This();
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const assert = std.debug.assert;
|
|
||||||
const c = @import("c.zig").c;
|
|
||||||
|
|
||||||
const Window = @import("Window.zig");
|
|
||||||
const Tab = @import("Tab.zig");
|
|
||||||
const adwaita = @import("adwaita.zig");
|
|
||||||
|
|
||||||
const log = std.log.scoped(.gtk);
|
|
||||||
|
|
||||||
/// the tab view
|
|
||||||
tab_view: *c.AdwTabView,
|
|
||||||
|
|
||||||
/// Set to true so that the adw close-page handler knows we're forcing
|
|
||||||
/// and to allow a close to happen with no confirm. This is a bit of a hack
|
|
||||||
/// because we currently use GTK alerts to confirm tab close and they
|
|
||||||
/// don't carry with them the ADW state that we are confirming or not.
|
|
||||||
/// Long term we should move to ADW alerts so we can know if we are
|
|
||||||
/// confirming or not.
|
|
||||||
forcing_close: bool = false,
|
|
||||||
|
|
||||||
pub fn init(self: *Notebook) void {
|
|
||||||
const window: *Window = @fieldParentPtr("notebook", self);
|
|
||||||
|
|
||||||
const tab_view: *c.AdwTabView = c.adw_tab_view_new() orelse unreachable;
|
|
||||||
c.gtk_widget_add_css_class(@ptrCast(@alignCast(tab_view)), "notebook");
|
|
||||||
|
|
||||||
if (adwaita.versionAtLeast(1, 2, 0)) {
|
|
||||||
// Adwaita enables all of the shortcuts by default.
|
|
||||||
// We want to manage keybindings ourselves.
|
|
||||||
c.adw_tab_view_remove_shortcuts(tab_view, c.ADW_TAB_VIEW_SHORTCUT_ALL_SHORTCUTS);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.* = .{
|
|
||||||
.tab_view = tab_view,
|
|
||||||
};
|
|
||||||
|
|
||||||
_ = 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, "close-page", c.G_CALLBACK(&adwClosePage), 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, "notify::selected-page", c.G_CALLBACK(&adwSelectPage), window, null, c.G_CONNECT_DEFAULT);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn asWidget(self: *Notebook) *c.GtkWidget {
|
|
||||||
return @ptrCast(@alignCast(self.tab_view));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn nPages(self: *Notebook) c_int {
|
|
||||||
return c.adw_tab_view_get_n_pages(self.tab_view);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the index of the currently selected page.
|
|
||||||
/// Returns null if the notebook has no pages.
|
|
||||||
fn currentPage(self: *Notebook) ?c_int {
|
|
||||||
const page = c.adw_tab_view_get_selected_page(self.tab_view) orelse return null;
|
|
||||||
return c.adw_tab_view_get_page_position(self.tab_view, page);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the currently selected tab or null if there are none.
|
|
||||||
pub fn currentTab(self: *Notebook) ?*Tab {
|
|
||||||
const page = c.adw_tab_view_get_selected_page(self.tab_view) orelse return null;
|
|
||||||
const child = c.adw_tab_page_get_child(page);
|
|
||||||
return @ptrCast(@alignCast(
|
|
||||||
c.g_object_get_data(@ptrCast(child), Tab.GHOSTTY_TAB) orelse return null,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn gotoNthTab(self: *Notebook, position: c_int) bool {
|
|
||||||
const page_to_select = c.adw_tab_view_get_nth_page(self.tab_view, position) orelse return false;
|
|
||||||
c.adw_tab_view_set_selected_page(self.tab_view, page_to_select);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getTabPosition(self: *Notebook, tab: *Tab) ?c_int {
|
|
||||||
const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box)) orelse return null;
|
|
||||||
return c.adw_tab_view_get_page_position(self.tab_view, page);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn gotoPreviousTab(self: *Notebook, tab: *Tab) bool {
|
|
||||||
const page_idx = self.getTabPosition(tab) orelse return false;
|
|
||||||
|
|
||||||
// The next index is the previous or we wrap around.
|
|
||||||
const next_idx = if (page_idx > 0) page_idx - 1 else next_idx: {
|
|
||||||
const max = self.nPages();
|
|
||||||
break :next_idx max -| 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Do nothing if we have one tab
|
|
||||||
if (next_idx == page_idx) return false;
|
|
||||||
|
|
||||||
return self.gotoNthTab(next_idx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn gotoNextTab(self: *Notebook, tab: *Tab) bool {
|
|
||||||
const page_idx = self.getTabPosition(tab) orelse return false;
|
|
||||||
|
|
||||||
const max = self.nPages() -| 1;
|
|
||||||
const next_idx = if (page_idx < max) page_idx + 1 else 0;
|
|
||||||
if (next_idx == page_idx) return false;
|
|
||||||
|
|
||||||
return self.gotoNthTab(next_idx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn moveTab(self: *Notebook, tab: *Tab, position: c_int) void {
|
|
||||||
const page_idx = self.getTabPosition(tab) orelse return;
|
|
||||||
|
|
||||||
const max = self.nPages() -| 1;
|
|
||||||
var new_position: c_int = page_idx + position;
|
|
||||||
|
|
||||||
if (new_position < 0) {
|
|
||||||
new_position = max + new_position + 1;
|
|
||||||
} else if (new_position > max) {
|
|
||||||
new_position = new_position - max - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (new_position == page_idx) return;
|
|
||||||
self.reorderPage(tab, new_position);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reorderPage(self: *Notebook, tab: *Tab, position: c_int) void {
|
|
||||||
const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box));
|
|
||||||
_ = c.adw_tab_view_reorder_page(self.tab_view, page, position);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setTabLabel(self: *Notebook, tab: *Tab, title: [:0]const u8) void {
|
|
||||||
const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box));
|
|
||||||
c.adw_tab_page_set_title(page, title.ptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setTabTooltip(self: *Notebook, tab: *Tab, tooltip: [:0]const u8) void {
|
|
||||||
const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box));
|
|
||||||
c.adw_tab_page_set_tooltip(page, tooltip.ptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn newTabInsertPosition(self: *Notebook, tab: *Tab) c_int {
|
|
||||||
const numPages = self.nPages();
|
|
||||||
return switch (tab.window.app.config.@"window-new-tab-position") {
|
|
||||||
.current => if (self.currentPage()) |page| page + 1 else numPages,
|
|
||||||
.end => numPages,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a new tab with the given title to the notebook.
|
|
||||||
pub fn addTab(self: *Notebook, tab: *Tab, title: [:0]const u8) void {
|
|
||||||
const position = self.newTabInsertPosition(tab);
|
|
||||||
const box_widget: *c.GtkWidget = @ptrCast(tab.box);
|
|
||||||
const page = c.adw_tab_view_insert(self.tab_view, box_widget, position);
|
|
||||||
c.adw_tab_page_set_title(page, title.ptr);
|
|
||||||
c.adw_tab_view_set_selected_page(self.tab_view, page);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn closeTab(self: *Notebook, tab: *Tab) void {
|
|
||||||
// closeTab always expects to close unconditionally so we mark this
|
|
||||||
// as true so that the close_page call below doesn't request
|
|
||||||
// confirmation.
|
|
||||||
self.forcing_close = true;
|
|
||||||
const n = self.nPages();
|
|
||||||
defer {
|
|
||||||
// self becomes invalid if we close the last page because we close
|
|
||||||
// the whole window
|
|
||||||
if (n > 1) self.forcing_close = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box)) orelse return;
|
|
||||||
c.adw_tab_view_close_page(self.tab_view, page);
|
|
||||||
|
|
||||||
// If we have no more tabs we close the window
|
|
||||||
if (self.nPages() == 0) {
|
|
||||||
const window = tab.window.window;
|
|
||||||
|
|
||||||
// libadw versions <= 1.3.x leak the final page view
|
|
||||||
// which causes our surface to not properly cleanup. We
|
|
||||||
// unref to force the cleanup. This will trigger a critical
|
|
||||||
// warning from GTK, but I don't know any other workaround.
|
|
||||||
// Note: I'm not actually sure if 1.4.0 contains the fix,
|
|
||||||
// I just know that 1.3.x is broken and 1.5.1 is fixed.
|
|
||||||
// If we know that 1.4.0 is fixed, we can change this.
|
|
||||||
if (!adwaita.versionAtLeast(1, 4, 0)) {
|
|
||||||
c.g_object_unref(tab.box);
|
|
||||||
}
|
|
||||||
|
|
||||||
// `self` will become invalid after this call because it will have
|
|
||||||
// been freed up as part of the process of closing the window.
|
|
||||||
c.gtk_window_destroy(window);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn createWindow(currentWindow: *Window) !*Window {
|
|
||||||
const alloc = currentWindow.app.core_app.alloc;
|
|
||||||
const app = currentWindow.app;
|
|
||||||
|
|
||||||
// Create a new window
|
|
||||||
return Window.create(alloc, app);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn adwPageAttached(_: *c.AdwTabView, page: *c.AdwTabPage, _: c_int, ud: ?*anyopaque) callconv(.C) void {
|
|
||||||
const window: *Window = @ptrCast(@alignCast(ud.?));
|
|
||||||
|
|
||||||
const child = c.adw_tab_page_get_child(page);
|
|
||||||
const tab: *Tab = @ptrCast(@alignCast(c.g_object_get_data(@ptrCast(child), Tab.GHOSTTY_TAB) orelse return));
|
|
||||||
tab.window = window;
|
|
||||||
|
|
||||||
window.focusCurrentTab();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn adwClosePage(
|
|
||||||
_: *c.AdwTabView,
|
|
||||||
page: *c.AdwTabPage,
|
|
||||||
ud: ?*anyopaque,
|
|
||||||
) callconv(.C) c.gboolean {
|
|
||||||
const child = c.adw_tab_page_get_child(page);
|
|
||||||
const tab: *Tab = @ptrCast(@alignCast(c.g_object_get_data(
|
|
||||||
@ptrCast(child),
|
|
||||||
Tab.GHOSTTY_TAB,
|
|
||||||
) orelse return 0));
|
|
||||||
|
|
||||||
const window: *Window = @ptrCast(@alignCast(ud.?));
|
|
||||||
const notebook = window.notebook;
|
|
||||||
c.adw_tab_view_close_page_finish(
|
|
||||||
notebook.tab_view,
|
|
||||||
page,
|
|
||||||
@intFromBool(notebook.forcing_close),
|
|
||||||
);
|
|
||||||
if (!notebook.forcing_close) tab.closeWithConfirmation();
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn adwTabViewCreateWindow(
|
|
||||||
_: *c.AdwTabView,
|
|
||||||
ud: ?*anyopaque,
|
|
||||||
) callconv(.C) ?*c.AdwTabView {
|
|
||||||
const currentWindow: *Window = @ptrCast(@alignCast(ud.?));
|
|
||||||
const window = createWindow(currentWindow) catch |err| {
|
|
||||||
log.warn("error creating new window error={}", .{err});
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
return window.notebook.tab_view;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn adwSelectPage(_: *c.GObject, _: *c.GParamSpec, ud: ?*anyopaque) void {
|
|
||||||
const window: *Window = @ptrCast(@alignCast(ud.?));
|
|
||||||
const page = c.adw_tab_view_get_selected_page(window.notebook.tab_view) orelse return;
|
|
||||||
const title = c.adw_tab_page_get_title(page);
|
|
||||||
window.setTitle(std.mem.span(title));
|
|
||||||
}
|
|
Reference in New Issue
Block a user