From 750dfd98af7d40727d7d46210c3f9f345273da3a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 22 Sep 2016 17:04:39 +0200 Subject: [PATCH] Generalize statusbar-attached website overlays We already had some duplicated logic for completion/keyhint/messageview, and plan to add prompt overlays too now - so here we refactor related code to have a list of overlays instead, which are all resized/positioned by the mainwindow when needed. This also changes the size management, which gets moved into the sizeHint of the respective overlay widgets. --- qutebrowser/completion/completionwidget.py | 50 +++++-- qutebrowser/mainwindow/mainwindow.py | 135 ++++++++---------- qutebrowser/mainwindow/messageview.py | 19 +-- qutebrowser/misc/keyhintwidget.py | 8 +- .../unit/completion/test_completionwidget.py | 13 +- tests/unit/mainwindow/test_messageview.py | 6 +- 6 files changed, 124 insertions(+), 107 deletions(-) diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py index 8eadcc599..8a8f9cfb6 100644 --- a/qutebrowser/completion/completionwidget.py +++ b/qutebrowser/completion/completionwidget.py @@ -24,12 +24,12 @@ subclasses to provide completions. """ from PyQt5.QtWidgets import QStyle, QTreeView, QSizePolicy -from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel +from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel, QSize from qutebrowser.config import config, style from qutebrowser.completion import completiondelegate from qutebrowser.completion.models import base -from qutebrowser.utils import utils, usertypes +from qutebrowser.utils import utils, usertypes, objreg from qutebrowser.commands import cmdexc, cmdutils @@ -49,7 +49,7 @@ class CompletionView(QTreeView): _active: Whether a selection is active. Signals: - resize_completion: Emitted when the completion should be resized. + update_geometry: Emitted when the completion should be resized. selection_changed: Emitted when the completion item selection changes. """ @@ -102,7 +102,7 @@ class CompletionView(QTreeView): } """ - resize_completion = pyqtSignal() + update_geometry = pyqtSignal() selection_changed = pyqtSignal(str) def __init__(self, win_id, parent=None): @@ -110,6 +110,7 @@ class CompletionView(QTreeView): self._win_id = win_id # FIXME handle new aliases. # objreg.get('config').changed.connect(self.init_command_completion) + objreg.get('config').changed.connect(self._on_config_changed) self._column_widths = base.BaseCompletionModel.COLUMN_WIDTHS self._active = False @@ -117,7 +118,7 @@ class CompletionView(QTreeView): self._delegate = completiondelegate.CompletionItemDelegate(self) self.setItemDelegate(self._delegate) style.set_register_stylesheet(self) - self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Minimum) + self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.setHeaderHidden(True) self.setAlternatingRowColors(True) self.setIndentation(0) @@ -138,6 +139,13 @@ class CompletionView(QTreeView): def __repr__(self): return utils.get_repr(self) + @pyqtSlot(str, str) + def _on_config_changed(self, section, option): + if section != 'completion': + return + if option in ['height', 'shrink']: + self.update_geometry.emit() + def _resize_columns(self): """Resize the completion columns based on column_widths.""" width = self.size().width() @@ -287,13 +295,12 @@ class CompletionView(QTreeView): self._column_widths = model.srcmodel.COLUMN_WIDTHS self._resize_columns() - self.maybe_resize_completion() + self._maybe_update_geometry() - @pyqtSlot() - def maybe_resize_completion(self): - """Emit the resize_completion signal if the config says so.""" + def _maybe_update_geometry(self): + """Emit the update_geometry signal if the config says so.""" if config.get('completion', 'shrink'): - self.resize_completion.emit() + self.update_geometry.emit() @pyqtSlot() def on_clear_completion_selection(self): @@ -304,6 +311,27 @@ class CompletionView(QTreeView): selmod.clearSelection() selmod.clearCurrentIndex() + def sizeHint(self): + """Get the completion size according to the config.""" + # Get the configured height/percentage. + confheight = str(config.get('completion', 'height')) + if confheight.endswith('%'): + perc = int(confheight.rstrip('%')) + height = self.window().height() * perc / 100 + else: + height = int(confheight) + # Shrink to content size if needed and shrinking is enabled + if config.get('completion', 'shrink'): + contents_height = ( + self.viewportSizeHint().height() + + self.horizontalScrollBar().sizeHint().height()) + if contents_height <= height: + height = contents_height + else: + contents_height = -1 + # The width isn't really relevant as we're expanding anyways. + return QSize(-1, height) + def selectionChanged(self, selected, deselected): """Extend selectionChanged to call completers selection_changed.""" if not self._active: @@ -322,7 +350,7 @@ class CompletionView(QTreeView): def showEvent(self, e): """Adjust the completion size and scroll when it's freshly shown.""" - self.resize_completion.emit() + self.update_geometry.emit() scrollbar = self.verticalScrollBar() if scrollbar is not None: scrollbar.setValue(scrollbar.minimum()) diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index 2b72e5f06..3393373d5 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -25,7 +25,7 @@ import itertools import functools from PyQt5.QtCore import pyqtSlot, QRect, QPoint, QTimer, Qt -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QSizePolicy from qutebrowser.commands import runners, cmdutils from qutebrowser.config import config @@ -125,6 +125,7 @@ class MainWindow(QWidget): _downloadview: The DownloadView widget. _vbox: The main QVBoxLayout. _commandrunner: The main CommandRunner instance. + _overlays: Widgets shown as overlay for the current webpage. """ def __init__(self, geometry=None, parent=None): @@ -137,6 +138,7 @@ class MainWindow(QWidget): super().__init__(parent) self.setAttribute(Qt.WA_DeleteOnClose) self._commandrunner = None + self._overlays = [] self.win_id = next(win_id_gen) self.registry = objreg.ObjectRegistry() objreg.window_registry[self.win_id] = self @@ -177,7 +179,10 @@ class MainWindow(QWidget): partial_match=True) self._keyhint = keyhintwidget.KeyHintView(self.win_id, self) + self._overlays.append((self._keyhint, self._keyhint.update_geometry)) self._messageview = messageview.MessageView(parent=self) + self._overlays.append((self._messageview, + self._messageview.update_geometry)) log.init.debug("Initializing modes...") modeman.init(self.win_id, self) @@ -196,11 +201,49 @@ class MainWindow(QWidget): # When we're here the statusbar might not even really exist yet, so # resizing will fail. Therefore, we use singleShot QTimers to make sure # we defer this until everything else is initialized. - QTimer.singleShot(0, self._connect_resize_signals) + QTimer.singleShot(0, self._connect_overlay_signals) objreg.get('config').changed.connect(self.on_config_changed) objreg.get("app").new_window.emit(self) + def _update_overlay_geometry(self, widget=None): + """Reposition/resize the given overlay. + + If no widget is given, reposition/resize all overlays. + """ + if widget is None: + for w, _signal in self._overlays: + self._update_overlay_geometry(w) + return + + if not widget.isVisible(): + return + + size_hint = widget.sizeHint() + if widget.sizePolicy().horizontalPolicy() == QSizePolicy.Expanding: + width = self.width() + else: + width = size_hint.width() + + status_position = config.get('ui', 'status-position') + if status_position == 'bottom': + top = self.height() - self.status.height() - size_hint.height() + top = qtutils.check_overflow(top, 'int', fatal=False) + topleft = QPoint(0, top) + bottomright = QPoint(width, self.status.geometry().top()) + elif status_position == 'top': + topleft = self.status.geometry().bottomLeft() + bottom = self.status.height() + widget.height() + bottom = qtutils.check_overflow(bottom, 'int', fatal=False) + bottomright = QPoint(size_hint.width(), bottom) + else: + raise ValueError("Invalid position {}!".format(status_position)) + + rect = QRect(topleft, bottomright) + log.misc.debug('new geometry for {!r}: {}'.format(widget, rect)) + if rect.isValid(): + widget.setGeometry(rect) + def _init_downloadmanager(self): log.init.debug("Initializing downloads...") download_manager = downloads.DownloadManager(self.win_id, self) @@ -218,6 +261,8 @@ class MainWindow(QWidget): completer_obj.on_selection_changed) objreg.register('completion', self._completion, scope='window', window=self.win_id) + self._overlays.append((self._completion, + self._completion.update_geometry)) def _init_command_dispatcher(self): dispatcher = commands.CommandDispatcher(self.win_id, @@ -234,15 +279,15 @@ class MainWindow(QWidget): @pyqtSlot(str, str) def on_config_changed(self, section, option): """Resize the completion if related config options changed.""" - if section == 'completion' and option in ['height', 'shrink']: - self.resize_completion() - elif section == 'ui' and option == 'statusbar-padding': - self.resize_completion() - elif section == 'ui' and option == 'downloads-position': + if section != 'ui': + return + if option == 'statusbar-padding': + self._update_overlay_geometry() + elif option == 'downloads-position': self._add_widgets() - elif section == 'ui' and option == 'status-position': + elif option == 'status-position': self._add_widgets() - self.resize_completion() + self._update_overlay_geometry() def _add_widgets(self): """Add or readd all widgets to the VBox.""" @@ -303,14 +348,12 @@ class MainWindow(QWidget): log.init.warning("Error while loading geometry.") self._set_default_geometry() - def _connect_resize_signals(self): + def _connect_overlay_signals(self): """Connect the resize signal and resize everything once.""" - self._completion.resize_completion.connect(self.resize_completion) - self._keyhint.reposition_keyhint.connect(self.reposition_keyhint) - self._messageview.reposition.connect(self._reposition_messageview) - self.resize_completion() - self.reposition_keyhint() - self._reposition_messageview() + for widget, signal in self._overlays: + signal.connect( + functools.partial(self._update_overlay_geometry, widget)) + self._update_overlay_geometry(widget) def _set_default_geometry(self): """Set some sensible default geometry.""" @@ -407,63 +450,6 @@ class MainWindow(QWidget): raise ValueError("Invalid position {}!".format(status_position)) return QRect(topleft, bottomright) - @pyqtSlot() - def resize_completion(self): - """Adjust completion according to config.""" - if not self._completion.isVisible(): - # It doesn't make sense to resize the completion as long as it's - # not shown anyways. - return - # Get the configured height/percentage. - confheight = str(config.get('completion', 'height')) - if confheight.endswith('%'): - perc = int(confheight.rstrip('%')) - height = self.height() * perc / 100 - else: - height = int(confheight) - # Shrink to content size if needed and shrinking is enabled - if config.get('completion', 'shrink'): - contents_height = ( - self._completion.viewportSizeHint().height() + - self._completion.horizontalScrollBar().sizeHint().height()) - if contents_height <= height: - height = contents_height - else: - contents_height = -1 - rect = self._get_overlay_position(height) - log.misc.debug('completion rect: {}'.format(rect)) - if rect.isValid(): - self._completion.setGeometry(rect) - - @pyqtSlot() - def reposition_keyhint(self): - """Adjust keyhint according to config.""" - if not self._keyhint.isVisible(): - return - # Shrink the window to the shown text and place it at the bottom left - width = self._keyhint.width() - height = self._keyhint.height() - topleft_y = self.height() - self.status.height() - height - topleft_y = qtutils.check_overflow(topleft_y, 'int', fatal=False) - topleft = QPoint(0, topleft_y) - bottomright = (self.status.geometry().topLeft() + - QPoint(width, 0)) - rect = QRect(topleft, bottomright) - log.misc.debug('keyhint rect: {}'.format(rect)) - if rect.isValid(): - self._keyhint.setGeometry(rect) - - @pyqtSlot() - def _reposition_messageview(self): - """Position the message view correctly.""" - if not self._messageview.isVisible(): - return - height = self._messageview.message_height() - rect = self._get_overlay_position(height) - log.misc.debug('messageview rect: {}'.format(rect)) - if rect.isValid(): - self._messageview.setGeometry(rect) - @cmdutils.register(instance='main-window', scope='window') @pyqtSlot() def close(self): @@ -490,8 +476,7 @@ class MainWindow(QWidget): e: The QResizeEvent """ super().resizeEvent(e) - self.resize_completion() - self.reposition_keyhint() + self._update_overlay_geometry() self._downloadview.updateGeometry() self.tabbed_browser.tabBar().refresh() diff --git a/qutebrowser/mainwindow/messageview.py b/qutebrowser/mainwindow/messageview.py index fe96af822..3afbff102 100644 --- a/qutebrowser/mainwindow/messageview.py +++ b/qutebrowser/mainwindow/messageview.py @@ -20,8 +20,8 @@ """Showing messages above the statusbar.""" -from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer, Qt -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel +from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer, Qt, QSize +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QSizePolicy from qutebrowser.config import config, style from qutebrowser.utils import usertypes, objreg @@ -71,13 +71,14 @@ class MessageView(QWidget): """Widget which stacks error/warning/info messages.""" - reposition = pyqtSignal() + update_geometry = pyqtSignal() def __init__(self, parent=None): super().__init__(parent) self._vbox = QVBoxLayout(self) self._vbox.setContentsMargins(0, 0, 0, 0) self._vbox.setSpacing(0) + self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self._clear_timer = QTimer() self._clear_timer.timeout.connect(self._clear_messages) @@ -87,15 +88,17 @@ class MessageView(QWidget): self._last_text = None self._messages = [] + def sizeHint(self): + """Get the proposed height for the view.""" + height = sum(label.sizeHint().height() for label in self._messages) + # The width isn't really relevant as we're expanding anyways. + return QSize(-1, height) + @config.change_filter('ui', 'message-timeout') def _set_clear_timer_interval(self): """Configure self._clear_timer according to the config.""" self._clear_timer.setInterval(config.get('ui', 'message-timeout')) - def message_height(self): - """Get the total height of all messages.""" - return sum(label.sizeHint().height() for label in self._messages) - @pyqtSlot() def _clear_messages(self): """Hide and delete all messages.""" @@ -121,4 +124,4 @@ class MessageView(QWidget): self._messages.append(widget) self._last_text = text self.show() - self.reposition.emit() + self.update_geometry.emit() diff --git a/qutebrowser/misc/keyhintwidget.py b/qutebrowser/misc/keyhintwidget.py index f9589edef..ef5f4ba87 100644 --- a/qutebrowser/misc/keyhintwidget.py +++ b/qutebrowser/misc/keyhintwidget.py @@ -42,7 +42,7 @@ class KeyHintView(QLabel): _win_id: Window ID of parent. Signals: - reposition_keyhint: Emitted when this widget should be resized. + update_geometry: Emitted when this widget should be resized/positioned. """ STYLESHEET = """ @@ -55,7 +55,7 @@ class KeyHintView(QLabel): } """ - reposition_keyhint = pyqtSignal() + update_geometry = pyqtSignal() def __init__(self, win_id, parent=None): super().__init__(parent) @@ -73,7 +73,7 @@ class KeyHintView(QLabel): def showEvent(self, e): """Adjust the keyhint size when it's freshly shown.""" - self.reposition_keyhint.emit() + self.update_geometry.emit() super().showEvent(e) @pyqtSlot(str) @@ -126,4 +126,4 @@ class KeyHintView(QLabel): self.setText(text) self.adjustSize() - self.reposition_keyhint.emit() + self.update_geometry.emit() diff --git a/tests/unit/completion/test_completionwidget.py b/tests/unit/completion/test_completionwidget.py index 0f316a928..5a4ee87c0 100644 --- a/tests/unit/completion/test_completionwidget.py +++ b/tests/unit/completion/test_completionwidget.py @@ -39,6 +39,7 @@ def completionview(qtbot, status_command_stub, config_stub, win_registry, 'scrollbar-padding': 2, 'shrink': False, 'quick-complete': False, + 'height': '50%', }, 'colors': { 'completion.fg': QColor(), @@ -87,13 +88,13 @@ def test_set_pattern(completionview): model.set_pattern.assert_called_with('foo') -def test_maybe_resize_completion(completionview, config_stub, qtbot): +def test_maybe_update_geometry(completionview, config_stub, qtbot): """Ensure completion is resized only if shrink is True.""" - with qtbot.assertNotEmitted(completionview.resize_completion): - completionview.maybe_resize_completion() - config_stub.data = {'completion': {'shrink': True}} - with qtbot.waitSignal(completionview.resize_completion): - completionview.maybe_resize_completion() + with qtbot.assertNotEmitted(completionview.update_geometry): + completionview._maybe_update_geometry() + config_stub.data['completion']['shrink'] = True + with qtbot.waitSignal(completionview.update_geometry): + completionview._maybe_update_geometry() @pytest.mark.parametrize('which, tree, expected', [ diff --git a/tests/unit/mainwindow/test_messageview.py b/tests/unit/mainwindow/test_messageview.py index b4e5b34bf..191d14eeb 100644 --- a/tests/unit/mainwindow/test_messageview.py +++ b/tests/unit/mainwindow/test_messageview.py @@ -67,13 +67,13 @@ def test_message_hiding(qtbot, view): assert not view._messages -def test_message_height(view): +def test_size_hint(view): """The message height should increase with more messages.""" view.show_message(usertypes.MessageLevel.info, 'test1') - height1 = view.message_height() + height1 = view.sizeHint().height() assert height1 > 0 view.show_message(usertypes.MessageLevel.info, 'test2') - height2 = view.message_height() + height2 = view.sizeHint().height() assert height2 == height1 * 2