From 9350389ecb96be4809bed57507e96c312a7a29b2 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sun, 10 Nov 2024 12:20:26 -0600 Subject: [PATCH] Add "move_tab_to_new_window" action. Fixes #2630 Requires #2529 for the refactoring. Implemented for GTK/Adwaita only. --- include/ghostty.h | 1 + macos/Sources/Ghostty/Ghostty.App.swift | 2 + src/Surface.zig | 6 ++ src/apprt/action.zig | 4 + src/apprt/glfw.zig | 1 + src/apprt/gtk/App.zig | 20 ++++ src/apprt/gtk/Window.zig | 23 +++++ src/apprt/gtk/notebook.zig | 7 ++ src/apprt/gtk/notebook_adw.zig | 132 ++++++++++++++++-------- src/input/Binding.zig | 5 + 10 files changed, 159 insertions(+), 42 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 41e3b3fe2..032fe04a7 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -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, diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 489493ad3..609adbbb3 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -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: diff --git a/src/Surface.zig b/src/Surface.zig index fbb589638..23aa56522 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -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, diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 1f954c37c..20ced89c7 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -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, diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 54c53139c..d78edc9ab 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -214,6 +214,7 @@ pub const App = struct { .toggle_visibility, .goto_tab, .move_tab, + .move_tab_to_new_window, .inspector, .render_inspector, .quit_timer, diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 424a97c3a..804893fc2 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -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"); } { diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index c98c53969..76c2b5c31 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -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, diff --git a/src/apprt/gtk/notebook.zig b/src/apprt/gtk/notebook.zig index e9bdaacc7..f05c388bb 100644 --- a/src/apprt/gtk/notebook.zig +++ b/src/apprt/gtk/notebook.zig @@ -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), diff --git a/src/apprt/gtk/notebook_adw.zig b/src/apprt/gtk/notebook_adw.zig index 773d8f490..47d2b9257 100644 --- a/src/apprt/gtk/notebook_adw.zig +++ b/src/apprt/gtk/notebook_adw.zig @@ -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); +} diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 347bc56d2..fe2d49e01 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -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,