From 33c17fd23893f773bb759819845c74077f699b05 Mon Sep 17 00:00:00 2001 From: Stilian Iliev Date: Mon, 29 Dec 2025 22:14:12 +0200 Subject: [PATCH] make changes more extensible and less intrusive --- qutebrowser/browser/commands.py | 2 +- qutebrowser/mainwindow/tabbedbrowser.py | 117 +++++++++++++++--------- 2 files changed, 76 insertions(+), 43 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 548ac6738..36e3bda40 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -237,7 +237,7 @@ class CommandDispatcher: else: old_selection_behavior = tabbar.selectionBehaviorOnRemove() tabbar.setSelectionBehaviorOnRemove(selection_override) - self._tabbed_browser.close_tab(tab, allow_firefox_behavior=False) + self._tabbed_browser.close_tab(tab, allow_selection_strategy=False) tabbar.setSelectionBehaviorOnRemove(old_selection_behavior) @cmdutils.register(instance='command-dispatcher', scope='window') diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index 52c96f795..60c55aec4 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -135,6 +135,62 @@ class TabDeletedError(Exception): """Exception raised when _tab_index is called for a deleted tab.""" +class SelectionStrategy: + + """Base class for tab selection strategies (on remove).""" + + def on_tab_opened(self, tabbed_browser: "TabbedBrowser", tab: browsertab.AbstractTab, + related: bool, background: bool) -> None: + """Called when a new tab is opened.""" + + def on_current_changed(self, tabbed_browser: "TabbedBrowser", + tab: browsertab.AbstractTab) -> None: + """Called when the current tab changes.""" + + def should_select_parent(self, tabbed_browser: "TabbedBrowser", + tab: browsertab.AbstractTab) -> bool: + """Return True if we should select the parent/opener instead of default behavior.""" + return False + + +class FirefoxSelectionStrategy(SelectionStrategy): + + """Strategy implementing Firefox-like "return to parent" behavior.""" + + def __init__(self) -> None: + self._opened_tab: Optional[weakref.ReferenceType[browsertab.AbstractTab]] = None + + def on_tab_opened(self, tabbed_browser: "TabbedBrowser", tab: browsertab.AbstractTab, + related: bool, background: bool) -> None: + # Track opened tab + if tabbed_browser.widget.count() > 0: + if self._opened_tab is not None and background: + self._opened_tab = None + else: + self._opened_tab = weakref.ref(tab) + + def on_current_changed(self, tabbed_browser: "TabbedBrowser", + tab: browsertab.AbstractTab) -> None: + # Clear state if user switched to a tab that's not the opened tab + if self._opened_tab is not None: + opened = self._opened_tab() + if tab is not opened: + # User navigated to a third tab, forget opened tab + self._opened_tab = None + + def should_select_parent(self, tabbed_browser: "TabbedBrowser", + tab: browsertab.AbstractTab) -> bool: + if self._opened_tab is None: + return False + + opened = self._opened_tab() + if opened is tab: + self._opened_tab = None # Consume state + return True + + return False + + class TabbedBrowser(QWidget): """A TabWidget with QWebViews inside. @@ -242,8 +298,8 @@ class TabbedBrowser(QWidget): self.default_window_icon = self._window().windowIcon() self.is_private = private self.tab_deque = TabDeque() - # Last opened tab tracking for tabs.select_on_remove = 'firefox' - self._opened_tab: Optional[weakref.ReferenceType[browsertab.AbstractTab]] = None + self._selection_strategy: SelectionStrategy = SelectionStrategy() + self._update_selection_strategy() config.instance.changed.connect(self._on_config_changed) quitter.instance.shutting_down.connect(self.shutdown) @@ -254,6 +310,16 @@ class TabbedBrowser(QWidget): # We can't resize a collections.deque so just recreate it >:( self.undo_stack = collections.deque(self.undo_stack, maxlen=newsize) + def _update_selection_strategy(self): + """Update the selection strategy based on config.""" + strategy = config.val.tabs.select_on_remove + if strategy == 'firefox': + if not isinstance(self._selection_strategy, FirefoxSelectionStrategy): + self._selection_strategy = FirefoxSelectionStrategy() + else: + if isinstance(self._selection_strategy, FirefoxSelectionStrategy): + self._selection_strategy = SelectionStrategy() + def __repr__(self): return utils.get_repr(self, count=self.widget.count()) @@ -269,6 +335,8 @@ class TabbedBrowser(QWidget): self.widget.update_tab_titles() elif option == "tabs.focus_stack_size": self.tab_deque.update_size() + elif option == "tabs.select_on_remove": + self._update_selection_strategy() def _tab_index(self, tab): """Get the index of a given tab. @@ -446,32 +514,8 @@ class TabbedBrowser(QWidget): else: yes_action() - def _should_select_opener(self, tab): - """Check if we should select the opener tab (behave like last-used). - - Args: - tab: The tab that is about to be closed. - - Return: - True if we should fall back to 'last-used' behavior to select the opener. - False if we should stick to the default (next). - """ - # Only apply if config is 'firefox' mode - if config.val.tabs.select_on_remove != 'firefox': - return False - - if self._opened_tab is None: - return False - - opened = self._opened_tab() - if opened is tab: - self._opened_tab = None # Consume state - return True - - return False - def close_tab(self, tab, *, add_undo=True, new_undo=True, transfer=False, - allow_firefox_behavior=True): + allow_selection_strategy=True): """Close a tab. Args: @@ -479,7 +523,7 @@ class TabbedBrowser(QWidget): add_undo: Whether the tab close can be undone. new_undo: Whether the undo entry should be a new item in the stack. transfer: Whether the tab is closing because it is moving to a new window. - allow_firefox_behavior: Whether to try selecting the 'opener' tab (if configured). + allow_selection_strategy: Whether to try selecting the 'opener' tab (if configured). """ if config.val.tabs.tabs_are_windows or transfer: last_close = 'close' @@ -491,11 +535,9 @@ class TabbedBrowser(QWidget): if last_close == 'ignore' and count == 1: return - # Handle 'firefox' selection mode restore_behavior = None - if allow_firefox_behavior and self._should_select_opener(tab): + if allow_selection_strategy and self._selection_strategy.should_select_parent(self, tab): # Temporarily switch to 'last-used' behavior, which will select the 'opener' - # since we verified the relationship and navigation state. tabbar = self.widget.tab_bar() restore_behavior = tabbar.selectionBehaviorOnRemove() tabbar.setSelectionBehaviorOnRemove(QTabBar.SelectionBehavior.SelectPreviousTab) @@ -700,12 +742,7 @@ class TabbedBrowser(QWidget): if background is None: background = config.val.tabs.background - # Track opened tab for tabs.select_on_remove = 'firefox' - if self.widget.count() > 0: - if self._opened_tab is not None and background: - self._opened_tab = None - else: - self._opened_tab = weakref.ref(tab) + self._selection_strategy.on_tab_opened(self, tab, related, background) if background: # Make sure the background tab has the correct initial size. @@ -953,11 +990,7 @@ class TabbedBrowser(QWidget): return # Clear state if user switched to a tab that's not the opened tab - if self._opened_tab is not None: - opened = self._opened_tab() - if tab is not opened: - # User navigated to a third tab, forget opened tab - self._opened_tab = None + self._selection_strategy.on_current_changed(self, tab) log.modes.debug("Current tab changed, focusing {!r}".format(tab)) tab.setFocus()