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