From 4d7e39470e62590031c093152e9cdd9691b1c879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Klinkovsk=C3=BD?= Date: Thu, 21 Jan 2016 22:02:08 +0100 Subject: [PATCH 1/6] Added paste-primary command The Shift+Ins key should arguably insert primary selection, not the clipboard selection as every Qt program does. This commit makes it possible via the hidden paste-primary command (enabled by default). Unfortunately QtWebKit does not provide any straightforward way to insert text at cursor position into editable fields, so we work around this by executing a JavaScript snippet - inspired by this SO answer: http://stackoverflow.com/a/11077016 --- qutebrowser/browser/commands.py | 33 ++++++++++++++++++++++++++++++++ qutebrowser/config/configdata.py | 4 +++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index e1d293781..aac584621 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1307,6 +1307,39 @@ class CommandDispatcher: except webelem.IsNullError: raise cmdexc.CommandError("Element vanished while editing!") + @cmdutils.register(instance='command-dispatcher', + modes=[KeyMode.insert], hide=True, scope='window') + def paste_primary(self): + """Paste the primary selection at cursor position into the curently + selected form field. + """ + frame = self._current_widget().page().currentFrame() + try: + elem = webelem.focus_elem(frame) + except webelem.IsNullError: + raise cmdexc.CommandError("No element focused!") + if not elem.is_editable(strict=True): + raise cmdexc.CommandError("Focused element is not editable!") + + clipboard = QApplication.clipboard() + if clipboard.supportsSelection(): + sel = clipboard.text(QClipboard.Selection) + log.misc.debug("Pasting selection: '{}'".format(sel)) + elem.evaluateJavaScript(""" + var sel = '%s'; + if (this.selectionStart || this.selectionStart == '0') { + var startPos = this.selectionStart; + var endPos = this.selectionEnd; + this.value = this.value.substring(0, startPos) + + sel + + this.value.substring(endPos, this.value.length); + this.selectionStart = startPos + sel.length; + this.selectionEnd = startPos + sel.length; + } else { + this.value += sel; + } + """ % sel) + def _clear_search(self, view, text): """Clear search string/highlights for the given view. diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index a9fbb2669..d94ad572e 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1324,7 +1324,8 @@ KEY_SECTION_DESC = { "Since normal keypresses are passed through, only special keys are " "supported in this mode.\n" "Useful hidden commands to map in this section:\n\n" - " * `open-editor`: Open a texteditor with the focused field."), + " * `open-editor`: Open a texteditor with the focused field.\n" + " * `paste-primary`: Paste primary selection at cursor position."), 'hint': ( "Keybindings for hint mode.\n" "Since normal keypresses are passed through, only special keys are " @@ -1495,6 +1496,7 @@ KEY_DATA = collections.OrderedDict([ ('insert', collections.OrderedDict([ ('open-editor', ['']), + ('paste-primary', ['']), ])), ('hint', collections.OrderedDict([ From 35e16a8e6ea7ae4aa33a44ccbe4c7ad29f205899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Klinkovsk=C3=BD?= Date: Fri, 22 Jan 2016 18:18:17 +0100 Subject: [PATCH 2/6] paste-primary: fix undo/redo not working It seems that unlike Gecko, WebKit does not support undo/redo operations when the textarea's `value` attribute is changed directly. Fortunately there is a WebKit-specific workaround using textInput event. References: * http://stackoverflow.com/a/7554295 * http://help.dottoro.com/ljuecqgv.php --- qutebrowser/browser/commands.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index aac584621..b254efbcf 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1324,21 +1324,14 @@ class CommandDispatcher: clipboard = QApplication.clipboard() if clipboard.supportsSelection(): sel = clipboard.text(QClipboard.Selection) - log.misc.debug("Pasting selection: '{}'".format(sel)) + log.misc.debug("Pasting primary selection into element {} " + "(selection is '{}')".format(elem.debug_text(), sel)) elem.evaluateJavaScript(""" - var sel = '%s'; - if (this.selectionStart || this.selectionStart == '0') { - var startPos = this.selectionStart; - var endPos = this.selectionEnd; - this.value = this.value.substring(0, startPos) - + sel - + this.value.substring(endPos, this.value.length); - this.selectionStart = startPos + sel.length; - this.selectionEnd = startPos + sel.length; - } else { - this.value += sel; - } - """ % sel) + var sel = '{}'; + var event = document.createEvent('TextEvent'); + event.initTextEvent('textInput', true, true, null, sel); + this.dispatchEvent(event); + """.format(sel)) def _clear_search(self, view, text): """Clear search string/highlights for the given view. From db6a0d53ca838a1ca7057066436a9e7333354192 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Klinkovsk=C3=BD?= Date: Wed, 27 Jan 2016 10:04:24 +0100 Subject: [PATCH 3/6] Addressed code-quality remarks --- qutebrowser/browser/commands.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index b254efbcf..6092d3516 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1308,10 +1308,10 @@ class CommandDispatcher: raise cmdexc.CommandError("Element vanished while editing!") @cmdutils.register(instance='command-dispatcher', - modes=[KeyMode.insert], hide=True, scope='window') + modes=[KeyMode.insert], hide=True, scope='window', + needs_js=True) def paste_primary(self): - """Paste the primary selection at cursor position into the curently - selected form field. + """Paste the primary selection at cursor position. """ frame = self._current_widget().page().currentFrame() try: @@ -1324,14 +1324,14 @@ class CommandDispatcher: clipboard = QApplication.clipboard() if clipboard.supportsSelection(): sel = clipboard.text(QClipboard.Selection) - log.misc.debug("Pasting primary selection into element {} " - "(selection is '{}')".format(elem.debug_text(), sel)) + log.misc.debug("Pasting primary selection into element {}".format( + elem.debug_text())) elem.evaluateJavaScript(""" var sel = '{}'; var event = document.createEvent('TextEvent'); event.initTextEvent('textInput', true, true, null, sel); this.dispatchEvent(event); - """.format(sel)) + """.format(webelem.javascript_escape(sel))) def _clear_search(self, view, text): """Clear search string/highlights for the given view. From b358566156e446c8048849e730f643fe2ab5534e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Klinkovsk=C3=BD?= Date: Sat, 30 Jan 2016 13:57:26 +0100 Subject: [PATCH 4/6] Added tests for paste-primary command --- tests/integration/data/paste_primary.html | 10 ++++ tests/integration/features/yankpaste.feature | 50 ++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 tests/integration/data/paste_primary.html diff --git a/tests/integration/data/paste_primary.html b/tests/integration/data/paste_primary.html new file mode 100644 index 000000000..ff6e9ed54 --- /dev/null +++ b/tests/integration/data/paste_primary.html @@ -0,0 +1,10 @@ + + + + + Paste primary selection + + + + + diff --git a/tests/integration/features/yankpaste.feature b/tests/integration/features/yankpaste.feature index b09bf7ce5..33704dfc1 100644 --- a/tests/integration/features/yankpaste.feature +++ b/tests/integration/features/yankpaste.feature @@ -170,3 +170,53 @@ Feature: Yanking and pasting. history: - active: true url: http://localhost:*/data/hello3.txt + + Scenario: Pasting the primary selection into an empty text field + When selection is supported + And I open data/paste_primary.html + And I put "Hello world" into the primary selection + # Click the text field + And I run :hint all + And I run :follow-hint a + And I run :paste-primary + # Compare + And I run :jseval console.log(document.getElementById('qute-textarea').value); + Then the javascript message "Hello world" should be logged + + Scenario: Pasting the primary selection into a text field at specific position + When selection is supported + And I open data/paste_primary.html + And I run :jseval document.getElementById('qute-textarea').value = 'one two three four'; + And I put " Hello world" into the primary selection + # Click the text field + And I run :hint all + And I run :follow-hint a + # Move to the beginning and two words to the right + And I press the keys "" + And I press the key "" + And I press the key "" + And I run :paste-primary + # Compare + And I run :jseval console.log(document.getElementById('qute-textarea').value); + Then the javascript message "one two Hello world three four" should be logged + + Scenario: Pasting the primary selection into a text field with undo + When selection is supported + And I open data/paste_primary.html + And I run :jseval document.getElementById('qute-textarea').value = 'one two three four'; + And I put " Hello world" into the primary selection + # Click the text field + And I run :hint all + And I run :follow-hint a + # Move to the beginning and after the first word + And I press the keys "" + And I press the key "" + # Paste and undo + And I run :paste-primary + And I press the key "" + # One word to the right + And I press the key "" + And I run :paste-primary + # Compare + And I run :jseval console.log(document.getElementById('qute-textarea').value); + Then the javascript message "one two Hello world three four" should be logged From 5ec224d1f9cda4583108d99037852dd79e395267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Klinkovsk=C3=BD?= Date: Sat, 30 Jan 2016 14:03:54 +0100 Subject: [PATCH 5/6] Simplified test for paste-primary command We don't need to move around for this test... --- tests/integration/features/yankpaste.feature | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/integration/features/yankpaste.feature b/tests/integration/features/yankpaste.feature index 33704dfc1..3001a54d3 100644 --- a/tests/integration/features/yankpaste.feature +++ b/tests/integration/features/yankpaste.feature @@ -203,20 +203,16 @@ Feature: Yanking and pasting. Scenario: Pasting the primary selection into a text field with undo When selection is supported And I open data/paste_primary.html - And I run :jseval document.getElementById('qute-textarea').value = 'one two three four'; - And I put " Hello world" into the primary selection # Click the text field And I run :hint all And I run :follow-hint a - # Move to the beginning and after the first word - And I press the keys "" - And I press the key "" # Paste and undo + And I put "This text should be undone" into the primary selection And I run :paste-primary And I press the key "" - # One word to the right - And I press the key "" + # Paste final text + And I put "This text should stay" into the primary selection And I run :paste-primary # Compare And I run :jseval console.log(document.getElementById('qute-textarea').value); - Then the javascript message "one two Hello world three four" should be logged + Then the javascript message "This text should stay" should be logged From cc8e7007b4b9831157d4e650170f313378ae3ddc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Klinkovsk=C3=BD?= Date: Sat, 30 Jan 2016 14:13:41 +0100 Subject: [PATCH 6/6] Fixed docstring formatting error --- qutebrowser/browser/commands.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 6092d3516..da01cd06f 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1311,8 +1311,7 @@ class CommandDispatcher: modes=[KeyMode.insert], hide=True, scope='window', needs_js=True) def paste_primary(self): - """Paste the primary selection at cursor position. - """ + """Paste the primary selection at cursor position.""" frame = self._current_widget().page().currentFrame() try: elem = webelem.focus_elem(frame)