diff --git a/terminatorlib/config.py b/terminatorlib/config.py index f77dab55..7d7d1c22 100644 --- a/terminatorlib/config.py +++ b/terminatorlib/config.py @@ -207,7 +207,8 @@ 'previous_profile' : '', 'preferences' : '', 'preferences_keybindings' : 'k', - 'help' : 'F1' + 'help' : 'F1', + 'detach_tab' : 'd' }, 'profiles': { 'default': { diff --git a/terminatorlib/notebook.py b/terminatorlib/notebook.py index 540e6579..1e938cc6 100644 --- a/terminatorlib/notebook.py +++ b/terminatorlib/notebook.py @@ -38,6 +38,7 @@ def __init__(self, window): self.connect('switch-page', self.deferred_on_tab_switch) self.connect('scroll-event', self.on_scroll_event) self.connect('create-window', self.create_window_detach) + self.connect_after('drag-failed', self.on_tab_drag_failed) self.configure() self.set_can_focus(False) @@ -79,21 +80,37 @@ def configure(self): self.last_active_term = {} def create_window_detach(self, notebook, widget, x, y): - """Create a window to contain a detached tab""" - dbg('creating window for detached tab: %s' % widget) - maker = Factory() + """Defer detaching a dropped tab until the drag has ended""" + dbg('deferring detach of dropped tab: %s' % widget) + GObject.idle_add(self.detach_tab_to_new_window, widget, x, y) + + def detach_tab_to_new_window(self, widget, x=None, y=None): + """Detach the tab containing widget into a newly created window""" + child = self.find_tab_root(widget) + if self.page_num(child) == -1: + err('could not find tab to detach for %s' % widget) + return False + dbg('detaching tab into new window: %s' % child) + maker = Factory() window = maker.make('Window') - window.move(x, y) + if x is not None and y is not None: + window.move(x, y) size = self.window.get_size() window.resize(size.width, size.height) - self.detach_tab(widget) - self.disconnect_child(widget) + self.detach_tab(child) + self.disconnect_child(child) self.hoover() - window.add(widget) + window.add(child) window.show_all() + return False + + def on_tab_drag_failed(self, notebook, context, result): + """Re-allocate after a failed tab drag so the label repaints""" + GObject.idle_add(self.queue_resize) + return False def create_layout(self, layout): """Apply layout configuration""" @@ -290,6 +307,7 @@ def newtab(self, debugtab=False, widget=None, cwd=None, metadata=None, profile=N 'group-tab-toggle': top_window.group_tab_toggle, 'ungroup-tab': top_window.ungroup_tab, 'move-tab': top_window.move_tab, + 'detach-tab': top_window.detach_tab_to_new_window, 'tab-new': [top_window.tab_new, widget], 'navigate': top_window.navigate_terminal, 'zoom': top_window.zoom, diff --git a/terminatorlib/paned.py b/terminatorlib/paned.py index 0019cfa2..2d0080bd 100644 --- a/terminatorlib/paned.py +++ b/terminatorlib/paned.py @@ -111,6 +111,7 @@ def add(self, widget, metadata=None): 'group-tab-toggle': top_window.group_tab_toggle, 'ungroup-tab': top_window.ungroup_tab, 'move-tab': top_window.move_tab, + 'detach-tab': top_window.detach_tab_to_new_window, 'maximise': [top_window.zoom, False], 'tab-new': [top_window.tab_new, widget], 'navigate': top_window.navigate_terminal, diff --git a/terminatorlib/prefseditor.py b/terminatorlib/prefseditor.py index 0660a40e..fb942573 100755 --- a/terminatorlib/prefseditor.py +++ b/terminatorlib/prefseditor.py @@ -143,6 +143,7 @@ class PrefsEditor: 'resize_right' : _('Resize the terminal right'), 'move_tab_right' : _('Move the tab right'), 'move_tab_left' : _('Move the tab left'), + 'detach_tab' : _('Detach the tab into a new window'), 'toggle_zoom' : _('Maximize terminal'), 'scaled_zoom' : _('Zoom terminal'), 'next_tab' : _('Switch to the next tab'), diff --git a/terminatorlib/terminal.py b/terminatorlib/terminal.py index da977b83..b20cead7 100644 --- a/terminatorlib/terminal.py +++ b/terminatorlib/terminal.py @@ -74,6 +74,7 @@ class Terminal(Gtk.VBox): 'group-all-toggle': (GObject.SignalFlags.RUN_LAST, None, ()), 'move-tab': (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_STRING,)), + 'detach-tab': (GObject.SignalFlags.RUN_LAST, None, ()), 'group-win': (GObject.SignalFlags.RUN_LAST, None, ()), 'group-win-toggle': (GObject.SignalFlags.RUN_LAST, None, ()), 'ungroup-win': (GObject.SignalFlags.RUN_LAST, None, ()), @@ -2088,6 +2089,9 @@ def key_move_tab_right(self): def key_move_tab_left(self): self.emit('move-tab', 'left') + def key_detach_tab(self): + self.emit('detach-tab') + def key_toggle_zoom(self): if self.is_zoomed(): self.unzoom() diff --git a/terminatorlib/terminal_popup_menu.py b/terminatorlib/terminal_popup_menu.py index 29f21528..601544d6 100644 --- a/terminatorlib/terminal_popup_menu.py +++ b/terminatorlib/terminal_popup_menu.py @@ -221,6 +221,13 @@ def show(self, widget, event=None): terminal.emit('tab-new', True, terminal)) menu.append(item) + if isinstance(terminal.get_toplevel().get_child(), Gtk.Notebook): + item = self.menu_item(Gtk.MenuItem, 'detach_tab', + _('Detach Ta_b')) + item.connect('activate', lambda x: + terminal.emit('detach-tab')) + menu.append(item) + menu.append(Gtk.SeparatorMenuItem()) item = self.menu_item(Gtk.ImageMenuItem, 'close_term', _('_Close')) diff --git a/terminatorlib/window.py b/terminatorlib/window.py index b1a6bf0f..c5d8d0e3 100644 --- a/terminatorlib/window.py +++ b/terminatorlib/window.py @@ -588,6 +588,7 @@ def add(self, widget, metadata=None): 'group-tab-toggle': self.group_tab_toggle, 'ungroup-tab': self.ungroup_tab, 'move-tab': self.move_tab, + 'detach-tab': self.detach_tab_to_new_window, 'tab-new': [self.tab_new, widget], 'navigate': self.navigate_terminal, 'rotate-cw': [self.rotate, True], @@ -1035,6 +1036,20 @@ def move_tab(self, widget, direction): notebook.reorder_child(child, page) + def detach_tab_to_new_window(self, widget): + """Detach the tab containing widget into a new window""" + if self.is_zoomed(): + self.unzoom() + + maker = Factory() + notebook = self.get_child() + + if not maker.isinstance(notebook, 'Notebook'): + dbg('not in a notebook, refusing to detach tab') + return + + notebook.detach_tab_to_new_window(widget) + def navigate_terminal(self, terminal, direction): """Navigate around terminals""" if self.is_zoomed(): diff --git a/tests/test_prefseditor_keybindings.py b/tests/test_prefseditor_keybindings.py index 5f1079cc..7f2ef023 100644 --- a/tests/test_prefseditor_keybindings.py +++ b/tests/test_prefseditor_keybindings.py @@ -68,17 +68,17 @@ def test_non_empty_default_keybinding_accels_are_distinct(): "accel_params,expected", [ # 1) 'edit_tab_title' Ctrl+Alt+A - (("9", 97, CONTROL_ALT_MOD, 38), False,), + (("10", 97, CONTROL_ALT_MOD, 38), False,), # 2) 'edit_terminal_title' Ctrl+Alt+A - (("10", 97, CONTROL_ALT_MOD, 38), True,), + (("11", 97, CONTROL_ALT_MOD, 38), True,), # 3) 'edit_window_title' F11 - (("11", 65480, Gdk.ModifierType(0), 95), True,), + (("12", 65480, Gdk.ModifierType(0), 95), True,), # 4) 'zoom_in' Shift+Ctrl+Z - (("70", 122, CONTROL_SHIFT_MOD, 52), True,), + (("71", 122, CONTROL_SHIFT_MOD, 52), True,), # 5) 'close_terminal' Ctrl+Alt+{ (("3", 123, CONTROL_ALT_SHIFT_MOD, 34), False,), # 6) 'zoom_in' Shift+Ctrl+B - (("70", 98, CONTROL_SHIFT_MOD, 56), False,), + (("71", 98, CONTROL_SHIFT_MOD, 56), False,), ], ) def test_message_dialog_is_shown_on_duplicate_accel_assignment( @@ -120,13 +120,13 @@ def test_message_dialog_is_shown_on_duplicate_accel_assignment( "accel_params", [ # 1) 'edit_tab_title' Ctrl+Alt+A - ("9", 97, CONTROL_ALT_MOD, 38), - # 2) 'edit_terminal_title' Ctrl+Alt+A ("10", 97, CONTROL_ALT_MOD, 38), + # 2) 'edit_terminal_title' Ctrl+Alt+A + ("11", 97, CONTROL_ALT_MOD, 38), # 3) 'edit_window_title' F11 - ("11", 65480, Gdk.ModifierType(0), 95), + ("12", 65480, Gdk.ModifierType(0), 95), # 4) 'zoom_in' Shift+Ctrl+Z - ("70", 122, CONTROL_SHIFT_MOD, 52), + ("71", 122, CONTROL_SHIFT_MOD, 52), ], ) def test_duplicate_accels_not_possible_to_set(accel_params):