Implement caret selection and positioning

Added option to webview for selection enabled caret mode.
In status bar checking value of this option to identificate about it.

Added bindings: <Space> for toggle selection mode, <Ctrl+Space> drop
selection and keep selection mode enabled.

In webview added javascript snippet to position caret at top of the
viewport after caret enabling. This code mostly was taken from cVim sources.
This commit is contained in:
Artur Shaik 2015-05-05 10:18:24 +06:00
parent aeaa20c3b7
commit 489c913e58
7 changed files with 177 additions and 92 deletions

View File

@ -1153,152 +1153,152 @@ class CommandDispatcher:
view.search(view.search_text, flags)
@cmdutils.register(instance='command-dispatcher', hide=True,
modes=[KeyMode.caret, KeyMode.visual], scope='window')
modes=[KeyMode.caret], scope='window')
def move_to_next_line(self):
"""Move the cursor or select to the next line."""
modemanager = modeman.get_modeman(self._win_id)
if modemanager.mode == KeyMode.caret:
webview = self._current_widget()
if not webview.selection_enabled:
act = QWebPage.MoveToNextLine
else:
act = QWebPage.SelectNextLine
self._current_widget().triggerPageAction(act)
webview.triggerPageAction(act)
@cmdutils.register(instance='command-dispatcher', hide=True,
modes=[KeyMode.caret, KeyMode.visual], scope='window')
modes=[KeyMode.caret], scope='window')
def move_to_prev_line(self):
"""Move the cursor or select to the prev line."""
modemanager = modeman.get_modeman(self._win_id)
if modemanager.mode == KeyMode.caret:
webview = self._current_widget()
if not webview.selection_enabled:
act = QWebPage.MoveToPreviousLine
else:
act = QWebPage.SelectPreviousLine
self._current_widget().triggerPageAction(act)
webview.triggerPageAction(act)
@cmdutils.register(instance='command-dispatcher', hide=True,
modes=[KeyMode.caret, KeyMode.visual], scope='window')
modes=[KeyMode.caret], scope='window')
def move_to_next_char(self):
"""Move the cursor or select to the next char."""
modemanager = modeman.get_modeman(self._win_id)
if modemanager.mode == KeyMode.caret:
webview = self._current_widget()
if not webview.selection_enabled:
act = QWebPage.MoveToNextChar
else:
act = QWebPage.SelectNextChar
self._current_widget().triggerPageAction(act)
webview.triggerPageAction(act)
@cmdutils.register(instance='command-dispatcher', hide=True,
modes=[KeyMode.caret, KeyMode.visual], scope='window')
modes=[KeyMode.caret], scope='window')
def move_to_prev_char(self):
"""Move the cursor or select to the prev char."""
modemanager = modeman.get_modeman(self._win_id)
if modemanager.mode == KeyMode.caret:
webview = self._current_widget()
if not webview.selection_enabled:
act = QWebPage.MoveToPreviousChar
else:
act = QWebPage.SelectPreviousChar
self._current_widget().triggerPageAction(act)
webview.triggerPageAction(act)
@cmdutils.register(instance='command-dispatcher', hide=True,
modes=[KeyMode.caret, KeyMode.visual], scope='window')
modes=[KeyMode.caret], scope='window')
def move_to_end_of_word(self):
"""Move the cursor or select to the next word."""
modemanager = modeman.get_modeman(self._win_id)
if modemanager.mode == KeyMode.caret:
webview = self._current_widget()
if not webview.selection_enabled:
act = QWebPage.MoveToNextWord
else:
act = QWebPage.SelectNextWord
self._current_widget().triggerPageAction(act)
webview.triggerPageAction(act)
@cmdutils.register(instance='command-dispatcher', hide=True,
modes=[KeyMode.caret, KeyMode.visual],
modes=[KeyMode.caret],
scope='window')
def move_to_next_word(self):
"""Move the cursor or select to the next word."""
modemanager = modeman.get_modeman(self._win_id)
if modemanager.mode == KeyMode.caret:
webview = self._current_widget()
if not webview.selection_enabled:
act = [QWebPage.MoveToNextWord, QWebPage.MoveToNextChar]
else:
act = [QWebPage.SelectNextWord, QWebPage.SelectNextChar]
for a in act:
self._current_widget().triggerPageAction(a)
webview.triggerPageAction(a)
@cmdutils.register(instance='command-dispatcher', hide=True,
modes=[KeyMode.caret, KeyMode.visual], scope='window')
modes=[KeyMode.caret], scope='window')
def move_to_prev_word(self):
"""Move the cursor or select to the prev word."""
modemanager = modeman.get_modeman(self._win_id)
if modemanager.mode == KeyMode.caret:
webview = self._current_widget()
if not webview.selection_enabled:
act = QWebPage.MoveToPreviousWord
else:
act = QWebPage.SelectPreviousWord
self._current_widget().triggerPageAction(act)
webview.triggerPageAction(act)
@cmdutils.register(instance='command-dispatcher', hide=True,
modes=[KeyMode.caret, KeyMode.visual], scope='window')
modes=[KeyMode.caret], scope='window')
def move_to_start_of_line(self):
"""Move the cursor or select to the start of line."""
modemanager = modeman.get_modeman(self._win_id)
if modemanager.mode == KeyMode.caret:
webview = self._current_widget()
if not webview.selection_enabled:
act = QWebPage.MoveToStartOfLine
else:
act = QWebPage.SelectStartOfLine
self._current_widget().triggerPageAction(act)
webview.triggerPageAction(act)
@cmdutils.register(instance='command-dispatcher', hide=True,
modes=[KeyMode.caret, KeyMode.visual], scope='window')
modes=[KeyMode.caret], scope='window')
def move_to_end_of_line(self):
"""Move the cursor or select to the end of line."""
modemanager = modeman.get_modeman(self._win_id)
if modemanager.mode == KeyMode.caret:
webview = self._current_widget()
if not webview.selection_enabled:
act = QWebPage.MoveToEndOfLine
else:
act = QWebPage.SelectEndOfLine
self._current_widget().triggerPageAction(act)
webview.triggerPageAction(act)
@cmdutils.register(instance='command-dispatcher', hide=True,
modes=[KeyMode.caret, KeyMode.visual], scope='window')
modes=[KeyMode.caret], scope='window')
def move_to_start_of_block(self):
"""Move the cursor or select to the start of block."""
modemanager = modeman.get_modeman(self._win_id)
if modemanager.mode == KeyMode.caret:
webview = self._current_widget()
if not webview.selection_enabled:
act = QWebPage.MoveToStartOfBlock
else:
act = QWebPage.SelectStartOfBlock
self._current_widget().triggerPageAction(act)
webview.triggerPageAction(act)
@cmdutils.register(instance='command-dispatcher', hide=True,
modes=[KeyMode.caret, KeyMode.visual], scope='window')
modes=[KeyMode.caret], scope='window')
def move_to_end_of_block(self):
"""Move the cursor or select to the end of block."""
modemanager = modeman.get_modeman(self._win_id)
if modemanager.mode == KeyMode.caret:
webview = self._current_widget()
if not webview.selection_enabled:
act = QWebPage.MoveToEndOfBlock
else:
act = QWebPage.SelectEndOfBlock
self._current_widget().triggerPageAction(act)
webview.triggerPageAction(act)
@cmdutils.register(instance='command-dispatcher', hide=True,
modes=[KeyMode.caret, KeyMode.visual], scope='window')
modes=[KeyMode.caret], scope='window')
def move_to_start_of_document(self):
"""Move the cursor or select to the start of document."""
modemanager = modeman.get_modeman(self._win_id)
if modemanager.mode == KeyMode.caret:
webview = self._current_widget()
if not webview.selection_enabled:
act = QWebPage.MoveToStartOfDocument
else:
act = QWebPage.SelectStartOfDocument
self._current_widget().triggerPageAction(act)
webview.triggerPageAction(act)
@cmdutils.register(instance='command-dispatcher', hide=True,
modes=[KeyMode.caret, KeyMode.visual], scope='window')
modes=[KeyMode.caret], scope='window')
def move_to_end_of_document(self):
"""Move the cursor or select to the end of document."""
modemanager = modeman.get_modeman(self._win_id)
if modemanager.mode == KeyMode.caret:
webview = self._current_widget()
if not webview.selection_enabled:
act = QWebPage.MoveToEndOfDocument
else:
act = QWebPage.SelectEndOfDocument
self._current_widget().triggerPageAction(act)
webview.triggerPageAction(act)
@cmdutils.register(instance='command-dispatcher', hide=True,
modes=[KeyMode.visual], scope='window')
modes=[KeyMode.caret], scope='window')
def yank_selected(self, sel=False):
"""Yank selected text to the clipboard or primary selection.
@ -1323,9 +1323,17 @@ class CommandDispatcher:
len(s), "char" if len(s) == 1 else "chars", target))
@cmdutils.register(instance='command-dispatcher', hide=True,
modes=[KeyMode.visual], scope='window')
modes=[KeyMode.caret], scope='window')
def toggle_selection(self):
"""Toggle caret selection mode."""
self._current_widget().selection_enabled = not self._current_widget().selection_enabled
mainwindow = objreg.get('main-window', scope='window', window=self._win_id)
mainwindow.status.on_mode_entered(usertypes.KeyMode.caret)
@cmdutils.register(instance='command-dispatcher', hide=True,
modes=[KeyMode.caret], scope='window')
def drop_selection(self):
"""Drop selection and stay in visual mode."""
"""Drop selection and keep selection mode enabled."""
self._current_widget().triggerPageAction(QWebPage.MoveToNextChar)
@cmdutils.register(instance='command-dispatcher', scope='window',

View File

@ -106,6 +106,7 @@ class WebView(QWebView):
self.keep_icon = False
self.search_text = None
self.search_flags = 0
self.selection_enabled = False;
self.init_neighborlist()
cfg = objreg.get('config')
cfg.changed.connect(self.init_neighborlist)
@ -180,6 +181,79 @@ class WebView(QWebView):
self.load_status = val
self.load_status_changed.emit(val.name)
def _position_caret(self):
"""
JS snippet to position caret at top of the screen.
Was borrowed from cVim source code
"""
self.page().currentFrame().evaluateJavaScript("""
function isElementInViewport(node) {
var i;
var boundingRect = node.getClientRects()[0] || node.getBoundingClientRect();
if (boundingRect.width <= 1 && boundingRect.height <= 1) {
var rects = node.getClientRects();
for (i = 0; i < rects.length; i++) {
if (rects[i].width > rects[0].height && rects[i].height > rects[0].height) {
boundingRect = rects[i];
}
}
}
if (boundingRect === void 0) return null;
if (boundingRect.top > innerHeight || boundingRect.left > innerWidth) {
return null;
}
if (boundingRect.width <= 1 || boundingRect.height <= 1) {
var children = node.children;
var visibleChildNode = false;
for (i = 0, l = children.length; i < l; ++i) {
boundingRect = children[i].getClientRects()[0] || children[i].getBoundingClientRect();
if (boundingRect.width > 1 && boundingRect.height > 1) {
visibleChildNode = true;
break;
}
}
if (visibleChildNode === false) return null;
}
if (boundingRect.top + boundingRect.height < 10 || boundingRect.left + boundingRect.width < -10) {
return null;
}
var computedStyle = window.getComputedStyle(node, null);
if (computedStyle.visibility !== 'visible' ||
computedStyle.display === 'none' ||
node.hasAttribute('disabled') ||
parseInt(computedStyle.width, '10') === 0 ||
parseInt(computedStyle.height, '10') === 0) {
return null;
}
return boundingRect.top >= -20;
}
var walker = document.createTreeWalker(document.body, 4, null, false);
var node;
var textNodes = [];
while (node = walker.nextNode()) {
if (node.nodeType === 3 && node.data.trim() !== '') {
textNodes.push(node);
}
}
for (var i = 0; i < textNodes.length; i++) {
var element = textNodes[i].parentElement;
if (isElementInViewport(element.parentElement)) {
el = element;
break;
}
}
if (el !== undefined) {
var range = document.createRange();
range.setStart(el, 0);
range.setEnd(el, 0);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
""")
@pyqtSlot(str, str)
def on_config_changed(self, section, option):
"""Reinitialize the zoom neighborlist if related config changed."""
@ -435,11 +509,17 @@ class WebView(QWebView):
log.webview.debug("Ignoring focus because mode {} was "
"entered.".format(mode))
self.setFocusPolicy(Qt.NoFocus)
elif mode in (usertypes.KeyMode.caret, usertypes.KeyMode.visual):
elif mode == usertypes.KeyMode.caret:
settings = self.settings()
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True)
self.clearFocus()
self.setFocus(Qt.OtherFocusReason)
self.selection_enabled = False
tabbed = objreg.get('tabbed-browser', scope='window', window=self.win_id)
if tabbed.currentWidget().tab_id == self.tab_id:
self.clearFocus()
self.setFocus(Qt.OtherFocusReason)
self._position_caret()
@pyqtSlot(usertypes.KeyMode)
def on_mode_left(self, mode):
@ -448,13 +528,14 @@ class WebView(QWebView):
usertypes.KeyMode.yesno):
log.webview.debug("Restoring focus policy because mode {} was "
"left.".format(mode))
elif mode in (usertypes.KeyMode.caret, usertypes.KeyMode.visual):
elif mode == usertypes.KeyMode.caret:
settings = self.settings()
if settings.testAttribute(QWebSettings.CaretBrowsingEnabled):
if mode == usertypes.KeyMode.visual and self.hasSelection():
if self.selection_enabled and self.hasSelection():
# Remove selection if exist
self.triggerPageAction(QWebPage.MoveToNextChar)
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, False)
self.selection_enabled = False;
self.setFocusPolicy(Qt.WheelFocus)

View File

@ -820,9 +820,9 @@ def data(readonly=False):
SettingValue(typ.QssColor(), 'purple'),
"Background color of the statusbar in caret mode."),
('statusbar.bg.visual',
('statusbar.bg.caret_selection',
SettingValue(typ.QssColor(), '#a12dff'),
"Background color of the statusbar in visual mode."),
"Background color of the statusbar in caret selection enabled mode."),
('statusbar.progress.bg',
SettingValue(typ.QssColor(), 'white'),
@ -1259,19 +1259,10 @@ KEY_DATA = collections.OrderedDict([
('rl-backward-delete-char', ['<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']),
('toggle-selection', ['<Space>']),
('drop-selection', ['<Ctrl-Space>']),
('enter-mode normal', ['c']),
])),
('caret,visual', collections.OrderedDict([
('move-to-next-line', ['j']),
('move-to-prev-line', ['k']),
('move-to-next-char', ['l']),
@ -1283,6 +1274,8 @@ KEY_DATA = collections.OrderedDict([
('move-to-end-of-line', ['$']),
('move-to-start-of-document', ['gg']),
('move-to-end-of-document', ['G']),
('yank-selected', ['y']),
('yank-selected -p', ['Y']),
('scroll -50 0', ['H']),
('scroll 0 50', ['J']),
('scroll 0 -50', ['K']),

View File

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

View File

@ -92,7 +92,7 @@ class StatusBar(QWidget):
_prompt_active = False
_insert_active = False
_caret_active = False
_visual_active = False
_caret_selection_active = False
STYLESHEET = """
QWidget#StatusBar {
@ -107,8 +107,8 @@ class StatusBar(QWidget):
{{ color['statusbar.bg.caret'] }}
}
QWidget#StatusBar[visual_active="true"] {
{{ color['statusbar.bg.visual'] }}
QWidget#StatusBar[caret_selection_active="true"] {
{{ color['statusbar.bg.caret_selection'] }}
}
QWidget#StatusBar[prompt_active="true"] {
@ -269,12 +269,12 @@ class StatusBar(QWidget):
return self._caret_active
@pyqtProperty(bool)
def visual_active(self):
"""Getter for self.visual_active, so it can be used as Qt property."""
return self._visual_active
def caret_selection_active(self):
"""Getter for self.caret_active, so it can be used as Qt property."""
return self._caret_selection_active
def _set_mode_active(self, mode, val):
"""Setter for self.{insert,caret,visual}_active.
"""Setter for self.{insert,caret}_active.
Re-set the stylesheet after setting the value, so everything gets
updated by Qt properly.
@ -284,10 +284,17 @@ class StatusBar(QWidget):
self._insert_active = val
elif mode == usertypes.KeyMode.caret:
log.statusbar.debug("Setting caret_active to {}".format(val))
self._caret_active = val
elif mode == usertypes.KeyMode.visual:
log.statusbar.debug("Setting visual_active to {}".format(val))
self._visual_active = val
webview = objreg.get("tabbed-browser", scope="window", window=self._win_id).currentWidget()
if val and webview.selection_enabled:
self._set_mode_text("{} selection".format(mode.name))
self._caret_selection_active = val
self._caret_active = False
else:
if val:
self._set_mode_text(mode.name)
self._caret_active = val
self._caret_selection_active = False
self.setStyleSheet(style.get_stylesheet(self.STYLESHEET))
def _set_mode_text(self, mode):
@ -465,8 +472,7 @@ class StatusBar(QWidget):
window=self._win_id)
if mode in mode_manager.passthrough:
self._set_mode_text(mode.name)
if mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret,
usertypes.KeyMode.visual):
if mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret):
self._set_mode_active(mode, True)
@pyqtSlot(usertypes.KeyMode, usertypes.KeyMode)
@ -479,8 +485,7 @@ class StatusBar(QWidget):
self._set_mode_text(new_mode.name)
else:
self.txt.set_text(self.txt.Text.normal, '')
if old_mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret,
usertypes.KeyMode.visual):
if old_mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret):
self._set_mode_active(old_mode, False)
@config.change_filter('ui', 'message-timeout')

View File

@ -519,7 +519,7 @@ class TabbedBrowser(tabwidget.TabWidget):
log.modes.debug("Current tab changed, focusing {!r}".format(tab))
tab.setFocus()
for mode in (usertypes.KeyMode.hint, usertypes.KeyMode.insert,
usertypes.KeyMode.caret, usertypes.KeyMode.visual):
usertypes.KeyMode.caret):
modeman.maybe_leave(self._win_id, mode, 'tab changed')
if self._now_focused is not None:
objreg.register('last-focused-tab', self._now_focused, update=True,

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