Merge branch 'tabbar'
This commit is contained in:
commit
86b7ea4dfc
19
doc/BUGS
19
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
|
||||
|
2
doc/TODO
2
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.
|
||||
|
@ -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."),
|
||||
|
@ -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."))
|
||||
|
@ -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):
|
||||
|
@ -1,79 +0,0 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""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)
|
@ -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."""
|
||||
|
@ -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.
|
||||
|
||||
|
@ -17,34 +17,27 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""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
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user