mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
Add "move_tab_to_new_window" action.
Fixes #2630 Requires #2529 for the refactoring. Implemented for GTK/Adwaita only.
This commit is contained in:
@ -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,
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -214,6 +214,7 @@ pub const App = struct {
|
||||
.toggle_visibility,
|
||||
.goto_tab,
|
||||
.move_tab,
|
||||
.move_tab_to_new_window,
|
||||
.inspector,
|
||||
.render_inspector,
|
||||
.quit_timer,
|
||||
|
@ -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");
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -375,6 +375,7 @@ fn initActions(self: *Window) void {
|
||||
.{ "split_down", >kActionSplitDown },
|
||||
.{ "split_left", >kActionSplitLeft },
|
||||
.{ "split_up", >kActionSplitUp },
|
||||
.{ "move_tab_to_new_window", >kActionMoveTabToNewWindow },
|
||||
.{ "toggle_inspector", >kActionToggleInspector },
|
||||
.{ "copy", >kActionCopy },
|
||||
.{ "paste", >kActionPaste },
|
||||
@ -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,
|
||||
|
@ -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),
|
||||
|
@ -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,49 +46,62 @@ pub const NotebookAdw = struct {
|
||||
const self = ¬ebook.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 {
|
||||
{
|
||||
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);
|
||||
// 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,
|
||||
"{s}-{x:8>0}",
|
||||
.{ menu_item[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(menu_item[2]),
|
||||
self,
|
||||
null,
|
||||
c.G_CONNECT_DEFAULT,
|
||||
);
|
||||
c.g_action_map_add_action(@ptrCast(window.window), @ptrCast(action));
|
||||
}
|
||||
|
||||
inline for (menu_items) |menu_item| {
|
||||
var buf: [48]u8 = undefined;
|
||||
const action_name = std.fmt.bufPrintZ(
|
||||
&buf,
|
||||
"win.{s}-{x:8>0}",
|
||||
.{ menu_item[0], @intFromPtr(self) },
|
||||
) catch unreachable;
|
||||
|
||||
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);
|
||||
}
|
||||
|
@ -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,
|
||||
|
Reference in New Issue
Block a user