diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index d0a273754..98d6bdbc7 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -336,7 +336,7 @@ class TabBar(QTabBar): return self.parent().currentWidget() @pyqtSlot(str) - def _on_config_changed(self, option): + def _on_config_changed(self, option: str): if option == 'fonts.tabs': self._set_font() elif option == 'tabs.favicons.scale': @@ -351,6 +351,12 @@ class TabBar(QTabBar): if option.startswith('colors.tabs.'): self.update() + # Clear _minimum_tab_size_hint_helper cache when appropriate + if option in ["tabs.indicator_padding", + "tabs.padding", + "tabs.width.indicator"]: + self._minimum_tab_size_hint_helper.cache_clear() + def _on_show_switching_delay_changed(self): """Set timer interval when tabs.show_switching_delay got changed.""" self._auto_hide_timer.setInterval(config.val.tabs.show_switching_delay) @@ -459,7 +465,7 @@ class TabBar(QTabBar): return super().mousePressEvent(e) - def minimumTabSizeHint(self, index, ellipsis: bool = True): + def minimumTabSizeHint(self, index, ellipsis: bool = True) -> QSize: """Set the minimum tab size to indicator/icon/... text. Args: @@ -469,38 +475,47 @@ class TabBar(QTabBar): Return: A QSize of the smallest tab size we can make. """ - text = '\u2026' if ellipsis else self.tabText(index) + icon = self.tabIcon(index) + extent = self.style().pixelMetric(QStyle.PM_TabBarIconSize, None, self) + if icon.isNull(): + icon_width = 0 + else: + icon_width = icon.actualSize(QSize(extent, extent)).width() + return self._minimum_tab_size_hint_helper(self.tabText(index), + icon_width, + ellipsis) + + @functools.lru_cache(maxsize=2**9) + def _minimum_tab_size_hint_helper(self, tab_text: str, + icon_width: int, + ellipsis: bool) -> QSize: + """Helper function to cache tab results. + + Config values accessed in here should be added to _on_config_changed to + ensure cache is flushed when needed. + """ + text = '\u2026' if ellipsis else tab_text # Don't ever shorten if text is shorter than the ellipsis text_width = min(self.fontMetrics().width(text), - self.fontMetrics().width(self.tabText(index))) - icon = self.tabIcon(index) + self.fontMetrics().width(tab_text)) padding = config.val.tabs.padding indicator_padding = config.val.tabs.indicator_padding padding_h = padding.left + padding.right padding_h += indicator_padding.left + indicator_padding.right padding_v = padding.top + padding.bottom - if icon.isNull(): - icon_size = QSize(0, 0) - else: - extent = self.style().pixelMetric(QStyle.PM_TabBarIconSize, None, - self) - icon_size = icon.actualSize(QSize(extent, extent)) height = self.fontMetrics().height() + padding_v - width = (text_width + icon_size.width() + + width = (text_width + icon_width + padding_h + config.val.tabs.width.indicator) return QSize(width, height) - def _tab_total_width_pinned(self): - """Get the current total width of pinned tabs. - - This width is calculated assuming no shortening due to ellipsis.""" - return sum(self.minimumTabSizeHint(idx, ellipsis=False).width() - for idx in range(self.count()) - if self._tab_pinned(idx)) - - def _pinnedCount(self) -> int: - """Get the number of pinned tabs.""" - return sum(self._tab_pinned(idx) for idx in range(self.count())) + def _pinned_statistics(self) -> (int, int): + """Get the number of pinned tabs and the total width of pinned tabs.""" + pinned_list = [idx for idx in range(self.count()) + if self._tab_pinned(idx)] + pinned_count = len(pinned_list) + pinned_width = sum(self.minimumTabSizeHint(idx, ellipsis=False).width() + for idx in pinned_list) + return (pinned_count, pinned_width) def _tab_pinned(self, index: int) -> bool: """Return True if tab is pinned.""" @@ -539,8 +554,8 @@ class TabBar(QTabBar): return QSize() else: pinned = self._tab_pinned(index) - no_pinned_count = self.count() - self._pinnedCount() - pinned_width = self._tab_total_width_pinned() + pinned_count, pinned_width = self._pinned_statistics() + no_pinned_count = self.count() - pinned_count no_pinned_width = self.width() - pinned_width if pinned: diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py index a61b4c6ea..d52793669 100644 --- a/qutebrowser/misc/utilcmds.py +++ b/qutebrowser/misc/utilcmds.py @@ -182,9 +182,17 @@ def debug_cache_stats(): except ImportError: pass + tabbed_browser = objreg.get('tabbed-browser', scope='window', + window='last-focused') + # pylint: disable=protected-access + tab_bar = tabbed_browser.tabBar() + tabbed_browser_info = tab_bar._minimum_tab_size_hint_helper.cache_info() + # pylint: enable=protected-access + log.misc.debug('is_valid_prefix: {}'.format(prefix_info)) log.misc.debug('_render_stylesheet: {}'.format(render_stylesheet_info)) log.misc.debug('history: {}'.format(history_info)) + log.misc.debug('tab width cache: {}'.format(tabbed_browser_info)) @cmdutils.register(debug=True) diff --git a/tests/unit/mainwindow/test_tabwidget.py b/tests/unit/mainwindow/test_tabwidget.py index e05e6a164..dd87c7ce2 100644 --- a/tests/unit/mainwindow/test_tabwidget.py +++ b/tests/unit/mainwindow/test_tabwidget.py @@ -52,3 +52,16 @@ class TestTabWidget: with qtbot.waitExposed(widget): widget.show() + + def test_update_tab_titles_benchmark(self, benchmark, widget, + qtbot, fake_web_tab): + """Benchmark for update_tab_titles.""" + widget.addTab(fake_web_tab(), 'foobar') + widget.addTab(fake_web_tab(), 'foobar2') + widget.addTab(fake_web_tab(), 'foobar3') + widget.addTab(fake_web_tab(), 'foobar4') + + with qtbot.waitExposed(widget): + widget.show() + + benchmark(widget._update_tab_titles)