From 90614d1fe3eb4406963886c4a48df57988f49189 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 14 Jun 2016 18:08:46 +0200 Subject: [PATCH] Initial caret browsing support --- qutebrowser/browser/commands.py | 144 ++----------- qutebrowser/browser/tab.py | 75 ++++++- qutebrowser/browser/webengine/webenginetab.py | 10 +- qutebrowser/browser/webkit/webkittab.py | 189 +++++++++++++++++- qutebrowser/browser/webkit/webview.py | 29 --- qutebrowser/mainwindow/statusbar/bar.py | 6 +- 6 files changed, 291 insertions(+), 162 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index c4a01c1d3..01b995078 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1496,13 +1496,7 @@ class CommandDispatcher: Args: count: How many lines to move. """ - webview = self._current_widget() - if not webview.selection_enabled: - act = QWebPage.MoveToNextLine - else: - act = QWebPage.SelectNextLine - for _ in range(count): - webview.triggerPageAction(act) + self._current_widget().caret.move_to_next_line(count) @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window') @@ -1513,13 +1507,7 @@ class CommandDispatcher: Args: count: How many lines to move. """ - webview = self._current_widget() - if not webview.selection_enabled: - act = QWebPage.MoveToPreviousLine - else: - act = QWebPage.SelectPreviousLine - for _ in range(count): - webview.triggerPageAction(act) + self._current_widget().caret.move_to_prev_line(count) @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window') @@ -1530,13 +1518,7 @@ class CommandDispatcher: Args: count: How many lines to move. """ - webview = self._current_widget() - if not webview.selection_enabled: - act = QWebPage.MoveToNextChar - else: - act = QWebPage.SelectNextChar - for _ in range(count): - webview.triggerPageAction(act) + self._current_widget().caret.move_to_next_char(count) @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window') @@ -1547,13 +1529,7 @@ class CommandDispatcher: Args: count: How many chars to move. """ - webview = self._current_widget() - if not webview.selection_enabled: - act = QWebPage.MoveToPreviousChar - else: - act = QWebPage.SelectPreviousChar - for _ in range(count): - webview.triggerPageAction(act) + self._current_widget().caret.move_to_prev_char(count) @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window') @@ -1564,18 +1540,7 @@ class CommandDispatcher: Args: count: How many words to move. """ - webview = self._current_widget() - if not webview.selection_enabled: - act = [QWebPage.MoveToNextWord] - if sys.platform == 'win32': # pragma: no cover - act.append(QWebPage.MoveToPreviousChar) - else: - act = [QWebPage.SelectNextWord] - if sys.platform == 'win32': # pragma: no cover - act.append(QWebPage.SelectPreviousChar) - for _ in range(count): - for a in act: - webview.triggerPageAction(a) + self._current_widget().caret.move_to_end_of_word(count) @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window') @@ -1586,18 +1551,7 @@ class CommandDispatcher: Args: count: How many words to move. """ - webview = self._current_widget() - if not webview.selection_enabled: - act = [QWebPage.MoveToNextWord] - if sys.platform != 'win32': # pragma: no branch - act.append(QWebPage.MoveToNextChar) - else: - act = [QWebPage.SelectNextWord] - if sys.platform != 'win32': # pragma: no branch - act.append(QWebPage.SelectNextChar) - for _ in range(count): - for a in act: - webview.triggerPageAction(a) + self._current_widget().caret.move_to_next_word(count) @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window') @@ -1608,35 +1562,19 @@ class CommandDispatcher: Args: count: How many words to move. """ - webview = self._current_widget() - if not webview.selection_enabled: - act = QWebPage.MoveToPreviousWord - else: - act = QWebPage.SelectPreviousWord - for _ in range(count): - webview.triggerPageAction(act) + self._current_widget().caret.move_to_prev_word(count) @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window') def move_to_start_of_line(self): """Move the cursor or selection to the start of the line.""" - webview = self._current_widget() - if not webview.selection_enabled: - act = QWebPage.MoveToStartOfLine - else: - act = QWebPage.SelectStartOfLine - webview.triggerPageAction(act) + self._current_widget().caret.move_to_start_of_line() @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window') def move_to_end_of_line(self): """Move the cursor or selection to the end of line.""" - webview = self._current_widget() - if not webview.selection_enabled: - act = QWebPage.MoveToEndOfLine - else: - act = QWebPage.SelectEndOfLine - webview.triggerPageAction(act) + self._current_widget().caret.move_to_end_of_line() @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window') @@ -1647,16 +1585,7 @@ class CommandDispatcher: Args: count: How many blocks to move. """ - webview = self._current_widget() - if not webview.selection_enabled: - act = [QWebPage.MoveToNextLine, - QWebPage.MoveToStartOfBlock] - else: - act = [QWebPage.SelectNextLine, - QWebPage.SelectStartOfBlock] - for _ in range(count): - for a in act: - webview.triggerPageAction(a) + self._current_widget().caret.move_to_start_of_next_block(count) @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window') @@ -1667,16 +1596,7 @@ class CommandDispatcher: Args: count: How many blocks to move. """ - webview = self._current_widget() - if not webview.selection_enabled: - act = [QWebPage.MoveToPreviousLine, - QWebPage.MoveToStartOfBlock] - else: - act = [QWebPage.SelectPreviousLine, - QWebPage.SelectStartOfBlock] - for _ in range(count): - for a in act: - webview.triggerPageAction(a) + self._current_widget().caret.move_to_start_of_prev_block(count) @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window') @@ -1687,16 +1607,7 @@ class CommandDispatcher: Args: count: How many blocks to move. """ - webview = self._current_widget() - if not webview.selection_enabled: - act = [QWebPage.MoveToNextLine, - QWebPage.MoveToEndOfBlock] - else: - act = [QWebPage.SelectNextLine, - QWebPage.SelectEndOfBlock] - for _ in range(count): - for a in act: - webview.triggerPageAction(a) + self._current_widget().caret.move_to_end_of_next_block(count) @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window') @@ -1707,36 +1618,19 @@ class CommandDispatcher: Args: count: How many blocks to move. """ - webview = self._current_widget() - if not webview.selection_enabled: - act = [QWebPage.MoveToPreviousLine, QWebPage.MoveToEndOfBlock] - else: - act = [QWebPage.SelectPreviousLine, QWebPage.SelectEndOfBlock] - for _ in range(count): - for a in act: - webview.triggerPageAction(a) + self._current_widget().caret.move_to_end_of_prev_block(count) @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window') def move_to_start_of_document(self): """Move the cursor or selection to the start of the document.""" - webview = self._current_widget() - if not webview.selection_enabled: - act = QWebPage.MoveToStartOfDocument - else: - act = QWebPage.SelectStartOfDocument - webview.triggerPageAction(act) + self._current_widget().caret.move_to_start_of_document() @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window') def move_to_end_of_document(self): """Move the cursor or selection to the end of the document.""" - webview = self._current_widget() - if not webview.selection_enabled: - act = QWebPage.MoveToEndOfDocument - else: - act = QWebPage.SelectEndOfDocument - webview.triggerPageAction(act) + self._current_widget().caret.move_to_end_of_document() @cmdutils.register(instance='command-dispatcher', scope='window') def yank_selected(self, sel=False, keep=False): @@ -1766,17 +1660,13 @@ class CommandDispatcher: modes=[KeyMode.caret], scope='window') def toggle_selection(self): """Toggle caret selection mode.""" - widget = self._current_widget() - widget.selection_enabled = not widget.selection_enabled - mainwindow = objreg.get('main-window', scope='window', - window=self._win_id) - mainwindow.status.set_mode_active(usertypes.KeyMode.caret, True) + self._current_widget().caret.toggle_selection() @cmdutils.register(instance='command-dispatcher', hide=True, modes=[KeyMode.caret], scope='window') def drop_selection(self): """Drop selection and keep selection mode enabled.""" - self._current_widget().triggerPageAction(QWebPage.MoveToNextChar) + self._current_widget().caret.drop_selection() @cmdutils.register(instance='command-dispatcher', scope='window', debug=True) diff --git a/qutebrowser/browser/tab.py b/qutebrowser/browser/tab.py index ae153b0fb..59ca0d3a3 100644 --- a/qutebrowser/browser/tab.py +++ b/qutebrowser/browser/tab.py @@ -25,7 +25,7 @@ from PyQt5.QtCore import pyqtSignal, QUrl, QObject from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QWidget, QLayout -from qutebrowser.utils import utils +from qutebrowser.utils import utils, objreg tab_id_gen = itertools.count(0) @@ -55,6 +55,77 @@ class WrapperLayout(QLayout): self._widget.setGeometry(r) +class AbstractCaret: + + """Attribute of AbstractTab for caret browsing.""" + + def __init__(self, win_id): + self._win_id = win_id + self.widget = None + self.selection_enabled = False + mode_manager = objreg.get('mode-manager', scope='window', + window=win_id) + mode_manager.entered.connect(self.on_mode_entered) + mode_manager.left.connect(self.on_mode_left) + + def on_mode_entered(self): + raise NotImplementedError + + def on_mode_left(self): + raise NotImplementedError + + def move_to_next_line(self, count=1): + raise NotImplementedError + + def move_to_prev_line(self, count=1): + raise NotImplementedError + + def move_to_next_char(self, count=1): + raise NotImplementedError + + def move_to_prev_char(self, count=1): + raise NotImplementedError + + def move_to_end_of_word(self, count=1): + raise NotImplementedError + + def move_to_next_word(self, count=1): + raise NotImplementedError + + def move_to_prev_word(self, count=1): + raise NotImplementedError + + def move_to_start_of_line(self): + raise NotImplementedError + + def move_to_end_of_line(self): + raise NotImplementedError + + def move_to_start_of_next_block(self, count=1): + raise NotImplementedError + + def move_to_start_of_prev_block(self, count=1): + raise NotImplementedError + + def move_to_end_of_next_block(self, count=1): + raise NotImplementedError + + def move_to_end_of_prev_block(self, count=1): + raise NotImplementedError + + def move_to_start_of_document(self): + raise NotImplementedError + + def move_to_end_of_document(self): + raise NotImplementedError + + def toggle_selection(self): + raise NotImplementedError + + def drop_selection(self): + raise NotImplementedError + + class AbstractScroller(QObject): """Attribute of AbstractTab to manage scroll position.""" @@ -188,6 +259,7 @@ class AbstractTab(QWidget): super().__init__(parent) self.history = AbstractHistory(self) self.scroll = AbstractScroller(parent=self) + self.caret = AbstractCaret(win_id=win_id) self._layout = None self._widget = None self.keep_icon = False # FIXME:refactor get rid of this? @@ -197,6 +269,7 @@ class AbstractTab(QWidget): self._widget = widget self.history.history = widget.history() self.scroll.widget = widget + self.caret.widget = widget widget.setParent(self) @property diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 6431db42c..5ae2974b9 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 WebEngineCaret(tab.AbstractCaret): + + ## TODO + + pass + + class WebEngineScroller(tab.AbstractScroller): ## TODO @@ -75,7 +82,8 @@ class WebEngineViewTab(tab.AbstractTab): super().__init__(win_id) widget = QWebEngineView() self.history = WebEngineHistory(self) - self.scroll = WebEngineScroller(parent=self) + self.scroll = WebEngineScroller() + self.caret = WebEngineCaret(win_id=win_id) self._set_widget(widget) self._connect_signals() diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index e21f0fcd8..70eb79b22 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -19,13 +19,199 @@ """Wrapper over our (QtWebKit) WebView.""" +import sys + from PyQt5.QtCore import pyqtSlot, Qt, QEvent from PyQt5.QtGui import QKeyEvent from PyQt5.QtWebKitWidgets import QWebPage +from PyQt5.QtWebKit import QWebSettings from qutebrowser.browser import tab from qutebrowser.browser.webkit import webview -from qutebrowser.utils import qtutils +from qutebrowser.utils import qtutils, objreg, usertypes, utils + + +class WebViewCaret(tab.AbstractCaret): + + @pyqtSlot(usertypes.KeyMode) + def on_mode_entered(self, mode): + if mode != usertypes.KeyMode.caret: + return + + settings = self.widget.settings() + settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True) + self.selection_enabled = bool(self.widget.page().selectedText()) + + if self.widget.isVisible(): + # Sometimes the caret isn't immediately visible, but unfocusing + # and refocusing it fixes that. + self.widget.clearFocus() + self.widget.setFocus(Qt.OtherFocusReason) + + # Move the caret to the first element in the viewport if there + # isn't any text which is already selected. + # + # Note: We can't use hasSelection() here, as that's always + # true in caret mode. + if not self.widget.page().selectedText(): + # FIXME use self.tab here + self.widget.page().currentFrame().evaluateJavaScript( + utils.read_file('javascript/position_caret.js')) + + @pyqtSlot(usertypes.KeyMode) + def on_mode_left(self, mode): + settings = self.widget.settings() + if settings.testAttribute(QWebSettings.CaretBrowsingEnabled): + if self.selection_enabled and self.widget.hasSelection(): + # Remove selection if it exists + self.widget.triggerPageAction(QWebPage.MoveToNextChar) + settings.setAttribute(QWebSettings.CaretBrowsingEnabled, False) + self.selection_enabled = False + + def move_to_next_line(self, count=1): + if not self.selection_enabled: + act = QWebPage.MoveToNextLine + else: + act = QWebPage.SelectNextLine + for _ in range(count): + self.widget.triggerPageAction(act) + + def move_to_prev_line(self, count=1): + if not self.selection_enabled: + act = QWebPage.MoveToPreviousLine + else: + act = QWebPage.SelectPreviousLine + for _ in range(count): + self.widget.triggerPageAction(act) + + def move_to_next_char(self, count=1): + if not self.selection_enabled: + act = QWebPage.MoveToNextChar + else: + act = QWebPage.SelectNextChar + for _ in range(count): + self.widget.triggerPageAction(act) + + def move_to_prev_char(self, count=1): + if not self.selection_enabled: + act = QWebPage.MoveToPreviousChar + else: + act = QWebPage.SelectPreviousChar + for _ in range(count): + self.widget.triggerPageAction(act) + + def move_to_end_of_word(self, count=1): + if not self.selection_enabled: + act = [QWebPage.MoveToNextWord] + if sys.platform == 'win32': # pragma: no cover + act.append(QWebPage.MoveToPreviousChar) + else: + act = [QWebPage.SelectNextWord] + if sys.platform == 'win32': # pragma: no cover + act.append(QWebPage.SelectPreviousChar) + for _ in range(count): + for a in act: + self.widget.triggerPageAction(a) + + def move_to_next_word(self, count=1): + if not self.selection_enabled: + act = [QWebPage.MoveToNextWord] + if sys.platform != 'win32': # pragma: no branch + act.append(QWebPage.MoveToNextChar) + else: + act = [QWebPage.SelectNextWord] + if sys.platform != 'win32': # pragma: no branch + act.append(QWebPage.SelectNextChar) + for _ in range(count): + for a in act: + self.widget.triggerPageAction(a) + + def move_to_prev_word(self, count=1): + if not self.selection_enabled: + act = QWebPage.MoveToPreviousWord + else: + act = QWebPage.SelectPreviousWord + for _ in range(count): + self.widget.triggerPageAction(act) + + def move_to_start_of_line(self): + if not self.selection_enabled: + act = QWebPage.MoveToStartOfLine + else: + act = QWebPage.SelectStartOfLine + self.widget.triggerPageAction(act) + + def move_to_end_of_line(self): + if not self.selection_enabled: + act = QWebPage.MoveToEndOfLine + else: + act = QWebPage.SelectEndOfLine + self.widget.triggerPageAction(act) + + def move_to_start_of_next_block(self, count=1): + if not self.selection_enabled: + act = [QWebPage.MoveToNextLine, + QWebPage.MoveToStartOfBlock] + else: + act = [QWebPage.SelectNextLine, + QWebPage.SelectStartOfBlock] + for _ in range(count): + for a in act: + self.widget.triggerPageAction(a) + + def move_to_start_of_prev_block(self, count=1): + if not self.selection_enabled: + act = [QWebPage.MoveToPreviousLine, + QWebPage.MoveToStartOfBlock] + else: + act = [QWebPage.SelectPreviousLine, + QWebPage.SelectStartOfBlock] + for _ in range(count): + for a in act: + self.widget.triggerPageAction(a) + + def move_to_end_of_next_block(self, count=1): + if not self.selection_enabled: + act = [QWebPage.MoveToNextLine, + QWebPage.MoveToEndOfBlock] + else: + act = [QWebPage.SelectNextLine, + QWebPage.SelectEndOfBlock] + for _ in range(count): + for a in act: + self.widget.triggerPageAction(a) + + def move_to_end_of_prev_block(self, count=1): + if not self.selection_enabled: + act = [QWebPage.MoveToPreviousLine, QWebPage.MoveToEndOfBlock] + else: + act = [QWebPage.SelectPreviousLine, QWebPage.SelectEndOfBlock] + for _ in range(count): + for a in act: + self.widget.triggerPageAction(a) + + def move_to_start_of_document(self): + if not self.selection_enabled: + act = QWebPage.MoveToStartOfDocument + else: + act = QWebPage.SelectStartOfDocument + self.widget.triggerPageAction(act) + + def move_to_end_of_document(self): + if not self.selection_enabled: + act = QWebPage.MoveToEndOfDocument + else: + act = QWebPage.SelectEndOfDocument + self.widget.triggerPageAction(act) + + def toggle_selection(self): + self.selection_enabled = not self.selection_enabled + mainwindow = objreg.get('main-window', scope='window', + window=self._win_id) + mainwindow.status.set_mode_active(usertypes.KeyMode.caret, True) + + def drop_selection(self): + self.widget.triggerPageAction(QWebPage.MoveToNextChar) class WebViewScroller(tab.AbstractScroller): @@ -167,6 +353,7 @@ class WebViewTab(tab.AbstractTab): widget = webview.WebView(win_id, self.tab_id) self.history = WebViewHistory(self) self.scroll = WebViewScroller(parent=self) + self.caret = WebViewCaret(win_id=win_id) self._set_widget(widget) self._connect_signals() diff --git a/qutebrowser/browser/webkit/webview.py b/qutebrowser/browser/webkit/webview.py index 44dc233b9..efde518a8 100644 --- a/qutebrowser/browser/webkit/webview.py +++ b/qutebrowser/browser/webkit/webview.py @@ -100,7 +100,6 @@ class WebView(QWebView): self.keep_icon = False self.search_text = None self.search_flags = 0 - self.selection_enabled = False self.init_neighborlist() self._set_bg_color() cfg = objreg.get('config') @@ -478,25 +477,6 @@ class WebView(QWebView): log.webview.debug("Ignoring focus because mode {} was " "entered.".format(mode)) self.setFocusPolicy(Qt.NoFocus) - elif mode == usertypes.KeyMode.caret: - settings = self.settings() - settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True) - self.selection_enabled = bool(self.page().selectedText()) - - if self.isVisible(): - # Sometimes the caret isn't immediately visible, but unfocusing - # and refocusing it fixes that. - self.clearFocus() - self.setFocus(Qt.OtherFocusReason) - - # Move the caret to the first element in the viewport if there - # isn't any text which is already selected. - # - # Note: We can't use hasSelection() here, as that's always - # true in caret mode. - if not self.page().selectedText(): - self.page().currentFrame().evaluateJavaScript( - utils.read_file('javascript/position_caret.js')) @pyqtSlot(usertypes.KeyMode) def on_mode_left(self, mode): @@ -505,15 +485,6 @@ class WebView(QWebView): usertypes.KeyMode.yesno): log.webview.debug("Restoring focus policy because mode {} was " "left.".format(mode)) - elif mode == usertypes.KeyMode.caret: - settings = self.settings() - if settings.testAttribute(QWebSettings.CaretBrowsingEnabled): - if self.selection_enabled and self.hasSelection(): - # Remove selection if it exists - self.triggerPageAction(QWebPage.MoveToNextChar) - settings.setAttribute(QWebSettings.CaretBrowsingEnabled, False) - self.selection_enabled = False - self.setFocusPolicy(Qt.WheelFocus) def search(self, text, flags): diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index 464c7117d..9889462e6 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -329,12 +329,12 @@ class StatusBar(QWidget): log.statusbar.debug("Setting command_active to {}".format(val)) self._command_active = val elif mode == usertypes.KeyMode.caret: - webview = objreg.get('tabbed-browser', scope='window', + tab = objreg.get('tabbed-browser', scope='window', window=self._win_id).currentWidget() log.statusbar.debug("Setting caret_mode - val {}, selection " - "{}".format(val, webview.selection_enabled)) + "{}".format(val, tab.caret.selection_enabled)) if val: - if webview.selection_enabled: + if tab.caret.selection_enabled: self._set_mode_text("{} selection".format(mode.name)) self._caret_mode = CaretMode.selection else: