diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 7dc0a35fd..93488bd04 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -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) diff --git a/qutebrowser/browser/webview.py b/qutebrowser/browser/webview.py index ee88e4089..19c34f9c3 100644 --- a/qutebrowser/browser/webview.py +++ b/qutebrowser/browser/webview.py @@ -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. diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index fea34ab91..079b0da87 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -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', ['']), ('rl-backward-delete-char', ['']), ])), + + ('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']), + ])), ]) diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index 4699de8e9..d956cfc08 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -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 diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 7ca4eabc7..4a545f53e 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -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) diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index 30cdf1245..5e2d40cf0 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -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, diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index 7371f3114..ba5957155 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -231,7 +231,7 @@ ClickTarget = enum('ClickTarget', ['normal', 'tab', 'tab_bg', 'window']) # Key input modes KeyMode = enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt', - 'insert', 'passthrough']) + 'insert', 'passthrough', 'caret', 'visual']) # Available command completions