From 489c913e586a37a6e982ddf152d6ed7fb6984b8c Mon Sep 17 00:00:00 2001 From: Artur Shaik Date: Tue, 5 May 2015 10:18:24 +0600 Subject: [PATCH] Implement caret selection and positioning Added option to webview for selection enabled caret mode. In status bar checking value of this option to identificate about it. Added bindings: for toggle selection mode, drop selection and keep selection mode enabled. In webview added javascript snippet to position caret at top of the viewport after caret enabling. This code mostly was taken from cVim sources. --- qutebrowser/browser/commands.py | 118 +++++++++++++----------- qutebrowser/browser/webview.py | 91 +++++++++++++++++- qutebrowser/config/configdata.py | 19 ++-- qutebrowser/keyinput/modeman.py | 2 - qutebrowser/mainwindow/statusbar/bar.py | 35 ++++--- qutebrowser/mainwindow/tabbedbrowser.py | 2 +- qutebrowser/utils/usertypes.py | 2 +- 7 files changed, 177 insertions(+), 92 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 0adc9c57a..56d516c7e 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1153,152 +1153,152 @@ class CommandDispatcher: view.search(view.search_text, flags) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_next_line(self): """Move the cursor or select to the next line.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToNextLine else: act = QWebPage.SelectNextLine - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_prev_line(self): """Move the cursor or select to the prev line.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToPreviousLine else: act = QWebPage.SelectPreviousLine - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_next_char(self): """Move the cursor or select to the next char.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToNextChar else: act = QWebPage.SelectNextChar - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_prev_char(self): """Move the cursor or select to the prev char.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToPreviousChar else: act = QWebPage.SelectPreviousChar - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_end_of_word(self): """Move the cursor or select to the next word.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToNextWord else: act = QWebPage.SelectNextWord - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], + modes=[KeyMode.caret], scope='window') def move_to_next_word(self): """Move the cursor or select to the next word.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = [QWebPage.MoveToNextWord, QWebPage.MoveToNextChar] else: act = [QWebPage.SelectNextWord, QWebPage.SelectNextChar] for a in act: - self._current_widget().triggerPageAction(a) + webview.triggerPageAction(a) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_prev_word(self): """Move the cursor or select to the prev word.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToPreviousWord else: act = QWebPage.SelectPreviousWord - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_start_of_line(self): """Move the cursor or select to the start of line.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToStartOfLine else: act = QWebPage.SelectStartOfLine - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_end_of_line(self): """Move the cursor or select to the end of line.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToEndOfLine else: act = QWebPage.SelectEndOfLine - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_start_of_block(self): """Move the cursor or select to the start of block.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToStartOfBlock else: act = QWebPage.SelectStartOfBlock - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_end_of_block(self): """Move the cursor or select to the end of block.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToEndOfBlock else: act = QWebPage.SelectEndOfBlock - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_start_of_document(self): """Move the cursor or select to the start of document.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToStartOfDocument else: act = QWebPage.SelectStartOfDocument - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.caret, KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def move_to_end_of_document(self): """Move the cursor or select to the end of document.""" - modemanager = modeman.get_modeman(self._win_id) - if modemanager.mode == KeyMode.caret: + webview = self._current_widget() + if not webview.selection_enabled: act = QWebPage.MoveToEndOfDocument else: act = QWebPage.SelectEndOfDocument - self._current_widget().triggerPageAction(act) + webview.triggerPageAction(act) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') def yank_selected(self, sel=False): """Yank selected text to the clipboard or primary selection. @@ -1323,9 +1323,17 @@ class CommandDispatcher: len(s), "char" if len(s) == 1 else "chars", target)) @cmdutils.register(instance='command-dispatcher', hide=True, - modes=[KeyMode.visual], scope='window') + modes=[KeyMode.caret], scope='window') + def toggle_selection(self): + """Toggle caret selection mode.""" + self._current_widget().selection_enabled = not self._current_widget().selection_enabled + mainwindow = objreg.get('main-window', scope='window', window=self._win_id) + mainwindow.status.on_mode_entered(usertypes.KeyMode.caret) + + @cmdutils.register(instance='command-dispatcher', hide=True, + modes=[KeyMode.caret], scope='window') def drop_selection(self): - """Drop selection and stay in visual mode.""" + """Drop selection and keep selection mode enabled.""" self._current_widget().triggerPageAction(QWebPage.MoveToNextChar) @cmdutils.register(instance='command-dispatcher', scope='window', diff --git a/qutebrowser/browser/webview.py b/qutebrowser/browser/webview.py index d123d3c21..2afd9dac3 100644 --- a/qutebrowser/browser/webview.py +++ b/qutebrowser/browser/webview.py @@ -106,6 +106,7 @@ class WebView(QWebView): self.keep_icon = False self.search_text = None self.search_flags = 0 + self.selection_enabled = False; self.init_neighborlist() cfg = objreg.get('config') cfg.changed.connect(self.init_neighborlist) @@ -180,6 +181,79 @@ class WebView(QWebView): self.load_status = val self.load_status_changed.emit(val.name) + def _position_caret(self): + """ + JS snippet to position caret at top of the screen. + Was borrowed from cVim source code + """ + self.page().currentFrame().evaluateJavaScript(""" + + function isElementInViewport(node) { + var i; + var boundingRect = node.getClientRects()[0] || node.getBoundingClientRect(); + if (boundingRect.width <= 1 && boundingRect.height <= 1) { + var rects = node.getClientRects(); + for (i = 0; i < rects.length; i++) { + if (rects[i].width > rects[0].height && rects[i].height > rects[0].height) { + boundingRect = rects[i]; + } + } + } + if (boundingRect === void 0) return null; + if (boundingRect.top > innerHeight || boundingRect.left > innerWidth) { + return null; + } + if (boundingRect.width <= 1 || boundingRect.height <= 1) { + var children = node.children; + var visibleChildNode = false; + for (i = 0, l = children.length; i < l; ++i) { + boundingRect = children[i].getClientRects()[0] || children[i].getBoundingClientRect(); + if (boundingRect.width > 1 && boundingRect.height > 1) { + visibleChildNode = true; + break; + } + } + if (visibleChildNode === false) return null; + } + if (boundingRect.top + boundingRect.height < 10 || boundingRect.left + boundingRect.width < -10) { + return null; + } + var computedStyle = window.getComputedStyle(node, null); + if (computedStyle.visibility !== 'visible' || + computedStyle.display === 'none' || + node.hasAttribute('disabled') || + parseInt(computedStyle.width, '10') === 0 || + parseInt(computedStyle.height, '10') === 0) { + return null; + } + return boundingRect.top >= -20; + } + + var walker = document.createTreeWalker(document.body, 4, null, false); + var node; + var textNodes = []; + while (node = walker.nextNode()) { + if (node.nodeType === 3 && node.data.trim() !== '') { + textNodes.push(node); + } + } + for (var i = 0; i < textNodes.length; i++) { + var element = textNodes[i].parentElement; + if (isElementInViewport(element.parentElement)) { + el = element; + break; + } + } + if (el !== undefined) { + var range = document.createRange(); + range.setStart(el, 0); + range.setEnd(el, 0); + var sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + } + """) + @pyqtSlot(str, str) def on_config_changed(self, section, option): """Reinitialize the zoom neighborlist if related config changed.""" @@ -435,11 +509,17 @@ class WebView(QWebView): log.webview.debug("Ignoring focus because mode {} was " "entered.".format(mode)) self.setFocusPolicy(Qt.NoFocus) - elif mode in (usertypes.KeyMode.caret, usertypes.KeyMode.visual): + elif mode == usertypes.KeyMode.caret: settings = self.settings() settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True) - self.clearFocus() - self.setFocus(Qt.OtherFocusReason) + self.selection_enabled = False + + tabbed = objreg.get('tabbed-browser', scope='window', window=self.win_id) + if tabbed.currentWidget().tab_id == self.tab_id: + self.clearFocus() + self.setFocus(Qt.OtherFocusReason) + + self._position_caret() @pyqtSlot(usertypes.KeyMode) def on_mode_left(self, mode): @@ -448,13 +528,14 @@ class WebView(QWebView): usertypes.KeyMode.yesno): log.webview.debug("Restoring focus policy because mode {} was " "left.".format(mode)) - elif mode in (usertypes.KeyMode.caret, usertypes.KeyMode.visual): + elif mode == usertypes.KeyMode.caret: settings = self.settings() if settings.testAttribute(QWebSettings.CaretBrowsingEnabled): - if mode == usertypes.KeyMode.visual and self.hasSelection(): + if self.selection_enabled and self.hasSelection(): # Remove selection if exist self.triggerPageAction(QWebPage.MoveToNextChar) settings.setAttribute(QWebSettings.CaretBrowsingEnabled, False) + self.selection_enabled = False; self.setFocusPolicy(Qt.WheelFocus) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 85e532ad4..219ab0f95 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -820,9 +820,9 @@ def data(readonly=False): SettingValue(typ.QssColor(), 'purple'), "Background color of the statusbar in caret mode."), - ('statusbar.bg.visual', + ('statusbar.bg.caret_selection', SettingValue(typ.QssColor(), '#a12dff'), - "Background color of the statusbar in visual mode."), + "Background color of the statusbar in caret selection enabled mode."), ('statusbar.progress.bg', SettingValue(typ.QssColor(), 'white'), @@ -1259,19 +1259,10 @@ KEY_DATA = collections.OrderedDict([ ('rl-backward-delete-char', ['']), ])), - ('visual', collections.OrderedDict([ - ('yank-selected', ['y']), - ('yank-selected -s', ['Y']), - ('drop-selection', ['v']), - ('enter-mode caret', ['c']), - ])), - ('caret', collections.OrderedDict([ - ('enter-mode visual', ['v']), + ('toggle-selection', ['']), + ('drop-selection', ['']), ('enter-mode normal', ['c']), - ])), - - ('caret,visual', collections.OrderedDict([ ('move-to-next-line', ['j']), ('move-to-prev-line', ['k']), ('move-to-next-char', ['l']), @@ -1283,6 +1274,8 @@ KEY_DATA = collections.OrderedDict([ ('move-to-end-of-line', ['$']), ('move-to-start-of-document', ['gg']), ('move-to-end-of-document', ['G']), + ('yank-selected', ['y']), + ('yank-selected -p', ['Y']), ('scroll -50 0', ['H']), ('scroll 0 50', ['J']), ('scroll 0 -50', ['K']), diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index 665d3c197..3d4760849 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -79,7 +79,6 @@ def init(win_id, parent): warn=False), KM.yesno: modeparsers.PromptKeyParser(win_id, modeman), KM.caret: modeparsers.CaretKeyParser(win_id, modeman), - KM.visual: modeparsers.VisualKeyParser(win_id, modeman), } objreg.register('keyparsers', keyparsers, scope='window', window=win_id) modeman.destroyed.connect( @@ -95,7 +94,6 @@ def init(win_id, parent): modeman.register(KM.prompt, keyparsers[KM.prompt].handle, passthrough=True) modeman.register(KM.yesno, keyparsers[KM.yesno].handle) modeman.register(KM.caret, keyparsers[KM.caret].handle, passthrough=True) - modeman.register(KM.visual, keyparsers[KM.visual].handle, passthrough=True) return modeman diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index 0b041c5dc..54c77d3dc 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -92,7 +92,7 @@ class StatusBar(QWidget): _prompt_active = False _insert_active = False _caret_active = False - _visual_active = False + _caret_selection_active = False STYLESHEET = """ QWidget#StatusBar { @@ -107,8 +107,8 @@ class StatusBar(QWidget): {{ color['statusbar.bg.caret'] }} } - QWidget#StatusBar[visual_active="true"] { - {{ color['statusbar.bg.visual'] }} + QWidget#StatusBar[caret_selection_active="true"] { + {{ color['statusbar.bg.caret_selection'] }} } QWidget#StatusBar[prompt_active="true"] { @@ -269,12 +269,12 @@ class StatusBar(QWidget): return self._caret_active @pyqtProperty(bool) - def visual_active(self): - """Getter for self.visual_active, so it can be used as Qt property.""" - return self._visual_active + def caret_selection_active(self): + """Getter for self.caret_active, so it can be used as Qt property.""" + return self._caret_selection_active def _set_mode_active(self, mode, val): - """Setter for self.{insert,caret,visual}_active. + """Setter for self.{insert,caret}_active. Re-set the stylesheet after setting the value, so everything gets updated by Qt properly. @@ -284,10 +284,17 @@ class StatusBar(QWidget): self._insert_active = val elif mode == usertypes.KeyMode.caret: log.statusbar.debug("Setting caret_active to {}".format(val)) - self._caret_active = val - elif mode == usertypes.KeyMode.visual: - log.statusbar.debug("Setting visual_active to {}".format(val)) - self._visual_active = val + webview = objreg.get("tabbed-browser", scope="window", window=self._win_id).currentWidget() + if val and webview.selection_enabled: + self._set_mode_text("{} selection".format(mode.name)) + self._caret_selection_active = val + self._caret_active = False + else: + if val: + self._set_mode_text(mode.name) + self._caret_active = val + self._caret_selection_active = False + self.setStyleSheet(style.get_stylesheet(self.STYLESHEET)) def _set_mode_text(self, mode): @@ -465,8 +472,7 @@ class StatusBar(QWidget): window=self._win_id) if mode in mode_manager.passthrough: self._set_mode_text(mode.name) - if mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret, - usertypes.KeyMode.visual): + if mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret): self._set_mode_active(mode, True) @pyqtSlot(usertypes.KeyMode, usertypes.KeyMode) @@ -479,8 +485,7 @@ class StatusBar(QWidget): self._set_mode_text(new_mode.name) else: self.txt.set_text(self.txt.Text.normal, '') - if old_mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret, - usertypes.KeyMode.visual): + if old_mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret): self._set_mode_active(old_mode, False) @config.change_filter('ui', 'message-timeout') diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index d65b3ac20..df236b077 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -519,7 +519,7 @@ class TabbedBrowser(tabwidget.TabWidget): log.modes.debug("Current tab changed, focusing {!r}".format(tab)) tab.setFocus() for mode in (usertypes.KeyMode.hint, usertypes.KeyMode.insert, - usertypes.KeyMode.caret, usertypes.KeyMode.visual): + usertypes.KeyMode.caret): modeman.maybe_leave(self._win_id, mode, 'tab changed') if self._now_focused is not None: objreg.register('last-focused-tab', self._now_focused, update=True, diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index ba5957155..ba568ab56 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -231,7 +231,7 @@ ClickTarget = enum('ClickTarget', ['normal', 'tab', 'tab_bg', 'window']) # Key input modes KeyMode = enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt', - 'insert', 'passthrough', 'caret', 'visual']) + 'insert', 'passthrough', 'caret']) # Available command completions