Basic caret and visual modes implementation

Allow user switch in caret mode for browsing with caret, and visual mode
for select and yank text with keyboard.

Default keybindings is c or v for caret mode, and again v for visual mode. All
basic movements provided by WebAction enum implemened with vim-like
bindings. Yanking with y and Y for selection and clipboard respectively.

There is bug/feature in WebKit that after caret enabled, caret doesn't
show until mouse click (or sometimes Tab helps). So I add some workaround
for that with mouse event. I think should be better aproach.

Signed-off-by: Artur Shaik <ashaihullin@gmail.com>
This commit is contained in:
Artur Shaik 2015-04-09 22:55:42 +06:00
parent 068947ba7e
commit 695712e50c
7 changed files with 293 additions and 3 deletions

View File

@ -40,6 +40,7 @@ from qutebrowser.browser import webelem
from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils,
objreg, utils)
from qutebrowser.misc import editor
from qutebrowser.keyinput import modeman
class CommandDispatcher:
@ -1071,3 +1072,193 @@ class CommandDispatcher:
elem.evaluateJavaScript("this.value='{}'".format(text))
except webelem.IsNullError:
raise cmdexc.CommandError("Element vanished while editing!")
@cmdutils.register(instance='command-dispatcher',
modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual],
hide=True, scope='window')
def move_to_next_line(self):
"""Move the cursor or select to the next line."""
modemanager = modeman._get_modeman(self._win_id)
if modemanager.mode == usertypes.KeyMode.caret:
act = QWebPage.MoveToNextLine
else:
act = QWebPage.SelectNextLine
self._current_widget().triggerPageAction(act)
@cmdutils.register(instance='command-dispatcher',
modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual],
hide=True, scope='window')
def move_to_prev_line(self):
"""Move the cursor or select to the prev line."""
modemanager = modeman._get_modeman(self._win_id)
if modemanager.mode == usertypes.KeyMode.caret:
act = QWebPage.MoveToPreviousLine
else:
act = QWebPage.SelectPreviousLine
self._current_widget().triggerPageAction(act)
@cmdutils.register(instance='command-dispatcher',
modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual],
hide=True, scope='window')
def move_to_next_char(self):
"""Move the cursor or select to the next char."""
modemanager = modeman._get_modeman(self._win_id)
if modemanager.mode == usertypes.KeyMode.caret:
act = QWebPage.MoveToNextChar
else:
act = QWebPage.SelectNextChar
self._current_widget().triggerPageAction(act)
@cmdutils.register(instance='command-dispatcher',
modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual],
hide=True, scope='window')
def move_to_prev_char(self):
"""Move the cursor or select to the prev char."""
modemanager = modeman._get_modeman(self._win_id)
if modemanager.mode == usertypes.KeyMode.caret:
act = QWebPage.MoveToPreviousChar
else:
act = QWebPage.SelectPreviousChar
self._current_widget().triggerPageAction(act)
@cmdutils.register(instance='command-dispatcher',
modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual],
hide=True, scope='window')
def move_to_end_of_word(self):
"""Move the cursor or select to the next word."""
modemanager = modeman._get_modeman(self._win_id)
if modemanager.mode == usertypes.KeyMode.caret:
act = QWebPage.MoveToNextWord
else:
act = QWebPage.SelectNextWord
self._current_widget().triggerPageAction(act)
@cmdutils.register(instance='command-dispatcher',
modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual],
hide=True, scope='window')
def move_to_next_word(self):
"""Move the cursor or select to the next word."""
modemanager = modeman._get_modeman(self._win_id)
if modemanager.mode == usertypes.KeyMode.caret:
act = [QWebPage.MoveToNextWord, QWebPage.MoveToNextChar]
else:
act = [QWebPage.SelectNextWord, QWebPage.SelectNextChar]
for a in act:
self._current_widget().triggerPageAction(a)
@cmdutils.register(instance='command-dispatcher',
modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual],
hide=True, scope='window')
def move_to_prev_word(self):
"""Move the cursor or select to the prev word."""
modemanager = modeman._get_modeman(self._win_id)
if modemanager.mode == usertypes.KeyMode.caret:
act = QWebPage.MoveToPreviousWord
else:
act = QWebPage.SelectPreviousWord
self._current_widget().triggerPageAction(act)
@cmdutils.register(instance='command-dispatcher',
modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual],
hide=True, scope='window')
def move_to_start_of_line(self):
"""Move the cursor or select to the start of line."""
modemanager = modeman._get_modeman(self._win_id)
if modemanager.mode == usertypes.KeyMode.caret:
act = QWebPage.MoveToStartOfLine
else:
act = QWebPage.SelectStartOfLine
self._current_widget().triggerPageAction(act)
@cmdutils.register(instance='command-dispatcher',
modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual],
hide=True, scope='window')
def move_to_end_of_line(self):
"""Move the cursor or select to the end of line."""
modemanager = modeman._get_modeman(self._win_id)
if modemanager.mode == usertypes.KeyMode.caret:
act = QWebPage.MoveToEndOfLine
else:
act = QWebPage.SelectEndOfLine
self._current_widget().triggerPageAction(act)
@cmdutils.register(instance='command-dispatcher',
modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual],
hide=True, scope='window')
def move_to_start_of_block(self):
"""Move the cursor or select to the start of block."""
modemanager = modeman._get_modeman(self._win_id)
if modemanager.mode == usertypes.KeyMode.caret:
act = QWebPage.MoveToStartOfBlock
else:
act = QWebPage.SelectStartOfBlock
self._current_widget().triggerPageAction(act)
@cmdutils.register(instance='command-dispatcher',
modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual],
hide=True, scope='window')
def move_to_end_of_block(self):
"""Move the cursor or select to the end of block."""
modemanager = modeman._get_modeman(self._win_id)
if modemanager.mode == usertypes.KeyMode.caret:
act = QWebPage.MoveToEndOfBlock
else:
act = QWebPage.SelectEndOfBlock
self._current_widget().triggerPageAction(act)
@cmdutils.register(instance='command-dispatcher',
modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual],
hide=True, scope='window')
def move_to_start_of_document(self):
"""Move the cursor or select to the start of document."""
modemanager = modeman._get_modeman(self._win_id)
if modemanager.mode == usertypes.KeyMode.caret:
act = QWebPage.MoveToStartOfDocument
else:
act = QWebPage.SelectStartOfDocument
self._current_widget().triggerPageAction(act)
@cmdutils.register(instance='command-dispatcher',
modes=[usertypes.KeyMode.caret, usertypes.KeyMode.visual],
hide=True, scope='window')
def move_to_end_of_document(self):
"""Move the cursor or select to the end of document."""
modemanager = modeman._get_modeman(self._win_id)
if modemanager.mode == usertypes.KeyMode.caret:
act = QWebPage.MoveToEndOfDocument
else:
act = QWebPage.SelectEndOfDocument
self._current_widget().triggerPageAction(act)
@cmdutils.register(instance='command-dispatcher',
modes=[usertypes.KeyMode.visual],
hide=True, scope='window')
def yank_selected(self, sel=False):
"""Yank selected text to the clipboard or primary selection.
Args:
sel: Use the primary selection instead of the clipboard.
"""
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))
@cmdutils.register(instance='command-dispatcher',
modes=[usertypes.KeyMode.visual],
hide=True, scope='window')
def drop_selection(self):
"""Drop selection and stay in visual mode."""
self._current_widget().triggerPageAction(QWebPage.MoveToNextChar)

View File

@ -23,10 +23,11 @@ import sys
import itertools
import functools
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QTimer, QUrl
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QTimer, QUrl, QPoint
from PyQt5.QtWidgets import QApplication, QStyleFactory
from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtWebKitWidgets import QWebView, QWebPage
from PyQt5.QtGui import QMouseEvent
from qutebrowser.config import config
from qutebrowser.keyinput import modeman
@ -69,6 +70,7 @@ class WebView(QWebView):
_check_insertmode: If True, in mouseReleaseEvent we should check if we
need to enter/leave insert mode.
_default_zoom_changed: Whether the zoom was changed from the default.
_caret_exist: Whether caret already has focus element
Signals:
scroll_pos_changed: Scroll percentage of current tab changed.
@ -142,6 +144,7 @@ class WebView(QWebView):
self.viewing_source = False
self.setZoomFactor(float(config.get('ui', 'default-zoom')) / 100)
self._default_zoom_changed = False
self._caret_exist = False
objreg.get('config').changed.connect(self.on_config_changed)
if config.get('input', 'rocker-gestures'):
self.setContextMenuPolicy(Qt.PreventContextMenu)
@ -427,6 +430,31 @@ class WebView(QWebView):
"entered.".format(mode))
self.setFocusPolicy(Qt.NoFocus)
self._caret_exist = False
elif mode in (usertypes.KeyMode.caret, usertypes.KeyMode.visual):
self.settings().setAttribute(QWebSettings.CaretBrowsingEnabled, True)
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self.win_id)
if self.tab_id == tabbed_browser._now_focused.tab_id and not self._caret_exist:
"""
Here is a workaround for auto position enabled caret.
Unfortunatly, caret doesn't appear until you click
mouse button on element. I have such behavior in dwb,
so I decided to implement this workaround.
May be should be reworked.
"""
frame = self.page().currentFrame()
halfWidth = frame.scrollBarGeometry(Qt.Horizontal).width() / 2
point = QPoint(halfWidth,1)
event = QMouseEvent(QMouseEvent.MouseButtonPress, point, point,
point, Qt.LeftButton, Qt.LeftButton, Qt.NoModifier)
QApplication.sendEvent(self, event)
self._caret_exist = True
else:
self._caret_exist = False
@pyqtSlot(usertypes.KeyMode)
def on_mode_left(self, mode):
"""Restore focus policy if status-input modes were left."""
@ -434,8 +462,16 @@ class WebView(QWebView):
usertypes.KeyMode.yesno):
log.webview.debug("Restoring focus policy because mode {} was "
"left.".format(mode))
elif mode in (usertypes.KeyMode.caret, usertypes.KeyMode.visual):
if self.settings().testAttribute(QWebSettings.CaretBrowsingEnabled):
if mode == usertypes.KeyMode.visual and self.hasSelection():
# Remove selection if exist
self.triggerPageAction(QWebPage.MoveToNextChar)
self.settings().setAttribute(QWebSettings.CaretBrowsingEnabled, False)
self.setFocusPolicy(Qt.WheelFocus)
def createWindow(self, wintype):
"""Called by Qt when a page wants to create a new window.

View File

@ -1019,6 +1019,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': (
""),
}
@ -1083,6 +1085,7 @@ KEY_DATA = collections.OrderedDict([
('search-next', ['n']),
('search-prev', ['N']),
('enter-mode insert', ['i']),
('enter-mode caret', ['c', 'v']),
('yank', ['yy']),
('yank -s', ['yY']),
('yank -t', ['yt']),
@ -1178,6 +1181,36 @@ KEY_DATA = collections.OrderedDict([
('rl-delete-char', ['<Ctrl-?>']),
('rl-backward-delete-char', ['<Ctrl-H>']),
])),
('visual', collections.OrderedDict([
('yank-selected', ['y']),
('yank-selected -s', ['Y']),
('drop-selection', ['v']),
('enter-mode caret', ['c']),
])),
('caret', collections.OrderedDict([
('enter-mode visual', ['v']),
('enter-mode normal', ['c']),
])),
('caret,visual', collections.OrderedDict([
('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-line', ['0']),
('move-to-end-of-line', ['$']),
('move-to-start-of-document', ['gg']),
('move-to-end-of-document', ['G']),
('scroll -50 0', ['H']),
('scroll 0 50', ['J']),
('scroll 0 -50', ['K']),
('scroll 50 0', ['L']),
])),
])

View File

@ -25,6 +25,7 @@ from PyQt5.QtGui import QWindow
from PyQt5.QtCore import pyqtSignal, Qt, QObject, QEvent
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebKitWidgets import QWebView
from PyQt5.QtWebKit import QWebSettings
from qutebrowser.keyinput import modeparsers, keyparser
from qutebrowser.config import config
@ -79,6 +80,8 @@ 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),
KM.visual: modeparsers.VisualKeyParser(win_id, modeman),
}
objreg.register('keyparsers', keyparsers, scope='window', window=win_id)
modeman.destroyed.connect(
@ -93,6 +96,8 @@ 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)
modeman.register(KM.visual, keyparsers[KM.visual].handle, passthrough=True)
return modeman

View File

@ -218,3 +218,27 @@ 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')
def __repr__(self):
return utils.get_repr(self)
class VisualKeyParser(keyparser.CommandKeyParser):
"""KeyParser for Visual mode."""
def __init__(self, win_id, parent=None):
super().__init__(win_id, parent, supports_count=True,
supports_chains=True)
self.read_config('visual')
def __repr__(self):
return utils.get_repr(self)

View File

@ -546,7 +546,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, usertypes.KeyMode.visual):
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,

View File

@ -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', 'visual'])
# Available command completions