From 4acf046ed2c66306bf001702ff996f03fd169b37 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 11 Jul 2014 19:42:53 +0200 Subject: [PATCH 01/36] Force tab text to be left-aligned --- qutebrowser/utils/style.py | 44 +++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/qutebrowser/utils/style.py b/qutebrowser/utils/style.py index 92d5006a9..cf46b08a7 100644 --- a/qutebrowser/utils/style.py +++ b/qutebrowser/utils/style.py @@ -24,6 +24,8 @@ We might also use this to do more in the future. import functools +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QPalette from PyQt5.QtWidgets import QCommonStyle, QStyle @@ -53,11 +55,10 @@ class Style(QCommonStyle): """ self._style = style for method in ('drawComplexControl', 'drawControl', 'drawItemPixmap', - 'drawItemText', 'generatedIconPixmap', - 'hitTestComplexControl', 'itemPixmapRect', - 'itemTextRect', 'pixelMetric', 'polish', 'styleHint', - 'subControlRect', 'subElementRect', 'unpolish', - 'sizeFromContents'): + 'generatedIconPixmap', 'hitTestComplexControl', + 'itemPixmapRect', 'itemTextRect', 'pixelMetric', + 'polish', 'styleHint', 'subControlRect', + 'subElementRect', 'unpolish', 'sizeFromContents'): target = getattr(self._style, method) setattr(self, method, functools.partial(target)) super().__init__() @@ -77,3 +78,36 @@ class Style(QCommonStyle): if element == QStyle.PE_FrameFocusRect: return return self._style.drawPrimitive(element, option, painter, widget) + + def drawItemText(self, painter, rectangle, alignment, palette, enabled, + text, textRole=QPalette.NoRole): + """Extend QCommonStyle::drawItemText to not center-align text. + + Since Qt hardcodes the text alignment for tabbar tabs in QCommonStyle, + we need to undo this here by deleting the flag again, and align left + instead. + + + Draws the given text in the specified rectangle using the provided + painter and palette. + + The text is drawn using the painter's pen, and aligned and wrapped + according to the specified alignment. If an explicit textRole is + specified, the text is drawn using the palette's color for the given + role. The enabled parameter indicates whether or not the item is + enabled; when reimplementing this function, the enabled parameter + should influence how the item is drawn. + + Args: + painter: QPainter * + rectangle: const QRect & + alignment int (Qt::Alignment) + palette: const QPalette & + enabled: bool + text: const QString & + textRole: QPalette::ColorRole textRole + """ + alignment &=~ Qt.AlignHCenter + alignment |= Qt.AlignLeft + super().drawItemText(painter, rectangle, alignment, palette, enabled, + text, textRole) From 90adc100a27625621d2610bd55e8c9aa5401395d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 13 Jul 2014 22:10:15 +0200 Subject: [PATCH 02/36] Move style to widgets.tabwidget --- qutebrowser/utils/style.py | 113 ------------------------------- qutebrowser/widgets/tabwidget.py | 100 +++++++++++++++++++++++++-- 2 files changed, 96 insertions(+), 117 deletions(-) delete mode 100644 qutebrowser/utils/style.py diff --git a/qutebrowser/utils/style.py b/qutebrowser/utils/style.py deleted file mode 100644 index cf46b08a7..000000000 --- a/qutebrowser/utils/style.py +++ /dev/null @@ -1,113 +0,0 @@ -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: - -# Copyright 2014 Florian Bruhin (The Compiler) -# -# This file is part of qutebrowser. -# -# qutebrowser is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# qutebrowser is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with qutebrowser. If not, see . - -"""Qt style to remove Ubuntu focus rectangle uglyness. - -We might also use this to do more in the future. -""" - -import functools - -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QPalette -from PyQt5.QtWidgets import QCommonStyle, QStyle - - -class Style(QCommonStyle): - - """Qt style to remove Ubuntu focus rectangle uglyness. - - Unfortunately PyQt doesn't support QProxyStyle, so we need to do this the - hard way... - - Based on: - - http://stackoverflow.com/a/17294081 - https://code.google.com/p/makehuman/source/browse/trunk/makehuman/lib/qtgui.py - - Attributes: - _style: The base/"parent" style. - """ - - def __init__(self, style): - """Initialize all functions we're not overriding. - - This simply calls the corresponding function in self._style. - - Args: - style: The base/"parent" style. - """ - self._style = style - for method in ('drawComplexControl', 'drawControl', 'drawItemPixmap', - 'generatedIconPixmap', 'hitTestComplexControl', - 'itemPixmapRect', 'itemTextRect', 'pixelMetric', - 'polish', 'styleHint', 'subControlRect', - 'subElementRect', 'unpolish', 'sizeFromContents'): - target = getattr(self._style, method) - setattr(self, method, functools.partial(target)) - super().__init__() - - def drawPrimitive(self, element, option, painter, widget=None): - """Override QCommonStyle.drawPrimitive. - - Call the genuine drawPrimitive of self._style, except when a focus - rectangle should be drawn. - - Args: - element: PrimitiveElement pe - option: const QStyleOption * opt - painter: QPainter * p - widget: const QWidget * widget - """ - if element == QStyle.PE_FrameFocusRect: - return - return self._style.drawPrimitive(element, option, painter, widget) - - def drawItemText(self, painter, rectangle, alignment, palette, enabled, - text, textRole=QPalette.NoRole): - """Extend QCommonStyle::drawItemText to not center-align text. - - Since Qt hardcodes the text alignment for tabbar tabs in QCommonStyle, - we need to undo this here by deleting the flag again, and align left - instead. - - - Draws the given text in the specified rectangle using the provided - painter and palette. - - The text is drawn using the painter's pen, and aligned and wrapped - according to the specified alignment. If an explicit textRole is - specified, the text is drawn using the palette's color for the given - role. The enabled parameter indicates whether or not the item is - enabled; when reimplementing this function, the enabled parameter - should influence how the item is drawn. - - Args: - painter: QPainter * - rectangle: const QRect & - alignment int (Qt::Alignment) - palette: const QPalette & - enabled: bool - text: const QString & - textRole: QPalette::ColorRole textRole - """ - alignment &=~ Qt.AlignHCenter - alignment |= Qt.AlignLeft - super().drawItemText(painter, rectangle, alignment, palette, enabled, - text, textRole) diff --git a/qutebrowser/widgets/tabwidget.py b/qutebrowser/widgets/tabwidget.py index 49418143b..8f83626a5 100644 --- a/qutebrowser/widgets/tabwidget.py +++ b/qutebrowser/widgets/tabwidget.py @@ -19,13 +19,15 @@ """The tab widget used for TabbedBrowser from browser.py.""" +import functools + from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QSize -from PyQt5.QtWidgets import QTabWidget, QTabBar, QSizePolicy -from PyQt5.QtGui import QIcon, QPixmap +from PyQt5.QtWidgets import (QTabWidget, QTabBar, QSizePolicy, QCommonStyle, + QStyle) +from PyQt5.QtGui import QIcon, QPixmap, QPalette import qutebrowser.config.config as config from qutebrowser.config.style import set_register_stylesheet -from qutebrowser.utils.style import Style from qutebrowser.utils.qt import qt_ensure_valid @@ -84,7 +86,7 @@ class TabWidget(QTabWidget): super().__init__(parent) self.setTabBar(TabBar()) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) - self.setStyle(Style(self.style())) + self.setStyle(TabWidgetStyle(self.style())) set_register_stylesheet(self) self.setDocumentMode(True) self.setElideMode(Qt.ElideRight) @@ -182,3 +184,93 @@ class TabBar(QTabBar): size = super().tabSizeHint(index) qt_ensure_valid(size) return size + + +class TabWidgetStyle(QCommonStyle): + + """Qt style used by TabWidget to fix some issues with the default one. + + This fixes the following things: + - Remove the focus rectangle Ubuntu draws on tabs. + - Force text to be left-aligned even though Qt has "centered" + hardcoded. + + Unfortunately PyQt doesn't support QProxyStyle, so we need to do this the + hard way... + + Based on: + + http://stackoverflow.com/a/17294081 + https://code.google.com/p/makehuman/source/browse/trunk/makehuman/lib/qtgui.py + + Attributes: + _style: The base/"parent" style. + """ + + def __init__(self, style): + """Initialize all functions we're not overriding. + + This simply calls the corresponding function in self._style. + + Args: + style: The base/"parent" style. + """ + self._style = style + for method in ('drawComplexControl', 'drawControl', 'drawItemPixmap', + 'generatedIconPixmap', 'hitTestComplexControl', + 'itemPixmapRect', 'itemTextRect', 'pixelMetric', + 'polish', 'styleHint', 'subControlRect', + 'subElementRect', 'unpolish', 'sizeFromContents'): + target = getattr(self._style, method) + setattr(self, method, functools.partial(target)) + super().__init__() + + def drawPrimitive(self, element, option, painter, widget=None): + """Override QCommonStyle.drawPrimitive. + + Call the genuine drawPrimitive of self._style, except when a focus + rectangle should be drawn. + + Args: + element: PrimitiveElement pe + option: const QStyleOption * opt + painter: QPainter * p + widget: const QWidget * widget + """ + if element == QStyle.PE_FrameFocusRect: + return + return self._style.drawPrimitive(element, option, painter, widget) + + def drawItemText(self, painter, rectangle, alignment, palette, enabled, + text, textRole=QPalette.NoRole): + """Extend QCommonStyle::drawItemText to not center-align text. + + Since Qt hardcodes the text alignment for tabbar tabs in QCommonStyle, + we need to undo this here by deleting the flag again, and align left + instead. + + + Draws the given text in the specified rectangle using the provided + painter and palette. + + The text is drawn using the painter's pen, and aligned and wrapped + according to the specified alignment. If an explicit textRole is + specified, the text is drawn using the palette's color for the given + role. The enabled parameter indicates whether or not the item is + enabled; when reimplementing this function, the enabled parameter + should influence how the item is drawn. + + Args: + painter: QPainter * + rectangle: const QRect & + alignment int (Qt::Alignment) + palette: const QPalette & + enabled: bool + text: const QString & + textRole: QPalette::ColorRole textRole + """ + # pylint: disable=invalid-name + alignment &= ~Qt.AlignHCenter + alignment |= Qt.AlignLeft + super().drawItemText(painter, rectangle, alignment, palette, enabled, + text, textRole) From ef237bc081a4e143ed77ba4402a2b1f7e3310a92 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 13 Jul 2014 22:38:44 +0200 Subject: [PATCH 03/36] Remove obsolete tabbar options --- qutebrowser/config/configdata.py | 17 ----------------- qutebrowser/widgets/tabwidget.py | 10 ++-------- 2 files changed, 2 insertions(+), 25 deletions(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index b8e347a63..f659a7a7e 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -409,10 +409,6 @@ DATA = OrderedDict([ SettingValue(types.Bool(), 'false'), "Whether tabs should have close-buttons."), - ('scroll-buttons', - SettingValue(types.Bool(), 'true'), - "Whether there should be scroll buttons if there are too many tabs."), - ('position', SettingValue(types.Position(), 'north'), "The position of the tab bar."), @@ -429,22 +425,9 @@ DATA = OrderedDict([ SettingValue(types.Bool(), 'true'), "Whether to wrap when changing tabs."), - ('min-tab-width', - SettingValue(types.Int(minval=1), '100'), - "The minimum width of a tab."), - - ('max-tab-width', - SettingValue(types.Int(minval=1), '200'), - "The maximum width of a tab."), - ('show-favicons', SettingValue(types.Bool(), 'true'), "Whether to show favicons in the tab bar."), - - ('expand', - SettingValue(types.Bool(), 'false'), - "Whether to expand tabs to use the full window width."), - )), ('storage', sect.KeyValue( diff --git a/qutebrowser/widgets/tabwidget.py b/qutebrowser/widgets/tabwidget.py index 8f83626a5..0c5540c6f 100644 --- a/qutebrowser/widgets/tabwidget.py +++ b/qutebrowser/widgets/tabwidget.py @@ -72,8 +72,6 @@ class TabWidget(QTabWidget): {color[tab.bg]} {color[tab.fg]} border-right: 2px solid {color[tab.seperator]}; - min-width: {config[tabbar][min-tab-width]}px; - max-width: {config[tabbar][max-tab-width]}px; margin: 0px; }} @@ -108,7 +106,6 @@ class TabWidget(QTabWidget): } self.setMovable(config.get('tabbar', 'movable')) self.setTabsClosable(config.get('tabbar', 'close-buttons')) - self.setUsesScrollButtons(config.get('tabbar', 'scroll-buttons')) posstr = config.get('tabbar', 'position') selstr = config.get('tabbar', 'select-on-remove') self.setTabPosition(position_conv[posstr]) @@ -177,11 +174,8 @@ class TabBar(QTabBar): https://wiki.python.org/moin/PyQt/Customising%20tab%20bars """ - if config.get('tabbar', 'expand'): - height = super().tabSizeHint(index).height() - size = QSize(self.width() / self.count(), height) - else: - size = super().tabSizeHint(index) + height = super().tabSizeHint(index).height() + size = QSize(self.width() / self.count(), height) qt_ensure_valid(size) return size From bdd6a7ab48d77fcf3f08984ab5ceb4c17c94cc50 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 13 Jul 2014 22:41:45 +0200 Subject: [PATCH 04/36] Update TODO/BUGS --- doc/BUGS | 19 ------------------- doc/TODO | 2 -- 2 files changed, 21 deletions(-) diff --git a/doc/BUGS b/doc/BUGS index 7f058545a..9d8c59e38 100644 --- a/doc/BUGS +++ b/doc/BUGS @@ -7,17 +7,6 @@ Bugs e.g. loading a page and immediately yanking the (non-resolved) URL should work. -- When setting tabbar -> expand to true on xmonad (?), the window expands - continously: - - egan │ Sorry, what I mean is that the length of the tab starts at a specific - size and grows to the right continually. - - egan │ In fact the web page rendered in the tab also scrolls in that - direction until it is no longer visible - - egan │ This is accompanied by high CPU usage - - seir sometimes sees "-- COMMAND MODE --" even though that should never happen. @@ -77,10 +66,6 @@ Bugs - Shutdown is still flaky. (see notes) -- Eliding doesn't work correctly in tabs (cuts off start) - This especially happens when there's no favicon - (will be solved by tabbar reimplementation) - - Opening via commandline / startpage doesn't work with absolute file paths. - Relative file paths and ~ don't work at all. @@ -98,10 +83,6 @@ Bugs is loaded (probably *because* we're loading the error page and that succeeds). -- When tabbar -> scroll-buttons is disabled and there are too many tabs, the - window has a bigger size hint instead of tabs getting smaller than the - minimum size (iggy) - - Opening editor is broken on http://p.cmpl.cc/ - Segfault on subsonic when clicking next track diff --git a/doc/TODO b/doc/TODO index 30dceb2a5..f5e39da2e 100644 --- a/doc/TODO +++ b/doc/TODO @@ -76,7 +76,6 @@ Improvements / minor features - Commandline argument to delete config - Settings dialog - Tab groups (tagging/filtering for displayed tabs) -- Reimplement tabbar to paint it by ourselves to look like dwb - Save cookies in Netscape format so it can be used by wget. (see notes) - Zoom with ctrl + mousewheel - debug-Command for set loglevel/RAM capacity @@ -94,7 +93,6 @@ Improvements / minor features - Ctrl+A/X to increase/decrease last number in URL - Add more element-selection-detection code (with options?) (see notes) - somehow unfocus elements (hide blinking cursor) when insert mode is left? -- tabs: some more padding? - Copy link location on crash mail should not copy mailto: - Drag&Drop of tabs to other windows - Use QNetworkAccessManager per QWebPage again so we can set proxy per tab. From b4aba0d0ee7011a001f4e36dafdc09437f79cc36 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 13 Jul 2014 23:42:44 +0200 Subject: [PATCH 05/36] Don't display tab separator if unneeded --- qutebrowser/widgets/tabwidget.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/qutebrowser/widgets/tabwidget.py b/qutebrowser/widgets/tabwidget.py index 0c5540c6f..350c36701 100644 --- a/qutebrowser/widgets/tabwidget.py +++ b/qutebrowser/widgets/tabwidget.py @@ -71,10 +71,17 @@ class TabWidget(QTabWidget): QTabBar::tab {{ {color[tab.bg]} {color[tab.fg]} - border-right: 2px solid {color[tab.seperator]}; margin: 0px; }} + QTabBar::tab:first, QTabBar::tab:middle {{ + border-right: 2px solid {color[tab.seperator]}; + }} + + QTabBar::tab:only-one {{ + border-right: 0px; + }} + QTabBar::tab:selected {{ {color[tab.bg.selected]} }} From 9b21f37659772117eb9e908f26e07e632e211e44 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 14 Jul 2014 07:49:52 +0200 Subject: [PATCH 06/36] Try changing tabbar color --- qutebrowser/widgets/tabwidget.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/qutebrowser/widgets/tabwidget.py b/qutebrowser/widgets/tabwidget.py index 350c36701..ed58f3e74 100644 --- a/qutebrowser/widgets/tabwidget.py +++ b/qutebrowser/widgets/tabwidget.py @@ -24,7 +24,7 @@ import functools from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QSize from PyQt5.QtWidgets import (QTabWidget, QTabBar, QSizePolicy, QCommonStyle, QStyle) -from PyQt5.QtGui import QIcon, QPixmap, QPalette +from PyQt5.QtGui import QIcon, QPixmap, QPalette, QColor import qutebrowser.config.config as config from qutebrowser.config.style import set_register_stylesheet @@ -186,6 +186,13 @@ class TabBar(QTabBar): qt_ensure_valid(size) return size + def initStyleOption(self, option, tabIndex): + """Override initStyleOption so we can mark odd tabs.""" + super().initStyleOption(option, tabIndex) + if tabIndex % 2: + option |= 0x10000000 + return option + class TabWidgetStyle(QCommonStyle): @@ -217,7 +224,7 @@ class TabWidgetStyle(QCommonStyle): style: The base/"parent" style. """ self._style = style - for method in ('drawComplexControl', 'drawControl', 'drawItemPixmap', + for method in ('drawComplexControl', 'drawItemPixmap', 'generatedIconPixmap', 'hitTestComplexControl', 'itemPixmapRect', 'itemTextRect', 'pixelMetric', 'polish', 'styleHint', 'subControlRect', @@ -275,3 +282,12 @@ class TabWidgetStyle(QCommonStyle): alignment |= Qt.AlignLeft super().drawItemText(painter, rectangle, alignment, palette, enabled, text, textRole) + + def drawControl(self, element, opt, p, widget=None): + """Override drawControl to draw odd tabs in a different color.""" + if element != QStyle.CE_TabBarTab: + super().drawControl(element, opt, p, widget) + return + raise Exception + opt.palette.setColor(QPalette.Base, QColor('red')) + self._style.drawControl(element, opt, p, widget) From c3428db02938a5affd9897eb3fed882637e47480 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 14 Jul 2014 07:50:25 +0200 Subject: [PATCH 07/36] Use self._style for overridden drawItemText --- qutebrowser/widgets/tabwidget.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/widgets/tabwidget.py b/qutebrowser/widgets/tabwidget.py index ed58f3e74..74bb09131 100644 --- a/qutebrowser/widgets/tabwidget.py +++ b/qutebrowser/widgets/tabwidget.py @@ -280,8 +280,8 @@ class TabWidgetStyle(QCommonStyle): # pylint: disable=invalid-name alignment &= ~Qt.AlignHCenter alignment |= Qt.AlignLeft - super().drawItemText(painter, rectangle, alignment, palette, enabled, - text, textRole) + self._style.drawItemText(painter, rectangle, alignment, palette, + enabled, text, textRole) def drawControl(self, element, opt, p, widget=None): """Override drawControl to draw odd tabs in a different color.""" From 256d7e90b98b46069e7f64e3a5073513385dfafd Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 14 Jul 2014 13:34:44 +0200 Subject: [PATCH 08/36] Apply tabbar style to bar only --- qutebrowser/widgets/tabwidget.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/qutebrowser/widgets/tabwidget.py b/qutebrowser/widgets/tabwidget.py index 74bb09131..c46c93850 100644 --- a/qutebrowser/widgets/tabwidget.py +++ b/qutebrowser/widgets/tabwidget.py @@ -89,13 +89,14 @@ class TabWidget(QTabWidget): def __init__(self, parent): super().__init__(parent) - self.setTabBar(TabBar()) + bar = TabBar() + self.setTabBar(bar) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) - self.setStyle(TabWidgetStyle(self.style())) set_register_stylesheet(self) self.setDocumentMode(True) self.setElideMode(Qt.ElideRight) - self.tabBar().setDrawBase(False) + bar.setDrawBase(False) + bar.setStyle(TabBarStyle(bar.style())) self._init_config() def _init_config(self): @@ -194,9 +195,9 @@ class TabBar(QTabBar): return option -class TabWidgetStyle(QCommonStyle): +class TabBarStyle(QCommonStyle): - """Qt style used by TabWidget to fix some issues with the default one. + """Qt style used by TabBar to fix some issues with the default one. This fixes the following things: - Remove the focus rectangle Ubuntu draws on tabs. From 92d28a66d506e6d63deb50425dfc2a4618e98ab6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 14 Jul 2014 15:30:36 +0200 Subject: [PATCH 09/36] Paint odd/even tabs in different colors --- qutebrowser/config/configdata.py | 10 ++-- qutebrowser/widgets/tabwidget.py | 89 ++++++++++++++++++++------------ 2 files changed, 62 insertions(+), 37 deletions(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index f659a7a7e..8501779e5 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -856,9 +856,13 @@ DATA = OrderedDict([ SettingValue(types.Color(), 'white'), "Foreground color of tabs."), - ('tab.bg', - SettingValue(types.Color(), 'grey'), - "Background color of unselected tabs."), + ('tab.bg.odd', + SettingValue(types.QtColor(), 'grey'), + "Background color of unselected odd tabs."), + + ('tab.bg.even', + SettingValue(types.QtColor(), 'darkgrey'), + "Background color of unselected even tabs."), ('tab.bg.selected', SettingValue(types.Color(), 'black'), diff --git a/qutebrowser/widgets/tabwidget.py b/qutebrowser/widgets/tabwidget.py index c46c93850..4e1988ba4 100644 --- a/qutebrowser/widgets/tabwidget.py +++ b/qutebrowser/widgets/tabwidget.py @@ -23,7 +23,7 @@ import functools from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QSize from PyQt5.QtWidgets import (QTabWidget, QTabBar, QSizePolicy, QCommonStyle, - QStyle) + QStyle, QStylePainter, QStyleOptionTab) from PyQt5.QtGui import QIcon, QPixmap, QPalette, QColor import qutebrowser.config.config as config @@ -67,24 +67,6 @@ class TabWidget(QTabWidget): {font[tabbar]} {color[tab.bg.bar]} }} - - QTabBar::tab {{ - {color[tab.bg]} - {color[tab.fg]} - margin: 0px; - }} - - QTabBar::tab:first, QTabBar::tab:middle {{ - border-right: 2px solid {color[tab.seperator]}; - }} - - QTabBar::tab:only-one {{ - border-right: 0px; - }} - - QTabBar::tab:selected {{ - {color[tab.bg.selected]} - }} """ def __init__(self, parent): @@ -96,7 +78,6 @@ class TabWidget(QTabWidget): self.setDocumentMode(True) self.setElideMode(Qt.ElideRight) bar.setDrawBase(False) - bar.setStyle(TabBarStyle(bar.style())) self._init_config() def _init_config(self): @@ -141,6 +122,10 @@ class TabBar(QTabBar): tab_rightclicked = pyqtSignal(int) + def __init__(self, parent=None): + super().__init__(parent) + self.setStyle(TabBarStyle(self.style())) + def __repr__(self): return '<{} with {} tabs>'.format(self.__class__.__name__, self.count()) @@ -182,17 +167,32 @@ class TabBar(QTabBar): https://wiki.python.org/moin/PyQt/Customising%20tab%20bars """ - height = super().tabSizeHint(index).height() + height = self.fontMetrics().height() size = QSize(self.width() / self.count(), height) qt_ensure_valid(size) return size - def initStyleOption(self, option, tabIndex): - """Override initStyleOption so we can mark odd tabs.""" - super().initStyleOption(option, tabIndex) - if tabIndex % 2: - option |= 0x10000000 - return option + def paintEvent(self, e): + """Override paintEvent to draw the tabs like we want to.""" + p = QStylePainter(self) + tab = QStyleOptionTab() + selected = self.currentIndex() + for idx in range(self.count()): + self.initStyleOption(tab, idx) + if idx == selected: + color = config.get('colors', 'tab.bg.selected') + elif idx % 2: + color = config.get('colors', 'tab.bg.odd') + else: + color = config.get('colors', 'tab.bg.even') + tab.palette.setColor(QPalette.Window, QColor(color)) + tab.palette.setColor(QPalette.WindowText, + QColor(config.get('colors', 'tab.fg'))) + if tab.rect.right() < 0 or tab.rect.left() > self.width(): + # Don't bother drawing a tab if the entire tab is outside of + # the visible tab bar. + continue + p.drawControl(QStyle.CE_TabBarTab, tab) class TabBarStyle(QCommonStyle): @@ -285,10 +285,31 @@ class TabBarStyle(QCommonStyle): enabled, text, textRole) def drawControl(self, element, opt, p, widget=None): - """Override drawControl to draw odd tabs in a different color.""" - if element != QStyle.CE_TabBarTab: - super().drawControl(element, opt, p, widget) - return - raise Exception - opt.palette.setColor(QPalette.Base, QColor('red')) - self._style.drawControl(element, opt, p, widget) + """Override drawControl to draw odd tabs in a different color. + + Draws the given element with the provided painter with the style + options specified by option. + + Args: + element: ControlElement + option: const QStyleOption * + painter: QPainter * + widget: const QWidget * + """ + if element == QStyle.CE_TabBarTab: + # We override this so we can control TabBarTabShape/TabBarTabLabel. + self.drawControl(QStyle.CE_TabBarTabShape, opt, p, widget) + self.drawControl(QStyle.CE_TabBarTabLabel, opt, p, widget) + elif element == QStyle.CE_TabBarTabShape: + # We use super() rather than self._style here because we don't want + # any sophisticated drawing. + p.fillRect(opt.rect, opt.palette.window()) + super().drawControl(QStyle.CE_TabBarTabShape, opt, p, widget) + elif element == QStyle.CE_TabBarTabLabel: + # We use super() rather than self._style here so our drawItemText() + # gets called. + super().drawControl(QStyle.CE_TabBarTabLabel, opt, p, widget) + else: + # For any other elements we just delegate the work to our real + # style. + self._style.drawControl(element, opt, p, widget) From e608ccbe3629e795e52e922901e05579fb44726f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 14 Jul 2014 18:01:02 +0200 Subject: [PATCH 10/36] Draw tabbar ourselves #2 --- qutebrowser/widgets/tabbedbrowser.py | 6 +- qutebrowser/widgets/tabwidget.py | 194 ++++++++++++++++----------- 2 files changed, 119 insertions(+), 81 deletions(-) diff --git a/qutebrowser/widgets/tabbedbrowser.py b/qutebrowser/widgets/tabbedbrowser.py index c23b138d2..0a4ebd1ce 100644 --- a/qutebrowser/widgets/tabbedbrowser.py +++ b/qutebrowser/widgets/tabbedbrowser.py @@ -23,12 +23,13 @@ from functools import partial from PyQt5.QtWidgets import QSizePolicy from PyQt5.QtCore import pyqtSignal, pyqtSlot, QSize +from PyQt5.QtGui import QIcon import qutebrowser.config.config as config import qutebrowser.commands.utils as cmdutils import qutebrowser.keyinput.modeman as modeman import qutebrowser.utils.log as log -from qutebrowser.widgets.tabwidget import TabWidget, EmptyTabIcon +from qutebrowser.widgets.tabwidget import TabWidget from qutebrowser.widgets.webview import WebView from qutebrowser.browser.signalfilter import SignalFilter from qutebrowser.browser.commands import CommandDispatcher @@ -348,7 +349,7 @@ class TabbedBrowser(TabWidget): if show: self.setTabIcon(i, tab.icon()) else: - self.setTabIcon(i, EmptyTabIcon()) + self.setTabIcon(i, QIcon()) @pyqtSlot() def on_load_started(self, tab): @@ -362,7 +363,6 @@ class TabbedBrowser(TabWidget): # We can get signals for tabs we already deleted... log.webview.debug("Got invalid tab {}!".format(tab)) return - self.setTabIcon(idx, EmptyTabIcon()) @pyqtSlot() def on_cur_load_started(self): diff --git a/qutebrowser/widgets/tabwidget.py b/qutebrowser/widgets/tabwidget.py index 4e1988ba4..7a2912ef4 100644 --- a/qutebrowser/widgets/tabwidget.py +++ b/qutebrowser/widgets/tabwidget.py @@ -21,34 +21,16 @@ import functools -from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QSize +from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QSize, QRect from PyQt5.QtWidgets import (QTabWidget, QTabBar, QSizePolicy, QCommonStyle, QStyle, QStylePainter, QStyleOptionTab) -from PyQt5.QtGui import QIcon, QPixmap, QPalette, QColor +from PyQt5.QtGui import QIcon, QPalette, QColor import qutebrowser.config.config as config from qutebrowser.config.style import set_register_stylesheet from qutebrowser.utils.qt import qt_ensure_valid -class EmptyTabIcon(QIcon): - - """An empty icon for a tab. - - Qt somehow cuts text off when padding is used for the tabbar, see - https://bugreports.qt-project.org/browse/QTBUG-15203 - - Until we find a better solution we use this hack of using a simple - transparent icon to get some padding, because when a real favicon is set, - the padding seems to be fine... - """ - - def __init__(self): - pix = QPixmap(2, 16) - pix.fill(Qt.transparent) - super().__init__(pix) - - class TabWidget(QTabWidget): """The tabwidget used for TabbedBrowser. @@ -172,7 +154,7 @@ class TabBar(QTabBar): qt_ensure_valid(size) return size - def paintEvent(self, e): + def paintEvent(self, _e): """Override paintEvent to draw the tabs like we want to.""" p = QStylePainter(self) tab = QStyleOptionTab() @@ -227,63 +209,14 @@ class TabBarStyle(QCommonStyle): self._style = style for method in ('drawComplexControl', 'drawItemPixmap', 'generatedIconPixmap', 'hitTestComplexControl', - 'itemPixmapRect', 'itemTextRect', 'pixelMetric', - 'polish', 'styleHint', 'subControlRect', - 'subElementRect', 'unpolish', 'sizeFromContents'): + 'pixelMetric', 'subElementRect', + 'itemPixmapRect', 'itemTextRect', 'polish', 'styleHint', + 'subControlRect', 'unpolish', 'drawPrimitive', + 'drawItemText', 'sizeFromContents'): target = getattr(self._style, method) setattr(self, method, functools.partial(target)) super().__init__() - def drawPrimitive(self, element, option, painter, widget=None): - """Override QCommonStyle.drawPrimitive. - - Call the genuine drawPrimitive of self._style, except when a focus - rectangle should be drawn. - - Args: - element: PrimitiveElement pe - option: const QStyleOption * opt - painter: QPainter * p - widget: const QWidget * widget - """ - if element == QStyle.PE_FrameFocusRect: - return - return self._style.drawPrimitive(element, option, painter, widget) - - def drawItemText(self, painter, rectangle, alignment, palette, enabled, - text, textRole=QPalette.NoRole): - """Extend QCommonStyle::drawItemText to not center-align text. - - Since Qt hardcodes the text alignment for tabbar tabs in QCommonStyle, - we need to undo this here by deleting the flag again, and align left - instead. - - - Draws the given text in the specified rectangle using the provided - painter and palette. - - The text is drawn using the painter's pen, and aligned and wrapped - according to the specified alignment. If an explicit textRole is - specified, the text is drawn using the palette's color for the given - role. The enabled parameter indicates whether or not the item is - enabled; when reimplementing this function, the enabled parameter - should influence how the item is drawn. - - Args: - painter: QPainter * - rectangle: const QRect & - alignment int (Qt::Alignment) - palette: const QPalette & - enabled: bool - text: const QString & - textRole: QPalette::ColorRole textRole - """ - # pylint: disable=invalid-name - alignment &= ~Qt.AlignHCenter - alignment |= Qt.AlignLeft - self._style.drawItemText(painter, rectangle, alignment, palette, - enabled, text, textRole) - def drawControl(self, element, opt, p, widget=None): """Override drawControl to draw odd tabs in a different color. @@ -303,13 +236,118 @@ class TabBarStyle(QCommonStyle): elif element == QStyle.CE_TabBarTabShape: # We use super() rather than self._style here because we don't want # any sophisticated drawing. - p.fillRect(opt.rect, opt.palette.window()) super().drawControl(QStyle.CE_TabBarTabShape, opt, p, widget) elif element == QStyle.CE_TabBarTabLabel: - # We use super() rather than self._style here so our drawItemText() - # gets called. - super().drawControl(QStyle.CE_TabBarTabLabel, opt, p, widget) + p.fillRect(opt.rect, opt.palette.window()) + text_rect, icon_rect = self._tab_layout(opt, widget) + if not opt.icon.isNull(): + icon_mode = (QIcon.Normal if opt.state & QStyle.State_Enabled + else QIcon.Disabled) + icon_state = (QIcon.On if opt.state & QStyle.State_Selected + else QIcon.Off) + icon = opt.icon.pixmap(opt.iconSize, icon_mode, icon_state) + p.drawPixmap(icon_rect.x(), icon_rect.y(), icon) + self._style.drawItemText(p, text_rect, + Qt.AlignLeft | Qt.AlignVCenter, + opt.palette, + opt.state & QStyle.State_Enabled, + opt.text, QPalette.WindowText) else: # For any other elements we just delegate the work to our real # style. self._style.drawControl(element, opt, p, widget) + + def pixelMetric(self, metric, option=None, widget=None): + """Override pixelMetric to not shift the selected tab. + + Args: + metric: PixelMetric + option: const QStyleOption * + widget: const QWidget * + + Return: + An int. + """ + if (metric == QStyle.PM_TabBarTabShiftHorizontal or + metric == QStyle.PM_TabBarTabShiftVertical or + metric == QStyle.PM_TabBarTabHSpace or + metric == QStyle.PM_TabBarTabVSpace): + return 0 + else: + return self._style.pixelMetric(metric, option, widget) + + def subElementRect(self, sr, option, widget=None): + """Override subElementRect to use our own _tab_layout implementation. + + Args: + sr: SubElement + opt: QStyleOption + widget: QWidget + + Return: + A QRect. + """ + if sr == QStyle.SE_TabBarTabText: + text_rect, _icon_rect = self._tab_layout(option, widget) + return text_rect + else: + return self._style.subElementRect(sr, option, widget) + + def _tab_layout(self, opt, _widget): + """Compute the text/icon rect from the opt rect. + + This is based on Qt's QCommonStylePrivate::tabLayout + (qtbase/src/widgets/styles/qcommonstyle.cpp) as we can't use the + private implementation. + + Args: + opt: QStyleOptionTab + _widget: QWidget + + Return: + A (text_rect, icon_rect) tuple (both QRects). + """ + padding = 4 + icon_rect = QRect() + text_rect = opt.rect + qt_ensure_valid(text_rect) + text_rect.adjust(padding, 0, 0, 0) + if not opt.leftButtonSize.isEmpty(): + text_rect.adjust(opt.leftButtonSize.width(), 0, 0, 0) + if not opt.rightButtonSize.isEmpty(): + text_rect.adjust(0, 0, -opt.rightButtonSize.width(), 0) + if not opt.icon.isNull(): + icon_rect = self._get_icon_rect(opt, text_rect) + text_rect.adjust(icon_rect.width() + 4, 0, 0, 0) + text_rect = self._style.visualRect(opt.direction, opt.rect, text_rect) + qt_ensure_valid(text_rect) + qt_ensure_valid(icon_rect) + return (text_rect, icon_rect) + + def _get_icon_rect(self, opt, text_rect): + """Get a QRect for the icon to draw. + + Args: + opt: QStyleOptionTab + text_rect: The QRect for the text. + + Return: + A QRect. + """ + icon_size = opt.iconSize + if not icon_size.isValid(): + icon_extent = self._style.pixelMetric(QStyle.PM_SmallIconSize) + icon_size = QSize(icon_extent, icon_extent) + icon_mode = (QIcon.Normal if opt.state & QStyle.State_Enabled + else QIcon.Disabled) + icon_state = (QIcon.On if opt.state & QStyle.State_Selected + else QIcon.Off) + tab_icon_size = opt.icon.actualSize(icon_size, icon_mode, icon_state) + tab_icon_size = QSize(min(tab_icon_size.width(), icon_size.width()), + min(tab_icon_size.height(), icon_size.height())) + icon_rect = QRect(text_rect.left(), + text_rect.center().y() - tab_icon_size.height() / 2, + tab_icon_size.width(), tab_icon_size.height()) + icon_rect = self._style.visualRect(opt.direction, opt.rect, icon_rect) + qt_ensure_valid(icon_rect) + return icon_rect From 87e80451c7d06b7eec82b3ef9765210e8b1a24ac Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 14 Jul 2014 18:35:35 +0200 Subject: [PATCH 11/36] Remove wrong validity check --- qutebrowser/widgets/tabwidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/widgets/tabwidget.py b/qutebrowser/widgets/tabwidget.py index 7a2912ef4..c49302067 100644 --- a/qutebrowser/widgets/tabwidget.py +++ b/qutebrowser/widgets/tabwidget.py @@ -241,6 +241,7 @@ class TabBarStyle(QCommonStyle): p.fillRect(opt.rect, opt.palette.window()) text_rect, icon_rect = self._tab_layout(opt, widget) if not opt.icon.isNull(): + qt_ensure_valid(icon_rect) icon_mode = (QIcon.Normal if opt.state & QStyle.State_Enabled else QIcon.Disabled) icon_state = (QIcon.On if opt.state & QStyle.State_Selected @@ -321,7 +322,6 @@ class TabBarStyle(QCommonStyle): text_rect.adjust(icon_rect.width() + 4, 0, 0, 0) text_rect = self._style.visualRect(opt.direction, opt.rect, text_rect) qt_ensure_valid(text_rect) - qt_ensure_valid(icon_rect) return (text_rect, icon_rect) def _get_icon_rect(self, opt, text_rect): From 1f9597153e39de740b3b35e21d8dd0f485077546 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 14 Jul 2014 18:48:38 +0200 Subject: [PATCH 12/36] Cleanup --- qutebrowser/widgets/tabwidget.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/qutebrowser/widgets/tabwidget.py b/qutebrowser/widgets/tabwidget.py index c49302067..7cc4fc071 100644 --- a/qutebrowser/widgets/tabwidget.py +++ b/qutebrowser/widgets/tabwidget.py @@ -144,10 +144,16 @@ class TabBar(QTabBar): height = super().tabSizeHint(index).height() return QSize(1, height) - def tabSizeHint(self, index): + def tabSizeHint(self, _index): """Override tabSizeHint so all tabs are the same size. https://wiki.python.org/moin/PyQt/Customising%20tab%20bars + + Args: + _index: The index of the tab. + + Return: + A QSize. """ height = self.fontMetrics().height() size = QSize(self.width() / self.count(), height) From 465ff7406cfbec46cd24e0510aaa0c9eacf46b98 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 14 Jul 2014 23:28:55 +0200 Subject: [PATCH 13/36] Use self instead of super() --- qutebrowser/widgets/tabwidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/widgets/tabwidget.py b/qutebrowser/widgets/tabwidget.py index 7cc4fc071..a2d86e280 100644 --- a/qutebrowser/widgets/tabwidget.py +++ b/qutebrowser/widgets/tabwidget.py @@ -141,7 +141,7 @@ class TabBar(QTabBar): Return: A QSize. """ - height = super().tabSizeHint(index).height() + height = self.tabSizeHint(index).height() return QSize(1, height) def tabSizeHint(self, _index): From 1d31f1eb48d00e4ffa5a2e9ed0c79b1828b0880d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 14 Jul 2014 23:29:45 +0200 Subject: [PATCH 14/36] Don't pass useless widget to tab_layout. --- qutebrowser/widgets/tabwidget.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/qutebrowser/widgets/tabwidget.py b/qutebrowser/widgets/tabwidget.py index a2d86e280..06706c74f 100644 --- a/qutebrowser/widgets/tabwidget.py +++ b/qutebrowser/widgets/tabwidget.py @@ -245,7 +245,7 @@ class TabBarStyle(QCommonStyle): super().drawControl(QStyle.CE_TabBarTabShape, opt, p, widget) elif element == QStyle.CE_TabBarTabLabel: p.fillRect(opt.rect, opt.palette.window()) - text_rect, icon_rect = self._tab_layout(opt, widget) + text_rect, icon_rect = self._tab_layout(opt) if not opt.icon.isNull(): qt_ensure_valid(icon_rect) icon_mode = (QIcon.Normal if opt.state & QStyle.State_Enabled @@ -295,12 +295,12 @@ class TabBarStyle(QCommonStyle): A QRect. """ if sr == QStyle.SE_TabBarTabText: - text_rect, _icon_rect = self._tab_layout(option, widget) + text_rect, _icon_rect = self._tab_layout(option) return text_rect else: return self._style.subElementRect(sr, option, widget) - def _tab_layout(self, opt, _widget): + def _tab_layout(self, opt): """Compute the text/icon rect from the opt rect. This is based on Qt's QCommonStylePrivate::tabLayout @@ -309,7 +309,6 @@ class TabBarStyle(QCommonStyle): Args: opt: QStyleOptionTab - _widget: QWidget Return: A (text_rect, icon_rect) tuple (both QRects). From 27d3c8d20f19119d24127236545ccdfdae81b709 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 14 Jul 2014 23:30:08 +0200 Subject: [PATCH 15/36] Rename option to opt. --- qutebrowser/widgets/tabwidget.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qutebrowser/widgets/tabwidget.py b/qutebrowser/widgets/tabwidget.py index 06706c74f..e18271b3d 100644 --- a/qutebrowser/widgets/tabwidget.py +++ b/qutebrowser/widgets/tabwidget.py @@ -283,7 +283,7 @@ class TabBarStyle(QCommonStyle): else: return self._style.pixelMetric(metric, option, widget) - def subElementRect(self, sr, option, widget=None): + def subElementRect(self, sr, opt, widget=None): """Override subElementRect to use our own _tab_layout implementation. Args: @@ -295,10 +295,10 @@ class TabBarStyle(QCommonStyle): A QRect. """ if sr == QStyle.SE_TabBarTabText: - text_rect, _icon_rect = self._tab_layout(option) + text_rect, _icon_rect = self._tab_layout(opt) return text_rect else: - return self._style.subElementRect(sr, option, widget) + return self._style.subElementRect(sr, opt, widget) def _tab_layout(self, opt): """Compute the text/icon rect from the opt rect. From 7ad0dc6416614cabd1e6260b39f8116f6cc6f7de Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 14 Jul 2014 23:30:25 +0200 Subject: [PATCH 16/36] Remove magic constant. --- qutebrowser/widgets/tabwidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/widgets/tabwidget.py b/qutebrowser/widgets/tabwidget.py index e18271b3d..058f033da 100644 --- a/qutebrowser/widgets/tabwidget.py +++ b/qutebrowser/widgets/tabwidget.py @@ -324,7 +324,7 @@ class TabBarStyle(QCommonStyle): text_rect.adjust(0, 0, -opt.rightButtonSize.width(), 0) if not opt.icon.isNull(): icon_rect = self._get_icon_rect(opt, text_rect) - text_rect.adjust(icon_rect.width() + 4, 0, 0, 0) + text_rect.adjust(icon_rect.width() + padding, 0, 0, 0) text_rect = self._style.visualRect(opt.direction, opt.rect, text_rect) qt_ensure_valid(text_rect) return (text_rect, icon_rect) From cd19697a77cd77a9505c0d7c5be7ff0dc60c2531 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 14 Jul 2014 23:31:09 +0200 Subject: [PATCH 17/36] Construct a copy of text_rect in tab_layout. --- qutebrowser/widgets/tabwidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/widgets/tabwidget.py b/qutebrowser/widgets/tabwidget.py index 058f033da..2b99ab3bc 100644 --- a/qutebrowser/widgets/tabwidget.py +++ b/qutebrowser/widgets/tabwidget.py @@ -315,7 +315,7 @@ class TabBarStyle(QCommonStyle): """ padding = 4 icon_rect = QRect() - text_rect = opt.rect + text_rect = QRect(opt.rect) qt_ensure_valid(text_rect) text_rect.adjust(padding, 0, 0, 0) if not opt.leftButtonSize.isEmpty(): From 367ecb0e449ae58060b87f726bf40d8a2a378f18 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 14 Jul 2014 23:31:45 +0200 Subject: [PATCH 18/36] Override subElementRect properly. --- qutebrowser/widgets/tabwidget.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/qutebrowser/widgets/tabwidget.py b/qutebrowser/widgets/tabwidget.py index 2b99ab3bc..0fa5b92ef 100644 --- a/qutebrowser/widgets/tabwidget.py +++ b/qutebrowser/widgets/tabwidget.py @@ -215,10 +215,9 @@ class TabBarStyle(QCommonStyle): self._style = style for method in ('drawComplexControl', 'drawItemPixmap', 'generatedIconPixmap', 'hitTestComplexControl', - 'pixelMetric', 'subElementRect', - 'itemPixmapRect', 'itemTextRect', 'polish', 'styleHint', - 'subControlRect', 'unpolish', 'drawPrimitive', - 'drawItemText', 'sizeFromContents'): + 'pixelMetric', 'itemPixmapRect', 'itemTextRect', + 'polish', 'styleHint', 'subControlRect', 'unpolish', + 'drawPrimitive', 'drawItemText', 'sizeFromContents'): target = getattr(self._style, method) setattr(self, method, functools.partial(target)) super().__init__() From 9a430b22a3f4f3cb9c64ab306ce34d588452910f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 15 Jul 2014 06:40:01 +0200 Subject: [PATCH 19/36] Calculate close button position correctly --- qutebrowser/widgets/tabwidget.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/qutebrowser/widgets/tabwidget.py b/qutebrowser/widgets/tabwidget.py index 0fa5b92ef..f6f21c90f 100644 --- a/qutebrowser/widgets/tabwidget.py +++ b/qutebrowser/widgets/tabwidget.py @@ -19,6 +19,7 @@ """The tab widget used for TabbedBrowser from browser.py.""" +from math import ceil import functools from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QSize, QRect @@ -296,6 +297,21 @@ class TabBarStyle(QCommonStyle): if sr == QStyle.SE_TabBarTabText: text_rect, _icon_rect = self._tab_layout(opt) return text_rect + if (sr == QStyle.SE_TabBarTabLeftButton or + sr == QStyle.SE_TabBarTabRightButton): + size = (opt.leftButtonSize if sr == QStyle.SE_TabBarTabLeftButton + else opt.rightButtonSize) + width = size.width() + height = size.height() + mid_height = ceil((opt.rect.height() - height) / 2) + mid_width = (opt.rect.width() - width) / 2 + if sr == QStyle.SE_TabBarTabLeftButton: + rect = QRect(opt.rect.x(), mid_height, width, height) + else: + rect = QRect(opt.rect.right() - width, mid_height, width, + height) + rect = self._style.visualRect(opt.direction, opt.rect, rect) + return rect else: return self._style.subElementRect(sr, opt, widget) From b042b5cbc43eddc782c725570d504b0f63b061be Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 15 Jul 2014 07:45:53 +0200 Subject: [PATCH 20/36] Try drawing our own tab button --- qutebrowser/widgets/tabwidget.py | 37 ++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/qutebrowser/widgets/tabwidget.py b/qutebrowser/widgets/tabwidget.py index f6f21c90f..9193b3de7 100644 --- a/qutebrowser/widgets/tabwidget.py +++ b/qutebrowser/widgets/tabwidget.py @@ -22,7 +22,7 @@ from math import ceil import functools -from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QSize, QRect +from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QSize, QRect, QRectF from PyQt5.QtWidgets import (QTabWidget, QTabBar, QSizePolicy, QCommonStyle, QStyle, QStylePainter, QStyleOptionTab) from PyQt5.QtGui import QIcon, QPalette, QColor @@ -183,6 +183,16 @@ class TabBar(QTabBar): continue p.drawControl(QStyle.CE_TabBarTab, tab) + def tabInserted(self, index): + """Extend tabInserted to set close button size policy. + + FIXME is this needed? + """ + super().tabInserted(index) + button = self.tabButton(index, QTabBar.RightSide) + if button is not None: + button.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) + class TabBarStyle(QCommonStyle): @@ -218,7 +228,7 @@ class TabBarStyle(QCommonStyle): 'generatedIconPixmap', 'hitTestComplexControl', 'pixelMetric', 'itemPixmapRect', 'itemTextRect', 'polish', 'styleHint', 'subControlRect', 'unpolish', - 'drawPrimitive', 'drawItemText', 'sizeFromContents'): + 'drawItemText', 'sizeFromContents'): target = getattr(self._style, method) setattr(self, method, functools.partial(target)) super().__init__() @@ -264,6 +274,29 @@ class TabBarStyle(QCommonStyle): # style. self._style.drawControl(element, opt, p, widget) + def drawPrimitive(self, element, opt, painter, widget=None): + """Draws the given primitive element. + + Args: + element: PrimitiveElement + opt: const QStyleOption * + painter: QPainter * + widget: const QWidget * + """ + if element != QStyle.PE_IndicatorTabClose: + self._style.drawPrimitive(element, opt, painter, widget) + return + painter.save() + painter.setPen(QColor(config.get('colors', 'tab.fg'))) + rect = QRectF(opt.rect) + center = rect.center() + new_size = rect.size() * 0.4 + rect.setSize(new_size) + rect.moveCenter(center) + painter.drawLine(rect.topLeft(), rect.bottomRight()) + painter.drawLine(rect.topRight(), rect.bottomLeft()) + painter.restore() + def pixelMetric(self, metric, option=None, widget=None): """Override pixelMetric to not shift the selected tab. From ca74fa28ac474c3fc4a3309979c78c9bf69e0a44 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 15 Jul 2014 11:06:56 +0200 Subject: [PATCH 21/36] Use a nicer Color() class for tests. --- qutebrowser/test/utils/test_misc.py | 60 ++++++++++++++++++----------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/qutebrowser/test/utils/test_misc.py b/qutebrowser/test/utils/test_misc.py index f92682e0c..2a0795dca 100644 --- a/qutebrowser/test/utils/test_misc.py +++ b/qutebrowser/test/utils/test_misc.py @@ -34,6 +34,22 @@ import qutebrowser.utils.misc as utils from qutebrowser.test.helpers import environ_set_temp, fake_keyevent +class Color(QColor): + + """A QColor with a nicer repr().""" + + def __repr__(self): + return 'Color({}, {}, {}, {})'.format( + self.red(), self.green(), self.blue(), self.alpha()) + + def __eq__(self, other): + """The default operator= of QColor seems to be rather strict.""" + return (self.red() == other.red() and + self.green() == other.green() and + self.blue() == other.blue() and + self.alpha() == other.alpha()) + + class ElidingTests(unittest.TestCase): """Test elide.""" @@ -330,23 +346,23 @@ class InterpolateColorTests(unittest.TestCase): """Tests for interpolate_color. Attributes: - white: The QColor white as a valid QColor for tests. - white: The QColor black as a valid QColor for tests. + white: The Color white as a valid Color for tests. + white: The Color black as a valid Color for tests. """ def setUp(self): - self.white = QColor('white') - self.black = QColor('black') + self.white = Color('white') + self.black = Color('black') def test_invalid_start(self): """Test an invalid start color.""" with self.assertRaises(ValueError): - utils.interpolate_color(QColor(), self.white, 0) + utils.interpolate_color(Color(), self.white, 0) def test_invalid_end(self): """Test an invalid end color.""" with self.assertRaises(ValueError): - utils.interpolate_color(self.white, QColor(), 0) + utils.interpolate_color(self.white, Color(), 0) def test_invalid_percentage(self): """Test an invalid percentage.""" @@ -365,52 +381,52 @@ class InterpolateColorTests(unittest.TestCase): white = utils.interpolate_color(self.white, self.black, 0, QColor.Rgb) black = utils.interpolate_color(self.white, self.black, 100, QColor.Rgb) - self.assertEqual(white, self.white) - self.assertEqual(black, self.black) + self.assertEqual(Color(white), self.white) + self.assertEqual(Color(black), self.black) def test_valid_percentages_hsv(self): """Test 0% and 100% in the HSV colorspace.""" white = utils.interpolate_color(self.white, self.black, 0, QColor.Hsv) black = utils.interpolate_color(self.white, self.black, 100, QColor.Hsv) - self.assertEqual(white, self.white) - self.assertEqual(black, self.black) + self.assertEqual(Color(white), self.white) + self.assertEqual(Color(black), self.black) def test_valid_percentages_hsl(self): """Test 0% and 100% in the HSL colorspace.""" white = utils.interpolate_color(self.white, self.black, 0, QColor.Hsl) black = utils.interpolate_color(self.white, self.black, 100, QColor.Hsl) - self.assertEqual(white, self.white) - self.assertEqual(black, self.black) + self.assertEqual(Color(white), self.white) + self.assertEqual(Color(black), self.black) def test_interpolation_rgb(self): """Test an interpolation in the RGB colorspace.""" - color = utils.interpolate_color(QColor(0, 40, 100), QColor(0, 20, 200), + color = utils.interpolate_color(Color(0, 40, 100), Color(0, 20, 200), 50, QColor.Rgb) - self.assertEqual(color, QColor(0, 30, 150)) + self.assertEqual(Color(color), Color(0, 30, 150)) def test_interpolation_hsv(self): """Test an interpolation in the HSV colorspace.""" - start = QColor() - stop = QColor() + start = Color() + stop = Color() start.setHsv(0, 40, 100) stop.setHsv(0, 20, 200) color = utils.interpolate_color(start, stop, 50, QColor.Hsv) - expected = QColor() + expected = Color() expected.setHsv(0, 30, 150) - self.assertEqual(color, expected) + self.assertEqual(Color(color), expected) def test_interpolation_hsl(self): """Test an interpolation in the HSL colorspace.""" - start = QColor() - stop = QColor() + start = Color() + stop = Color() start.setHsl(0, 40, 100) stop.setHsl(0, 20, 200) color = utils.interpolate_color(start, stop, 50, QColor.Hsl) - expected = QColor() + expected = Color() expected.setHsl(0, 30, 150) - self.assertEqual(color, expected) + self.assertEqual(Color(color), expected) class FormatSecondsTests(unittest.TestCase): From 20e8c464e60c92d6bcf9c1d3d7fcf12d5bdd7feb Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 15 Jul 2014 11:07:15 +0200 Subject: [PATCH 22/36] Check for QtValueErrors in interpolate_color tests. --- qutebrowser/test/utils/test_misc.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qutebrowser/test/utils/test_misc.py b/qutebrowser/test/utils/test_misc.py index 2a0795dca..799459e4a 100644 --- a/qutebrowser/test/utils/test_misc.py +++ b/qutebrowser/test/utils/test_misc.py @@ -32,6 +32,7 @@ from PyQt5.QtGui import QColor import qutebrowser.utils.misc as utils from qutebrowser.test.helpers import environ_set_temp, fake_keyevent +from qutebrowser.utils.qt import QtValueError class Color(QColor): @@ -356,12 +357,12 @@ class InterpolateColorTests(unittest.TestCase): def test_invalid_start(self): """Test an invalid start color.""" - with self.assertRaises(ValueError): + with self.assertRaises(QtValueError): utils.interpolate_color(Color(), self.white, 0) def test_invalid_end(self): """Test an invalid end color.""" - with self.assertRaises(ValueError): + with self.assertRaises(QtValueError): utils.interpolate_color(self.white, Color(), 0) def test_invalid_percentage(self): From b47d53953b352fd834dcf810a160a9a37e449718 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 15 Jul 2014 11:07:43 +0200 Subject: [PATCH 23/36] Add a highlight_color util function. --- qutebrowser/test/utils/test_misc.py | 31 ++++++++++++++++++++++++++++ qutebrowser/utils/misc.py | 32 ++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/qutebrowser/test/utils/test_misc.py b/qutebrowser/test/utils/test_misc.py index 799459e4a..888a4ca86 100644 --- a/qutebrowser/test/utils/test_misc.py +++ b/qutebrowser/test/utils/test_misc.py @@ -576,5 +576,36 @@ class NormalizeTests(unittest.TestCase): self.assertEqual(utils.normalize_keystr(orig), repl, orig) +class HighlightColorTests(unittest.TestCase): + + """Test highlight_color.""" + + def test_invalid(self): + """Test with an invalid Color.""" + col = Color() + with self.assertRaises(QtValueError): + utils.highlight_color(col) + + def test_large_factor(self): + """Test with a too large factor.""" + col = Color('black') + with self.assertRaises(OverflowError): + utils.highlight_color(col, 2 ** 31) + + def test_white(self): + """Test highlighting white.""" + col = Color('white') + c1 = Color(utils.highlight_color(col, 1)) + c2 = Color(127, 127, 127) + self.assertEqual(c1, c2) + + def test_black(self): + """Test highlighting black.""" + col = Color('black') + self.assertEqual(Color(utils.highlight_color(col, 0.5)), + Color(204, 204, 204)) + + + if __name__ == '__main__': unittest.main() diff --git a/qutebrowser/utils/misc.py b/qutebrowser/utils/misc.py index 735e78bc7..1213ad07b 100644 --- a/qutebrowser/utils/misc.py +++ b/qutebrowser/utils/misc.py @@ -36,7 +36,8 @@ from pkg_resources import resource_string import qutebrowser import qutebrowser.utils.log as log -from qutebrowser.utils.qt import qt_version_check, qt_ensure_valid +from qutebrowser.utils.qt import (qt_version_check, qt_ensure_valid, + check_overflow) def elide(text, length): @@ -489,3 +490,32 @@ def normalize_keystr(keystr): for mod in ('Ctrl', 'Meta', 'Alt', 'Shift'): keystr = keystr.replace(mod + '-', mod + '+') return keystr.lower() + + +def highlight_color(color, factor=0.5): + """Get a highlighted color based on a base color. + + If the passed color is dark, it will get lighter. If it is light, it will + get darker. + + Args: + color: A QColor with the initial color. + factor: How much to lighten/darken the color (0.5: 50% lighter/darker). + + Return: + A QColor with the highlighted color. + """ + qt_ensure_valid(color) + qt_factor = 100 * (1 + factor) + check_overflow(qt_factor, 'int') + if color == QColor('black'): + # Black isn't handled by QColor::lighter because that just multiplies + # the HSV value by a factor. + # We also add an additional 30% because the effect would be way too + # subtle if we didn't. + return interpolate_color(QColor('black'), QColor('white'), + min((100 * factor) + 30, 100)) + elif color.value() < 128: + return color.lighter(qt_factor) + else: + return color.darker(qt_factor) From 7c8471e64c9c48862718563c6747a76592bb10a4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 15 Jul 2014 11:08:10 +0200 Subject: [PATCH 24/36] Nicer drawing of close cross. --- qutebrowser/widgets/tabwidget.py | 54 ++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/qutebrowser/widgets/tabwidget.py b/qutebrowser/widgets/tabwidget.py index 9193b3de7..02b08402d 100644 --- a/qutebrowser/widgets/tabwidget.py +++ b/qutebrowser/widgets/tabwidget.py @@ -30,6 +30,7 @@ from PyQt5.QtGui import QIcon, QPalette, QColor import qutebrowser.config.config as config from qutebrowser.config.style import set_register_stylesheet from qutebrowser.utils.qt import qt_ensure_valid +from qutebrowser.utils.misc import highlight_color class TabWidget(QTabWidget): @@ -275,27 +276,40 @@ class TabBarStyle(QCommonStyle): self._style.drawControl(element, opt, p, widget) def drawPrimitive(self, element, opt, painter, widget=None): - """Draws the given primitive element. + """Draw the given primitive element. - Args: - element: PrimitiveElement - opt: const QStyleOption * - painter: QPainter * - widget: const QWidget * - """ - if element != QStyle.PE_IndicatorTabClose: - self._style.drawPrimitive(element, opt, painter, widget) - return - painter.save() - painter.setPen(QColor(config.get('colors', 'tab.fg'))) - rect = QRectF(opt.rect) - center = rect.center() - new_size = rect.size() * 0.4 - rect.setSize(new_size) - rect.moveCenter(center) - painter.drawLine(rect.topLeft(), rect.bottomRight()) - painter.drawLine(rect.topRight(), rect.bottomLeft()) - painter.restore() + Overriden to draw our own tab close button. + + Args: + element: PrimitiveElement + opt: const QStyleOption * + painter: QPainter * + widget: const QWidget * + """ + if element != QStyle.PE_IndicatorTabClose: + self._style.drawPrimitive(element, opt, painter, widget) + return + painter.save() + # This fixes some weird off-by-ones in Qt. + # See http://stackoverflow.com/a/17019898/2085149 + painter.translate(0.5, 0.5) + color = QColor(config.get('colors', 'tab.fg')) + if opt.state & QStyle.State_Raised: + color = highlight_color(color, factor=0.2) + elif opt.state & QStyle.State_Sunken: + color = highlight_color(color, factor=0.3) + painter.setPen(color) + side = min(opt.rect.width(), opt.rect.height()) + square = QRect() + square.setSize(QSize(side, side) * 0.4) + square.moveCenter(opt.rect.center()) + if square.width() % 2 == 0: + # An X is easier to paint in a square with an odd count of + # pixels. + square.adjust(-1, -1, 0, 0) + painter.drawLine(square.topLeft(), square.bottomRight()) + painter.drawLine(square.topRight(), square.bottomLeft()) + painter.restore() def pixelMetric(self, metric, option=None, widget=None): """Override pixelMetric to not shift the selected tab. From 93f2e4b24ddfea04b0c08cd2af772ba65682dac8 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 15 Jul 2014 11:08:25 +0200 Subject: [PATCH 25/36] Cleanup. --- qutebrowser/widgets/tabwidget.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qutebrowser/widgets/tabwidget.py b/qutebrowser/widgets/tabwidget.py index 02b08402d..55f337313 100644 --- a/qutebrowser/widgets/tabwidget.py +++ b/qutebrowser/widgets/tabwidget.py @@ -22,7 +22,7 @@ from math import ceil import functools -from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QSize, QRect, QRectF +from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QSize, QRect from PyQt5.QtWidgets import (QTabWidget, QTabBar, QSizePolicy, QCommonStyle, QStyle, QStylePainter, QStyleOptionTab) from PyQt5.QtGui import QIcon, QPalette, QColor @@ -351,7 +351,6 @@ class TabBarStyle(QCommonStyle): width = size.width() height = size.height() mid_height = ceil((opt.rect.height() - height) / 2) - mid_width = (opt.rect.width() - width) / 2 if sr == QStyle.SE_TabBarTabLeftButton: rect = QRect(opt.rect.x(), mid_height, width, height) else: From 81ff6222912bcaf617a7e258843984ce9349bc97 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 15 Jul 2014 11:18:56 +0200 Subject: [PATCH 26/36] Improve highlight_color tests. --- qutebrowser/test/utils/test_misc.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/qutebrowser/test/utils/test_misc.py b/qutebrowser/test/utils/test_misc.py index 888a4ca86..14ec85e7e 100644 --- a/qutebrowser/test/utils/test_misc.py +++ b/qutebrowser/test/utils/test_misc.py @@ -595,16 +595,26 @@ class HighlightColorTests(unittest.TestCase): def test_white(self): """Test highlighting white.""" col = Color('white') - c1 = Color(utils.highlight_color(col, 1)) - c2 = Color(127, 127, 127) - self.assertEqual(c1, c2) + self.assertEqual(Color(utils.highlight_color(col, 1)), + Color(127, 127, 127), col) def test_black(self): """Test highlighting black.""" col = Color('black') self.assertEqual(Color(utils.highlight_color(col, 0.5)), - Color(204, 204, 204)) + Color(204, 204, 204), col) + def test_yellow(self): + """Test highlighting yellow.""" + col = Color('yellow') + self.assertEqual(Color(utils.highlight_color(col, 0.5)), + Color(170, 170, 0), col) + + def test_darkblue(self): + """Test highlighting darkblue.""" + col = Color('darkblue') + self.assertEqual(Color(utils.highlight_color(col, 0.5)), + Color(0, 0, 93), col) if __name__ == '__main__': From 81f3abd40242e98c8522a2bd5e44b84c8fc0a69b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 15 Jul 2014 11:21:35 +0200 Subject: [PATCH 27/36] Remove unneeded tabInserted. --- qutebrowser/widgets/tabwidget.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/qutebrowser/widgets/tabwidget.py b/qutebrowser/widgets/tabwidget.py index 55f337313..94c895c9a 100644 --- a/qutebrowser/widgets/tabwidget.py +++ b/qutebrowser/widgets/tabwidget.py @@ -184,16 +184,6 @@ class TabBar(QTabBar): continue p.drawControl(QStyle.CE_TabBarTab, tab) - def tabInserted(self, index): - """Extend tabInserted to set close button size policy. - - FIXME is this needed? - """ - super().tabInserted(index) - button = self.tabButton(index, QTabBar.RightSide) - if button is not None: - button.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) - class TabBarStyle(QCommonStyle): From 854d6dffcc5764705e8ad0bbda0314f156a85546 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 15 Jul 2014 14:20:10 +0200 Subject: [PATCH 28/36] Various tab rendering fixes. --- qutebrowser/widgets/tabwidget.py | 62 ++++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 11 deletions(-) diff --git a/qutebrowser/widgets/tabwidget.py b/qutebrowser/widgets/tabwidget.py index 94c895c9a..b3b32d990 100644 --- a/qutebrowser/widgets/tabwidget.py +++ b/qutebrowser/widgets/tabwidget.py @@ -17,7 +17,12 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -"""The tab widget used for TabbedBrowser from browser.py.""" +"""The tab widget used for TabbedBrowser from browser.py. + +Module attributes: + PM_TabBarPadding: The PixelMetric value for TabBarStyle to get the padding + between items. +""" from math import ceil import functools @@ -33,6 +38,9 @@ from qutebrowser.utils.qt import qt_ensure_valid from qutebrowser.utils.misc import highlight_color +PM_TabBarPadding = QStyle.PM_CustomBase + + class TabWidget(QTabWidget): """The tabwidget used for TabbedBrowser. @@ -61,6 +69,7 @@ class TabWidget(QTabWidget): set_register_stylesheet(self) self.setDocumentMode(True) self.setElideMode(Qt.ElideRight) + self.setUsesScrollButtons(True) bar.setDrawBase(False) self._init_config() @@ -143,22 +152,52 @@ class TabBar(QTabBar): Return: A QSize. """ - height = self.tabSizeHint(index).height() - return QSize(1, height) + icon = self.tabIcon(index) + padding_count = 0 + if not icon.isNull(): + extent = self.style().pixelMetric(QStyle.PM_TabBarIconSize, None, + self) + icon_size = icon.actualSize(QSize(extent, extent)) + padding_count += 1 + else: + icon_size = QSize(0, 0) + button_width = 0 + btn1 = self.tabButton(index, QTabBar.LeftSide) + btn2 = self.tabButton(index, QTabBar.RightSide) + if btn1 is not None: + button_width += btn1.size().width() + padding_count += 1 + if btn2 is not None: + button_width += btn2.size().width() + padding_count += 1 + padding_width = self.style().pixelMetric(PM_TabBarPadding, None, self) + height = self.fontMetrics().height() + width = (self.fontMetrics().size(0, '\u2026').width() + + icon_size.width() + button_width + + padding_count * padding_width) + return QSize(width, height) - def tabSizeHint(self, _index): + def tabSizeHint(self, index): """Override tabSizeHint so all tabs are the same size. https://wiki.python.org/moin/PyQt/Customising%20tab%20bars Args: - _index: The index of the tab. + index: The index of the tab. Return: A QSize. """ - height = self.fontMetrics().height() - size = QSize(self.width() / self.count(), height) + minimum_size = self.minimumTabSizeHint(index) + if self.count() * minimum_size.width() > self.width(): + # If we don't have enough space, we return the minimum size so we + # get scroll buttons as soon as needed. + size = minimum_size + else: + # If we *do* have enough space, tabs should occupy the whole window + # width. + height = self.fontMetrics().height() + size = QSize(self.width() / self.count(), height) qt_ensure_valid(size) return size @@ -217,7 +256,7 @@ class TabBarStyle(QCommonStyle): self._style = style for method in ('drawComplexControl', 'drawItemPixmap', 'generatedIconPixmap', 'hitTestComplexControl', - 'pixelMetric', 'itemPixmapRect', 'itemTextRect', + 'itemPixmapRect', 'itemTextRect', 'polish', 'styleHint', 'subControlRect', 'unpolish', 'drawItemText', 'sizeFromContents'): target = getattr(self._style, method) @@ -317,6 +356,8 @@ class TabBarStyle(QCommonStyle): metric == QStyle.PM_TabBarTabHSpace or metric == QStyle.PM_TabBarTabVSpace): return 0 + elif metric == PM_TabBarPadding: + return 4 else: return self._style.pixelMetric(metric, option, widget) @@ -364,7 +405,7 @@ class TabBarStyle(QCommonStyle): Return: A (text_rect, icon_rect) tuple (both QRects). """ - padding = 4 + padding = self.pixelMetric(PM_TabBarPadding, opt) icon_rect = QRect() text_rect = QRect(opt.rect) qt_ensure_valid(text_rect) @@ -377,7 +418,6 @@ class TabBarStyle(QCommonStyle): icon_rect = self._get_icon_rect(opt, text_rect) text_rect.adjust(icon_rect.width() + padding, 0, 0, 0) text_rect = self._style.visualRect(opt.direction, opt.rect, text_rect) - qt_ensure_valid(text_rect) return (text_rect, icon_rect) def _get_icon_rect(self, opt, text_rect): @@ -392,7 +432,7 @@ class TabBarStyle(QCommonStyle): """ icon_size = opt.iconSize if not icon_size.isValid(): - icon_extent = self._style.pixelMetric(QStyle.PM_SmallIconSize) + icon_extent = self.pixelMetric(QStyle.PM_SmallIconSize) icon_size = QSize(icon_extent, icon_extent) icon_mode = (QIcon.Normal if opt.state & QStyle.State_Enabled else QIcon.Disabled) From 7bcd8192a76da71107210d523d90644944d17c45 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 15 Jul 2014 16:29:41 +0200 Subject: [PATCH 29/36] Add FIXME --- qutebrowser/widgets/tabwidget.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/qutebrowser/widgets/tabwidget.py b/qutebrowser/widgets/tabwidget.py index b3b32d990..d354cf54e 100644 --- a/qutebrowser/widgets/tabwidget.py +++ b/qutebrowser/widgets/tabwidget.py @@ -102,7 +102,11 @@ class TabWidget(QTabWidget): class TabBar(QTabBar): - """Custom tabbar to close tabs on right click. + """Custom tabbar with our own style. + + FIXME: This acts funny when dragging tabs, especially when close buttons + are enabled. However, fixing this would be a lot of effort, so we'll + postpone it until we're reimplementing drag&drop for other reasons. Signals: tab_rightclicked: Emitted when a tab was right-clicked and should be From 6289ef7981eb06d543394704b70a2a17ce2a7641 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 15 Jul 2014 21:20:57 +0200 Subject: [PATCH 30/36] Adjust vertical tabbar width --- qutebrowser/config/configdata.py | 6 ++++++ qutebrowser/widgets/mainwindow.py | 1 + qutebrowser/widgets/tabwidget.py | 27 ++++++++++++++++++++++----- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 8501779e5..a7abb5181 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -428,6 +428,12 @@ DATA = OrderedDict([ ('show-favicons', SettingValue(types.Bool(), 'true'), "Whether to show favicons in the tab bar."), + + ('width', + SettingValue(types.PercOrInt(minperc=0, maxperc=100, minint=1), + '20%'), + "The width of the tab bar if it's vertical, in px or as percentage " + "of the window."), )), ('storage', sect.KeyValue( diff --git a/qutebrowser/widgets/mainwindow.py b/qutebrowser/widgets/mainwindow.py index 28d00798a..ac9b23cd7 100644 --- a/qutebrowser/widgets/mainwindow.py +++ b/qutebrowser/widgets/mainwindow.py @@ -165,6 +165,7 @@ class MainWindow(QWidget): super().resizeEvent(e) self.resize_completion() self.downloadview.updateGeometry() + self.tabs.tabBar().updateGeometry() def closeEvent(self, e): """Override closeEvent to display a confirmation if needed.""" diff --git a/qutebrowser/widgets/tabwidget.py b/qutebrowser/widgets/tabwidget.py index d354cf54e..8624934c9 100644 --- a/qutebrowser/widgets/tabwidget.py +++ b/qutebrowser/widgets/tabwidget.py @@ -29,7 +29,8 @@ import functools from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QSize, QRect from PyQt5.QtWidgets import (QTabWidget, QTabBar, QSizePolicy, QCommonStyle, - QStyle, QStylePainter, QStyleOptionTab) + QStyle, QStylePainter, QStyleOptionTab, + QApplication) from PyQt5.QtGui import QIcon, QPalette, QColor import qutebrowser.config.config as config @@ -86,12 +87,16 @@ class TabWidget(QTabWidget): 'right': QTabBar.SelectRightTab, 'previous': QTabBar.SelectPreviousTab, } + tabbar = self.tabBar() self.setMovable(config.get('tabbar', 'movable')) self.setTabsClosable(config.get('tabbar', 'close-buttons')) posstr = config.get('tabbar', 'position') selstr = config.get('tabbar', 'select-on-remove') - self.setTabPosition(position_conv[posstr]) - self.tabBar().setSelectionBehaviorOnRemove(select_conv[selstr]) + position = position_conv[posstr] + self.setTabPosition(position) + tabbar.vertical = position in (QTabWidget.West, QTabWidget.East) + tabbar.setSelectionBehaviorOnRemove(select_conv[selstr]) + tabbar.updateGeometry() @pyqtSlot(str, str) def on_config_changed(self, section, _option): @@ -108,6 +113,9 @@ class TabBar(QTabBar): are enabled. However, fixing this would be a lot of effort, so we'll postpone it until we're reimplementing drag&drop for other reasons. + Attributes: + vertical: When the tab bar is currently vertical. + Signals: tab_rightclicked: Emitted when a tab was right-clicked and should be closed. We use this rather than tabCloseRequested @@ -122,6 +130,7 @@ class TabBar(QTabBar): def __init__(self, parent=None): super().__init__(parent) self.setStyle(TabBarStyle(self.style())) + self.vertical = False def __repr__(self): return '<{} with {} tabs>'.format(self.__class__.__name__, @@ -193,14 +202,22 @@ class TabBar(QTabBar): A QSize. """ minimum_size = self.minimumTabSizeHint(index) - if self.count() * minimum_size.width() > self.width(): + height = self.fontMetrics().height() + if self.vertical: + confwidth = str(config.get('tabbar', 'width')) + if confwidth.endswith('%'): + perc = int(confwidth.rstrip('%')) + width = QApplication.instance().mainwindow.width() * perc / 100 + else: + width = int(confwidth) + size = QSize(max(minimum_size.width(), width), height) + elif self.count() * minimum_size.width() > self.width(): # If we don't have enough space, we return the minimum size so we # get scroll buttons as soon as needed. size = minimum_size else: # If we *do* have enough space, tabs should occupy the whole window # width. - height = self.fontMetrics().height() size = QSize(self.width() / self.count(), height) qt_ensure_valid(size) return size From 2d3575f6d149677965d6877b2ab93cc6039ef1c3 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 15 Jul 2014 22:14:16 +0200 Subject: [PATCH 31/36] Remove tab close buttons --- qutebrowser/config/configdata.py | 4 -- qutebrowser/test/utils/test_misc.py | 41 ------------ qutebrowser/utils/misc.py | 32 +--------- qutebrowser/widgets/tabbedbrowser.py | 1 - qutebrowser/widgets/tabwidget.py | 93 +++------------------------- 5 files changed, 10 insertions(+), 161 deletions(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index a7abb5181..3229e62f5 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -405,10 +405,6 @@ DATA = OrderedDict([ SettingValue(types.Bool(), 'false'), "Whether tabs should close when right-clicked."), - ('close-buttons', - SettingValue(types.Bool(), 'false'), - "Whether tabs should have close-buttons."), - ('position', SettingValue(types.Position(), 'north'), "The position of the tab bar."), diff --git a/qutebrowser/test/utils/test_misc.py b/qutebrowser/test/utils/test_misc.py index 14ec85e7e..799459e4a 100644 --- a/qutebrowser/test/utils/test_misc.py +++ b/qutebrowser/test/utils/test_misc.py @@ -576,46 +576,5 @@ class NormalizeTests(unittest.TestCase): self.assertEqual(utils.normalize_keystr(orig), repl, orig) -class HighlightColorTests(unittest.TestCase): - - """Test highlight_color.""" - - def test_invalid(self): - """Test with an invalid Color.""" - col = Color() - with self.assertRaises(QtValueError): - utils.highlight_color(col) - - def test_large_factor(self): - """Test with a too large factor.""" - col = Color('black') - with self.assertRaises(OverflowError): - utils.highlight_color(col, 2 ** 31) - - def test_white(self): - """Test highlighting white.""" - col = Color('white') - self.assertEqual(Color(utils.highlight_color(col, 1)), - Color(127, 127, 127), col) - - def test_black(self): - """Test highlighting black.""" - col = Color('black') - self.assertEqual(Color(utils.highlight_color(col, 0.5)), - Color(204, 204, 204), col) - - def test_yellow(self): - """Test highlighting yellow.""" - col = Color('yellow') - self.assertEqual(Color(utils.highlight_color(col, 0.5)), - Color(170, 170, 0), col) - - def test_darkblue(self): - """Test highlighting darkblue.""" - col = Color('darkblue') - self.assertEqual(Color(utils.highlight_color(col, 0.5)), - Color(0, 0, 93), col) - - if __name__ == '__main__': unittest.main() diff --git a/qutebrowser/utils/misc.py b/qutebrowser/utils/misc.py index 1213ad07b..735e78bc7 100644 --- a/qutebrowser/utils/misc.py +++ b/qutebrowser/utils/misc.py @@ -36,8 +36,7 @@ from pkg_resources import resource_string import qutebrowser import qutebrowser.utils.log as log -from qutebrowser.utils.qt import (qt_version_check, qt_ensure_valid, - check_overflow) +from qutebrowser.utils.qt import qt_version_check, qt_ensure_valid def elide(text, length): @@ -490,32 +489,3 @@ def normalize_keystr(keystr): for mod in ('Ctrl', 'Meta', 'Alt', 'Shift'): keystr = keystr.replace(mod + '-', mod + '+') return keystr.lower() - - -def highlight_color(color, factor=0.5): - """Get a highlighted color based on a base color. - - If the passed color is dark, it will get lighter. If it is light, it will - get darker. - - Args: - color: A QColor with the initial color. - factor: How much to lighten/darken the color (0.5: 50% lighter/darker). - - Return: - A QColor with the highlighted color. - """ - qt_ensure_valid(color) - qt_factor = 100 * (1 + factor) - check_overflow(qt_factor, 'int') - if color == QColor('black'): - # Black isn't handled by QColor::lighter because that just multiplies - # the HSV value by a factor. - # We also add an additional 30% because the effect would be way too - # subtle if we didn't. - return interpolate_color(QColor('black'), QColor('white'), - min((100 * factor) + 30, 100)) - elif color.value() < 128: - return color.lighter(qt_factor) - else: - return color.darker(qt_factor) diff --git a/qutebrowser/widgets/tabbedbrowser.py b/qutebrowser/widgets/tabbedbrowser.py index 0a4ebd1ce..c4f5af38a 100644 --- a/qutebrowser/widgets/tabbedbrowser.py +++ b/qutebrowser/widgets/tabbedbrowser.py @@ -103,7 +103,6 @@ class TabbedBrowser(TabWidget): def __init__(self, parent=None): super().__init__(parent) - self.tabBar().tab_rightclicked.connect(self.on_tab_close_requested) self.tabCloseRequested.connect(self.on_tab_close_requested) self.currentChanged.connect(self.on_current_changed) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) diff --git a/qutebrowser/widgets/tabwidget.py b/qutebrowser/widgets/tabwidget.py index 8624934c9..1854bac7e 100644 --- a/qutebrowser/widgets/tabwidget.py +++ b/qutebrowser/widgets/tabwidget.py @@ -24,10 +24,9 @@ Module attributes: between items. """ -from math import ceil import functools -from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QSize, QRect +from PyQt5.QtCore import pyqtSlot, Qt, QSize, QRect from PyQt5.QtWidgets import (QTabWidget, QTabBar, QSizePolicy, QCommonStyle, QStyle, QStylePainter, QStyleOptionTab, QApplication) @@ -36,7 +35,6 @@ from PyQt5.QtGui import QIcon, QPalette, QColor import qutebrowser.config.config as config from qutebrowser.config.style import set_register_stylesheet from qutebrowser.utils.qt import qt_ensure_valid -from qutebrowser.utils.misc import highlight_color PM_TabBarPadding = QStyle.PM_CustomBase @@ -66,6 +64,7 @@ class TabWidget(QTabWidget): super().__init__(parent) bar = TabBar() self.setTabBar(bar) + bar.tabCloseRequested.connect(self.tabCloseRequested) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) set_register_stylesheet(self) self.setDocumentMode(True) @@ -89,7 +88,7 @@ class TabWidget(QTabWidget): } tabbar = self.tabBar() self.setMovable(config.get('tabbar', 'movable')) - self.setTabsClosable(config.get('tabbar', 'close-buttons')) + self.setTabsClosable(False) posstr = config.get('tabbar', 'position') selstr = config.get('tabbar', 'select-on-remove') position = position_conv[posstr] @@ -109,24 +108,14 @@ class TabBar(QTabBar): """Custom tabbar with our own style. - FIXME: This acts funny when dragging tabs, especially when close buttons - are enabled. However, fixing this would be a lot of effort, so we'll - postpone it until we're reimplementing drag&drop for other reasons. + FIXME: Dragging tabs doesn't look as nice as it does in QTabBar. However, + fixing this would be a lot of effort, so we'll postpone it until we're + reimplementing drag&drop for other reasons. Attributes: vertical: When the tab bar is currently vertical. - - Signals: - tab_rightclicked: Emitted when a tab was right-clicked and should be - closed. We use this rather than tabCloseRequested - because tabCloseRequested is sometimes connected by - Qt to the tabwidget and sometimes not, depending on - if close buttons are enabled. - arg: The tab index to be closed. """ - tab_rightclicked = pyqtSignal(int) - def __init__(self, parent=None): super().__init__(parent) self.setStyle(TabBarStyle(self.style())) @@ -147,7 +136,7 @@ class TabBar(QTabBar): return e.accept() if config.get('tabbar', 'close-on-right-click'): - self.tab_rightclicked.emit(idx) + self.tabCloseRequested.emit(idx) def minimumTabSizeHint(self, index): """Override minimumTabSizeHint because we want no hard minimum. @@ -174,20 +163,10 @@ class TabBar(QTabBar): padding_count += 1 else: icon_size = QSize(0, 0) - button_width = 0 - btn1 = self.tabButton(index, QTabBar.LeftSide) - btn2 = self.tabButton(index, QTabBar.RightSide) - if btn1 is not None: - button_width += btn1.size().width() - padding_count += 1 - if btn2 is not None: - button_width += btn2.size().width() - padding_count += 1 padding_width = self.style().pixelMetric(PM_TabBarPadding, None, self) height = self.fontMetrics().height() width = (self.fontMetrics().size(0, '\u2026').width() + - icon_size.width() + button_width + - padding_count * padding_width) + icon_size.width() + padding_count * padding_width) return QSize(width, height) def tabSizeHint(self, index): @@ -279,7 +258,7 @@ class TabBarStyle(QCommonStyle): 'generatedIconPixmap', 'hitTestComplexControl', 'itemPixmapRect', 'itemTextRect', 'polish', 'styleHint', 'subControlRect', 'unpolish', - 'drawItemText', 'sizeFromContents'): + 'drawItemText', 'sizeFromContents', 'drawPrimitive'): target = getattr(self._style, method) setattr(self, method, functools.partial(target)) super().__init__() @@ -325,42 +304,6 @@ class TabBarStyle(QCommonStyle): # style. self._style.drawControl(element, opt, p, widget) - def drawPrimitive(self, element, opt, painter, widget=None): - """Draw the given primitive element. - - Overriden to draw our own tab close button. - - Args: - element: PrimitiveElement - opt: const QStyleOption * - painter: QPainter * - widget: const QWidget * - """ - if element != QStyle.PE_IndicatorTabClose: - self._style.drawPrimitive(element, opt, painter, widget) - return - painter.save() - # This fixes some weird off-by-ones in Qt. - # See http://stackoverflow.com/a/17019898/2085149 - painter.translate(0.5, 0.5) - color = QColor(config.get('colors', 'tab.fg')) - if opt.state & QStyle.State_Raised: - color = highlight_color(color, factor=0.2) - elif opt.state & QStyle.State_Sunken: - color = highlight_color(color, factor=0.3) - painter.setPen(color) - side = min(opt.rect.width(), opt.rect.height()) - square = QRect() - square.setSize(QSize(side, side) * 0.4) - square.moveCenter(opt.rect.center()) - if square.width() % 2 == 0: - # An X is easier to paint in a square with an odd count of - # pixels. - square.adjust(-1, -1, 0, 0) - painter.drawLine(square.topLeft(), square.bottomRight()) - painter.drawLine(square.topRight(), square.bottomLeft()) - painter.restore() - def pixelMetric(self, metric, option=None, widget=None): """Override pixelMetric to not shift the selected tab. @@ -396,20 +339,6 @@ class TabBarStyle(QCommonStyle): if sr == QStyle.SE_TabBarTabText: text_rect, _icon_rect = self._tab_layout(opt) return text_rect - if (sr == QStyle.SE_TabBarTabLeftButton or - sr == QStyle.SE_TabBarTabRightButton): - size = (opt.leftButtonSize if sr == QStyle.SE_TabBarTabLeftButton - else opt.rightButtonSize) - width = size.width() - height = size.height() - mid_height = ceil((opt.rect.height() - height) / 2) - if sr == QStyle.SE_TabBarTabLeftButton: - rect = QRect(opt.rect.x(), mid_height, width, height) - else: - rect = QRect(opt.rect.right() - width, mid_height, width, - height) - rect = self._style.visualRect(opt.direction, opt.rect, rect) - return rect else: return self._style.subElementRect(sr, opt, widget) @@ -431,10 +360,6 @@ class TabBarStyle(QCommonStyle): text_rect = QRect(opt.rect) qt_ensure_valid(text_rect) text_rect.adjust(padding, 0, 0, 0) - if not opt.leftButtonSize.isEmpty(): - text_rect.adjust(opt.leftButtonSize.width(), 0, 0, 0) - if not opt.rightButtonSize.isEmpty(): - text_rect.adjust(0, 0, -opt.rightButtonSize.width(), 0) if not opt.icon.isNull(): icon_rect = self._get_icon_rect(opt, text_rect) text_rect.adjust(icon_rect.width() + padding, 0, 0, 0) From bcbdf9090ffac92eecbb2573756a63c66c9dab1c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 15 Jul 2014 22:22:10 +0200 Subject: [PATCH 32/36] Make tab close mouse button configurable --- qutebrowser/config/configdata.py | 6 +++--- qutebrowser/config/conftypes.py | 9 +++++++++ qutebrowser/widgets/tabwidget.py | 21 ++++++++++----------- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 3229e62f5..8160b4b7a 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -401,9 +401,9 @@ DATA = OrderedDict([ SettingValue(types.Bool(), 'true'), "Whether tabs should be movable."), - ('close-on-right-click', - SettingValue(types.Bool(), 'false'), - "Whether tabs should close when right-clicked."), + ('close-mouse-button', + SettingValue(types.CloseButton(), 'middle'), + "On which mouse button to close tabs."), ('position', SettingValue(types.Position(), 'north'), diff --git a/qutebrowser/config/conftypes.py b/qutebrowser/config/conftypes.py index 17c7234b0..c9ea0c2c9 100644 --- a/qutebrowser/config/conftypes.py +++ b/qutebrowser/config/conftypes.py @@ -935,3 +935,12 @@ class ForwardUnboundKeys(BaseType): ('auto', "Forward unbound non-alphanumeric " "keys."), ('none', "Don't forward any keys.")) + + +class CloseButton(BaseType): + + """Whether to forward unbound keys.""" + + valid_values = ValidValues(('right', "Close tabs on right-click."), + ('middle', "Close tabs on middle-click."), + ('none', "Don't close tabs using the mouse.")) diff --git a/qutebrowser/widgets/tabwidget.py b/qutebrowser/widgets/tabwidget.py index 1854bac7e..23597d8fd 100644 --- a/qutebrowser/widgets/tabwidget.py +++ b/qutebrowser/widgets/tabwidget.py @@ -126,17 +126,16 @@ class TabBar(QTabBar): self.count()) def mousePressEvent(self, e): - """Override mousePressEvent to emit tabCloseRequested on rightclick.""" - if e.button() != Qt.RightButton: - super().mousePressEvent(e) - return - idx = self.tabAt(e.pos()) - if idx == -1: - super().mousePressEvent(e) - return - e.accept() - if config.get('tabbar', 'close-on-right-click'): - self.tabCloseRequested.emit(idx) + """Override mousePressEvent to close tabs if configured.""" + button = config.get('tabbar', 'close-mouse-button') + if (e.button() == Qt.RightButton and button == 'right' or + e.button() == Qt.MiddleButton and button == 'middle'): + idx = self.tabAt(e.pos()) + if idx != -1: + e.accept() + self.tabCloseRequested.emit(idx) + return + super().mousePressEvent(e) def minimumTabSizeHint(self, index): """Override minimumTabSizeHint because we want no hard minimum. From 52c52e067521f6408f8c81dfebf0de1738f7c1a4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 16 Jul 2014 13:51:16 +0200 Subject: [PATCH 33/36] Add a tab progress indicator --- qutebrowser/config/configdata.py | 24 +++++++++++++++++++++++ qutebrowser/widgets/tabbedbrowser.py | 24 +++++++++++++++++++++++ qutebrowser/widgets/tabwidget.py | 29 +++++++++++++++++++++++++--- 3 files changed, 74 insertions(+), 3 deletions(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 8160b4b7a..9cd29f3b5 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -430,6 +430,14 @@ DATA = OrderedDict([ '20%'), "The width of the tab bar if it's vertical, in px or as percentage " "of the window."), + + ('indicator-width', + SettingValue(types.Int(minval=0), '3'), + "Width of the progress indicator."), + + ('indicator-space', + SettingValue(types.Int(minval=0), '3'), + "Spacing between tab edge and indicator."), )), ('storage', sect.KeyValue( @@ -874,6 +882,22 @@ DATA = OrderedDict([ SettingValue(types.Color(), '#555555'), "Background color of the tabbar."), + ('tab.indicator.start', + SettingValue(types.QtColor(), '#0000aa'), + "Color gradient start for the tab indicator."), + + ('tab.indicator.stop', + SettingValue(types.QtColor(), '#00aa00'), + "Color gradient end for the tab indicator."), + + ('tab.indicator.error', + SettingValue(types.QtColor(), '#ff0000'), + "Color for the tab indicator on errors.."), + + ('tab.indicator.system', + SettingValue(types.ColorSystem(), 'rgb'), + "Color gradient interpolation system for the tab indicator."), + ('tab.seperator', SettingValue(types.Color(), '#555555'), "Color for the tab seperator."), diff --git a/qutebrowser/widgets/tabbedbrowser.py b/qutebrowser/widgets/tabbedbrowser.py index c4f5af38a..eab090da0 100644 --- a/qutebrowser/widgets/tabbedbrowser.py +++ b/qutebrowser/widgets/tabbedbrowser.py @@ -29,6 +29,7 @@ import qutebrowser.config.config as config import qutebrowser.commands.utils as cmdutils import qutebrowser.keyinput.modeman as modeman import qutebrowser.utils.log as log +import qutebrowser.utils.misc as utils from qutebrowser.widgets.tabwidget import TabWidget from qutebrowser.widgets.webview import WebView from qutebrowser.browser.signalfilter import SignalFilter @@ -181,6 +182,8 @@ class TabbedBrowser(TabWidget): # misc tab.titleChanged.connect(partial(self.on_title_changed, tab)) tab.iconChanged.connect(partial(self.on_icon_changed, tab)) + tab.loadProgress.connect(partial(self.on_load_progress, tab)) + frame.loadFinished.connect(partial(self.on_load_finished, tab)) frame.loadStarted.connect(partial(self.on_load_started, tab)) page.windowCloseRequested.connect( partial(self.on_window_close_requested, tab)) @@ -442,6 +445,27 @@ class TabbedBrowser(TabWidget): self.current_tab_changed.emit(tab) self.title_changed.emit('{} - qutebrowser'.format(self.tabText(idx))) + def on_load_progress(self, tab, perc): + """Adjust tab indicator on load progress.""" + idx = self.indexOf(tab) + start = config.get('colors', 'tab.indicator.start') + stop = config.get('colors', 'tab.indicator.stop') + system = config.get('colors', 'tab.indicator.system') + color = utils.interpolate_color(start, stop, perc, system) + self.tabBar().set_tab_indicator_color(idx, color) + + def on_load_finished(self, tab, ok): + """Adjust tab indicator when loading finished.""" + idx = self.indexOf(tab) + if ok: + start = config.get('colors', 'tab.indicator.start') + stop = config.get('colors', 'tab.indicator.stop') + system = config.get('colors', 'tab.indicator.system') + color = utils.interpolate_color(start, stop, 100, system) + else: + color = config.get('colors', 'tab.indicator.error') + self.tabBar().set_tab_indicator_color(idx, color) + def resizeEvent(self, e): """Extend resizeEvent of QWidget to emit a resized signal afterwards. diff --git a/qutebrowser/widgets/tabwidget.py b/qutebrowser/widgets/tabwidget.py index 23597d8fd..ab65e7f68 100644 --- a/qutebrowser/widgets/tabwidget.py +++ b/qutebrowser/widgets/tabwidget.py @@ -26,15 +26,15 @@ Module attributes: import functools -from PyQt5.QtCore import pyqtSlot, Qt, QSize, QRect +from PyQt5.QtCore import pyqtSlot, Qt, QSize, QRect, QPoint from PyQt5.QtWidgets import (QTabWidget, QTabBar, QSizePolicy, QCommonStyle, QStyle, QStylePainter, QStyleOptionTab, QApplication) from PyQt5.QtGui import QIcon, QPalette, QColor -import qutebrowser.config.config as config from qutebrowser.config.style import set_register_stylesheet from qutebrowser.utils.qt import qt_ensure_valid +import qutebrowser.config.config as config PM_TabBarPadding = QStyle.PM_CustomBase @@ -125,6 +125,16 @@ class TabBar(QTabBar): return '<{} with {} tabs>'.format(self.__class__.__name__, self.count()) + def set_tab_indicator_color(self, idx, color): + """Set the tab indicator color. + + Args: + idx: The tab index. + color: A QColor. + """ + self.setTabData(idx, color) + self.update(self.tabRect(idx)) + def mousePressEvent(self, e): """Override mousePressEvent to close tabs if configured.""" button = config.get('tabbar', 'close-mouse-button') @@ -216,6 +226,10 @@ class TabBar(QTabBar): tab.palette.setColor(QPalette.Window, QColor(color)) tab.palette.setColor(QPalette.WindowText, QColor(config.get('colors', 'tab.fg'))) + indicator_color = self.tabData(idx) + if indicator_color is None: + indicator_color = QColor() + tab.palette.setColor(QPalette.Base, indicator_color) if tab.rect.right() < 0 or tab.rect.left() > self.width(): # Don't bother drawing a tab if the entire tab is outside of # the visible tab bar. @@ -279,11 +293,18 @@ class TabBarStyle(QCommonStyle): self.drawControl(QStyle.CE_TabBarTabShape, opt, p, widget) self.drawControl(QStyle.CE_TabBarTabLabel, opt, p, widget) elif element == QStyle.CE_TabBarTabShape: + p.fillRect(opt.rect, opt.palette.window()) + indicator_color = opt.palette.base().color() + indicator_width = config.get('tabbar', 'indicator-width') + if indicator_color.isValid() and indicator_width != 0: + topleft = opt.rect.topLeft() + topleft += QPoint(config.get('tabbar', 'indicator-space'), 2) + p.fillRect(topleft.x(), topleft.y(), indicator_width, + opt.rect.height() - 4, indicator_color) # We use super() rather than self._style here because we don't want # any sophisticated drawing. super().drawControl(QStyle.CE_TabBarTabShape, opt, p, widget) elif element == QStyle.CE_TabBarTabLabel: - p.fillRect(opt.rect, opt.palette.window()) text_rect, icon_rect = self._tab_layout(opt) if not opt.icon.isNull(): qt_ensure_valid(icon_rect) @@ -359,6 +380,8 @@ class TabBarStyle(QCommonStyle): text_rect = QRect(opt.rect) qt_ensure_valid(text_rect) text_rect.adjust(padding, 0, 0, 0) + text_rect.adjust(config.get('tabbar', 'indicator-width') + + config.get('tabbar', 'indicator-space'), 0, 0, 0) if not opt.icon.isNull(): icon_rect = self._get_icon_rect(opt, text_rect) text_rect.adjust(icon_rect.width() + padding, 0, 0, 0) From 1a1473a18e2f1f56f02b16340035271039e25559 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 16 Jul 2014 14:10:46 +0200 Subject: [PATCH 34/36] Handle dirty tabbar layout correctly --- qutebrowser/widgets/mainwindow.py | 2 +- qutebrowser/widgets/tabwidget.py | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/qutebrowser/widgets/mainwindow.py b/qutebrowser/widgets/mainwindow.py index ac9b23cd7..fd79fa0ab 100644 --- a/qutebrowser/widgets/mainwindow.py +++ b/qutebrowser/widgets/mainwindow.py @@ -165,7 +165,7 @@ class MainWindow(QWidget): super().resizeEvent(e) self.resize_completion() self.downloadview.updateGeometry() - self.tabs.tabBar().updateGeometry() + self.tabs.tabBar().refresh() def closeEvent(self, e): """Override closeEvent to display a confirmation if needed.""" diff --git a/qutebrowser/widgets/tabwidget.py b/qutebrowser/widgets/tabwidget.py index ab65e7f68..f398e9ee2 100644 --- a/qutebrowser/widgets/tabwidget.py +++ b/qutebrowser/widgets/tabwidget.py @@ -95,7 +95,7 @@ class TabWidget(QTabWidget): self.setTabPosition(position) tabbar.vertical = position in (QTabWidget.West, QTabWidget.East) tabbar.setSelectionBehaviorOnRemove(select_conv[selstr]) - tabbar.updateGeometry() + tabbar.refresh() @pyqtSlot(str, str) def on_config_changed(self, section, _option): @@ -125,6 +125,13 @@ class TabBar(QTabBar): return '<{} with {} tabs>'.format(self.__class__.__name__, self.count()) + def refresh(self): + """Properly repaint the tab bar and relayout tabs.""" + # This is a horrible hack, but we need to do this so the underlaying Qt + # code sets layoutDirty so it actually relayouts the tabs. + self.setIconSize(self.iconSize()) + self.updateGeometry() + def set_tab_indicator_color(self, idx, color): """Set the tab indicator color. From 5e06d420f90bd578752082667d166583f3d9368f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 16 Jul 2014 14:11:28 +0200 Subject: [PATCH 35/36] Fix disconnecting tab signals --- qutebrowser/widgets/webview.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/qutebrowser/widgets/webview.py b/qutebrowser/widgets/webview.py index deb09437c..7c4d7027c 100644 --- a/qutebrowser/widgets/webview.py +++ b/qutebrowser/widgets/webview.py @@ -352,12 +352,15 @@ class WebView(QWebView): callback: Function to call after shutting down. """ self._shutdown_callback = callback + # Avoid loading finished signal when stopping try: - # Avoid loading finished signal when stopping self.loadFinished.disconnect() + except TypeError: + pass + try: self.page().mainFrame().loadFinished.disconnect() except TypeError: - log.destroy.exception("This should never happen.") + pass self.stop() self.close() self.settings().setAttribute(QWebSettings.JavascriptEnabled, False) From e3d9c252abe0c0147febcd0dd272310a286d6d7d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 16 Jul 2014 14:45:02 +0200 Subject: [PATCH 36/36] Remove unneeded updateGeometry --- qutebrowser/widgets/tabwidget.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qutebrowser/widgets/tabwidget.py b/qutebrowser/widgets/tabwidget.py index f398e9ee2..92a20e191 100644 --- a/qutebrowser/widgets/tabwidget.py +++ b/qutebrowser/widgets/tabwidget.py @@ -130,7 +130,6 @@ class TabBar(QTabBar): # This is a horrible hack, but we need to do this so the underlaying Qt # code sets layoutDirty so it actually relayouts the tabs. self.setIconSize(self.iconSize()) - self.updateGeometry() def set_tab_indicator_color(self, idx, color): """Set the tab indicator color.