Merge branch 'visual'
This commit is contained in:
commit
8941b5dc96
47
.eslintrc
Normal file
47
.eslintrc
Normal 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
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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']),
|
||||||
|
])),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
110
qutebrowser/javascript/position_caret.js
Normal file
110
qutebrowser/javascript/position_caret.js
Normal 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);
|
||||||
|
}
|
||||||
|
})();
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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')
|
||||||
|
@ -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):
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
Loading…
Reference in New Issue
Block a user