Merge branch 'visual'

This commit is contained in:
Florian Bruhin 2015-05-18 21:43:25 +02:00
commit 8941b5dc96
13 changed files with 574 additions and 14 deletions

47
.eslintrc Normal file
View File

@ -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

View File

@ -2,6 +2,7 @@ global-exclude __pycache__ *.pyc *.pyo
recursive-include qutebrowser/html *.html recursive-include qutebrowser/html *.html
recursive-include qutebrowser/test *.py recursive-include qutebrowser/test *.py
recursive-include qutebrowser/javascript *.js
graft icons graft icons
graft scripts/pylint_checkers graft scripts/pylint_checkers
graft doc/img graft doc/img
@ -29,4 +30,5 @@ exclude qutebrowser.rcc
exclude .coveragerc exclude .coveragerc
exclude .flake8 exclude .flake8
exclude .pylintrc exclude .pylintrc
exclude .eslintrc
exclude doc/help exclude doc/help

View File

@ -38,8 +38,10 @@ import pygments.formatters
from qutebrowser.commands import userscripts, cmdexc, cmdutils from qutebrowser.commands import userscripts, cmdexc, cmdutils
from qutebrowser.config import config, configexc from qutebrowser.config import config, configexc
from qutebrowser.browser import webelem, inspector from qutebrowser.browser import webelem, inspector
from qutebrowser.keyinput import modeman
from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils, from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils,
objreg, utils) objreg, utils)
from qutebrowser.utils.usertypes import KeyMode
from qutebrowser.misc import editor from qutebrowser.misc import editor
@ -1092,8 +1094,7 @@ class CommandDispatcher:
self._open(url, tab, bg, window) self._open(url, tab, bg, window)
@cmdutils.register(instance='command-dispatcher', @cmdutils.register(instance='command-dispatcher',
modes=[usertypes.KeyMode.insert], modes=[KeyMode.insert], hide=True, scope='window')
hide=True, scope='window')
def open_editor(self): def open_editor(self):
"""Open an external editor with the currently selected form field. """Open an external editor with the currently selected form field.
@ -1213,6 +1214,285 @@ class CommandDispatcher:
for _ in range(count): for _ in range(count):
view.search(view.search_text, flags) 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', @cmdutils.register(instance='command-dispatcher', scope='window',
count='count', debug=True) count='count', debug=True)
def debug_webaction(self, action, count=1): def debug_webaction(self, action, count=1):

View File

@ -106,6 +106,7 @@ class WebView(QWebView):
self.keep_icon = False self.keep_icon = False
self.search_text = None self.search_text = None
self.search_flags = 0 self.search_flags = 0
self.selection_enabled = False
self.init_neighborlist() self.init_neighborlist()
cfg = objreg.get('config') cfg = objreg.get('config')
cfg.changed.connect(self.init_neighborlist) cfg.changed.connect(self.init_neighborlist)
@ -443,6 +444,18 @@ class WebView(QWebView):
log.webview.debug("Ignoring focus because mode {} was " log.webview.debug("Ignoring focus because mode {} was "
"entered.".format(mode)) "entered.".format(mode))
self.setFocusPolicy(Qt.NoFocus) 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) @pyqtSlot(usertypes.KeyMode)
def on_mode_left(self, mode): def on_mode_left(self, mode):
@ -451,6 +464,15 @@ class WebView(QWebView):
usertypes.KeyMode.yesno): usertypes.KeyMode.yesno):
log.webview.debug("Restoring focus policy because mode {} was " log.webview.debug("Restoring focus policy because mode {} was "
"left.".format(mode)) "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) self.setFocusPolicy(Qt.WheelFocus)
def search(self, text, flags): def search(self, text, flags):

View File

@ -832,6 +832,15 @@ def data(readonly=False):
SettingValue(typ.QssColor(), 'darkgreen'), SettingValue(typ.QssColor(), 'darkgreen'),
"Background color of the statusbar in insert mode."), "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', ('statusbar.progress.bg',
SettingValue(typ.QssColor(), 'white'), SettingValue(typ.QssColor(), 'white'),
"Background color of the progress bar."), "Background color of the progress bar."),
@ -1104,6 +1113,8 @@ KEY_SECTION_DESC = {
" * `prompt-accept`: Confirm the entered value.\n" " * `prompt-accept`: Confirm the entered value.\n"
" * `prompt-yes`: Answer yes to a yes/no question.\n" " * `prompt-yes`: Answer yes to a yes/no question.\n"
" * `prompt-no`: Answer no to a yes/no question."), " * `prompt-no`: Answer no to a yes/no question."),
'caret': (
""),
} }
@ -1169,6 +1180,7 @@ KEY_DATA = collections.OrderedDict([
('search-next', ['n']), ('search-next', ['n']),
('search-prev', ['N']), ('search-prev', ['N']),
('enter-mode insert', ['i']), ('enter-mode insert', ['i']),
('enter-mode caret', ['v']),
('yank', ['yy']), ('yank', ['yy']),
('yank -s', ['yY']), ('yank -s', ['yY']),
('yank -t', ['yt']), ('yank -t', ['yt']),
@ -1269,6 +1281,33 @@ KEY_DATA = collections.OrderedDict([
('rl-delete-char', ['<Ctrl-?>']), ('rl-delete-char', ['<Ctrl-?>']),
('rl-backward-delete-char', ['<Ctrl-H>']), ('rl-backward-delete-char', ['<Ctrl-H>']),
])), ])),
('caret', collections.OrderedDict([
('toggle-selection', ['v', '<Space>']),
('drop-selection', ['<Ctrl-Space>']),
('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', '<Return>', '<Ctrl-J>']),
('scroll -50 0', ['H']),
('scroll 0 50', ['J']),
('scroll 0 -50', ['K']),
('scroll 50 0', ['L']),
])),
]) ])

View File

@ -0,0 +1,110 @@
/**
* Copyright 2015 Artur Shaik <ashaihullin@gmail.com>
* Copyright 2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
/* 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);
}
})();

View File

@ -137,6 +137,9 @@ class BaseKeyParser(QObject):
(countstr, cmd_input) = re.match(r'^(\d*)(.*)', (countstr, cmd_input) = re.match(r'^(\d*)(.*)',
self._keystring).groups() self._keystring).groups()
count = int(countstr) if countstr else None count = int(countstr) if countstr else None
if count == 0 and not cmd_input:
cmd_input = self._keystring
count = None
else: else:
cmd_input = self._keystring cmd_input = self._keystring
count = None count = None

View File

@ -78,6 +78,7 @@ def init(win_id, parent):
KM.prompt: keyparser.PassthroughKeyParser(win_id, 'prompt', modeman, KM.prompt: keyparser.PassthroughKeyParser(win_id, 'prompt', modeman,
warn=False), warn=False),
KM.yesno: modeparsers.PromptKeyParser(win_id, modeman), KM.yesno: modeparsers.PromptKeyParser(win_id, modeman),
KM.caret: modeparsers.CaretKeyParser(win_id, modeman),
} }
objreg.register('keyparsers', keyparsers, scope='window', window=win_id) objreg.register('keyparsers', keyparsers, scope='window', window=win_id)
modeman.destroyed.connect( modeman.destroyed.connect(
@ -92,6 +93,7 @@ def init(win_id, parent):
passthrough=True) passthrough=True)
modeman.register(KM.prompt, keyparsers[KM.prompt].handle, passthrough=True) modeman.register(KM.prompt, keyparsers[KM.prompt].handle, passthrough=True)
modeman.register(KM.yesno, keyparsers[KM.yesno].handle) modeman.register(KM.yesno, keyparsers[KM.yesno].handle)
modeman.register(KM.caret, keyparsers[KM.caret].handle, passthrough=True)
return modeman return modeman

View File

@ -218,3 +218,13 @@ class HintKeyParser(keyparser.CommandKeyParser):
hintmanager = objreg.get('hintmanager', scope='tab', hintmanager = objreg.get('hintmanager', scope='tab',
window=self._win_id, tab='current') window=self._win_id, tab='current')
hintmanager.handle_partial_key(keystr) 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')

View File

@ -36,6 +36,7 @@ from qutebrowser.mainwindow.statusbar import text as textwidget
PreviousWidget = usertypes.enum('PreviousWidget', ['none', 'prompt', PreviousWidget = usertypes.enum('PreviousWidget', ['none', 'prompt',
'command']) 'command'])
Severity = usertypes.enum('Severity', ['normal', 'warning', 'error']) Severity = usertypes.enum('Severity', ['normal', 'warning', 'error'])
CaretMode = usertypes.enum('CaretMode', ['off', 'on', 'selection'])
class StatusBar(QWidget): class StatusBar(QWidget):
@ -77,6 +78,11 @@ class StatusBar(QWidget):
For some reason we need to have this as class attribute For some reason we need to have this as class attribute
so pyqtProperty works correctly. 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: Signals:
resized: Emitted when the statusbar has resized, so the completion resized: Emitted when the statusbar has resized, so the completion
widget can adjust its size to it. widget can adjust its size to it.
@ -91,6 +97,7 @@ class StatusBar(QWidget):
_severity = None _severity = None
_prompt_active = False _prompt_active = False
_insert_active = False _insert_active = False
_caret_mode = CaretMode.off
STYLESHEET = """ STYLESHEET = """
QWidget#StatusBar { QWidget#StatusBar {
@ -101,6 +108,14 @@ class StatusBar(QWidget):
{{ color['statusbar.bg.insert'] }} {{ 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"] { QWidget#StatusBar[prompt_active="true"] {
{{ color['statusbar.bg.prompt'] }} {{ color['statusbar.bg.prompt'] }}
} }
@ -253,14 +268,34 @@ class StatusBar(QWidget):
"""Getter for self.insert_active, so it can be used as Qt property.""" """Getter for self.insert_active, so it can be used as Qt property."""
return self._insert_active return self._insert_active
def _set_insert_active(self, val): @pyqtProperty(str)
"""Setter for self.insert_active. 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 Re-set the stylesheet after setting the value, so everything gets
updated by Qt properly. updated by Qt properly.
""" """
log.statusbar.debug("Setting insert_active to {}".format(val)) if mode == usertypes.KeyMode.insert:
self._insert_active = val 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)) self.setStyleSheet(style.get_stylesheet(self.STYLESHEET))
def _set_mode_text(self, mode): def _set_mode_text(self, mode):
@ -438,8 +473,8 @@ class StatusBar(QWidget):
window=self._win_id) window=self._win_id)
if mode in mode_manager.passthrough: if mode in mode_manager.passthrough:
self._set_mode_text(mode.name) self._set_mode_text(mode.name)
if mode == usertypes.KeyMode.insert: if mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret):
self._set_insert_active(True) self.set_mode_active(mode, True)
@pyqtSlot(usertypes.KeyMode, usertypes.KeyMode) @pyqtSlot(usertypes.KeyMode, usertypes.KeyMode)
def on_mode_left(self, old_mode, new_mode): def on_mode_left(self, old_mode, new_mode):
@ -451,8 +486,8 @@ class StatusBar(QWidget):
self._set_mode_text(new_mode.name) self._set_mode_text(new_mode.name)
else: else:
self.txt.set_text(self.txt.Text.normal, '') self.txt.set_text(self.txt.Text.normal, '')
if old_mode == usertypes.KeyMode.insert: if old_mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret):
self._set_insert_active(False) self.set_mode_active(old_mode, False)
@config.change_filter('ui', 'message-timeout') @config.change_filter('ui', 'message-timeout')
def set_pop_timer_interval(self): def set_pop_timer_interval(self):

View File

@ -512,7 +512,8 @@ class TabbedBrowser(tabwidget.TabWidget):
tab = self.widget(idx) tab = self.widget(idx)
log.modes.debug("Current tab changed, focusing {!r}".format(tab)) log.modes.debug("Current tab changed, focusing {!r}".format(tab))
tab.setFocus() 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') modeman.maybe_leave(self._win_id, mode, 'tab changed')
if self._now_focused is not None: if self._now_focused is not None:
objreg.register('last-focused-tab', self._now_focused, update=True, objreg.register('last-focused-tab', self._now_focused, update=True,

View File

@ -231,7 +231,7 @@ ClickTarget = enum('ClickTarget', ['normal', 'tab', 'tab_bg', 'window'])
# Key input modes # Key input modes
KeyMode = enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt', KeyMode = enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt',
'insert', 'passthrough']) 'insert', 'passthrough', 'caret'])
# Available command completions # Available command completions

View File

@ -39,7 +39,8 @@ BINDINGS = {'test': {'<Ctrl-a>': 'ctrla',
'a': 'a', 'a': 'a',
'ba': 'ba', 'ba': 'ba',
'ax': 'ax', 'ax': 'ax',
'ccc': 'ccc'}, 'ccc': 'ccc',
'0': '0'},
'test2': {'foo': 'bar', '<Ctrl+X>': 'ctrlx'}} 'test2': {'foo': 'bar', '<Ctrl+X>': 'ctrlx'}}
@ -198,6 +199,12 @@ class TestKeyChain:
self.kp.execute.assert_called_once_with('ba', self.kp.Type.chain, None) self.kp.execute.assert_called_once_with('ba', self.kp.Type.chain, None)
assert self.kp._keystring == '' 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, def test_ambiguous_keychain(self, fake_keyevent_factory, config_stub,
mocker): mocker):
"""Test ambiguous keychain.""" """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_0, text='0'))
self.kp.handle(fake_keyevent_factory(Qt.Key_B, text='b')) self.kp.handle(fake_keyevent_factory(Qt.Key_B, text='b'))
self.kp.handle(fake_keyevent_factory(Qt.Key_A, text='a')) 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 == '' assert self.kp._keystring == ''
def test_count_42(self, fake_keyevent_factory): def test_count_42(self, fake_keyevent_factory):