From 66d3ec1c081e4a99890f26b0777233f3ae382d7f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 13 Feb 2015 17:30:36 +0100 Subject: [PATCH] Make it possible to configure tab titles. --- qutebrowser/browser/commands.py | 11 +- qutebrowser/config/configdata.py | 19 +- qutebrowser/mainwindow/tabbedbrowser.py | 23 ++- qutebrowser/mainwindow/tabwidget.py | 219 ++++++++++++++++++++++-- 4 files changed, 239 insertions(+), 33 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 048d8d9ed..125fd42a6 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -378,11 +378,12 @@ class CommandDispatcher: """ if bg and window: raise cmdexc.CommandError("Only one of -b/-w can be given!") - curtab = self._current_widget() tabbed_browser = self._tabbed_browser(window) + curtab = self._current_widget() + cur_title = tabbed_browser.page_title(self._current_index()) newtab = tabbed_browser.tabopen(background=bg, explicit=True) idx = tabbed_browser.indexOf(newtab) - tabbed_browser.setTabText(idx, curtab.title().replace('&', '&&')) + tabbed_browser.set_page_title(idx, cur_title) tabbed_browser.setTabIcon(idx, curtab.icon()) newtab.keep_icon = True newtab.setZoomFactor(curtab.zoomFactor()) @@ -594,7 +595,7 @@ class CommandDispatcher: """ clipboard = QApplication.clipboard() if title: - s = self._tabbed_browser().tabText(self._current_index()) + s = self._tabbed_browser().page_title(self._current_index()) else: s = self._current_url().toString( QUrl.FullyEncoded | QUrl.RemovePassword) @@ -788,7 +789,7 @@ class CommandDispatcher: tab = self._current_widget() cur_idx = self._current_index() icon = tabbed_browser.tabIcon(cur_idx) - label = tabbed_browser.tabText(cur_idx) + label = tabbed_browser.page_title(cur_idx) cmdutils.check_overflow(cur_idx, 'int') cmdutils.check_overflow(new_idx, 'int') tabbed_browser.setUpdatesEnabled(False) @@ -850,7 +851,7 @@ class CommandDispatcher: idx = self._current_index() tabbed_browser = self._tabbed_browser() if idx != -1: - env['QUTE_TITLE'] = tabbed_browser.tabText(idx) + env['QUTE_TITLE'] = tabbed_browser.page_title(idx) webview = tabbed_browser.currentWidget() if webview is not None and webview.hasSelection(): diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 4d09ad527..f3d364636 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -247,7 +247,7 @@ DATA = collections.OrderedDict([ ('window-title-format', SettingValue(typ.FormatString(fields=['perc', 'perc_raw', 'title', - 'title_sep']), + 'title_sep', 'id']), '{perc}{title}{title_sep}qutebrowser'), "The format to use for the window title. The following placeholders " "are defined:\n\n" @@ -255,7 +255,8 @@ DATA = collections.OrderedDict([ "* `{perc_raw}`: The raw percentage, e.g. `10`\n" "* `{title}`: The title of the current webpage\n" "* `{title_sep}`: The string ` - ` if a title is set, empty " - "otherwise.") + "otherwise.\n" + "* `{id}`: The internal window ID of this window."), )), ('network', sect.KeyValue( @@ -419,6 +420,20 @@ DATA = collections.OrderedDict([ ('tabs-are-windows', SettingValue(typ.Bool(), 'false'), "Whether to open windows instead of tabs."), + + ('title-format', + SettingValue(typ.FormatString(fields=['perc', 'perc_raw', 'title', + 'title_sep', 'index', 'id']), + '{index}: {title}'), + "The format to use for the tab title. The following placeholders " + "are defined:\n\n" + "* `{perc}`: The percentage as a string like `[10%]`.\n" + "* `{perc_raw}`: The raw percentage, e.g. `10`\n" + "* `{title}`: The title of the current webpage\n" + "* `{title_sep}`: The string ` - ` if a title is set, empty " + "otherwise.\n" + "* `{index}`: The index of this tab.\n" + "* `{id}`: The internal tab ID of this tab."), )), ('storage', sect.KeyValue( diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index 9e38b2f3c..6241dbc20 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -114,6 +114,7 @@ class TabbedBrowser(tabwidget.TabWidget): self.setIconSize(QSize(12, 12)) objreg.get('config').changed.connect(self.update_favicons) objreg.get('config').changed.connect(self.update_window_title) + objreg.get('config').changed.connect(self.update_tab_titles) def __repr__(self): return utils.get_repr(self, count=self.count()) @@ -133,7 +134,11 @@ class TabbedBrowser(tabwidget.TabWidget): def update_window_title(self): """Change the window title to match the current tab.""" idx = self.currentIndex() - tabtitle = self.tabText(idx) + if idx == -1: + # (e.g. last tab removed) + log.webview.debug("Not updating window title because index is -1") + return + tabtitle = self.page_title(idx) widget = self.widget(idx) fields = {} @@ -144,6 +149,7 @@ class TabbedBrowser(tabwidget.TabWidget): fields['perc_raw'] = widget.progress fields['title'] = tabtitle fields['title_sep'] = ' - ' if tabtitle else '' + fields['id'] = self._win_id fmt = config.get('ui', 'window-title-format') self.window().setWindowTitle(fmt.format(**fields)) @@ -429,6 +435,7 @@ class TabbedBrowser(tabwidget.TabWidget): # We can get signals for tabs we already deleted... log.webview.debug("Got invalid tab {}!".format(tab)) return + self.update_tab_title(idx) if tab.keep_icon: tab.keep_icon = False else: @@ -468,7 +475,7 @@ class TabbedBrowser(tabwidget.TabWidget): # We can get signals for tabs we already deleted... log.webview.debug("Got invalid tab {}!".format(tab)) return - self.setTabText(idx, text.replace('&', '&&')) + self.set_page_title(idx, text) if idx == self.currentIndex(): self.update_window_title() @@ -489,8 +496,8 @@ class TabbedBrowser(tabwidget.TabWidget): # We can get signals for tabs we already deleted... log.webview.debug("Got invalid tab {}!".format(tab)) return - if not self.tabText(idx): - self.setTabText(idx, url) + if not self.page_title(idx): + self.set_page_title(idx, url) @pyqtSlot(webview.WebView) def on_icon_changed(self, tab): @@ -542,7 +549,7 @@ class TabbedBrowser(tabwidget.TabWidget): scope='window', window=self._win_id) self._now_focused = tab self.current_tab_changed.emit(tab) - self.update_window_title() + QTimer.singleShot(0, self.update_window_title) self._tab_insert_idx_left = self.currentIndex() self._tab_insert_idx_right = self.currentIndex() + 1 @@ -562,7 +569,8 @@ class TabbedBrowser(tabwidget.TabWidget): stop = config.get('colors', 'tabs.indicator.stop') system = config.get('colors', 'tabs.indicator.system') color = utils.interpolate_color(start, stop, perc, system) - self.tabBar().set_tab_indicator_color(idx, color) + self.set_tab_indicator_color(idx, color) + self.update_tab_title(idx) if idx == self.currentIndex(): self.update_window_title() @@ -585,7 +593,8 @@ class TabbedBrowser(tabwidget.TabWidget): stop = config.get('colors', 'tabs.indicator.stop') system = config.get('colors', 'tabs.indicator.system') color = utils.interpolate_color(start, stop, 100, system) - self.tabBar().set_tab_indicator_color(idx, color) + self.set_tab_indicator_color(idx, color) + self.update_tab_title(idx) if idx == self.currentIndex(): self.update_window_title() diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index 26ed45349..46c2d991c 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -33,6 +33,7 @@ from PyQt5.QtGui import QIcon, QPalette, QColor from qutebrowser.utils import qtutils, objreg, utils from qutebrowser.config import config +from qutebrowser.browser import webview PM_TabBarPadding = QStyle.PM_CustomBase @@ -47,6 +48,8 @@ class TabWidget(QTabWidget): bar = TabBar(win_id) self.setTabBar(bar) bar.tabCloseRequested.connect(self.tabCloseRequested) + bar.tabMoved.connect(functools.partial( + QTimer.singleShot, 0, self.update_tab_titles)) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.setDocumentMode(True) self.setElideMode(Qt.ElideRight) @@ -68,6 +71,119 @@ class TabWidget(QTabWidget): tabbar.setSelectionBehaviorOnRemove(selection_behaviour) tabbar.refresh() + def set_tab_indicator_color(self, idx, color): + """Set the tab indicator color. + + Args: + idx: The tab index. + color: A QColor. + """ + bar = self.tabBar() + bar.set_tab_data(idx, 'indicator-color', color) + bar.update(bar.tabRect(idx)) + + def set_page_title(self, idx, title): + """Set the tab title user data.""" + self.tabBar().set_tab_data(idx, 'page-title', title.replace('&', '&&')) + self.update_tab_title(idx) + + def page_title(self, idx): + """Get the tab title user data.""" + return self.tabBar().page_title(idx) + + def update_tab_title(self, idx): + """Update the tab text for the given tab.""" + widget = self.widget(idx) + page_title = self.page_title(idx) + + fields = {} + if widget.load_status == webview.LoadStatus.loading: + fields['perc'] = '[{}%] '.format(widget.progress) + else: + fields['perc'] = '' + fields['perc_raw'] = widget.progress + fields['title'] = page_title + fields['index'] = idx + 1 + fields['id'] = widget.tab_id + fields['title_sep'] = ' - ' if page_title else '' + + fmt = config.get('tabs', 'title-format') + self.tabBar().setTabText(idx, fmt.format(**fields)) + + @config.change_filter('tabs', 'title-format') + def update_tab_titles(self): + """Update all texts.""" + for idx in range(self.count()): + self.update_tab_title(idx) + + def tabInserted(self, idx): + """Update titles when a tab was inserted.""" + super().tabInserted(idx) + self.update_tab_titles() + + def tabRemoved(self, idx): + """Update titles when a tab was removed.""" + super().tabRemoved(idx) + self.update_tab_titles() + + def addTab(self, page, icon_or_text, text_or_empty=None): + """Override addTab to use our own text setting logic. + + Unfortunately QTabWidget::addTab has these two overloads: + - QWidget * page, const QIcon & icon, const QString & label + - QWidget * page, const QString & label + + This means we'll get different arguments based on the chosen overload. + + Args: + page: The QWidget to add. + icon_or_text: Either the QIcon to add or the label. + text_or_empty: Either the label or None. + + Return: + The index of the newly added tab. + """ + if text_or_empty is None: + icon = None + text = icon_or_text + new_idx = super().addTab(page, '') + else: + icon = icon_or_text + text = text_or_empty + new_idx = super().addTab(page, icon, '') + self.set_page_title(new_idx, text) + return new_idx + + def insertTab(self, idx, page, icon_or_text, text_or_empty=None): + """Override insertTab to use our own text setting logic. + + Unfortunately QTabWidget::insertTab has these two overloads: + - int index, QWidget * page, const QIcon & icon, + const QString & label + - int index, QWidget * page, const QString & label + + This means we'll get different arguments based on the chosen overload. + + Args: + idx: Where to insert the widget. + page: The QWidget to add. + icon_or_text: Either the QIcon to add or the label. + text_or_empty: Either the label or None. + + Return: + The index of the newly added tab. + """ + if text_or_empty is None: + icon = None + text = icon_or_text + new_idx = super().insertTab(idx, page, '') + else: + icon = icon_or_text + text = text_or_empty + new_idx = super().insertTab(idx, page, icon, '') + self.set_page_title(new_idx, text) + return new_idx + class TabBar(QTabBar): @@ -122,15 +238,38 @@ class TabBar(QTabBar): else: self.show() - def _set_tab_data(self, idx, key, value): + def set_tab_data(self, idx, key, value): """Set tab data as a dictionary.""" + if not 0 <= idx < self.count(): + raise IndexError("Tab index ({}) out of range ({})!".format( + idx, self.count())) data = self.tabData(idx) + if data is None: + data = {} data[key] = value self.setTabData(idx, data) - def _tab_data(self, idx, key): + def tab_data(self, idx, key): """Get tab data for a given key.""" - return self.tabData(idx)[key] + if not 0 <= idx < self.count(): + raise IndexError("Tab index ({}) out of range ({})!".format( + idx, self.count())) + data = self.tabData(idx) + if data is None: + data = {} + return data[key] + + def page_title(self, idx): + """Get the tab title user data. + + Args: + idx: The tab index to get the title for. + handle_unset: Whether to return an emtpy string on KeyError. + """ + try: + return self.tab_data(idx, 'page-title') + except KeyError: + return '' def refresh(self): """Properly repaint the tab bar and relayout tabs.""" @@ -138,16 +277,6 @@ class TabBar(QTabBar): # code sets layoutDirty so it actually relayouts the tabs. self.setIconSize(self.iconSize()) - def set_tab_indicator_color(self, idx, color): - """Set the tab indicator color. - - Args: - idx: The tab index. - color: A QColor. - """ - self._set_tab_data(idx, 'indicator-color', color) - self.update(self.tabRect(idx)) - @config.change_filter('fonts', 'tabbar') def set_font(self): """Set the tabbar font.""" @@ -264,7 +393,7 @@ class TabBar(QTabBar): tab.palette.setColor(QPalette.Window, bg_color) tab.palette.setColor(QPalette.WindowText, fg_color) try: - indicator_color = self._tab_data(idx, 'indicator-color') + indicator_color = self.tab_data(idx, 'indicator-color') except KeyError: indicator_color = QColor() tab.palette.setColor(QPalette.Base, indicator_color) @@ -275,15 +404,67 @@ class TabBar(QTabBar): p.drawControl(QStyle.CE_TabBarTab, tab) def tabInserted(self, idx): - """Show the tabbar if configured to hide and >1 tab is open.""" - self._tabhide() - self.setTabData(idx, {}) + """Update visibility when a tab was inserted.""" super().tabInserted(idx) + self._tabhide() def tabRemoved(self, idx): - """Hide the tabbar if configured when only one tab is open.""" - self._tabhide() + """Update visibility when a tab was removed.""" super().tabRemoved(idx) + self._tabhide() + + def addTab(self, icon_or_text, text_or_empty=None): + """Override addTab to use our own text setting logic. + + Unfortunately QTabBar::addTab has these two overloads: + - const QIcon & icon, const QString & label + - const QString & label + + This means we'll get different arguments based on the chosen overload. + + Args: + icon_or_text: Either the QIcon to add or the label. + text_or_empty: Either the label or None. + + Return: + The index of the newly added tab. + """ + if text_or_empty is None: + icon = None + text = icon_or_text + new_idx = super().addTab('') + else: + icon = icon_or_text + text = text_or_empty + new_idx = super().addTab(icon, '') + self.set_page_title(new_idx, text) + + def insertTab(self, idx, icon_or_text, text_or_empty=None): + """Override insertTab to use our own text setting logic. + + Unfortunately QTabBar::insertTab has these two overloads: + - int index, const QIcon & icon, const QString & label + - int index, const QString & label + + This means we'll get different arguments based on the chosen overload. + + Args: + idx: Where to insert the widget. + icon_or_text: Either the QIcon to add or the label. + text_or_empty: Either the label or None. + + Return: + The index of the newly added tab. + """ + if text_or_empty is None: + icon = None + text = icon_or_text + new_idx = super().InsertTab(idx, '') + else: + icon = icon_or_text + text = text_or_empty + new_idx = super().insertTab(idx, icon, '') + self.set_page_title(new_idx, text) class TabBarStyle(QCommonStyle):