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. diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index b8e347a63..9cd29f3b5 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -401,17 +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-buttons', - 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."), + ('close-mouse-button', + SettingValue(types.CloseButton(), 'middle'), + "On which mouse button to close tabs."), ('position', SettingValue(types.Position(), 'north'), @@ -429,22 +421,23 @@ 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."), + ('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."), + ('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( @@ -873,9 +866,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'), @@ -885,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/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/test/utils/test_misc.py b/qutebrowser/test/utils/test_misc.py index f92682e0c..799459e4a 100644 --- a/qutebrowser/test/utils/test_misc.py +++ b/qutebrowser/test/utils/test_misc.py @@ -32,6 +32,23 @@ 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): + + """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): @@ -330,23 +347,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) + 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): - utils.interpolate_color(self.white, QColor(), 0) + with self.assertRaises(QtValueError): + utils.interpolate_color(self.white, Color(), 0) def test_invalid_percentage(self): """Test an invalid percentage.""" @@ -365,52 +382,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): diff --git a/qutebrowser/utils/style.py b/qutebrowser/utils/style.py deleted file mode 100644 index 92d5006a9..000000000 --- a/qutebrowser/utils/style.py +++ /dev/null @@ -1,79 +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.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', - 'drawItemText', '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) diff --git a/qutebrowser/widgets/mainwindow.py b/qutebrowser/widgets/mainwindow.py index 6991ab2d1..09d47bb5f 100644 --- a/qutebrowser/widgets/mainwindow.py +++ b/qutebrowser/widgets/mainwindow.py @@ -160,6 +160,7 @@ class MainWindow(QWidget): super().resizeEvent(e) self.resize_completion() self.downloadview.updateGeometry() + self.tabs.tabBar().refresh() def closeEvent(self, e): """Override closeEvent to display a confirmation if needed.""" diff --git a/qutebrowser/widgets/tabbedbrowser.py b/qutebrowser/widgets/tabbedbrowser.py index d42631abf..7145cbb31 100644 --- a/qutebrowser/widgets/tabbedbrowser.py +++ b/qutebrowser/widgets/tabbedbrowser.py @@ -23,12 +23,14 @@ 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 +import qutebrowser.utils.misc as utils +from qutebrowser.widgets.tabwidget import TabWidget from qutebrowser.widgets.webview import WebView from qutebrowser.browser.signalfilter import SignalFilter from qutebrowser.browser.commands import CommandDispatcher @@ -102,7 +104,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) @@ -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)) @@ -347,7 +350,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): @@ -361,7 +364,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): @@ -443,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 49418143b..92a20e191 100644 --- a/qutebrowser/widgets/tabwidget.py +++ b/qutebrowser/widgets/tabwidget.py @@ -17,34 +17,27 @@ # 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. -from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QSize -from PyQt5.QtWidgets import QTabWidget, QTabBar, QSizePolicy -from PyQt5.QtGui import QIcon, QPixmap +Module attributes: + PM_TabBarPadding: The PixelMetric value for TabBarStyle to get the padding + between items. +""" + +import functools + +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.style import Style from qutebrowser.utils.qt import qt_ensure_valid +import qutebrowser.config.config as config -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) +PM_TabBarPadding = QStyle.PM_CustomBase class TabWidget(QTabWidget): @@ -65,30 +58,19 @@ class TabWidget(QTabWidget): {font[tabbar]} {color[tab.bg.bar]} }} - - QTabBar::tab {{ - {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; - }} - - QTabBar::tab:selected {{ - {color[tab.bg.selected]} - }} """ def __init__(self, parent): super().__init__(parent) - self.setTabBar(TabBar()) + bar = TabBar() + self.setTabBar(bar) + bar.tabCloseRequested.connect(self.tabCloseRequested) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) - self.setStyle(Style(self.style())) set_register_stylesheet(self) self.setDocumentMode(True) self.setElideMode(Qt.ElideRight) - self.tabBar().setDrawBase(False) + self.setUsesScrollButtons(True) + bar.setDrawBase(False) self._init_config() def _init_config(self): @@ -104,13 +86,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')) - self.setUsesScrollButtons(config.get('tabbar', 'scroll-buttons')) + self.setTabsClosable(False) 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.refresh() @pyqtSlot(str, str) def on_config_changed(self, section, _option): @@ -121,35 +106,52 @@ class TabWidget(QTabWidget): class TabBar(QTabBar): - """Custom tabbar to close tabs on right click. + """Custom tabbar with our own style. - 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. + 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. """ - tab_rightclicked = pyqtSignal(int) + 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__, 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()) + + 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 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.tab_rightclicked.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. @@ -167,18 +169,255 @@ class TabBar(QTabBar): Return: A QSize. """ - height = super().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) + padding_width = self.style().pixelMetric(PM_TabBarPadding, None, self) + height = self.fontMetrics().height() + width = (self.fontMetrics().size(0, '\u2026').width() + + icon_size.width() + padding_count * padding_width) + return QSize(width, height) 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. """ - if config.get('tabbar', 'expand'): - height = super().tabSizeHint(index).height() - size = QSize(self.width() / self.count(), height) + minimum_size = self.minimumTabSizeHint(index) + 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: - size = super().tabSizeHint(index) + # If we *do* have enough space, tabs should occupy the whole window + # width. + size = QSize(self.width() / self.count(), height) qt_ensure_valid(size) return size + + 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'))) + 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. + continue + p.drawControl(QStyle.CE_TabBarTab, tab) + + +class TabBarStyle(QCommonStyle): + + """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. + - 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', 'drawItemPixmap', + 'generatedIconPixmap', 'hitTestComplexControl', + 'itemPixmapRect', 'itemTextRect', + 'polish', 'styleHint', 'subControlRect', 'unpolish', + 'drawItemText', 'sizeFromContents', 'drawPrimitive'): + target = getattr(self._style, method) + setattr(self, method, functools.partial(target)) + super().__init__() + + def drawControl(self, element, opt, p, widget=None): + """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: + 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: + 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 + 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 + elif metric == PM_TabBarPadding: + return 4 + else: + return self._style.pixelMetric(metric, option, widget) + + def subElementRect(self, sr, opt, 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(opt) + return text_rect + else: + return self._style.subElementRect(sr, opt, widget) + + def _tab_layout(self, opt): + """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 + + Return: + A (text_rect, icon_rect) tuple (both QRects). + """ + padding = self.pixelMetric(PM_TabBarPadding, opt) + icon_rect = QRect() + 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) + text_rect = self._style.visualRect(opt.direction, opt.rect, text_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.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 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)