Add "move_tab_to_new_window" action.

Fixes #2630
Requires #2529 for the refactoring.

Implemented for GTK/Adwaita only.
This commit is contained in:
Jeffrey C. Ollie
2024-11-10 12:20:26 -06:00
parent 0d0775d034
commit 9350389ecb
10 changed files with 159 additions and 42 deletions

View File

@ -544,6 +544,7 @@ typedef enum {
GHOSTTY_ACTION_TOGGLE_QUICK_TERMINAL,
GHOSTTY_ACTION_TOGGLE_VISIBILITY,
GHOSTTY_ACTION_MOVE_TAB,
GHOSTTY_ACTION_MOVE_TAB_TO_NEW_WINDOW,
GHOSTTY_ACTION_GOTO_TAB,
GHOSTTY_ACTION_GOTO_SPLIT,
GHOSTTY_ACTION_RESIZE_SPLIT,

View File

@ -524,6 +524,8 @@ extension Ghostty {
case GHOSTTY_ACTION_KEY_SEQUENCE:
keySequence(app, target: target, v: action.action.key_sequence)
case GHOSTTY_ACTION_MOVE_TAB_TO_NEW_WINDOW:
fallthrough
case GHOSTTY_ACTION_COLOR_CHANGE:
fallthrough
case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS:

View File

@ -3923,6 +3923,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
.{ .amount = position },
),
.move_tab_to_new_window => try self.rt_app.performAction(
.{ .surface = self },
.move_tab_to_new_window,
{},
),
.new_split => |direction| try self.rt_app.performAction(
.{ .surface = self },
.new_split,

View File

@ -107,6 +107,9 @@ pub const Action = union(Key) {
/// cyclically within the tab range.
move_tab: MoveTab,
/// Moves the tab that contains the target surface to a new window.
move_tab_to_new_window,
/// Jump to a specific tab. Must handle the scenario that the tab
/// value is invalid.
goto_tab: GotoTab,
@ -205,6 +208,7 @@ pub const Action = union(Key) {
toggle_quick_terminal,
toggle_visibility,
move_tab,
move_tab_to_new_window,
goto_tab,
goto_split,
resize_split,

View File

@ -214,6 +214,7 @@ pub const App = struct {
.toggle_visibility,
.goto_tab,
.move_tab,
.move_tab_to_new_window,
.inspector,
.render_inspector,
.quit_timer,

View File

@ -457,6 +457,7 @@ pub fn performAction(
.new_tab => try self.newTab(target),
.goto_tab => self.gotoTab(target, value),
.move_tab => self.moveTab(target, value),
.move_tab_to_new_window => self.moveTabToNewWindow(target),
.new_split => try self.newSplit(target, value),
.resize_split => self.resizeSplit(target, value),
.equalize_splits => self.equalizeSplits(target),
@ -547,6 +548,23 @@ fn moveTab(_: *App, target: apprt.Target, move_tab: apprt.action.MoveTab) void {
}
}
fn moveTabToNewWindow(_: *App, target: apprt.Target) void {
switch (target) {
.app => {},
.surface => |v| {
const window = v.rt_surface.container.window() orelse {
log.info(
"moveTabToNewWindow invalid for container={s}",
.{@tagName(v.rt_surface.container)},
);
return;
};
window.moveTabToNewWindow(v.rt_surface);
},
}
}
fn newSplit(
self: *App,
target: apprt.Target,
@ -887,6 +905,7 @@ fn syncActionAccelerators(self: *App) !void {
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.move_tab_to_new_window", .{ .move_tab_to_new_window = {} });
try self.syncActionAccelerator("win.copy", .{ .copy_to_clipboard = {} });
try self.syncActionAccelerator("win.paste", .{ .paste_from_clipboard = {} });
try self.syncActionAccelerator("win.reset", .{ .reset = {} });
@ -1545,6 +1564,7 @@ fn initContextMenu(self: *App) void {
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");
c.g_menu_append(section, "Move Tab to New Window", "win.move_tab_to_new_window");
}
{

View File

@ -375,6 +375,7 @@ fn initActions(self: *Window) void {
.{ "split_down", &gtkActionSplitDown },
.{ "split_left", &gtkActionSplitLeft },
.{ "split_up", &gtkActionSplitUp },
.{ "move_tab_to_new_window", &gtkActionMoveTabToNewWindow },
.{ "toggle_inspector", &gtkActionToggleInspector },
.{ "copy", &gtkActionCopy },
.{ "paste", &gtkActionPaste },
@ -462,6 +463,15 @@ pub fn moveTab(self: *Window, surface: *Surface, position: c_int) void {
self.notebook.moveTab(tab, position);
}
/// Move the current tab for a surface to a new window.
pub fn moveTabToNewWindow(self: *Window, surface: *Surface) void {
const tab = surface.container.tab() orelse {
log.info("surface is not attached to a tab bar, cannot navigate", .{});
return;
};
self.notebook.moveTabToNewWindow(tab);
}
/// Go to the next tab for a surface.
pub fn gotoLastTab(self: *Window) void {
const max = self.notebook.nPages() -| 1;
@ -853,6 +863,19 @@ fn gtkActionSplitUp(
};
}
fn gtkActionMoveTabToNewWindow(
_: *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(.{ .move_tab_to_new_window = {} }) catch |err| {
log.warn("error performing binding action error={}", .{err});
return;
};
}
fn gtkActionToggleInspector(
_: *c.GSimpleAction,
_: *c.GVariant,

View File

@ -121,6 +121,13 @@ pub const Notebook = union(enum) {
self.reorderPage(tab, new_position);
}
pub fn moveTabToNewWindow(self: *Notebook, tab: *Tab) void {
switch (self.*) {
.adw => |*adw| adw.moveTabToNewWindow(tab),
.gtk => log.warn("move_tab_to_new_window is not implemented for non-Adwaita notebooks", .{}),
}
}
pub fn reorderPage(self: *Notebook, tab: *Tab, position: c_int) void {
switch (self.*) {
.adw => |*adw| adw.reorderPage(tab, position),

View File

@ -14,6 +14,9 @@ const AdwTabView = if (adwaita.versionAtLeast(0, 0, 0)) c.AdwTabView else anyopa
const AdwTabPage = if (adwaita.versionAtLeast(0, 0, 0)) c.AdwTabPage else anyopaque;
pub const NotebookAdw = struct {
/// the window
window: *Window,
/// the tab view
tab_view: *AdwTabView,
@ -34,6 +37,7 @@ pub const NotebookAdw = struct {
notebook.* = .{
.adw = .{
.window = window,
.tab_view = tab_view,
.last_tab = null,
},
@ -42,19 +46,37 @@ pub const NotebookAdw = struct {
const self = &notebook.adw;
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, "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);
_ = c.g_signal_connect_data(tab_view, "create-window", c.G_CALLBACK(&adwTabViewCreateWindow), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(tab_view, "page-attached", c.G_CALLBACK(&adwTabViewPageAttached), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(tab_view, "page-reordered", c.G_CALLBACK(&adwTabViewPageAttached), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(tab_view, "setup-menu", c.G_CALLBACK(&adwTabViewSetupMenu), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(tab_view, "notify::selected-page", c.G_CALLBACK(&adwSelectPage), self, null, c.G_CONNECT_DEFAULT);
}
pub fn initContextMenu(self: *NotebookAdw, window: *Window) void {
const menu = c.g_menu_new();
errdefer c.g_object_unref(menu);
{
var buf: [32]u8 = undefined;
const section = c.g_menu_new();
defer c.g_object_unref(section);
c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
// The set of menu items. Each menu item has (in order):
// [0] The action name
// [1] The menu name
// [2] The callback function
const menu_items = .{
.{ "close-tab", "Close Tab", &adwTabViewCloseTab },
.{ "move-tab-to-new-window", "Move Tab to New Window", &adwTabViewMoveTabToNewWindow },
};
inline for (menu_items) |menu_item| {
var buf: [48]u8 = undefined;
const action_name = std.fmt.bufPrintZ(
&buf,
"close-tab-{x:8>0}",
.{@intFromPtr(self)},
"{s}-{x:8>0}",
.{ menu_item[0], @intFromPtr(self) },
) catch unreachable;
const action = c.g_simple_action_new(action_name, null);
@ -62,7 +84,7 @@ pub const NotebookAdw = struct {
_ = c.g_signal_connect_data(
action,
"activate",
c.G_CALLBACK(&adwTabViewCloseTab),
c.G_CALLBACK(menu_item[2]),
self,
null,
c.G_CONNECT_DEFAULT,
@ -70,21 +92,16 @@ pub const NotebookAdw = struct {
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;
inline for (menu_items) |menu_item| {
var buf: [48]u8 = undefined;
const action_name = std.fmt.bufPrintZ(
&buf,
"win.close-tab-{x:8>0}",
.{@intFromPtr(self)},
"win.{s}-{x:8>0}",
.{ menu_item[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.g_menu_append(section, menu_item[1], action_name);
}
}
c.adw_tab_view_set_menu_model(self.tab_view, @ptrCast(@alignCast(menu)));
@ -176,27 +193,53 @@ pub const NotebookAdw = struct {
c.g_object_unref(tab.box);
}
c.gtk_window_destroy(tab.window.window);
c.gtk_window_destroy(self.window.window);
}
}
pub fn moveTabToNewWindow(self: *NotebookAdw, tab: *Tab) void {
const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box)) orelse {
log.err("tab is not part of this notebook", .{});
return;
};
const other_window = createWindow(self.window) catch {
log.err("unable to create window", .{});
return;
};
switch (other_window.notebook.*) {
.adw => |*other| {
c.adw_tab_view_transfer_page(self.tab_view, page, other.tab_view, 0);
other_window.focusCurrentTab();
},
.gtk => {
log.err("expecting an Adwaita notebook!", .{});
c.gtk_window_destroy(other_window.window);
return;
},
}
if (self.nPages() == 0) {
c.gtk_window_destroy(self.window.window);
}
}
};
fn adwPageAttached(_: *AdwTabView, page: *c.AdwTabPage, _: c_int, ud: ?*anyopaque) callconv(.C) void {
const window: *Window = @ptrCast(@alignCast(ud.?));
fn adwTabViewPageAttached(_: *AdwTabView, page: *c.AdwTabPage, _: c_int, ud: ?*anyopaque) callconv(.C) void {
const self: *NotebookAdw = @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;
tab.window = self.window;
window.focusCurrentTab();
self.window.focusCurrentTab();
}
fn adwTabViewCreateWindow(
_: *AdwTabView,
ud: ?*anyopaque,
) callconv(.C) ?*AdwTabView {
const currentWindow: *Window = @ptrCast(@alignCast(ud.?));
const window = createWindow(currentWindow) catch |err| {
const self: *NotebookAdw = @ptrCast(@alignCast(ud.?));
const window = createWindow(self.window) catch |err| {
log.warn("error creating new window error={}", .{err});
return null;
};
@ -204,10 +247,10 @@ fn adwTabViewCreateWindow(
}
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.adw.tab_view) orelse return;
const self: *NotebookAdw = @ptrCast(@alignCast(ud.?));
const page = c.adw_tab_view_get_selected_page(self.window.notebook.adw.tab_view) orelse return;
const title = c.adw_tab_page_get_title(page);
c.gtk_window_set_title(window.window, title);
c.gtk_window_set_title(self.window.window, title);
}
fn adwTabViewSetupMenu(_: *AdwTabView, page: *AdwTabPage, ud: ?*anyopaque) callconv(.C) void {
@ -226,3 +269,8 @@ fn adwTabViewCloseTab(_: *c.GSimpleAction, _: *c.GVariant, ud: ?*anyopaque) call
const self: *NotebookAdw = @ptrCast(@alignCast(ud.?));
self.closeTab(self.last_tab orelse return);
}
fn adwTabViewMoveTabToNewWindow(_: *c.GSimpleAction, _: *c.GVariant, ud: ?*anyopaque) callconv(.C) void {
const self: *NotebookAdw = @ptrCast(@alignCast(ud.?));
self.moveTabToNewWindow(self.last_tab orelse return);
}

View File

@ -306,6 +306,10 @@ pub const Action = union(enum) {
/// If the new position is out of bounds, it wraps around cyclically within the tab range.
move_tab: isize,
/// Move tab to a new window, where it will become the first and only tab in
/// that window.
move_tab_to_new_window: void,
/// Toggle the tab overview.
/// This only works with libadwaita enabled currently.
toggle_tab_overview: void,
@ -653,6 +657,7 @@ pub const Action = union(enum) {
.last_tab,
.goto_tab,
.move_tab,
.move_tab_to_new_window,
.toggle_tab_overview,
.new_split,
.goto_split,