diff --git a/README.asciidoc b/README.asciidoc index 89804a26c..7e28ac966 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -153,6 +153,7 @@ Contributors, sorted by the number of commits in descending order: * Lamar Pavel * Marshall Lochbaum * Bruno Oliveira +* thuck * Martin Tournoij * Imran Sobir * Alexander Cogneau @@ -163,6 +164,7 @@ Contributors, sorted by the number of commits in descending order: * Joel Torstensson * Patric Schmitz * Tarcisio Fedrizzi +* Jay Kamat * Claude * Fritz Reichwald * Corentin Julé @@ -184,7 +186,6 @@ Contributors, sorted by the number of commits in descending order: * ZDarian * Milan Svoboda * John ShaggyTwoDope Jenkins -* Jay Kamat * Clayton Craft * Peter Vilim * Jacob Sword @@ -218,7 +219,6 @@ Contributors, sorted by the number of commits in descending order: * Jussi Timperi * Cosmin Popescu * Brian Jackson -* thuck * sbinix * rsteube * neeasade diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 3537a4a43..c3ff94d35 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -82,6 +82,7 @@ It is possible to run or bind multiple commands by separating them with `;;`. |<>|Move the current tab according to the argument and [count]. |<>|Switch to the next tab, or switch [count] tabs forward. |<>|Close all tabs except for the current one. +|<>|Pin/Unpin the current/[count]th tab. |<>|Switch to the previous tab, or switch [count] tabs back. |<>|Unbind a keychain. |<>|Re-open a closed tab (optionally skipping [count] closed tabs). @@ -835,7 +836,7 @@ Duplicate the current tab. [[tab-close]] === tab-close -Syntax: +:tab-close [*--prev*] [*--next*] [*--opposite*]+ +Syntax: +:tab-close [*--prev*] [*--next*] [*--opposite*] [*--force*]+ Close the current/[count]th tab. @@ -844,6 +845,7 @@ Close the current/[count]th tab. * +*-n*+, +*--next*+: Force selecting the tab after the current tab. * +*-o*+, +*--opposite*+: Force selecting the tab in the opposite direction of what's configured in 'tabs->select-on-remove'. +* +*-f*+, +*--force*+: Avoid confirmation for pinned tabs. ==== count The tab index to close @@ -896,13 +898,23 @@ How many tabs to switch forward. [[tab-only]] === tab-only -Syntax: +:tab-only [*--prev*] [*--next*]+ +Syntax: +:tab-only [*--prev*] [*--next*] [*--force*]+ Close all tabs except for the current one. ==== optional arguments * +*-p*+, +*--prev*+: Keep tabs before the current. * +*-n*+, +*--next*+: Keep tabs after the current. +* +*-f*+, +*--force*+: Avoid confirmation for pinned tabs. + +[[tab-pin]] +=== tab-pin +Pin/Unpin the current/[count]th tab. + +Pinning a tab shrinks it to tabs->pinned-width size. Attempting to close a pinned tab will cause a confirmation, unless --force is passed. + +==== count +The tab index to pin or unpin [[tab-prev]] === tab-prev diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 2e0783110..30fdb19cc 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -128,9 +128,11 @@ |<>|Whether to show favicons in the tab bar. |<>|Scale for favicons in the tab bar. The tab size is unchanged, so big favicons also require extra `tabs->padding`. |<>|The width of the tab bar if it's vertical, in px or as percentage of the window. +|<>|The width for pinned tabs with a horizontal tabbar, in px. |<>|Width of the progress indicator (0 to disable). |<>|Whether to open windows instead of tabs. |<>|The format to use for the tab title. The following placeholders are defined: +|<>|The format to use for the tab title for pinned tabs. The same placeholders like for title-format are defined. |<>|Alignment of the text inside of tabs |<>|Switch between tabs using the mouse wheel. |<>|Padding for tabs (top, bottom, left, right). @@ -1221,6 +1223,12 @@ The width of the tab bar if it's vertical, in px or as percentage of the window. Default: +pass:[20%]+ +[[tabs-pinned-width]] +=== pinned-width +The width for pinned tabs with a horizontal tabbar, in px. + +Default: +pass:[43]+ + [[tabs-indicator-width]] === indicator-width Width of the progress indicator (0 to disable). @@ -1254,6 +1262,12 @@ The format to use for the tab title. The following placeholders are defined: Default: +pass:[{index}: {title}]+ +[[tabs-title-format-pinned]] +=== title-format-pinned +The format to use for the tab title for pinned tabs. The same placeholders like for title-format are defined. + +Default: +pass:[{index}]+ + [[tabs-title-alignment]] === title-alignment Alignment of the text inside of tabs diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 7f7fcece1..f3887e7fe 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -96,6 +96,7 @@ class TabData: viewing_source: Set if we're currently showing a source view. override_target: Override for open_target for fake clicks (like hints). Only used for QtWebKit. + pinned: Flag to pin the tab. """ def __init__(self): @@ -103,6 +104,7 @@ class TabData: self.viewing_source = False self.inspector = None self.override_target = None + self.pinned = False class AbstractAction: diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 62539f101..e2344273b 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -202,24 +202,21 @@ class CommandDispatcher: "{!r}!".format(conf_selection)) return None - @cmdutils.register(instance='command-dispatcher', scope='window') - @cmdutils.argument('count', count=True) - def tab_close(self, prev=False, next_=False, opposite=False, count=None): - """Close the current/[count]th tab. + def _tab_close(self, tab, prev=False, next_=False, opposite=False): + """Helper function for tab_close be able to handle message.async. Args: + tab: Tab object to select be closed. prev: Force selecting the tab before the current tab. next_: Force selecting the tab after the current tab. opposite: Force selecting the tab in the opposite direction of what's configured in 'tabs->select-on-remove'. count: The tab index to close, or None """ - tab = self._cntwidget(count) - if tab is None: - return tabbar = self._tabbed_browser.tabBar() selection_override = self._get_selection_override(prev, next_, opposite) + if selection_override is None: self._tabbed_browser.close_tab(tab) else: @@ -228,6 +225,63 @@ class CommandDispatcher: self._tabbed_browser.close_tab(tab) tabbar.setSelectionBehaviorOnRemove(old_selection_behavior) + def _tab_close_prompt_if_pinned(self, tab, force, yes_action): + """Helper method for tab_close. + + If tab is pinned, prompt. If everything is good, run yes_action. + """ + if tab.data.pinned and not force: + message.confirm_async( + title='Pinned Tab', + text="Are you sure you want to close a pinned tab?", + yes_action=yes_action, default=False) + else: + yes_action() + + @cmdutils.register(instance='command-dispatcher', scope='window') + @cmdutils.argument('count', count=True) + def tab_close(self, prev=False, next_=False, opposite=False, + force=False, count=None): + """Close the current/[count]th tab. + + Args: + prev: Force selecting the tab before the current tab. + next_: Force selecting the tab after the current tab. + opposite: Force selecting the tab in the opposite direction of + what's configured in 'tabs->select-on-remove'. + force: Avoid confirmation for pinned tabs. + count: The tab index to close, or None + """ + tab = self._cntwidget(count) + if tab is None: + return + close = functools.partial(self._tab_close, tab, prev, + next_, opposite) + + self._tab_close_prompt_if_pinned(tab, force, close) + + @cmdutils.register(instance='command-dispatcher', scope='window', + name='tab-pin') + @cmdutils.argument('count', count=True) + def tab_pin(self, count=None): + """Pin/Unpin the current/[count]th tab. + + Pinning a tab shrinks it to tabs->pinned-width size. + Attempting to close a pinned tab will cause a confirmation, + unless --force is passed. + + Args: + count: The tab index to pin or unpin, or None + """ + tab = self._cntwidget(count) + if tab is None: + return + + to_pin = not tab.data.pinned + tab_index = self._current_index() if count is None else count - 1 + cmdutils.check_overflow(tab_index + 1, 'int') + self._tabbed_browser.set_tab_pinned(tab_index, to_pin) + @cmdutils.register(instance='command-dispatcher', name='open', maxsplit=0, scope='window') @cmdutils.argument('url', completion=usertypes.Completion.url) @@ -275,6 +329,8 @@ class CommandDispatcher: else: # Explicit count with a tab that doesn't exist. return + elif curtab.data.pinned: + message.info("Tab is pinned!") else: curtab.openurl(cur_url) @@ -457,6 +513,7 @@ class CommandDispatcher: newtab.data.keep_icon = True newtab.history.deserialize(history) newtab.zoom.set_factor(curtab.zoom.factor()) + new_tabbed_browser.set_tab_pinned(idx, curtab.data.pinned) return newtab @cmdutils.register(instance='command-dispatcher', scope='window') @@ -832,22 +889,36 @@ class CommandDispatcher: message.info("Zoom level: {}%".format(level), replace=True) @cmdutils.register(instance='command-dispatcher', scope='window') - def tab_only(self, prev=False, next_=False): + def tab_only(self, prev=False, next_=False, force=False): """Close all tabs except for the current one. Args: prev: Keep tabs before the current. next_: Keep tabs after the current. + force: Avoid confirmation for pinned tabs. """ cmdutils.check_exclusive((prev, next_), 'pn') cur_idx = self._tabbed_browser.currentIndex() assert cur_idx != -1 + def _to_close(i): + """Helper method to check if a tab should be closed or not.""" + return not (i == cur_idx or + (prev and i < cur_idx) or + (next_ and i > cur_idx)) + + # Check to see if we are closing any pinned tabs + if not force: + for i, tab in enumerate(self._tabbed_browser.widgets()): + if _to_close(i) and tab.data.pinned: + self._tab_close_prompt_if_pinned( + tab, force, + lambda: self.tab_only( + prev=prev, next_=next_, force=True)) + return + for i, tab in enumerate(self._tabbed_browser.widgets()): - if (i == cur_idx or (prev and i < cur_idx) or - (next_ and i > cur_idx)): - continue - else: + if _to_close(i): self._tabbed_browser.close_tab(tab) @cmdutils.register(instance='command-dispatcher', scope='window') diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 44453a4ef..20db5d11a 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -690,6 +690,11 @@ def data(readonly=False): "The width of the tab bar if it's vertical, in px or as " "percentage of the window."), + ('pinned-width', + SettingValue(typ.Int(minval=10), + '43'), + "The width for pinned tabs with a horizontal tabbar, in px."), + ('indicator-width', SettingValue(typ.Int(minval=0), '3'), "Width of the progress indicator (0 to disable)."), @@ -716,6 +721,14 @@ def data(readonly=False): "* `{host}`: The host of the current web page.\n" "* `{backend}`: Either 'webkit' or 'webengine'"), + ('title-format-pinned', + SettingValue(typ.FormatString( + fields=['perc', 'perc_raw', 'title', 'title_sep', 'index', + 'id', 'scroll_pos', 'host'], none_ok=True), + '{index}'), + "The format to use for the tab title for pinned tabs. " + "The same placeholders like for title-format are defined."), + ('title-alignment', SettingValue(typ.TextAlignment(), 'left'), "Alignment of the text inside of tabs"), @@ -1716,6 +1729,7 @@ KEY_DATA = collections.OrderedDict([ ('follow-selected', RETURN_KEYS), ('follow-selected -t', ['', '']), ('repeat-command', ['.']), + ('tab-pin', ['']), ('record-macro', ['q']), ('run-macro', ['@']), ])), diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index 1a3119ee9..03b7d09d2 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -34,7 +34,8 @@ from qutebrowser.utils import (log, usertypes, utils, qtutils, objreg, urlutils, message, jinja) -UndoEntry = collections.namedtuple('UndoEntry', ['url', 'history', 'index']) +UndoEntry = collections.namedtuple('UndoEntry', + ['url', 'history', 'index', 'pinned']) class TabDeletedError(Exception): @@ -244,6 +245,10 @@ class TabbedBrowser(tabwidget.TabWidget): if last_close == 'ignore' and count == 1: return + # If we are removing a pinned tab, decrease count + if tab.data.pinned: + self.tabBar().pinned_count -= 1 + self._remove_tab(tab, add_undo=add_undo) if count == 1: # We just closed the last tab above. @@ -294,7 +299,8 @@ class TabbedBrowser(tabwidget.TabWidget): except browsertab.WebTabError: pass # special URL else: - entry = UndoEntry(tab.url(), history_data, idx) + entry = UndoEntry(tab.url(), history_data, idx, + tab.data.pinned) self._undo_stack.append(entry) tab.shutdown() @@ -325,7 +331,7 @@ class TabbedBrowser(tabwidget.TabWidget): use_current_tab = (only_one_tab_open and no_history and last_close_url_used) - url, history_data, idx = self._undo_stack.pop() + url, history_data, idx, pinned = self._undo_stack.pop() if use_current_tab: self.openurl(url, newtab=False) @@ -334,6 +340,7 @@ class TabbedBrowser(tabwidget.TabWidget): newtab = self.tabopen(url, background=False, idx=idx) newtab.history.deserialize(history_data) + self.set_tab_pinned(idx, pinned) @pyqtSlot('QUrl', bool) def openurl(self, url, newtab): diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index e1cc2f5ef..3e2b2c9c8 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -94,6 +94,32 @@ class TabWidget(QTabWidget): bar.set_tab_data(idx, 'indicator-color', color) bar.update(bar.tabRect(idx)) + def set_tab_pinned(self, idx, pinned, *, loading=False): + """Set the tab status as pinned. + + Args: + idx: The tab index. + pinned: Pinned tab state to set. + loading: Whether to ignore current data state when + counting pinned_count. + """ + bar = self.tabBar() + tab = self.widget(idx) + + # Only modify pinned_count if we had a change + # always modify pinned_count if we are loading + if tab.data.pinned != pinned or loading: + if pinned: + bar.pinned_count += 1 + elif not pinned: + bar.pinned_count -= 1 + + bar.set_tab_data(idx, 'pinned', pinned) + tab.data.pinned = pinned + self.update_tab_title(idx) + + bar.refresh() + def tab_indicator_color(self, idx): """Get the tab indicator color for the given index.""" return self.tabBar().tab_indicator_color(idx) @@ -109,12 +135,19 @@ class TabWidget(QTabWidget): def update_tab_title(self, idx): """Update the tab text for the given tab.""" + tab = self.widget(idx) fields = self.get_tab_fields(idx) fields['title'] = fields['title'].replace('&', '&&') fields['index'] = idx + 1 fmt = config.get('tabs', 'title-format') - title = '' if fmt is None else fmt.format(**fields) + fmt_pinned = config.get('tabs', 'title-format-pinned') + + if tab.data.pinned: + title = '' if fmt_pinned is None else fmt_pinned.format(**fields) + else: + title = '' if fmt is None else fmt.format(**fields) + self.tabBar().setTabText(idx, title) def get_tab_fields(self, idx): @@ -155,11 +188,12 @@ class TabWidget(QTabWidget): fields['scroll_pos'] = scroll_pos return fields - @config.change_filter('tabs', 'title-format') - def update_tab_titles(self): + def update_tab_titles(self, section='tabs', option='title-format'): """Update all texts.""" - for idx in range(self.count()): - self.update_tab_title(idx) + if section == 'tabs' and option in ['title-format', + 'title-format-pinned']: + for idx in range(self.count()): + self.update_tab_title(idx) def tabInserted(self, idx): """Update titles when a tab was inserted.""" @@ -285,6 +319,7 @@ class TabBar(QTabBar): self._auto_hide_timer.timeout.connect(self._tabhide) self.setAutoFillBackground(True) self.set_colors() + self.pinned_count = 0 config_obj.changed.connect(self.set_colors) QTimer.singleShot(0, self._tabhide) config_obj.changed.connect(self.on_tab_colors_changed) @@ -472,9 +507,31 @@ class TabBar(QTabBar): # get scroll buttons as soon as needed. size = minimum_size else: + tab_width_pinned_conf = config.get('tabs', 'pinned-width') + + try: + pinned = self.tab_data(index, 'pinned') + except KeyError: + pinned = False + + if pinned: + size = QSize(tab_width_pinned_conf, height) + qtutils.ensure_valid(size) + return size + # If we *do* have enough space, tabs should occupy the whole window - # width. - width = self.width() / self.count() + # width. If there are pinned tabs their size will be subtracted + # from the total window width. + # During shutdown the self.count goes down, + # but the self.pinned_count not - this generates some odd behavior. + # To avoid this we compare self.count against self.pinned_count. + if self.pinned_count > 0 and self.count() > self.pinned_count: + pinned_width = tab_width_pinned_conf * self.pinned_count + no_pinned_width = self.width() - pinned_width + width = no_pinned_width / (self.count() - self.pinned_count) + else: + width = self.width() / self.count() + # If width is not divisible by count, add a pixel to some tabs so # that there is no ugly leftover space. if index < self.width() % self.count(): diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py index 3384856ef..7932d8456 100644 --- a/qutebrowser/misc/sessions.py +++ b/qutebrowser/misc/sessions.py @@ -195,6 +195,9 @@ class SessionManager(QObject): if 'scroll-pos' in user_data: pos = user_data['scroll-pos'] data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()} + + data['pinned'] = tab.data.pinned + return data def _save_tab(self, tab, active): @@ -352,6 +355,9 @@ class SessionManager(QObject): pos = histentry['scroll-pos'] user_data['scroll-pos'] = QPoint(pos['x'], pos['y']) + if 'pinned' in histentry: + new_tab.data.pinned = histentry['pinned'] + active = histentry.get('active', False) url = QUrl.fromEncoded(histentry['url'].encode('ascii')) if 'original-url' in histentry: @@ -397,6 +403,9 @@ class SessionManager(QObject): self._load_tab(new_tab, tab) if tab.get('active', False): tab_to_focus = i + if new_tab.data.pinned: + tabbed_browser.set_tab_pinned( + i, new_tab.data.pinned, loading=True) if tab_to_focus is not None: tabbed_browser.setCurrentIndex(tab_to_focus) if win.get('active', False): diff --git a/tests/end2end/features/conftest.py b/tests/end2end/features/conftest.py index bea896a8d..3b4652efc 100644 --- a/tests/end2end/features/conftest.py +++ b/tests/end2end/features/conftest.py @@ -159,8 +159,8 @@ def clean_open_tabs(quteproc): """Clean up open windows and tabs.""" quteproc.set_setting('tabs', 'last-close', 'blank') quteproc.send_cmd(':window-only') - quteproc.send_cmd(':tab-only') - quteproc.send_cmd(':tab-close') + quteproc.send_cmd(':tab-only --force') + quteproc.send_cmd(':tab-close --force') quteproc.wait_for_load_finished_url('about:blank') @@ -543,31 +543,46 @@ def check_open_tabs(quteproc, request, tabs): """ session = quteproc.get_session() active_suffix = ' (active)' + pinned_suffix = ' (pinned)' tabs = tabs.splitlines() assert len(session['windows']) == 1 assert len(session['windows'][0]['tabs']) == len(tabs) # If we don't have (active) anywhere, don't check it - has_active = any(line.endswith(active_suffix) for line in tabs) + has_active = any(active_suffix in line for line in tabs) + has_pinned = any(pinned_suffix in line for line in tabs) for i, line in enumerate(tabs): line = line.strip() assert line.startswith('- ') line = line[2:] # remove "- " prefix - if line.endswith(active_suffix): - path = line[:-len(active_suffix)] - active = True - else: - path = line - active = False + + active = False + pinned = False + + while line.endswith(active_suffix) or line.endswith(pinned_suffix): + if line.endswith(active_suffix): + # active + line = line[:-len(active_suffix)] + active = True + else: + # pinned + line = line[:-len(pinned_suffix)] + pinned = True session_tab = session['windows'][0]['tabs'][i] - assert session_tab['history'][-1]['url'] == quteproc.path_to_url(path) + current_page = session_tab['history'][-1] + assert current_page['url'] == quteproc.path_to_url(line) if active: assert session_tab['active'] elif has_active: assert 'active' not in session_tab + if pinned: + assert current_page['pinned'] + elif has_pinned: + assert not current_page['pinned'] + @bdd.then(bdd.parsers.re(r'the (?Pprimary selection|clipboard) should ' r'contain "(?P.*)"')) diff --git a/tests/end2end/features/sessions.feature b/tests/end2end/features/sessions.feature index 226d3107d..5ec6e168a 100644 --- a/tests/end2end/features/sessions.feature +++ b/tests/end2end/features/sessions.feature @@ -342,7 +342,7 @@ Feature: Saving and loading sessions Scenario: Loading a directory When I run :session-load (tmpdir) Then the error "Error while loading session: *" should be shown - + Scenario: Loading internal session without --force When I run :session-save --force _internal And I run :session-load _internal @@ -367,3 +367,24 @@ Feature: Saving and loading sessions Scenario: Loading a session which doesn't exist When I run :session-load inexistent_session Then the error "Session inexistent_session not found!" should be shown + + + # Test load/save of pinned tabs + + Scenario: Saving/Loading a session with pinned tabs + When I open data/numbers/1.txt + And I open data/numbers/2.txt in a new tab + And I open data/numbers/3.txt in a new tab + And I run :tab-pin with count 2 + And I run :session-save pin_session + And I run :tab-only --force + And I run :tab-close --force + And I run :session-load -c pin_session + And I wait until data/numbers/3.txt is loaded + And I run :tab-focus 2 + And I run :open hello world + Then the message "Tab is pinned!" should be shown + And the following tabs should be open: + - data/numbers/1.txt + - data/numbers/2.txt (active) (pinned) + - data/numbers/3.txt diff --git a/tests/end2end/features/tabs.feature b/tests/end2end/features/tabs.feature index 19007428c..00fe5c16a 100644 --- a/tests/end2end/features/tabs.feature +++ b/tests/end2end/features/tabs.feature @@ -1026,3 +1026,121 @@ Feature: Tab management - tabs: - history: - url: http://localhost:*/data/hello.txt + + # :tab-pin + + Scenario: :tab-pin command + When I open data/numbers/1.txt + And I open data/numbers/2.txt in a new tab + And I open data/numbers/3.txt in a new tab + And I run :tab-pin + Then the following tabs should be open: + - data/numbers/1.txt + - data/numbers/2.txt + - data/numbers/3.txt (active) (pinned) + + Scenario: :tab-pin unpin + When I open data/numbers/1.txt + And I run :tab-pin + And I open data/numbers/2.txt in a new tab + And I open data/numbers/3.txt in a new tab + And I run :tab-pin + And I run :tab-pin + Then the following tabs should be open: + - data/numbers/1.txt (pinned) + - data/numbers/2.txt + - data/numbers/3.txt (active) + + Scenario: :tab-pin to index 2 + When I open data/numbers/1.txt + And I open data/numbers/2.txt in a new tab + And I open data/numbers/3.txt in a new tab + And I run :tab-pin with count 2 + Then the following tabs should be open: + - data/numbers/1.txt + - data/numbers/2.txt (pinned) + - data/numbers/3.txt (active) + + Scenario: Pinned :tab-close prompt yes + When I open data/numbers/1.txt + And I run :tab-pin + And I open data/numbers/2.txt in a new tab + And I run :tab-pin + And I run :tab-close + And I wait for "*want to close a pinned tab*" in the log + And I run :prompt-accept yes + Then the following tabs should be open: + - data/numbers/1.txt (active) (pinned) + + Scenario: Pinned :tab-close prompt no + When I open data/numbers/1.txt + And I run :tab-pin + And I open data/numbers/2.txt in a new tab + And I run :tab-pin + And I run :tab-close + And I wait for "*want to close a pinned tab*" in the log + And I run :prompt-accept no + Then the following tabs should be open: + - data/numbers/1.txt (pinned) + - data/numbers/2.txt (active) (pinned) + + Scenario: Pinned :tab-only prompt yes + When I open data/numbers/1.txt + And I run :tab-pin + And I open data/numbers/2.txt in a new tab + And I run :tab-pin + And I run :tab-next + And I run :tab-only + And I wait for "*want to close a pinned tab*" in the log + And I run :prompt-accept yes + Then the following tabs should be open: + - data/numbers/1.txt (active) (pinned) + + Scenario: Pinned :tab-only prompt no + When I open data/numbers/1.txt + And I run :tab-pin + And I open data/numbers/2.txt in a new tab + And I run :tab-pin + And I run :tab-next + And I run :tab-only + And I wait for "*want to close a pinned tab*" in the log + And I run :prompt-accept no + Then the following tabs should be open: + - data/numbers/1.txt (active) (pinned) + - data/numbers/2.txt (pinned) + + Scenario: Pinned :tab-only close all but pinned tab + When I open data/numbers/1.txt + And I open data/numbers/2.txt in a new tab + And I run :tab-pin + And I run :tab-only + Then the following tabs should be open: + - data/numbers/2.txt (active) (pinned) + + Scenario: :tab-pin open url + When I open data/numbers/1.txt + And I run :tab-pin + And I open data/numbers/2.txt without waiting + Then the message "Tab is pinned!" should be shown + And the following tabs should be open: + - data/numbers/1.txt (active) (pinned) + + Scenario: Cloning a pinned tab + When I open data/numbers/1.txt + And I run :tab-pin + And I run :tab-clone + And I wait until data/numbers/1.txt is loaded + Then the following tabs should be open: + - data/numbers/1.txt (pinned) + - data/numbers/1.txt (pinned) (active) + + Scenario: Undo a pinned tab + When I open data/numbers/1.txt + And I open data/numbers/2.txt in a new tab + And I run :tab-pin + And I run :tab-close --force + And I run :undo + And I wait until data/numbers/2.txt is loaded + Then the following tabs should be open: + - data/numbers/1.txt + - data/numbers/2.txt (pinned) (active) diff --git a/tests/unit/mainwindow/test_tabwidget.py b/tests/unit/mainwindow/test_tabwidget.py index b8c12eaee..01a0c0ec8 100644 --- a/tests/unit/mainwindow/test_tabwidget.py +++ b/tests/unit/mainwindow/test_tabwidget.py @@ -49,6 +49,8 @@ class TestTabWidget: 'indicator-width': 3, 'indicator-padding': configtypes.PaddingValues(2, 2, 0, 4), 'title-format': '{index}: {title}', + 'title-format-pinned': '{index}', + 'pinned-width': 43, 'title-alignment': Qt.AlignLeft, }, 'colors': {