From 34d3d2cda64a2bf74381c951fe92dcc57125928d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 14 Jun 2016 17:32:36 +0200 Subject: [PATCH] Full scrolling implementation --- qutebrowser/browser/commands.py | 105 +++++------------- qutebrowser/browser/tab.py | 70 ++++++++++-- qutebrowser/browser/webengine/webenginetab.py | 16 +-- qutebrowser/browser/webkit/webkittab.py | 105 ++++++++++++++++-- .../mainwindow/statusbar/percentage.py | 2 +- qutebrowser/mainwindow/tabbedbrowser.py | 11 +- qutebrowser/mainwindow/tabwidget.py | 2 +- qutebrowser/misc/sessions.py | 2 +- 8 files changed, 204 insertions(+), 109 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 1ee0a44e8..c4a01c1d3 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -513,7 +513,7 @@ class CommandDispatcher: dy *= count cmdutils.check_overflow(dx, 'int') cmdutils.check_overflow(dy, 'int') - self._current_widget().page().currentFrame().scroll(dx, dy) + self._current_widget().scroll.delta(dx, dy) @cmdutils.register(instance='command-dispatcher', hide=True, scope='window') @@ -526,54 +526,29 @@ class CommandDispatcher: (up/down/left/right/top/bottom). count: multiplier """ - fake_keys = { - 'up': Qt.Key_Up, - 'down': Qt.Key_Down, - 'left': Qt.Key_Left, - 'right': Qt.Key_Right, - 'top': Qt.Key_Home, - 'bottom': Qt.Key_End, - 'page-up': Qt.Key_PageUp, - 'page-down': Qt.Key_PageDown, + tab = self._current_widget() + funcs = { + 'up': tab.scroll.up, + 'down': tab.scroll.down, + 'left': tab.scroll.left, + 'right': tab.scroll.right, + 'top': tab.scroll.top, + 'bottom': tab.scroll.bottom, + 'page-up': tab.scroll.page_up, + 'page-down': tab.scroll.page_down, } try: - key = fake_keys[direction] + func = funcs[direction] except KeyError: - expected_values = ', '.join(sorted(fake_keys)) + expected_values = ', '.join(sorted(funcs)) raise cmdexc.CommandError("Invalid value {!r} for direction - " "expected one of: {}".format( direction, expected_values)) - widget = self._current_widget() - frame = widget.page().currentFrame() - press_evt = QKeyEvent(QEvent.KeyPress, key, Qt.NoModifier, 0, 0, 0) - release_evt = QKeyEvent(QEvent.KeyRelease, key, Qt.NoModifier, 0, 0, 0) - - # Count doesn't make sense with top/bottom if direction in ('top', 'bottom'): - count = 1 - - max_min = { - 'up': [Qt.Vertical, frame.scrollBarMinimum], - 'down': [Qt.Vertical, frame.scrollBarMaximum], - 'left': [Qt.Horizontal, frame.scrollBarMinimum], - 'right': [Qt.Horizontal, frame.scrollBarMaximum], - 'page-up': [Qt.Vertical, frame.scrollBarMinimum], - 'page-down': [Qt.Vertical, frame.scrollBarMaximum], - } - - for _ in range(count): - # Abort scrolling if the minimum/maximum was reached. - try: - qt_dir, getter = max_min[direction] - except KeyError: - pass - else: - if frame.scrollBarValue(qt_dir) == getter(qt_dir): - return - - widget.keyPressEvent(press_evt) - widget.keyReleaseEvent(release_evt) + func() + else: + func(count=count) @cmdutils.register(instance='command-dispatcher', hide=True, scope='window') @@ -598,19 +573,14 @@ class CommandDispatcher: elif count is not None: perc = count - orientation = Qt.Horizontal if horizontal else Qt.Vertical - - if perc == 0 and orientation == Qt.Vertical: - self.scroll('top') - elif perc == 100 and orientation == Qt.Vertical: - self.scroll('bottom') + if horizontal: + x = perc + y = None else: - perc = qtutils.check_overflow(perc, 'int', fatal=False) - frame = self._current_widget().page().currentFrame() - m = frame.scrollBarMaximum(orientation) - if m == 0: - return - frame.setScrollBarValue(orientation, int(m * perc / 100)) + x = None + y = perc + + self._current_widget().scroll.to_perc(x, y) @cmdutils.register(instance='command-dispatcher', hide=True, scope='window') @@ -633,38 +603,19 @@ class CommandDispatcher: scrolling up at the top of the page. count: multiplier """ - frame = self._current_widget().page().currentFrame() - if not frame.url().isValid(): + tab = self._current_widget() + if not tab.cur_url.isValid(): # See https://github.com/The-Compiler/qutebrowser/issues/701 return - if (bottom_navigate is not None and - frame.scrollPosition().y() >= - frame.scrollBarMaximum(Qt.Vertical)): + if bottom_navigate is not None and tab.scroll.at_bottom(): self.navigate(bottom_navigate) return - elif top_navigate is not None and frame.scrollPosition().y() == 0: + elif top_navigate is not None and tab.scroll.at_top(): self.navigate(top_navigate) return - mult_x = count * x - mult_y = count * y - if mult_y.is_integer(): - if mult_y == 0: - pass - elif mult_y < 0: - self.scroll('page-up', count=-int(mult_y)) - elif mult_y > 0: # pragma: no branch - self.scroll('page-down', count=int(mult_y)) - mult_y = 0 - if mult_x == 0 and mult_y == 0: - return - size = frame.geometry() - dx = mult_x * size.width() - dy = mult_y * size.height() - cmdutils.check_overflow(dx, 'int') - cmdutils.check_overflow(dy, 'int') - frame.scroll(dx, dy) + tab.scroll.delta_page(count * x, count * y) @cmdutils.register(instance='command-dispatcher', scope='window') def yank(self, title=False, sel=False, domain=False, pretty=False): diff --git a/qutebrowser/browser/tab.py b/qutebrowser/browser/tab.py index 71daff9cc..ae153b0fb 100644 --- a/qutebrowser/browser/tab.py +++ b/qutebrowser/browser/tab.py @@ -21,7 +21,7 @@ import itertools -from PyQt5.QtCore import pyqtSignal, QUrl +from PyQt5.QtCore import pyqtSignal, QUrl, QObject from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QWidget, QLayout @@ -55,6 +55,65 @@ class WrapperLayout(QLayout): self._widget.setGeometry(r) +class AbstractScroller(QObject): + + """Attribute of AbstractTab to manage scroll position.""" + + perc_changed = pyqtSignal(int, int) + + def __init__(self, parent=None): + super().__init__(parent) + self.widget = None + + def pos_px(self): + raise NotImplementedError + + def pos_perc(self): + raise NotImplementedError + + def to_perc(self, x=None, y=None): + raise NotImplementedError + + def to_point(self, point): + raise NotImplementedError + + def delta(self, x=0, y=0): + raise NotImplementedError + + def delta_page(self, x=0, y=0): + raise NotImplementedError + + def up(self, count=1): + raise NotImplementedError + + def down(self, count=1): + raise NotImplementedError + + def left(self, count=1): + raise NotImplementedError + + def right(self, count=1): + raise NotImplementedError + + def top(self): + raise NotImplementedError + + def bottom(self): + raise NotImplementedError + + def page_up(self, count=1): + raise NotImplementedError + + def page_down(self, count=1): + raise NotImplementedError + + def at_top(self): + raise NotImplementedError + + def at_bottom(self): + raise NotImplementedError + + class AbstractHistory: """The history attribute of a AbstractTab.""" @@ -116,7 +175,6 @@ class AbstractTab(QWidget): load_started = pyqtSignal() load_progress = pyqtSignal(int) load_finished = pyqtSignal(bool) - scroll_pos_changed = pyqtSignal(int, int) icon_changed = pyqtSignal(QIcon) # FIXME:refactor get rid of this altogether? url_text_changed = pyqtSignal(str) @@ -129,6 +187,7 @@ class AbstractTab(QWidget): self.tab_id = next(tab_id_gen) super().__init__(parent) self.history = AbstractHistory(self) + self.scroll = AbstractScroller(parent=self) self._layout = None self._widget = None self.keep_icon = False # FIXME:refactor get rid of this? @@ -137,6 +196,7 @@ class AbstractTab(QWidget): self._layout = WrapperLayout(widget, self) self._widget = widget self.history.history = widget.history() + self.scroll.widget = widget widget.setParent(self) @property @@ -151,12 +211,6 @@ class AbstractTab(QWidget): def load_status(self): raise NotImplementedError - def scroll_pos_perc(self): - raise NotImplementedError - - def scroll_pos_px(self): - raise NotImplementedError - def openurl(self, url): raise NotImplementedError diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 2f497d30d..6431db42c 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -31,6 +31,13 @@ from qutebrowser.browser import tab from qutebrowser.utils import usertypes, qtutils +class WebEngineScroller(tab.AbstractScroller): + + ## TODO + + pass + + class WebEngineHistory(tab.AbstractHistory): def __iter__(self): @@ -68,6 +75,7 @@ class WebEngineViewTab(tab.AbstractTab): super().__init__(win_id) widget = QWebEngineView() self.history = WebEngineHistory(self) + self.scroll = WebEngineScroller(parent=self) self._set_widget(widget) self._connect_signals() @@ -86,12 +94,6 @@ class WebEngineViewTab(tab.AbstractTab): def load_status(self): return usertypes.LoadStatus.success - def scroll_pos_perc(self): - return (0, 0) # FIXME - - def scroll_pos_px(self): - return self._widget.page().scrollPosition() - def set_zoom_factor(self, factor): self._widget.setZoomFactor(factor) @@ -135,6 +137,6 @@ class WebEngineViewTab(tab.AbstractTab): page.loadFinished.connect(self.load_finished) # FIXME:refactor # view.iconChanged.connect(self.icon_changed) - # view.scroll_pos_changed.connect(self.scroll_pos_changed) + # view.scroll.pos_changed.connect(self.scroll.perc_changed) # view.url_text_changed.connect(self.url_text_changed) # view.load_status_changed.connect(self.load_status_changed) diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index 332d2b370..e21f0fcd8 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -19,7 +19,8 @@ """Wrapper over our (QtWebKit) WebView.""" -from PyQt5.QtCore import pyqtSlot +from PyQt5.QtCore import pyqtSlot, Qt, QEvent +from PyQt5.QtGui import QKeyEvent from PyQt5.QtWebKitWidgets import QWebPage from qutebrowser.browser import tab @@ -27,6 +28,97 @@ from qutebrowser.browser.webkit import webview from qutebrowser.utils import qtutils +class WebViewScroller(tab.AbstractScroller): + + def pos_px(self): + return self.widget.page().mainFrame().scrollPosition() + + def pos_perc(self): + return self.widget.scroll_pos + + def to_point(self, point): + self.widget.page().mainFrame().setScrollPosition(point) + + def delta(x=0, y=0): + qtutils.check_overflow(x, 'int') + qtutils.check_overflow(y, 'int') + self.widget.page().mainFrame().scroll(x, y) + + def delta_page(self, x=0, y=0): + if y.is_integer(): + y = int(y) + if y == 0: + pass + elif y < 0: + self.page_up(count=y) + elif y > 0: + self.page_down(count=y) + y = 0 + if x == 0 and y == 0: + return + size = frame.geometry() + self.delta(x * size.width(), y * size.height()) + + def to_perc(self, x=None, y=None): + if x is None and y == 0: + self.top() + elif x is None and y == 100: + self.bottom() + else: + for val, orientation in [(x, Qt.Horizontal), (y, Qt.Vertical)]: + perc = qtutils.check_overflow(val, 'int', fatal=False) + frame = self.widget.page().mainFrame() + m = frame.scrollBarMaximum(orientation) + if m == 0: + continue + frame.setScrollBarValue(orientation, int(m * val / 100)) + + def _key_press(self, key, count=1, getter_name=None, direction=None): + frame = self.widget.page().mainFrame() + press_evt = QKeyEvent(QEvent.KeyPress, key, Qt.NoModifier, 0, 0, 0) + release_evt = QKeyEvent(QEvent.KeyRelease, key, Qt.NoModifier, 0, 0, 0) + getter = None if getter_name is None else getattr(frame, getter_name) + + for _ in range(count): + # Abort scrolling if the minimum/maximum was reached. + if frame.scrollBarValue(direction) == getter(direction): + return + self.widget.keyPressEvent(press_evt) + self.widget.keyReleaseEvent(release_evt) + + def up(self, count=1): + self._key_press(Qt.Key_Up, count, 'scrollBarMinimum', Qt.Vertical) + + def down(self, count=1): + self._key_press(Qt.Key_Down, count, 'scrollBarMaximum', Qt.Vertical) + + def left(self, count=1): + self._key_press(Qt.Key_Left, count, 'scrollBarMinimum', Qt.Horizontal) + + def right(self, count=1): + self._key_press(Qt.Key_Right, count, 'scrollBarMaximum', Qt.Horizontal) + + def top(self): + self._key_press(Qt.Key_Home) + + def bottom(self): + self._key_press(Qt.Key_End) + + def page_up(self, count=1): + self._key_press(Qt.Key_PageUp, count, 'scrollBarMinimum', Qt.Vertical) + + def page_down(self, count=1): + self._key_press(Qt.Key_PageDown, count, 'scrollBarMaximum', + Qt.Vertical) + + def at_top(self): + return self.pos_px().y() == 0 + + def at_bottom(self): + frame = self.widget.page().currentFrame() + return self.pos_px().y() >= frame.scrollBarMaximum(Qt.Vertical) + + class WebViewHistory(tab.AbstractHistory): def __iter__(self): @@ -63,7 +155,7 @@ class WebViewHistory(tab.AbstractHistory): if 'zoom' in cur_data: self.tab.zoom_perc(cur_data['zoom'] * 100) if ('scroll-pos' in cur_data and - self.tab.scroll_pos_px() == QPoint(0, 0)): + self.tab.scroll.pos_px() == QPoint(0, 0)): QTimer.singleShot(0, functools.partial( self.tab.scroll, cur_data['scroll-pos'])) @@ -74,6 +166,7 @@ class WebViewTab(tab.AbstractTab): super().__init__(win_id) widget = webview.WebView(win_id, self.tab_id) self.history = WebViewHistory(self) + self.scroll = WebViewScroller(parent=self) self._set_widget(widget) self._connect_signals() @@ -92,12 +185,6 @@ class WebViewTab(tab.AbstractTab): def load_status(self): return self._widget.load_status - def scroll_pos_perc(self): - return self._widget.scroll_pos - - def scroll_pos_px(self): - return self._widget.page().mainFrame().scrollPosition() - def dump_async(self, callback=None, *, plain=False): frame = self._widget.page().mainFrame() if plain: @@ -138,7 +225,7 @@ class WebViewTab(tab.AbstractTab): page.linkHovered.connect(self.link_hovered) page.loadProgress.connect(self.load_progress) frame.loadStarted.connect(self.load_started) - view.scroll_pos_changed.connect(self.scroll_pos_changed) + view.scroll_pos_changed.connect(self.scroll.perc_changed) view.titleChanged.connect(self.title_changed) view.url_text_changed.connect(self.url_text_changed) view.load_status_changed.connect(self.load_status_changed) diff --git a/qutebrowser/mainwindow/statusbar/percentage.py b/qutebrowser/mainwindow/statusbar/percentage.py index dc52f7bfa..3a15b8308 100644 --- a/qutebrowser/mainwindow/statusbar/percentage.py +++ b/qutebrowser/mainwindow/statusbar/percentage.py @@ -52,4 +52,4 @@ class Percentage(textbase.TextBase): @pyqtSlot(webview.WebView) def on_tab_changed(self, tab): """Update scroll position when tab changed.""" - self.set_perc(*tab.scroll_pos_perc()) + self.set_perc(*tab.scroll.pos_perc()) diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index 867a2b04d..41085adcf 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -183,9 +183,9 @@ class TabbedBrowser(tabwidget.TabWidget): # https://github.com/The-Compiler/qutebrowser/issues/1579 # tab.statusBarMessage.connect( # self._filter.create(self.cur_statusbar_message, tab)) - tab.scroll_pos_changed.connect( + tab.scroll.perc_changed.connect( self._filter.create(self.cur_scroll_perc_changed, tab)) - tab.scroll_pos_changed.connect(self.on_scroll_pos_changed) + tab.scroll.perc_changed.connect(self.on_scroll_pos_changed) tab.url_text_changed.connect( self._filter.create(self.cur_url_text_changed, tab)) tab.load_status_changed.connect( @@ -654,7 +654,7 @@ class TabbedBrowser(tabwidget.TabWidget): if key != "'": message.error(self._win_id, "Failed to set mark: url invalid") return - point = self.currentWidget().page().currentFrame().scrollPosition() + point = self.currentWidget().scroll.pos_px() if key.isupper(): self._global_marks[key] = point, url @@ -675,7 +675,7 @@ class TabbedBrowser(tabwidget.TabWidget): except qtutils.QtValueError: urlkey = None - frame = self.currentWidget().page().currentFrame() + tab = self.currentWidget() if key.isupper(): if key in self._global_marks: @@ -686,6 +686,7 @@ class TabbedBrowser(tabwidget.TabWidget): if ok: self.cur_load_finished.disconnect(callback) frame.setScrollPosition(point) + tab.scroll.to_point(point) self.openurl(url, newtab=False) self.cur_load_finished.connect(callback) @@ -701,6 +702,6 @@ class TabbedBrowser(tabwidget.TabWidget): # "'" would just jump to the current position every time self.set_mark("'") - frame.setScrollPosition(point) + tab.scroll.to_point(point) else: message.error(self._win_id, "Mark {} is not set".format(key)) diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index d14e823e4..99e2051e5 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -127,7 +127,7 @@ class TabWidget(QTabWidget): except qtutils.QtValueError: fields['host'] = '' - y = widget.scroll_pos_perc()[1] + y = widget.scroll.pos_perc()[1] if y <= 0: scroll_pos = 'top' elif y >= 100: diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py index 5e2bbec68..e61c392a7 100644 --- a/qutebrowser/misc/sessions.py +++ b/qutebrowser/misc/sessions.py @@ -165,7 +165,7 @@ class SessionManager(QObject): user_data = item.userData() if tab.history.current_idx() == idx: - pos = tab.scroll_pos_px() + pos = tab.scroll.pos_px() item_data['zoom'] = tab.zoom_factor() item_data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()} elif user_data is not None: