diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 000000000..bef2e9c1a --- /dev/null +++ b/.eslintrc @@ -0,0 +1,47 @@ +# vim: ft=yaml + +env: + browser: true + +rules: + block-scoped-var: 2 + dot-location: 2 + default-case: 2 + guard-for-in: 2 + no-div-regex: 2 + no-param-reassign: 2 + no-eq-null: 2 + no-floating-decimal: 2 + no-self-compare: 2 + no-throw-literal: 2 + no-void: 2 + radix: 2 + wrap-iife: [2, "inside"] + brace-style: [2, "1tbs", {"allowSingleLine": true}] + comma-style: [2, "last"] + consistent-this: [2, "self"] + func-style: [2, "declaration"] + indent: [2, 4, {"indentSwitchCase": true}] + linebreak-style: [2, "unix"] + max-nested-callbacks: [2, 3] + no-lonely-if: 2 + no-multiple-empty-lines: [2, {"max": 2}] + no-nested-ternary: 2 + no-unneeded-ternary: 2 + operator-assignment: [2, "always"] + operator-linebreak: [2, "after"] + space-after-keywords: [2, "always"] + space-before-blocks: [2, "always"] + space-before-function-paren: [2, {"anonymous": "never", "named": "never"}] + space-in-brackets: [2, "never"] + space-in-parens: [2, "never"] + space-unary-ops: [2, {"words": true, "nonwords": false}] + spaced-line-comment: [2, "always"] + max-depth: [2, 5] + max-len: [2, 79, 4] + max-params: [2, 5] + max-statements: [2, 30] + no-bitwise: 2 + no-reserved-keys: 2 + global-strict: 0 + quotes: 0 diff --git a/MANIFEST.in b/MANIFEST.in index eabdf977b..7ecd44de2 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,6 +2,7 @@ global-exclude __pycache__ *.pyc *.pyo recursive-include qutebrowser/html *.html recursive-include qutebrowser/test *.py +recursive-include qutebrowser/javascript *.js graft icons graft scripts/pylint_checkers graft doc/img @@ -29,4 +30,5 @@ exclude qutebrowser.rcc exclude .coveragerc exclude .flake8 exclude .pylintrc +exclude .eslintrc exclude doc/help diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index b94a76853..68c2540b7 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -38,8 +38,10 @@ import pygments.formatters from qutebrowser.commands import userscripts, cmdexc, cmdutils from qutebrowser.config import config, configexc from qutebrowser.browser import webelem, inspector +from qutebrowser.keyinput import modeman from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils, objreg, utils) +from qutebrowser.utils.usertypes import KeyMode from qutebrowser.misc import editor @@ -1092,8 +1094,7 @@ class CommandDispatcher: self._open(url, tab, bg, window) @cmdutils.register(instance='command-dispatcher', - modes=[usertypes.KeyMode.insert], - hide=True, scope='window') + modes=[KeyMode.insert], hide=True, scope='window') def open_editor(self): """Open an external editor with the currently selected form field. @@ -1213,6 +1214,285 @@ class CommandDispatcher: for _ in range(count): view.search(view.search_text, flags) + @cmdutils.register(instance='command-dispatcher', hide=True, + modes=[KeyMode.caret], scope='window', count='count') + def move_to_next_line(self, count=1): + """Move the cursor or selection to the next line. + + 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) + + @cmdutils.register(instance='command-dispatcher', hide=True, + modes=[KeyMode.caret], scope='window', count='count') + def move_to_prev_line(self, count=1): + """Move the cursor or selection to the prev line. + + 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) + + @cmdutils.register(instance='command-dispatcher', hide=True, + modes=[KeyMode.caret], scope='window', count='count') + def move_to_next_char(self, count=1): + """Move the cursor or selection to the next char. + + 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) + + @cmdutils.register(instance='command-dispatcher', hide=True, + modes=[KeyMode.caret], scope='window', count='count') + def move_to_prev_char(self, count=1): + """Move the cursor or selection to the previous char. + + 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) + + @cmdutils.register(instance='command-dispatcher', hide=True, + modes=[KeyMode.caret], scope='window', count='count') + def move_to_end_of_word(self, count=1): + """Move the cursor or selection to the end of the word. + + Args: + count: How many words to move. + """ + webview = self._current_widget() + if not webview.selection_enabled: + act = QWebPage.MoveToNextWord + else: + act = QWebPage.SelectNextWord + for _ in range(count): + webview.triggerPageAction(act) + + @cmdutils.register(instance='command-dispatcher', hide=True, + modes=[KeyMode.caret], scope='window', count='count') + def move_to_next_word(self, count=1): + """Move the cursor or selection to the next word. + + Args: + count: How many words to move. + """ + webview = self._current_widget() + if not webview.selection_enabled: + act = [QWebPage.MoveToNextWord, QWebPage.MoveToNextChar] + else: + act = [QWebPage.SelectNextWord, QWebPage.SelectNextChar] + for _ in range(count): + for a in act: + webview.triggerPageAction(a) + + @cmdutils.register(instance='command-dispatcher', hide=True, + modes=[KeyMode.caret], scope='window', count='count') + def move_to_prev_word(self, count=1): + """Move the cursor or selection to the previous word. + + 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) + + @cmdutils.register(instance='command-dispatcher', hide=True, + modes=[KeyMode.caret], scope='window', count='count') + def move_to_start_of_line(self, count=1): + """Move the cursor or select to the start of line.""" + webview = self._current_widget() + if not webview.selection_enabled: + act = QWebPage.MoveToStartOfLine + else: + act = QWebPage.SelectStartOfLine + for _ in range(count): + webview.triggerPageAction(act) + + @cmdutils.register(instance='command-dispatcher', hide=True, + modes=[KeyMode.caret], scope='window', count='count') + def move_to_end_of_line(self, count=1): + """Move the cursor or select to the end of line.""" + webview = self._current_widget() + if not webview.selection_enabled: + act = QWebPage.MoveToEndOfLine + else: + act = QWebPage.SelectEndOfLine + for _ in range(count): + webview.triggerPageAction(act) + + @cmdutils.register(instance='command-dispatcher', hide=True, + modes=[KeyMode.caret], scope='window', count='count') + def move_to_start_of_next_block(self, count=1): + """Move the cursor or selection to the start of next block. + + Args: + count: How many blocks to move. + """ + webview = self._current_widget() + if not webview.selection_enabled: + act = [QWebPage.MoveToEndOfBlock, QWebPage.MoveToNextLine, + QWebPage.MoveToStartOfBlock] + else: + act = [QWebPage.SelectEndOfBlock, QWebPage.SelectNextLine, + QWebPage.SelectStartOfBlock] + for _ in range(count): + for a in act: + webview.triggerPageAction(a) + + @cmdutils.register(instance='command-dispatcher', hide=True, + modes=[KeyMode.caret], scope='window', count='count') + def move_to_start_of_prev_block(self, count=1): + """Move the cursor or selection to the start of previous block. + + Args: + count: How many blocks to move. + """ + webview = self._current_widget() + if not webview.selection_enabled: + act = [QWebPage.MoveToStartOfBlock, QWebPage.MoveToPreviousLine, + QWebPage.MoveToStartOfBlock] + else: + act = [QWebPage.SelectStartOfBlock, QWebPage.SelectPreviousLine, + QWebPage.SelectStartOfBlock] + for _ in range(count): + for a in act: + webview.triggerPageAction(a) + + @cmdutils.register(instance='command-dispatcher', hide=True, + modes=[KeyMode.caret], scope='window', count='count') + def move_to_end_of_next_block(self, count=1): + """Move the cursor or selection to the end of next block. + + Args: + count: How many blocks to move. + """ + webview = self._current_widget() + if not webview.selection_enabled: + act = [QWebPage.MoveToEndOfBlock, QWebPage.MoveToNextLine, + QWebPage.MoveToEndOfBlock] + else: + act = [QWebPage.SelectEndOfBlock, QWebPage.SelectNextLine, + QWebPage.SelectEndOfBlock] + for _ in range(count): + for a in act: + webview.triggerPageAction(a) + + @cmdutils.register(instance='command-dispatcher', hide=True, + modes=[KeyMode.caret], scope='window', count='count') + def move_to_end_of_prev_block(self, count=1): + """Move the cursor or selection to the end of previous block. + + Args: + count: How many blocks to move. + """ + webview = self._current_widget() + if not webview.selection_enabled: + act = [QWebPage.MoveToStartOfBlock, QWebPage.MoveToPreviousLine, + QWebPage.MoveToEndOfBlock] + else: + act = [QWebPage.SelectStartOfBlock, QWebPage.SelectPreviousLine, + QWebPage.SelectEndOfBlock] + for _ in range(count): + for a in act: + webview.triggerPageAction(a) + + @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) + + @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) + + @cmdutils.register(instance='command-dispatcher', hide=True, + modes=[KeyMode.caret], scope='window') + def yank_selected(self, sel=False, keep=False): + """Yank the selected text to the clipboard or primary selection. + + Args: + sel: Use the primary selection instead of the clipboard. + keep: If given, stay in visual mode after yanking. + """ + s = self._current_widget().selectedText() + if not self._current_widget().hasSelection() or len(s) == 0: + message.info(self._win_id, "Nothing to yank") + return + + clipboard = QApplication.clipboard() + if sel and clipboard.supportsSelection(): + mode = QClipboard.Selection + target = "primary selection" + else: + mode = QClipboard.Clipboard + target = "clipboard" + log.misc.debug("Yanking to {}: '{}'".format(target, s)) + clipboard.setText(s, mode) + message.info(self._win_id, "{} {} yanked to {}".format( + len(s), "char" if len(s) == 1 else "chars", target)) + if not keep: + modeman.leave(self._win_id, KeyMode.caret, "yank selected") + + @cmdutils.register(instance='command-dispatcher', hide=True, + 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) + + @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) + @cmdutils.register(instance='command-dispatcher', scope='window', count='count', debug=True) def debug_webaction(self, action, count=1): diff --git a/qutebrowser/browser/webview.py b/qutebrowser/browser/webview.py index 50635acb0..a215a1245 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) @@ -443,6 +444,18 @@ 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 = False + + if self.isVisible(): + # Sometimes the caret isn't immediately visible, but unfocusing + # and refocusing it fixes that. + self.clearFocus() + self.setFocus(Qt.OtherFocusReason) + self.page().currentFrame().evaluateJavaScript( + utils.read_file('javascript/position_caret.js')) @pyqtSlot(usertypes.KeyMode) def on_mode_left(self, mode): @@ -451,6 +464,15 @@ 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/config/configdata.py b/qutebrowser/config/configdata.py index 9011170c0..3bb1f1079 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -832,6 +832,15 @@ def data(readonly=False): SettingValue(typ.QssColor(), 'darkgreen'), "Background color of the statusbar in insert mode."), + ('statusbar.bg.caret', + SettingValue(typ.QssColor(), 'purple'), + "Background color of the statusbar in caret mode."), + + ('statusbar.bg.caret-selection', + SettingValue(typ.QssColor(), '#a12dff'), + "Background color of the statusbar in caret mode with a " + "selection"), + ('statusbar.progress.bg', SettingValue(typ.QssColor(), 'white'), "Background color of the progress bar."), @@ -1104,6 +1113,8 @@ KEY_SECTION_DESC = { " * `prompt-accept`: Confirm the entered value.\n" " * `prompt-yes`: Answer yes to a yes/no question.\n" " * `prompt-no`: Answer no to a yes/no question."), + 'caret': ( + ""), } @@ -1169,6 +1180,7 @@ KEY_DATA = collections.OrderedDict([ ('search-next', ['n']), ('search-prev', ['N']), ('enter-mode insert', ['i']), + ('enter-mode caret', ['v']), ('yank', ['yy']), ('yank -s', ['yY']), ('yank -t', ['yt']), @@ -1269,6 +1281,33 @@ KEY_DATA = collections.OrderedDict([ ('rl-delete-char', ['']), ('rl-backward-delete-char', ['']), ])), + + ('caret', collections.OrderedDict([ + ('toggle-selection', ['v', '']), + ('drop-selection', ['']), + ('enter-mode normal', ['c']), + ('move-to-next-line', ['j']), + ('move-to-prev-line', ['k']), + ('move-to-next-char', ['l']), + ('move-to-prev-char', ['h']), + ('move-to-end-of-word', ['e']), + ('move-to-next-word', ['w']), + ('move-to-prev-word', ['b']), + ('move-to-start-of-next-block', [']']), + ('move-to-start-of-prev-block', ['[']), + ('move-to-end-of-next-block', ['}']), + ('move-to-end-of-prev-block', ['{']), + ('move-to-start-of-line', ['0']), + ('move-to-end-of-line', ['$']), + ('move-to-start-of-document', ['gg']), + ('move-to-end-of-document', ['G']), + ('yank-selected -p', ['Y']), + ('yank-selected', ['y', '', '']), + ('scroll -50 0', ['H']), + ('scroll 0 50', ['J']), + ('scroll 0 -50', ['K']), + ('scroll 50 0', ['L']), + ])), ]) diff --git a/qutebrowser/javascript/position_caret.js b/qutebrowser/javascript/position_caret.js new file mode 100644 index 000000000..5ffd882de --- /dev/null +++ b/qutebrowser/javascript/position_caret.js @@ -0,0 +1,110 @@ +/** +* Copyright 2015 Artur Shaik +* Copyright 2015 Florian Bruhin (The Compiler) +* +* This file is part of qutebrowser. +* +* qutebrowser is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* qutebrowser is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with qutebrowser. If not, see . +*/ + +/* eslint-disable max-len */ + +/** + * Snippet to position caret at top of the page when caret mode is enabled. + * Some code was borrowed from: + * + * https://github.com/1995eaton/chromium-vim/blob/master/content_scripts/dom.js + * https://github.com/1995eaton/chromium-vim/blob/master/content_scripts/visual.js + */ + +/* eslint-enable max-len */ + +"use strict"; + +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 === undefined) { + 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; + var l = children.length; + for (i = 0; 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; +} + +(function() { + var walker = document.createTreeWalker(document.body, 4, null); + var node; + var textNodes = []; + var el; + 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); + } +})(); diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index c5e70f414..b52a39824 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -137,6 +137,9 @@ class BaseKeyParser(QObject): (countstr, cmd_input) = re.match(r'^(\d*)(.*)', self._keystring).groups() count = int(countstr) if countstr else None + if count == 0 and not cmd_input: + cmd_input = self._keystring + count = None else: cmd_input = self._keystring count = None diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index 7d77f872c..fc70ac76b 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -78,6 +78,7 @@ def init(win_id, parent): KM.prompt: keyparser.PassthroughKeyParser(win_id, 'prompt', modeman, warn=False), KM.yesno: modeparsers.PromptKeyParser(win_id, modeman), + KM.caret: modeparsers.CaretKeyParser(win_id, modeman), } objreg.register('keyparsers', keyparsers, scope='window', window=win_id) modeman.destroyed.connect( @@ -92,6 +93,7 @@ def init(win_id, parent): passthrough=True) 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) return modeman diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 7ca4eabc7..8d47de0c1 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -218,3 +218,13 @@ class HintKeyParser(keyparser.CommandKeyParser): hintmanager = objreg.get('hintmanager', scope='tab', window=self._win_id, tab='current') hintmanager.handle_partial_key(keystr) + + +class CaretKeyParser(keyparser.CommandKeyParser): + + """KeyParser for caret mode.""" + + def __init__(self, win_id, parent=None): + super().__init__(win_id, parent, supports_count=True, + supports_chains=True) + self.read_config('caret') diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index a1c8aabd0..fe1a274cc 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -36,6 +36,7 @@ from qutebrowser.mainwindow.statusbar import text as textwidget PreviousWidget = usertypes.enum('PreviousWidget', ['none', 'prompt', 'command']) Severity = usertypes.enum('Severity', ['normal', 'warning', 'error']) +CaretMode = usertypes.enum('CaretMode', ['off', 'on', 'selection']) class StatusBar(QWidget): @@ -77,6 +78,11 @@ class StatusBar(QWidget): For some reason we need to have this as class attribute so pyqtProperty works correctly. + _caret_mode: The current caret mode (off/on/selection). + + For some reason we need to have this as class attribute + so pyqtProperty works correctly. + Signals: resized: Emitted when the statusbar has resized, so the completion widget can adjust its size to it. @@ -91,6 +97,7 @@ class StatusBar(QWidget): _severity = None _prompt_active = False _insert_active = False + _caret_mode = CaretMode.off STYLESHEET = """ QWidget#StatusBar { @@ -101,6 +108,14 @@ class StatusBar(QWidget): {{ color['statusbar.bg.insert'] }} } + QWidget#StatusBar[caret_mode="on"] { + {{ color['statusbar.bg.caret'] }} + } + + QWidget#StatusBar[caret_mode="selection"] { + {{ color['statusbar.bg.caret-selection'] }} + } + QWidget#StatusBar[prompt_active="true"] { {{ color['statusbar.bg.prompt'] }} } @@ -253,14 +268,34 @@ class StatusBar(QWidget): """Getter for self.insert_active, so it can be used as Qt property.""" return self._insert_active - def _set_insert_active(self, val): - """Setter for self.insert_active. + @pyqtProperty(str) + def caret_mode(self): + """Getter for self._caret_mode, so it can be used as Qt property.""" + return self._caret_mode.name + + def set_mode_active(self, mode, val): + """Setter for self.{insert,caret}_active. Re-set the stylesheet after setting the value, so everything gets updated by Qt properly. """ - log.statusbar.debug("Setting insert_active to {}".format(val)) - self._insert_active = val + if mode == usertypes.KeyMode.insert: + log.statusbar.debug("Setting insert_active to {}".format(val)) + self._insert_active = val + elif mode == usertypes.KeyMode.caret: + webview = objreg.get("tabbed-browser", scope="window", + window=self._win_id).currentWidget() + log.statusbar.debug("Setting caret_mode - val {}, selection " + "{}".format(val, webview.selection_enabled)) + if val: + if webview.selection_enabled: + self._set_mode_text("{} selection".format(mode.name)) + self._caret_mode = CaretMode.selection + else: + self._set_mode_text(mode.name) + self._caret_mode = CaretMode.on + else: + self._caret_mode = CaretMode.off self.setStyleSheet(style.get_stylesheet(self.STYLESHEET)) def _set_mode_text(self, mode): @@ -438,8 +473,8 @@ class StatusBar(QWidget): window=self._win_id) if mode in mode_manager.passthrough: self._set_mode_text(mode.name) - if mode == usertypes.KeyMode.insert: - self._set_insert_active(True) + if mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret): + self.set_mode_active(mode, True) @pyqtSlot(usertypes.KeyMode, usertypes.KeyMode) def on_mode_left(self, old_mode, new_mode): @@ -451,8 +486,8 @@ class StatusBar(QWidget): self._set_mode_text(new_mode.name) else: self.txt.set_text(self.txt.Text.normal, '') - if old_mode == usertypes.KeyMode.insert: - self._set_insert_active(False) + if old_mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret): + self.set_mode_active(old_mode, False) @config.change_filter('ui', 'message-timeout') def set_pop_timer_interval(self): diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index 69bda97d4..c465c8ca4 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -512,7 +512,8 @@ class TabbedBrowser(tabwidget.TabWidget): tab = self.widget(idx) log.modes.debug("Current tab changed, focusing {!r}".format(tab)) tab.setFocus() - for mode in (usertypes.KeyMode.hint, usertypes.KeyMode.insert): + for mode in (usertypes.KeyMode.hint, usertypes.KeyMode.insert, + 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 29daf8c06..5d19ad515 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']) + 'insert', 'passthrough', 'caret']) # Available command completions diff --git a/tests/keyinput/test_basekeyparser.py b/tests/keyinput/test_basekeyparser.py index 8b0086b45..bcd43c5a8 100644 --- a/tests/keyinput/test_basekeyparser.py +++ b/tests/keyinput/test_basekeyparser.py @@ -39,7 +39,8 @@ BINDINGS = {'test': {'': 'ctrla', 'a': 'a', 'ba': 'ba', 'ax': 'ax', - 'ccc': 'ccc'}, + 'ccc': 'ccc', + '0': '0'}, 'test2': {'foo': 'bar', '': 'ctrlx'}} @@ -198,6 +199,12 @@ class TestKeyChain: self.kp.execute.assert_called_once_with('ba', self.kp.Type.chain, None) assert self.kp._keystring == '' + def test_0(self, fake_keyevent_factory): + """Test with 0 keypress.""" + self.kp.handle(fake_keyevent_factory(Qt.Key_0, text='0')) + self.kp.execute.assert_called_once_with('0', self.kp.Type.chain, None) + assert self.kp._keystring == '' + def test_ambiguous_keychain(self, fake_keyevent_factory, config_stub, mocker): """Test ambiguous keychain.""" @@ -251,7 +258,9 @@ class TestCount: self.kp.handle(fake_keyevent_factory(Qt.Key_0, text='0')) self.kp.handle(fake_keyevent_factory(Qt.Key_B, text='b')) self.kp.handle(fake_keyevent_factory(Qt.Key_A, text='a')) - self.kp.execute.assert_called_once_with('ba', self.kp.Type.chain, 0) + calls = [mock.call('0', self.kp.Type.chain, None), + mock.call('ba', self.kp.Type.chain, None)] + self.kp.execute.assert_has_calls(calls) assert self.kp._keystring == '' def test_count_42(self, fake_keyevent_factory):