From 0177dafbd004b88f8ec0a72be3d90407a0c04ae7 Mon Sep 17 00:00:00 2001 From: Jan Verbeek Date: Sat, 6 Aug 2016 22:03:50 +0200 Subject: [PATCH 01/24] Add {clipboard} and {primary} to replace :paste :paste is deprecated and replaced by equivalents using :open and the new variables, and :open supports opening newline-separated lists of URLs. --- doc/help/commands.asciidoc | 17 +---- qutebrowser/browser/commands.py | 79 +++++++++++++++--------- qutebrowser/commands/runners.py | 10 ++- qutebrowser/config/configdata.py | 12 ++-- qutebrowser/utils/utils.py | 6 ++ tests/end2end/features/yankpaste.feature | 38 ++++++------ 6 files changed, 93 insertions(+), 69 deletions(-) diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index f43be5822..ebe703a44 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -34,7 +34,6 @@ |<>|Show a log of past messages. |<>|Open typical prev/next links or navigate using the URL path. |<>|Open a URL in the current/[count]th tab. -|<>|Open a page from the clipboard. |<>|Print the current/[count]th tab. |<>|Add a new quickmark. |<>|Delete a quickmark. @@ -473,6 +472,8 @@ Syntax: +:open [*--implicit*] [*--bg*] [*--tab*] [*--window*] ['url']+ Open a URL in the current/[count]th tab. +If the URL contains newlines, each line gets opened in its own tab. + ==== positional arguments * +'url'+: The URL to open. @@ -489,20 +490,6 @@ The tab index to open the URL in. ==== note * This command does not split arguments after the last argument and handles quotes literally. -[[paste]] -=== paste -Syntax: +:paste [*--sel*] [*--tab*] [*--bg*] [*--window*]+ - -Open a page from the clipboard. - -If the pasted text contains newlines, each line gets opened in its own tab. - -==== optional arguments -* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard. -* +*-t*+, +*--tab*+: Open in a new tab. -* +*-b*+, +*--bg*+: Open in a background tab. -* +*-w*+, +*--window*+: Open in new window. - [[print]] === print Syntax: +:print [*--preview*] [*--pdf* 'file']+ diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 3728ebac4..3bfb09fd6 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -236,6 +236,8 @@ class CommandDispatcher: bg=False, tab=False, window=False, count=None): """Open a URL in the current/[count]th tab. + If the URL contains newlines, each line gets opened in its own tab. + Args: url: The URL to open. bg: Open in a new background tab. @@ -245,37 +247,60 @@ class CommandDispatcher: clicking on a link). count: The tab index to open the URL in, or None. """ + force_search = False if url is None: if tab or bg or window: - url = config.get('general', 'default-page') + urls = [config.get('general', 'default-page')] else: raise cmdexc.CommandError("No URL given, but -t/-b/-w is not " "set!") else: - try: - url = objreg.get('quickmark-manager').get(url) - except urlmarks.Error: - try: - url = urlutils.fuzzy_url(url) - except urlutils.InvalidUrlError as e: - # We don't use cmdexc.CommandError here as this can be - # called async from edit_url - message.error(self._win_id, str(e)) - return - if tab or bg or window: - self._open(url, tab, bg, window, not implicit) - else: - curtab = self._cntwidget(count) - if curtab is None: - if count is None: - # We want to open a URL in the current tab, but none exists - # yet. - self._tabbed_browser.tabopen(url) - else: - # Explicit count with a tab that doesn't exist. - return + urllist = [u for u in url.split('\n') if u.strip()] + if (len(urllist) > 1 and + any(not urlutils.is_url(u) and + urlutils.get_path_if_valid(u, check_exists=True) + is None for u in urllist)): + urllist = [url] + force_search = True + urls = [x for x in [self._parse_url(u, force_search=force_search) + for u in urllist] if x is not None] + for i, cur_url in enumerate(urls): + if not window and i > 0: + tab = False + bg = True + if tab or bg or window: + self._open(cur_url, tab, bg, window, not implicit) else: - curtab.openurl(url) + curtab = self._cntwidget(count) + if curtab is None: + if count is None: + # We want to open a URL in the current tab, but none + # exists yet. + self._tabbed_browser.tabopen(cur_url) + else: + curtab.openurl(cur_url) + + def _parse_url(self, url, force_search=False): + """Parse a URL or quickmark or search query. + + Args: + url: The URL to parse. + force_search: Whether to force a search even if the content can be + interpreted as a URL or a path. + + Return: + A URL that can be opened.""" + try: + return objreg.get('quickmark-manager').get(url) + except urlmarks.Error: + try: + return urlutils.fuzzy_url(url, force_search=force_search) + except urlutils.InvalidUrlError as e: + # We don't use cmdexc.CommandError here as this can be + # called async from edit_url + message.error(self._win_id, str(e)) + return + @cmdutils.register(instance='command-dispatcher', name='reload', scope='window') @@ -776,7 +801,8 @@ class CommandDispatcher: else: raise cmdexc.CommandError("Last tab") - @cmdutils.register(instance='command-dispatcher', scope='window') + @cmdutils.register(instance='command-dispatcher', scope='window', + deprecated="Use :open {clipboard}") def paste(self, sel=False, tab=False, bg=False, window=False): """Open a page from the clipboard. @@ -796,9 +822,6 @@ class CommandDispatcher: sel = False target = "Clipboard" text = utils.get_clipboard(selection=sel) - if not text.strip(): - raise cmdexc.CommandError("{} is empty.".format(target)) - log.misc.debug("{} contained: {!r}".format(target, text)) text_urls = [u for u in text.split('\n') if u.strip()] if (len(text_urls) > 1 and not urlutils.is_url(text_urls[0]) and urlutils.get_path_if_valid( diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index f020b9199..91c8bf3c9 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -26,7 +26,7 @@ from PyQt5.QtCore import pyqtSlot, QUrl, QObject from qutebrowser.config import config, configexc from qutebrowser.commands import cmdexc, cmdutils -from qutebrowser.utils import message, objreg, qtutils +from qutebrowser.utils import message, objreg, qtutils, utils from qutebrowser.misc import split @@ -57,11 +57,19 @@ def replace_variables(win_id, arglist): QUrl.RemovePassword) if '{url:pretty}' in arglist: pretty_url = _current_url(tabbed_browser).toString(QUrl.RemovePassword) + if '{clipboard}' in arglist: + clipboard = utils.get_clipboard() + if '{primary}' in arglist: + primary = utils.get_clipboard(selection=True) for arg in arglist: if arg == '{url}': args.append(url) elif arg == '{url:pretty}': args.append(pretty_url) + elif arg == '{clipboard}': + args.append(clipboard) + elif arg == '{primary}': + args.append(primary) else: args.append(arg) return args diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 8cbfe3938..9cc275502 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1512,12 +1512,12 @@ KEY_DATA = collections.OrderedDict([ ('yank -ds', ['yD']), ('yank -p', ['yp']), ('yank -ps', ['yP']), - ('paste', ['pp']), - ('paste -s', ['pP']), - ('paste -t', ['Pp']), - ('paste -ts', ['PP']), - ('paste -w', ['wp']), - ('paste -ws', ['wP']), + ('open {clipboard}', ['pp']), + ('open {primary}', ['pP']), + ('open -t {clipboard}', ['Pp']), + ('open -t {primary}', ['PP']), + ('open -w {clipboard}', ['wp']), + ('open -w {primary}', ['wP']), ('quickmark-save', ['m']), ('set-cmd-text -s :quickmark-load', ['b']), ('set-cmd-text -s :quickmark-load -t', ['B']), diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index fd8419d12..983827d46 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -37,6 +37,7 @@ import pkg_resources import qutebrowser from qutebrowser.utils import qtutils, log +from qutebrowser.commands import cmdexc fake_clipboard = None @@ -810,6 +811,11 @@ def get_clipboard(selection=False): mode = QClipboard.Selection if selection else QClipboard.Clipboard data = QApplication.clipboard().text(mode=mode) + target = "Primary selection" if selection else "Clipboard" + if not data.strip(): + raise cmdexc.CommandError("{} is empty.".format(target)) + log.misc.debug("{} contained: {!r}".format(target, data)) + return data diff --git a/tests/end2end/features/yankpaste.feature b/tests/end2end/features/yankpaste.feature index db39d2380..abf9cf9c3 100644 --- a/tests/end2end/features/yankpaste.feature +++ b/tests/end2end/features/yankpaste.feature @@ -1,6 +1,6 @@ Feature: Yanking and pasting. - :yank and :paste can be used to copy/paste the URL or title from/to the - clipboard and primary selection. + :yank, {clipboard} and {primary} can be used to copy/paste the URL or title + from/to the clipboard and primary selection. Background: Given I run :tab-only @@ -45,11 +45,11 @@ Feature: Yanking and pasting. Then the message "Yanked URL to clipboard: http://localhost:(port)/data/title with spaces.html" should be shown And the clipboard should contain "http://localhost:(port)/data/title with spaces.html" - #### :paste + #### {clipboard} and {primary} Scenario: Pasting a URL When I put "http://localhost:(port)/data/hello.txt" into the clipboard - And I run :paste + And I run :open {clipboard} And I wait until data/hello.txt is loaded Then the requests should be: data/hello.txt @@ -57,32 +57,32 @@ Feature: Yanking and pasting. Scenario: Pasting a URL from primary selection When selection is supported And I put "http://localhost:(port)/data/hello2.txt" into the primary selection - And I run :paste --sel + And I run :open {primary} And I wait until data/hello2.txt is loaded Then the requests should be: data/hello2.txt Scenario: Pasting with empty clipboard When I put "" into the clipboard - And I run :paste + And I run :open {clipboard} (invalid command) Then the error "Clipboard is empty." should be shown Scenario: Pasting with empty selection When selection is supported And I put "" into the primary selection - And I run :paste --sel + And I run :open {primary} (invalid command) Then the error "Primary selection is empty." should be shown Scenario: Pasting with a space in clipboard When I put " " into the clipboard - And I run :paste + And I run :open {clipboard} (invalid command) Then the error "Clipboard is empty." should be shown Scenario: Pasting in a new tab Given I open about:blank When I run :tab-only And I put "http://localhost:(port)/data/hello.txt" into the clipboard - And I run :paste -t + And I run :open -t {clipboard} And I wait until data/hello.txt is loaded Then the following tabs should be open: - about:blank @@ -92,7 +92,7 @@ Feature: Yanking and pasting. Given I open about:blank When I run :tab-only And I put "http://localhost:(port)/data/hello.txt" into the clipboard - And I run :paste -b + And I run :open -b {clipboard} And I wait until data/hello.txt is loaded Then the following tabs should be open: - about:blank (active) @@ -101,7 +101,7 @@ Feature: Yanking and pasting. Scenario: Pasting in a new window Given I have a fresh instance When I put "http://localhost:(port)/data/hello.txt" into the clipboard - And I run :paste -w + And I run :open -w {clipboard} And I wait until data/hello.txt is loaded Then the session should look like: windows: @@ -119,7 +119,7 @@ Feature: Yanking and pasting. Scenario: Pasting an invalid URL When I set general -> auto-search to false And I put "foo bar" into the clipboard - And I run :paste + And I run :open {clipboard} Then the error "Invalid URL" should be shown Scenario: Pasting multiple urls in a new tab @@ -128,7 +128,7 @@ Feature: Yanking and pasting. http://localhost:(port)/data/hello.txt http://localhost:(port)/data/hello2.txt http://localhost:(port)/data/hello3.txt - And I run :paste -t + And I run :open -t {clipboard} And I wait until data/hello.txt is loaded And I wait until data/hello2.txt is loaded And I wait until data/hello3.txt is loaded @@ -145,7 +145,7 @@ Feature: Yanking and pasting. this url: http://qutebrowser.org should not open - And I run :paste -t + And I run :open -t {clipboard} And I wait until data/hello.txt?q=this%20url%3A%0Ahttp%3A//qutebrowser.org%0Ashould%20not%20open is loaded Then the following tabs should be open: - about:blank @@ -159,7 +159,7 @@ Feature: Yanking and pasting. text: should open as search - And I run :paste -t + And I run :open -t {clipboard} And I wait until data/hello.txt?q=text%3A%0Ashould%20open%0Aas%20search is loaded Then the following tabs should be open: - about:blank @@ -172,7 +172,7 @@ Feature: Yanking and pasting. http://localhost:(port)/data/hello.txt http://localhost:(port)/data/hello2.txt http://localhost:(port)/data/hello3.txt - And I run :paste -b + And I run :open -b {clipboard} And I wait until data/hello.txt is loaded And I wait until data/hello2.txt is loaded And I wait until data/hello3.txt is loaded @@ -188,7 +188,7 @@ Feature: Yanking and pasting. http://localhost:(port)/data/hello.txt http://localhost:(port)/data/hello2.txt http://localhost:(port)/data/hello3.txt - And I run :paste -w + And I run :open -w {clipboard} And I wait until data/hello.txt is loaded And I wait until data/hello2.txt is loaded And I wait until data/hello3.txt is loaded @@ -218,13 +218,13 @@ Feature: Yanking and pasting. Scenario: Pasting multiple urls with an empty one When I open about:blank And I put "http://localhost:(port)/data/hello.txt\n\nhttp://localhost:(port)/data/hello2.txt" into the clipboard - And I run :paste -t + And I run :open -t {clipboard} Then no crash should happen Scenario: Pasting multiple urls with an almost empty one When I open about:blank And I put "http://localhost:(port)/data/hello.txt\n \nhttp://localhost:(port)/data/hello2.txt" into the clipboard - And I run :paste -t + And I run :open -t {clipboard} Then no crash should happen #### :paste-primary From 96146c55afbbf8603d800cfddfca8b7035cec39a Mon Sep 17 00:00:00 2001 From: Jan Verbeek Date: Sat, 6 Aug 2016 22:25:08 +0200 Subject: [PATCH 02/24] Remove useless clipboard target information --- qutebrowser/browser/commands.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 3bfb09fd6..f0ed6d67a 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -816,11 +816,8 @@ class CommandDispatcher: window: Open in new window. """ force_search = False - if sel and utils.supports_selection(): - target = "Primary selection" - else: + if not utils.supports_selection(): sel = False - target = "Clipboard" text = utils.get_clipboard(selection=sel) text_urls = [u for u in text.split('\n') if u.strip()] if (len(text_urls) > 1 and not urlutils.is_url(text_urls[0]) and From 38508274e063c12fc8caf49c746795c0abd57b94 Mon Sep 17 00:00:00 2001 From: Jan Verbeek Date: Sun, 7 Aug 2016 00:46:23 +0200 Subject: [PATCH 03/24] Improve clipboard exceptions, migrate bindings --- qutebrowser/browser/commands.py | 14 ++++++++++---- qutebrowser/commands/runners.py | 11 +++++++---- qutebrowser/config/configdata.py | 5 +++++ qutebrowser/misc/miscwidgets.py | 3 ++- qutebrowser/utils/utils.py | 8 ++++++-- tests/unit/config/test_config.py | 4 ++++ 6 files changed, 34 insertions(+), 11 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index f0ed6d67a..6dde5d40f 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -818,7 +818,10 @@ class CommandDispatcher: force_search = False if not utils.supports_selection(): sel = False - text = utils.get_clipboard(selection=sel) + try: + text = utils.get_clipboard(selection=sel) + except utils.ClipboardEmptyError as e: + raise cmdexc.CommandError(e) text_urls = [u for u in text.split('\n') if u.strip()] if (len(text_urls) > 1 and not urlutils.is_url(text_urls[0]) and urlutils.get_path_if_valid( @@ -1462,9 +1465,12 @@ class CommandDispatcher: raise cmdexc.CommandError("Focused element is not editable!") try: - sel = utils.get_clipboard(selection=True) - except utils.SelectionUnsupportedError: - sel = utils.get_clipboard() + try: + sel = utils.get_clipboard(selection=True) + except utils.SelectionUnsupportedError: + sel = utils.get_clipboard() + except utils.ClipboardEmptyError: + return log.misc.debug("Pasting primary selection into element {}".format( elem.debug_text())) diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index 91c8bf3c9..201fc4bbe 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -57,10 +57,13 @@ def replace_variables(win_id, arglist): QUrl.RemovePassword) if '{url:pretty}' in arglist: pretty_url = _current_url(tabbed_browser).toString(QUrl.RemovePassword) - if '{clipboard}' in arglist: - clipboard = utils.get_clipboard() - if '{primary}' in arglist: - primary = utils.get_clipboard(selection=True) + try: + if '{clipboard}' in arglist: + clipboard = utils.get_clipboard() + if '{primary}' in arglist: + primary = utils.get_clipboard(selection=True) + except utils.ClipboardEmptyError as e: + raise cmdexc.CommandError(e) for arg in arglist: if arg == '{url}': args.append(url) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 9cc275502..f770f5c05 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1677,4 +1677,9 @@ CHANGED_KEY_COMMANDS = [ (re.compile(r'^download-remove --all$'), r'download-clear'), (re.compile(r'^hint links fill "([^"]*)"$'), r'hint links fill \1'), + + (re.compile(r'^paste$'), r'open {clipboard}'), + (re.compile(r'^paste -([twb])$'), r'open -\1 {clipboard}'), + (re.compile(r'^paste -([twb])s$'), r'open -\1 {primary}'), + (re.compile(r'^paste -s([twb])$'), r'open -\1 {primary}'), ] diff --git a/qutebrowser/misc/miscwidgets.py b/qutebrowser/misc/miscwidgets.py index 63a5718c6..7e84300d2 100644 --- a/qutebrowser/misc/miscwidgets.py +++ b/qutebrowser/misc/miscwidgets.py @@ -47,7 +47,8 @@ class MinimalLineEditMixin: if e.key() == Qt.Key_Insert and e.modifiers() == Qt.ShiftModifier: try: text = utils.get_clipboard(selection=True) - except utils.SelectionUnsupportedError: + except (utils.SelectionUnsupportedError, + utils.ClipboardEmptyError): pass else: e.accept() diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index 983827d46..32696aacf 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -37,7 +37,6 @@ import pkg_resources import qutebrowser from qutebrowser.utils import qtutils, log -from qutebrowser.commands import cmdexc fake_clipboard = None @@ -49,6 +48,11 @@ class SelectionUnsupportedError(Exception): """Raised if [gs]et_clipboard is used and selection=True is unsupported.""" +class ClipboardEmptyError(Exception): + + """Raised if get_clipboard is used and the clipboard is empty.""" + + def elide(text, length): """Elide text so it uses a maximum of length chars.""" if length < 1: @@ -813,7 +817,7 @@ def get_clipboard(selection=False): target = "Primary selection" if selection else "Clipboard" if not data.strip(): - raise cmdexc.CommandError("{} is empty.".format(target)) + raise ClipboardEmptyError("{} is empty.".format(target)) log.misc.debug("{} contained: {!r}".format(target, data)) return data diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py index a6ad3d119..53664b718 100644 --- a/tests/unit/config/test_config.py +++ b/tests/unit/config/test_config.py @@ -286,6 +286,10 @@ class TestKeyConfigParser: 'hint links fill :open {hint-url}'), ('hint links fill ":open -t {hint-url}"', 'hint links fill :open -t {hint-url}'), + + ('paste', 'open {clipboard}'), + ('paste -t', 'open -t {clipboard}'), + ('paste -ws', 'open -w {primary}'), ] ) def test_migrations(self, old, new_expected): From 7e634a1e52b0bd6e07420226e0aff6c39f41d804 Mon Sep 17 00:00:00 2001 From: Jan Verbeek Date: Tue, 9 Aug 2016 16:20:02 +0200 Subject: [PATCH 04/24] Make vars declarative, new function for URL lists --- qutebrowser/browser/commands.py | 37 ++++++++++++++++++++++----------- qutebrowser/commands/runners.py | 36 +++++++++++++++----------------- 2 files changed, 42 insertions(+), 31 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 293602a53..42458f0d3 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -247,7 +247,6 @@ class CommandDispatcher: clicking on a link). count: The tab index to open the URL in, or None. """ - force_search = False if url is None: if tab or bg or window: urls = [config.get('general', 'default-page')] @@ -255,15 +254,7 @@ class CommandDispatcher: raise cmdexc.CommandError("No URL given, but -t/-b/-w is not " "set!") else: - urllist = [u for u in url.split('\n') if u.strip()] - if (len(urllist) > 1 and - any(not urlutils.is_url(u) and - urlutils.get_path_if_valid(u, check_exists=True) - is None for u in urllist)): - urllist = [url] - force_search = True - urls = [x for x in [self._parse_url(u, force_search=force_search) - for u in urllist] if x is not None] + urls = self._parse_url_input(url) for i, cur_url in enumerate(urls): if not window and i > 0: tab = False @@ -277,10 +268,13 @@ class CommandDispatcher: # We want to open a URL in the current tab, but none # exists yet. self._tabbed_browser.tabopen(cur_url) + else: + # Explicit count with a tab that doesn't exist. + return else: curtab.openurl(cur_url) - def _parse_url(self, url, force_search=False): + def _parse_url(self, url, *, force_search=False): """Parse a URL or quickmark or search query. Args: @@ -299,8 +293,27 @@ class CommandDispatcher: # We don't use cmdexc.CommandError here as this can be # called async from edit_url message.error(self._win_id, str(e)) - return + return None + def _parse_url_input(self, url): + """Parse a URL or newline-separated list of URLs. + + Args: + url: The URL or list to parse. + + Return: + A list of URLs that can be opened.""" + force_search = False + urllist = [u for u in url.split('\n') if u.strip()] + if (len(urllist) > 1 and not urlutils.is_url(urllist[0]) and + urlutils.get_path_if_valid(urllist[0], check_exists=True) + is None): + urllist = [url] + force_search = True + for cur_url in urllist: + parsed = self._parse_url(cur_url, force_search=force_search) + if parsed is not None: + yield parsed @cmdutils.register(instance='command-dispatcher', name='reload', scope='window') diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index 3762c4376..33bed1d1e 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -49,31 +49,29 @@ def _current_url(tabbed_browser): def replace_variables(win_id, arglist): """Utility function to replace variables like {url} in a list of args.""" + variables = { + '{url}': lambda: _current_url(tabbed_browser).toString( + QUrl.FullyEncoded | QUrl.RemovePassword), + '{url:pretty}': lambda: _current_url(tabbed_browser).toString( + QUrl.RemovePassword), + '{clipboard}': utils.get_clipboard, + '{primary}': lambda: utils.get_clipboard(selection=True), + } + values = {} args = [] tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id) - if any('{url}' in arg for arg in arglist): - url = _current_url(tabbed_browser).toString(QUrl.FullyEncoded | - QUrl.RemovePassword) - if any('{url:pretty}' in arg for arg in arglist): - pretty_url = _current_url(tabbed_browser).toString(QUrl.RemovePassword) + try: - if any('{clipboard}' in arg for arg in arglist): - clipboard = utils.get_clipboard() - if any('{primary}' in arg for arg in arglist): - primary = utils.get_clipboard(selection=True) + for arg in arglist: + for var, func in variables.items(): + if var in arg: + if var not in values: + values[var] = func() + arg = arg.replace(var, values[var]) + args.append(arg) except utils.ClipboardEmptyError as e: raise cmdexc.CommandError(e) - for arg in arglist: - if '{url}' in arg: - arg = arg.replace('{url}', url) - if '{url:pretty}' in arg: - arg = arg.replace('{url:pretty}', pretty_url) - if '{clipboard}' in arg: - arg = arg.replace('{clipboard}', clipboard) - if '{primary}' in arg: - arg = arg.replace('{primary}', primary) - args.append(arg) return args From 7d0064ff861ae6030294dcda22e897c0bec594c3 Mon Sep 17 00:00:00 2001 From: Jan Verbeek Date: Tue, 9 Aug 2016 19:23:38 +0200 Subject: [PATCH 05/24] Fix docstrings --- qutebrowser/browser/commands.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 42458f0d3..f8dbc0413 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -283,7 +283,8 @@ class CommandDispatcher: interpreted as a URL or a path. Return: - A URL that can be opened.""" + A URL that can be opened. + """ try: return objreg.get('quickmark-manager').get(url) except urlmarks.Error: @@ -302,7 +303,8 @@ class CommandDispatcher: url: The URL or list to parse. Return: - A list of URLs that can be opened.""" + A list of URLs that can be opened. + """ force_search = False urllist = [u for u in url.split('\n') if u.strip()] if (len(urllist) > 1 and not urlutils.is_url(urllist[0]) and From 63b9b61e753458306b2581b4788e0da5f08c6de5 Mon Sep 17 00:00:00 2001 From: Jan Verbeek Date: Wed, 10 Aug 2016 12:33:01 +0200 Subject: [PATCH 06/24] Add ClipboardError superexception --- qutebrowser/browser/commands.py | 2 +- qutebrowser/commands/runners.py | 2 +- qutebrowser/misc/miscwidgets.py | 3 +-- qutebrowser/utils/utils.py | 9 +++++++-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index f8dbc0413..37ce914ed 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -855,7 +855,7 @@ class CommandDispatcher: sel = False try: text = utils.get_clipboard(selection=sel) - except utils.ClipboardEmptyError as e: + except utils.ClipboardError as e: raise cmdexc.CommandError(e) text_urls = [u for u in text.split('\n') if u.strip()] if (len(text_urls) > 1 and not urlutils.is_url(text_urls[0]) and diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index 33bed1d1e..6a132598d 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -70,7 +70,7 @@ def replace_variables(win_id, arglist): values[var] = func() arg = arg.replace(var, values[var]) args.append(arg) - except utils.ClipboardEmptyError as e: + except utils.ClipboardError as e: raise cmdexc.CommandError(e) return args diff --git a/qutebrowser/misc/miscwidgets.py b/qutebrowser/misc/miscwidgets.py index 7e84300d2..ba21e541a 100644 --- a/qutebrowser/misc/miscwidgets.py +++ b/qutebrowser/misc/miscwidgets.py @@ -47,8 +47,7 @@ class MinimalLineEditMixin: if e.key() == Qt.Key_Insert and e.modifiers() == Qt.ShiftModifier: try: text = utils.get_clipboard(selection=True) - except (utils.SelectionUnsupportedError, - utils.ClipboardEmptyError): + except utils.ClipboardError: pass else: e.accept() diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index 32696aacf..72d6f83e2 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -43,12 +43,17 @@ fake_clipboard = None log_clipboard = False -class SelectionUnsupportedError(Exception): +class ClipboardError(Exception): + + """Raised if the clipboard contents are unavailable for some reason.""" + + +class SelectionUnsupportedError(ClipboardError): """Raised if [gs]et_clipboard is used and selection=True is unsupported.""" -class ClipboardEmptyError(Exception): +class ClipboardEmptyError(ClipboardError): """Raised if get_clipboard is used and the clipboard is empty.""" From 6293fad2ebbb67c98180b8dd984bcff3bf26017c Mon Sep 17 00:00:00 2001 From: Niklas Haas Date: Wed, 10 Aug 2016 15:56:41 +0200 Subject: [PATCH 07/24] Add general->new-instance-open-target.window=last-visible I usually use my browser with a one-window-per-workspace flow. If I click on a URL anywhere, I personally would prefer it to go to the browser instance that's on the same workspace. To this end, the easiest way to accomplish this is to simply track when windows are made visible and register them as the last visible object. (To get finer control for when you have multiple windows on the same workspace, focus changes also update the last visible object - the implication being here that focusing something also means you're looking at it) Not all users may like this behavior, so I consider it strictly optional. --- qutebrowser/app.py | 3 +++ qutebrowser/config/configdata.py | 4 +++- qutebrowser/mainwindow/mainwindow.py | 17 +++++++++++++++++ qutebrowser/utils/objreg.py | 8 ++++++++ 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index a25e3a4bb..70b256421 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -350,6 +350,9 @@ def on_focus_changed(_old, new): window = new.window() if isinstance(window, mainwindow.MainWindow): objreg.register('last-focused-main-window', window, update=True) + # A focused window must also be visible, and in this case we should + # consider it as the most recently looked-at window + objreg.register('last-visible-main-window', window, update=True) def open_desktopservices_url(url): diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 7ed51142c..f35126b77 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -233,7 +233,9 @@ def data(readonly=False): ('last-opened', "Open new tabs in the last opened " "window."), ('last-focused', "Open new tabs in the most recently " - "focused window.") + "focused window."), + ('last-visible', "Open new tabs in the most recently " + "visible window.") )), 'last-focused'), "Which window to choose when opening links as new tabs."), diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index a53c4f702..496245b8c 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -74,6 +74,8 @@ def get_window(via_ipc, force_window=False, force_tab=False, window = objreg.last_focused_window() elif win_mode == 'last-opened': window = objreg.last_window() + elif win_mode == 'last-visible': + window = objreg.last_visible_window() except objreg.NoWindow: # There is no window left, so we open a new one window = MainWindow() @@ -458,8 +460,23 @@ class MainWindow(QWidget): self._downloadview.updateGeometry() self.tabbed_browser.tabBar().refresh() + def showEvent(self, e): + """Extend showEvent to register us as the last-visible-main-window. + + Args: + e: The QShowEvent + """ + super().showEvent(e) + objreg.register('last-visible-main-window', self, update=True) + def _do_close(self): """Helper function for closeEvent.""" + last_visible = objreg.get('last-visible-main-window') + if self is last_visible: + try: + objreg.delete('last-visible-main-window') + except KeyError: + pass objreg.get('session-manager').save_last_window_session() self._save_geometry() log.destroy.debug("Closing window {}".format(self.win_id)) diff --git a/qutebrowser/utils/objreg.py b/qutebrowser/utils/objreg.py index 0691be1ee..072c45b4d 100644 --- a/qutebrowser/utils/objreg.py +++ b/qutebrowser/utils/objreg.py @@ -280,6 +280,14 @@ def dump_objects(): return lines +def last_visible_window(): + """Get the last visible window, or the last focused window if none.""" + try: + return get('last-visible-main-window') + except KeyError: + return last_focused_window() + + def last_focused_window(): """Get the last focused window, or the last window if none.""" try: From 4205e1c95b52277054765a2bca3b5b21fcd142ec Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 10 Aug 2016 17:40:27 +0200 Subject: [PATCH 08/24] Update docs --- CHANGELOG.asciidoc | 2 ++ doc/help/settings.asciidoc | 1 + 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 744cf8d8b..248eb50cc 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -58,6 +58,8 @@ Changed - With `new-instance-open-target` set to a tab option, the tab is now opened in the most recently focused (instead of the last opened) window. This can be configured with the new `new-instance-open-target.window` setting. + It can also be set to `last-visible` to show the pages in the most recently + visible window. - Word hints now are more clever about getting the element text from some elements. - Completions for `:help` and `:bind` now also show hidden commands - The `:buffer` completion now also filters using the first column (id). diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 277954815..a04e27878 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -456,6 +456,7 @@ Valid values: * +last-opened+: Open new tabs in the last opened window. * +last-focused+: Open new tabs in the most recently focused window. + * +last-visible+: Open new tabs in the most recently visible window. Default: +pass:[last-focused]+ From efa53ac25e7014f777c5f34eab92915a3cd3fbf6 Mon Sep 17 00:00:00 2001 From: Jan Verbeek Date: Wed, 10 Aug 2016 19:42:03 +0200 Subject: [PATCH 09/24] Add variable tests --- tests/end2end/features/misc.feature | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index 686f5a8b4..fa20d26ac 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -524,3 +524,16 @@ Feature: Various utility commands. Then the following tabs should be open: - data/hints/link_blank.html - data/hello.txt (active) + + ## Variables + + Scenario: {url} as part of an argument + When I open data/hello.txt + And I run :message-info foo{url} + Then the message "foohttp://localhost:*/hello.txt" should be shown + + Scenario: Multiple variables in an argument + When I open data/hello.txt + And I put "foo" into the clipboard + And I run :message-info {clipboard}bar{url} + Then the message "foobarhttp://localhost:*/hello.txt" should be shown From 64afc562b60d62446c67814b77b3ff5240ad1287 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 10 Aug 2016 19:42:24 +0200 Subject: [PATCH 10/24] Install MouseEventFilter on new WebEngine children For some reason, when e.g. visiting duckduckgo and then heise.de, QtWebEngine suddenly gets a new QOpenGLWidget as focusProxy. We install an extra eventFilter observing the ChildAdded event and re-adding the MouseEventFilter when that happens. --- qutebrowser/browser/browsertab.py | 9 +++-- qutebrowser/browser/mouse.py | 34 ++++++++++++++++--- qutebrowser/browser/webengine/webenginetab.py | 11 ++++-- qutebrowser/browser/webkit/webkittab.py | 4 +-- tests/unit/browser/test_tab.py | 3 -- 5 files changed, 44 insertions(+), 17 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 05b5673d7..eda0f62ee 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -485,9 +485,6 @@ class AbstractTab(QWidget): self._mouse_event_filter = mouse.MouseEventFilter(self, parent=self) self.backend = None - def _event_filter_target(self): - raise NotImplementedError - def _set_widget(self, widget): # pylint: disable=protected-access self._widget = widget @@ -499,8 +496,10 @@ class AbstractTab(QWidget): self.search._widget = widget self.printing._widget = widget widget.mouse_wheel_zoom.connect(self.zoom._on_mouse_wheel_zoom) - event_filter_target = self._event_filter_target() - event_filter_target.installEventFilter(self._mouse_event_filter) + self._install_event_filter() + + def _install_event_filter(self): + raise NotImplementedError def _set_load_status(self, val): """Setter for load_status.""" diff --git a/qutebrowser/browser/mouse.py b/qutebrowser/browser/mouse.py index 3bd3bf32b..d080f1f75 100644 --- a/qutebrowser/browser/mouse.py +++ b/qutebrowser/browser/mouse.py @@ -21,12 +21,38 @@ from qutebrowser.config import config -from qutebrowser.utils import message +from qutebrowser.utils import message, debug, log from PyQt5.QtCore import QObject, QEvent, Qt +class ChildEventFilter(QObject): + + """An event filter re-adding MouseEventFilter on ChildEvent. + + This is needed because QtWebEngine likes to randomly change its + focusProxy... + + FIXME:qtwebengine Add a test for this happening + """ + + def __init__(self, eventfilter, widget, parent=None): + super().__init__(parent) + self._filter = eventfilter + assert widget is not None + self._widget = widget + + def eventFilter(self, obj, event): + if event.type() == QEvent.ChildAdded: + child = event.child() + log.mouse.debug("{} got new child {}, installing filter".format( + obj, child)) + assert obj is self._widget + child.installEventFilter(self._filter) + return False + + class MouseEventFilter(QObject): """Handle mouse events on a tab.""" @@ -38,7 +64,7 @@ class MouseEventFilter(QObject): QEvent.MouseButtonPress: self._handle_mouse_press, } - def _handle_mouse_press(self, e): + def _handle_mouse_press(self, _obj, e): """Handle pressing of a mouse button.""" is_rocker_gesture = (config.get('input', 'rocker-gestures') and e.buttons() == Qt.LeftButton | Qt.RightButton) @@ -69,9 +95,9 @@ class MouseEventFilter(QObject): message.error(self._tab.win_id, "At end of history.", immediately=True) - def eventFilter(self, _obj, event): + def eventFilter(self, obj, event): """Filter events going to a QWeb(Engine)View.""" evtype = event.type() if evtype not in self._handlers: return False - return self._handlers[evtype](event) + return self._handlers[evtype](obj, event) diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 93e1275d9..f74adadf3 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -31,7 +31,7 @@ from PyQt5.QtWidgets import QApplication from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript # pylint: enable=no-name-in-module,import-error,useless-suppression -from qutebrowser.browser import browsertab +from qutebrowser.browser import browsertab, mouse from qutebrowser.browser.webengine import webview, webengineelem from qutebrowser.utils import usertypes, qtutils, log, javascript, utils @@ -335,6 +335,7 @@ class WebEngineTab(browsertab.AbstractTab): self.backend = usertypes.Backend.QtWebEngine # init js stuff self._init_js() + self._child_event_filter = None def _init_js(self): js_code = '\n'.join([ @@ -359,8 +360,12 @@ class WebEngineTab(browsertab.AbstractTab): # FIXME:qtwebengine what about runsOnSubFrames? page.scripts().insert(script) - def _event_filter_target(self): - return self._widget.focusProxy() + def _install_event_filter(self): + self._widget.focusProxy().installEventFilter(self._mouse_event_filter) + self._child_event_filter = mouse.ChildEventFilter( + eventfilter=self._mouse_event_filter, widget=self._widget, + parent=self) + self._widget.installEventFilter(self._child_event_filter) def openurl(self, url): self._openurl_prepare(url) diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index 8d47152cd..d4fdea2ff 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -510,8 +510,8 @@ class WebKitTab(browsertab.AbstractTab): self.zoom.set_default() self.backend = usertypes.Backend.QtWebKit - def _event_filter_target(self): - return self._widget + def _install_event_filter(self): + self._widget.installEventFilter(self._mouse_event_filter) def openurl(self, url): self._openurl_prepare(url) diff --git a/tests/unit/browser/test_tab.py b/tests/unit/browser/test_tab.py index cd6e9b374..2f11b07ab 100644 --- a/tests/unit/browser/test_tab.py +++ b/tests/unit/browser/test_tab.py @@ -107,9 +107,6 @@ class Tab(browsertab.AbstractTab): self.search = browsertab.AbstractSearch(parent=self) self.printing = browsertab.AbstractPrinting() - def _event_filter_target(self): - return self._widget - @pytest.mark.skipif(PYQT_VERSION < 0x050600, reason='Causes segfaults, see #1638') From b801d3316d5178f37da59f9b09f43395a66cb999 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 10 Aug 2016 20:23:41 +0200 Subject: [PATCH 11/24] Fix lint --- qutebrowser/browser/mouse.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qutebrowser/browser/mouse.py b/qutebrowser/browser/mouse.py index d080f1f75..bd2234e2a 100644 --- a/qutebrowser/browser/mouse.py +++ b/qutebrowser/browser/mouse.py @@ -21,7 +21,7 @@ from qutebrowser.config import config -from qutebrowser.utils import message, debug, log +from qutebrowser.utils import message, log from PyQt5.QtCore import QObject, QEvent, Qt @@ -44,6 +44,7 @@ class ChildEventFilter(QObject): self._widget = widget def eventFilter(self, obj, event): + """Act on ChildAdded events.""" if event.type() == QEvent.ChildAdded: child = event.child() log.mouse.debug("{} got new child {}, installing filter".format( From 9d510d744a28c4e8cf52b6083c7bae1a9f112767 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 10 Aug 2016 20:37:38 +0200 Subject: [PATCH 12/24] tests: Implement Tab._install_event_filter --- tests/unit/browser/test_tab.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit/browser/test_tab.py b/tests/unit/browser/test_tab.py index 2f11b07ab..a313c7968 100644 --- a/tests/unit/browser/test_tab.py +++ b/tests/unit/browser/test_tab.py @@ -107,6 +107,9 @@ class Tab(browsertab.AbstractTab): self.search = browsertab.AbstractSearch(parent=self) self.printing = browsertab.AbstractPrinting() + def _install_event_filter(self): + pass + @pytest.mark.skipif(PYQT_VERSION < 0x050600, reason='Causes segfaults, see #1638') From 3336766034f3db93c4b0cdd79c2221ab4f259858 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 10 Aug 2016 20:48:33 +0200 Subject: [PATCH 13/24] Update docs --- CHANGELOG.asciidoc | 8 ++++++++ README.asciidoc | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index dd72442df..22008ea54 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -30,6 +30,8 @@ Added (to report bugs which are difficult to reproduce). - New `hide-unmatched-rapid-hints` option to not hide hint unmatched hint labels in rapid mode. +- New `{clipboard}` and `{primary}` replacements for the commandline which + replace the `:paste` command. Changed ~~~~~~~ @@ -65,6 +67,12 @@ Changed - The `:buffer` completion now also filters using the first column (id). - `:undo` has been improved to reopen tabs at the position they were closed. +Deprecated +~~~~~~~~~~ + +- The `:paste` command got deprecated as `:open` with `{clipboard}` and + `{primary}` can be used instead. + Removed ~~~~~~~ diff --git a/README.asciidoc b/README.asciidoc index 06fbb74e7..8a0b1b30b 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -145,12 +145,12 @@ Contributors, sorted by the number of commits in descending order: * Antoni Boucher * Lamar Pavel * Bruno Oliveira +* Jan Verbeek * Alexander Cogneau * Marshall Lochbaum * Jakub Klinkovský * Felix Van der Jeugt * Martin Tournoij -* Jan Verbeek * Raphael Pierzina * Joel Torstensson * Patric Schmitz From 4a14ab5c06c1973158cdcf2c6db304cf95256fd0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 10 Aug 2016 21:29:08 +0200 Subject: [PATCH 14/24] Hide all deprecated commands in src2asciidoc --- scripts/dev/src2asciidoc.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/dev/src2asciidoc.py b/scripts/dev/src2asciidoc.py index f76d8bb59..0ca72d860 100755 --- a/scripts/dev/src2asciidoc.py +++ b/scripts/dev/src2asciidoc.py @@ -327,11 +327,13 @@ def generate_commands(filename): for name, cmd in cmdutils.cmd_dict.items(): if name in cmdutils.aliases: continue + if cmd.deprecated: + continue if cmd.hide: hidden_cmds.append((name, cmd)) elif cmd.debug: debug_cmds.append((name, cmd)) - elif not cmd.deprecated: + else: normal_cmds.append((name, cmd)) normal_cmds.sort() hidden_cmds.sort() From e7584b9e1ef8cfdda9bb653dc9f7a28b52c5c641 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 10 Aug 2016 21:44:16 +0200 Subject: [PATCH 15/24] scroll.js: Add commented debug output --- qutebrowser/javascript/scroll.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/qutebrowser/javascript/scroll.js b/qutebrowser/javascript/scroll.js index 5c2e87166..769458377 100644 --- a/qutebrowser/javascript/scroll.js +++ b/qutebrowser/javascript/scroll.js @@ -35,6 +35,21 @@ window._qutebrowser.scroll = (function() { y_px = (elem.scrollHeight - elem.clientHeight) / 100 * y; } + /* + console.log(JSON.stringify({ + "x": x, + "window.scrollX": window.scrollX, + "elem.clientWidth": elem.clientWidth, + "elem.scrollWidth": elem.scrollWidth, + "x_px": x_px, + "y": y, + "window.scrollY": window.scrollY, + "elem.clientHeight": elem.clientHeight, + "elem.scrollHeight": elem.scrollHeight, + "y_px": y_px, + })); + */ + window.scroll(x_px, y_px); }; From af97f9efae084bbe9ec394c2afc205dfd84e75ee Mon Sep 17 00:00:00 2001 From: Michael Hoang Date: Thu, 11 Aug 2016 07:44:11 +1000 Subject: [PATCH 16/24] Add add_undo parameter for closing tabs By default, closed tabs should be undoable, but when a tab is detached :undo should not reopen that tab in the old window. --- qutebrowser/browser/commands.py | 2 +- qutebrowser/mainwindow/tabbedbrowser.py | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 699546c6e..2964b6113 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -427,7 +427,7 @@ class CommandDispatcher: url = self._current_url() self._open(url, window=True) cur_widget = self._current_widget() - self._tabbed_browser.close_tab(cur_widget) + self._tabbed_browser.close_tab(cur_widget, add_undo=False) def _back_forward(self, tab, bg, window, count, forward): """Helper function for :back/:forward.""" diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index 9fa1dcf59..b5dfcb5d9 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -217,11 +217,12 @@ class TabbedBrowser(tabwidget.TabWidget): for tab in self.widgets(): self._remove_tab(tab) - def close_tab(self, tab): + def close_tab(self, tab, add_undo=True): """Close a tab. Args: tab: The QWebView to be closed. + add_undo: Whether the tab close can be undone. """ last_close = config.get('tabs', 'last-close') count = self.count() @@ -229,7 +230,7 @@ class TabbedBrowser(tabwidget.TabWidget): if last_close == 'ignore' and count == 1: return - self._remove_tab(tab) + self._remove_tab(tab, add_undo=add_undo) if count == 1: # We just closed the last tab above. if last_close == 'close': @@ -243,11 +244,12 @@ class TabbedBrowser(tabwidget.TabWidget): url = config.get('general', 'default-page') self.openurl(url, newtab=True) - def _remove_tab(self, tab): + def _remove_tab(self, tab, add_undo=True): """Remove a tab from the tab list and delete it properly. Args: tab: The QWebView to be closed. + add_undo: Whether the tab close can be undone. """ idx = self.indexOf(tab) if idx == -1: @@ -261,8 +263,9 @@ class TabbedBrowser(tabwidget.TabWidget): window=self._win_id) if tab.url().isValid(): history_data = tab.history.serialize() - entry = UndoEntry(tab.url(), history_data, idx) - self._undo_stack.append(entry) + if add_undo: + entry = UndoEntry(tab.url(), history_data, idx) + self._undo_stack.append(entry) elif tab.url().isEmpty(): # There are some good reasons why a URL could be empty # (target="_blank" with a download, see [1]), so we silently ignore From ed137c01aa987f3b8c100570d795911059294322 Mon Sep 17 00:00:00 2001 From: Michael Hoang Date: Thu, 11 Aug 2016 14:34:00 +1000 Subject: [PATCH 17/24] Make add_undo for tab close a keyword arg --- qutebrowser/mainwindow/tabbedbrowser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index b5dfcb5d9..1f0055df1 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -217,7 +217,7 @@ class TabbedBrowser(tabwidget.TabWidget): for tab in self.widgets(): self._remove_tab(tab) - def close_tab(self, tab, add_undo=True): + def close_tab(self, tab, *, add_undo=True): """Close a tab. Args: @@ -244,7 +244,7 @@ class TabbedBrowser(tabwidget.TabWidget): url = config.get('general', 'default-page') self.openurl(url, newtab=True) - def _remove_tab(self, tab, add_undo=True): + def _remove_tab(self, tab, *, add_undo=True): """Remove a tab from the tab list and delete it properly. Args: From c12aeea67051b4081ce983f5603ce64cee53eb1a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Aug 2016 11:47:37 +0200 Subject: [PATCH 18/24] tests: Move data/scroll.html to data/scroll/simple --- tests/end2end/data/hints/iframe_scroll.html | 2 +- .../data/{scroll.html => scroll/simple.html} | 0 tests/end2end/features/misc.feature | 6 +++--- tests/end2end/features/scroll.feature | 2 +- tests/end2end/features/tabs.feature | 14 +++++++------- 5 files changed, 12 insertions(+), 12 deletions(-) rename tests/end2end/data/{scroll.html => scroll/simple.html} (100%) diff --git a/tests/end2end/data/hints/iframe_scroll.html b/tests/end2end/data/hints/iframe_scroll.html index 4c3f56453..16fb0177c 100644 --- a/tests/end2end/data/hints/iframe_scroll.html +++ b/tests/end2end/data/hints/iframe_scroll.html @@ -6,6 +6,6 @@ Scrolling inside an iframe - + diff --git a/tests/end2end/data/scroll.html b/tests/end2end/data/scroll/simple.html similarity index 100% rename from tests/end2end/data/scroll.html rename to tests/end2end/data/scroll/simple.html diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index fa20d26ac..41b2e06f7 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -488,7 +488,7 @@ Feature: Various utility commands. Then the page should contain the plaintext "Local storage status: not working" Scenario: :repeat-command - Given I open data/scroll.html + Given I open data/scroll/simple.html And I run :tab-only When I run :scroll down And I run :repeat-command @@ -496,7 +496,7 @@ Feature: Various utility commands. Then the page should be scrolled vertically Scenario: :repeat-command with count - Given I open data/scroll.html + Given I open data/scroll/simple.html And I run :tab-only When I run :scroll down with count 3 And I run :scroll up @@ -504,7 +504,7 @@ Feature: Various utility commands. Then the page should not be scrolled Scenario: :repeat-command with not-normal command inbetween - Given I open data/scroll.html + Given I open data/scroll/simple.html And I run :tab-only When I run :scroll down with count 3 And I run :scroll up diff --git a/tests/end2end/features/scroll.feature b/tests/end2end/features/scroll.feature index 50c22971a..c58f40831 100644 --- a/tests/end2end/features/scroll.feature +++ b/tests/end2end/features/scroll.feature @@ -2,7 +2,7 @@ Feature: Scrolling Tests the various scroll commands. Background: - Given I open data/scroll.html + Given I open data/scroll/simple.html And I run :tab-only ## :scroll-px diff --git a/tests/end2end/features/tabs.feature b/tests/end2end/features/tabs.feature index 40c5215d4..e7ad31fc8 100644 --- a/tests/end2end/features/tabs.feature +++ b/tests/end2end/features/tabs.feature @@ -863,13 +863,13 @@ Feature: Tab management Scenario: :buffer with a matching title When I open data/title.html And I open data/search.html in a new tab - And I open data/scroll.html in a new tab + And I open data/scroll/simple.html in a new tab And I run :buffer "Searching text" And I wait for "Current tab changed, focusing " in the log Then the following tabs should be open: - data/title.html - data/search.html (active) - - data/scroll.html + - data/scroll/simple.html Scenario: :buffer with no matching title When I run :buffer "invalid title" @@ -878,11 +878,11 @@ Feature: Tab management Scenario: :buffer with matching title and two windows When I open data/title.html And I open data/search.html in a new tab - And I open data/scroll.html in a new tab + And I open data/scroll/simple.html in a new tab And I open data/caret.html in a new window And I open data/paste_primary.html in a new tab And I run :buffer "Scrolling" - And I wait for "Focus object changed: " in the log + And I wait for "Focus object changed: " in the log Then the session should look like: windows: - active: true @@ -894,7 +894,7 @@ Feature: Tab management - url: http://localhost:*/data/search.html - active: true history: - - url: http://localhost:*/data/scroll.html + - url: http://localhost:*/data/scroll/simple.html - tabs: - history: - url: http://localhost:*/data/caret.html @@ -916,7 +916,7 @@ Feature: Tab management Given I have a fresh instance When I open data/title.html And I open data/search.html in a new tab - And I open data/scroll.html in a new tab + And I open data/scroll/simple.html in a new tab And I run :open -w http://localhost:(port)/data/caret.html And I open data/paste_primary.html in a new tab And I wait until data/caret.html is loaded @@ -933,7 +933,7 @@ Feature: Tab management history: - url: http://localhost:*/data/search.html - history: - - url: http://localhost:*/data/scroll.html + - url: http://localhost:*/data/scroll/simple.html - tabs: - history: - url: http://localhost:*/data/caret.html From bb2eee6178a8bec0755913b59be1b1267d297711 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Aug 2016 11:49:56 +0200 Subject: [PATCH 19/24] Add a test for #1821 --- tests/end2end/data/scroll/no_doctype.html | 214 ++++++++++++++++++++++ tests/end2end/features/scroll.feature | 6 + 2 files changed, 220 insertions(+) create mode 100644 tests/end2end/data/scroll/no_doctype.html diff --git a/tests/end2end/data/scroll/no_doctype.html b/tests/end2end/data/scroll/no_doctype.html new file mode 100644 index 000000000..7a2ca5cfe --- /dev/null +++ b/tests/end2end/data/scroll/no_doctype.html @@ -0,0 +1,214 @@ + + + + + Scrolling without doctype + + +
+0
+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
+91
+92
+93
+94
+95
+96
+97
+98
+99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+This is a very long line so this page can be scrolled horizontally. Did you think this line would end here already? Nah, it does not. But now it will. Or will it? I think it's not long enough yet. But depending on your screen size, this is not even enough yet, so I'm typing some more gibberish here. Hopefully that helps. I'm glad if it did, I'm always happy to help. Really, you're welcome. Okay, okay, can I stop now?
+        
+ next link to test the --top-navigate argument for :scroll-page. + prev link to test the --bottom-navigate argument for :scroll-page. + + diff --git a/tests/end2end/features/scroll.feature b/tests/end2end/features/scroll.feature index c58f40831..4468ea35b 100644 --- a/tests/end2end/features/scroll.feature +++ b/tests/end2end/features/scroll.feature @@ -193,6 +193,12 @@ Feature: Scrolling Scenario: :scroll-perc with count and argument When I run :scroll-perc 0 with count 50 Then the page should be scrolled vertically + + # https://github.com/The-Compiler/qutebrowser/issues/1821 + Scenario: :scroll-perc without doctype + When I open data/scroll/no_doctype.html + And I run :scroll-perc 100 + Then the page should be scrolled vertically ## :scroll-page From aafdc225bca5393a666ac83254b57304d8eb934e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Aug 2016 11:56:11 +0200 Subject: [PATCH 20/24] tests: Use last history item to check scroll pos --- tests/end2end/features/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/end2end/features/conftest.py b/tests/end2end/features/conftest.py index d8a0b261b..6ff600f4e 100644 --- a/tests/end2end/features/conftest.py +++ b/tests/end2end/features/conftest.py @@ -497,7 +497,7 @@ def should_quit(qtbot, quteproc): def _get_scroll_values(quteproc): data = quteproc.get_session() - pos = data['windows'][0]['tabs'][0]['history'][0]['scroll-pos'] + pos = data['windows'][0]['tabs'][0]['history'][-1]['scroll-pos'] return (pos['x'], pos['y']) From a23f3a24b3068f864004017d9b865e00186754c2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Aug 2016 11:57:20 +0200 Subject: [PATCH 21/24] scroll.js: Use window.inner{Width,Height} It seems with document.documentElement.client{Height,Width} we sometimes (e.g. without Date: Thu, 11 Aug 2016 13:14:33 +0200 Subject: [PATCH 22/24] Update docs --- CHANGELOG.asciidoc | 1 + README.asciidoc | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 22008ea54..0239fe6c5 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -89,6 +89,7 @@ Fixed - `:bind` can now be used to bind to an alias (binding by editing `keys.conf` already worked before) - The command completion now updates correctly when changing aliases +- `:undo` now doesn't undo tabs "closed" by `:tab-detach` anymore. v0.8.3 (unreleased) ------------------- diff --git a/README.asciidoc b/README.asciidoc index 8a0b1b30b..7679ea734 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -192,6 +192,7 @@ Contributors, sorted by the number of commits in descending order: * Nick Ginther * Michał Góral * Michael Ilsaas +* Michael Hoang * Martin Zimmermann * Fritz Reichwald * Brian Jackson @@ -213,7 +214,6 @@ Contributors, sorted by the number of commits in descending order: * adam * Samir Benmendil * Regina Hug -* Michael Hoang * Mathias Fussenegger * Marcelo Santos * Jean-Louis Fuchs From 677bb292518c0132f10d5f630bfd5aab90285629 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Aug 2016 13:38:20 +0200 Subject: [PATCH 23/24] Document event filter attributes --- qutebrowser/browser/mouse.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/qutebrowser/browser/mouse.py b/qutebrowser/browser/mouse.py index bd2234e2a..17bd884a9 100644 --- a/qutebrowser/browser/mouse.py +++ b/qutebrowser/browser/mouse.py @@ -35,6 +35,10 @@ class ChildEventFilter(QObject): focusProxy... FIXME:qtwebengine Add a test for this happening + + Attributes: + _filter: The event filter to install. + _widget: The widget expected to send out childEvents. """ def __init__(self, eventfilter, widget, parent=None): @@ -56,7 +60,12 @@ class ChildEventFilter(QObject): class MouseEventFilter(QObject): - """Handle mouse events on a tab.""" + """Handle mouse events on a tab. + + Attributes: + _tab: The browsertab object this filter is installed on. + _handlers: A dict of handler functions for the handled events. + """ def __init__(self, tab, parent=None): super().__init__(parent) From 1a94cb551c829f9d8db37a07d9ea476e715aa94c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Aug 2016 13:50:06 +0200 Subject: [PATCH 24/24] Move mouse wheel zooming to MouseEventHandler --- qutebrowser/browser/browsertab.py | 14 ------------- qutebrowser/browser/mouse.py | 26 ++++++++++++++++++++++++ qutebrowser/browser/webengine/webview.py | 14 ------------- qutebrowser/browser/webkit/webview.py | 24 ---------------------- tests/unit/browser/test_tab.py | 14 ++++--------- 5 files changed, 30 insertions(+), 62 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index eda0f62ee..39d9de210 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -220,19 +220,6 @@ class AbstractZoom(QObject): default_zoom = config.get('ui', 'default-zoom') self._set_factor_internal(float(default_zoom) / 100) - @pyqtSlot(QPoint) - def _on_mouse_wheel_zoom(self, delta): - """Handle zooming via mousewheel requested by the web view.""" - divider = config.get('input', 'mouse-zoom-divider') - factor = self.factor() + delta.y() / divider - if factor < 0: - return - perc = int(100 * factor) - message.info(self._win_id, "Zoom level: {}%".format(perc)) - self._neighborlist.fuzzyval = perc - self._set_factor_internal(factor) - self._default_zoom_changed = True - class AbstractCaret(QObject): @@ -495,7 +482,6 @@ class AbstractTab(QWidget): self.zoom._widget = widget self.search._widget = widget self.printing._widget = widget - widget.mouse_wheel_zoom.connect(self.zoom._on_mouse_wheel_zoom) self._install_event_filter() def _install_event_filter(self): diff --git a/qutebrowser/browser/mouse.py b/qutebrowser/browser/mouse.py index 17bd884a9..6c7a1c25d 100644 --- a/qutebrowser/browser/mouse.py +++ b/qutebrowser/browser/mouse.py @@ -65,6 +65,7 @@ class MouseEventFilter(QObject): Attributes: _tab: The browsertab object this filter is installed on. _handlers: A dict of handler functions for the handled events. + _ignore_wheel_event: Whether to ignore the next wheelEvent. """ def __init__(self, tab, parent=None): @@ -72,7 +73,9 @@ class MouseEventFilter(QObject): self._tab = tab self._handlers = { QEvent.MouseButtonPress: self._handle_mouse_press, + QEvent.Wheel: self._handle_wheel, } + self._ignore_wheel_event = False def _handle_mouse_press(self, _obj, e): """Handle pressing of a mouse button.""" @@ -82,6 +85,29 @@ class MouseEventFilter(QObject): if e.button() in [Qt.XButton1, Qt.XButton2] or is_rocker_gesture: self._mousepress_backforward(e) return True + self._ignore_wheel_event = True + return False + + def _handle_wheel(self, _obj, e): + """Zoom on Ctrl-Mousewheel. + + Args: + e: The QWheelEvent. + """ + if self._ignore_wheel_event: + # See https://github.com/The-Compiler/qutebrowser/issues/395 + self._ignore_wheel_event = False + return True + + if e.modifiers() & Qt.ControlModifier: + divider = config.get('input', 'mouse-zoom-divider') + factor = self._tab.zoom.factor() + (e.angleDelta().y() / divider) + if factor < 0: + return False + perc = int(100 * factor) + message.info(self._tab.win_id, "Zoom level: {}%".format(perc)) + self._tab.zoom.set_factor(factor) + return False def _mousepress_backforward(self, e): diff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py index a4c33b6cb..ddf431744 100644 --- a/qutebrowser/browser/webengine/webview.py +++ b/qutebrowser/browser/webengine/webview.py @@ -33,24 +33,10 @@ class WebEngineView(QWebEngineView): """Custom QWebEngineView subclass with qutebrowser-specific features.""" - mouse_wheel_zoom = pyqtSignal(QPoint) - def __init__(self, parent=None): super().__init__(parent) self.setPage(WebEnginePage(self)) - def wheelEvent(self, e): - """Zoom on Ctrl-Mousewheel. - - Args: - e: The QWheelEvent. - """ - if e.modifiers() & Qt.ControlModifier: - e.accept() - self.mouse_wheel_zoom.emit(e.angleDelta()) - else: - super().wheelEvent(e) - class WebEnginePage(QWebEnginePage): diff --git a/qutebrowser/browser/webkit/webview.py b/qutebrowser/browser/webkit/webview.py index 1d3753510..fa1361e08 100644 --- a/qutebrowser/browser/webkit/webview.py +++ b/qutebrowser/browser/webkit/webview.py @@ -47,22 +47,16 @@ class WebView(QWebView): _old_scroll_pos: The old scroll position. _check_insertmode: If True, in mouseReleaseEvent we should check if we need to enter/leave insert mode. - _ignore_wheel_event: Ignore the next wheel event. - See https://github.com/The-Compiler/qutebrowser/issues/395 Signals: scroll_pos_changed: Scroll percentage of current tab changed. arg 1: x-position in %. arg 2: y-position in %. - mouse_wheel_zoom: Emitted when the page should be zoomed because the - mousewheel was used with ctrl. - arg 1: The angle delta of the wheel event (QPoint) shutting_down: Emitted when the view is shutting down. """ scroll_pos_changed = pyqtSignal(int, int) shutting_down = pyqtSignal() - mouse_wheel_zoom = pyqtSignal(QPoint) def __init__(self, win_id, tab_id, tab, parent=None): super().__init__(parent) @@ -75,7 +69,6 @@ class WebView(QWebView): self._check_insertmode = False self.scroll_pos = (-1, -1) self._old_scroll_pos = (-1, -1) - self._ignore_wheel_event = False self._set_bg_color() self._tab_id = tab_id @@ -388,7 +381,6 @@ class WebView(QWebView): """ self._mousepress_insertmode(e) self._mousepress_opentarget(e) - self._ignore_wheel_event = True super().mousePressEvent(e) def mouseReleaseEvent(self, e): @@ -404,19 +396,3 @@ class WebView(QWebView): self.shutting_down.connect(menu.close) modeman.instance(self.win_id).entered.connect(menu.close) menu.exec_(e.globalPos()) - - def wheelEvent(self, e): - """Zoom on Ctrl-Mousewheel. - - Args: - e: The QWheelEvent. - """ - if self._ignore_wheel_event: - self._ignore_wheel_event = False - # See https://github.com/The-Compiler/qutebrowser/issues/395 - return - if e.modifiers() & Qt.ControlModifier: - e.accept() - self.mouse_wheel_zoom.emit(e.angleDelta()) - else: - super().wheelEvent(e) diff --git a/tests/unit/browser/test_tab.py b/tests/unit/browser/test_tab.py index a313c7968..d23be3346 100644 --- a/tests/unit/browser/test_tab.py +++ b/tests/unit/browser/test_tab.py @@ -19,7 +19,7 @@ import pytest -from PyQt5.QtCore import PYQT_VERSION, pyqtSignal, QPoint +from PyQt5.QtCore import PYQT_VERSION, pyqtSignal from qutebrowser.browser import browsertab from qutebrowser.keyinput import modeman @@ -29,22 +29,16 @@ pytestmark = pytest.mark.usefixtures('redirect_xdg_data') try: from PyQt5.QtWebKitWidgets import QWebView - - class WebView(QWebView): - mouse_wheel_zoom = pyqtSignal(QPoint) except ImportError: - WebView = None + QWebView = None try: from PyQt5.QtWebEngineWidgets import QWebEngineView - - class WebEngineView(QWebEngineView): - mouse_wheel_zoom = pyqtSignal(QPoint) except ImportError: - WebEngineView = None + QWebEngineView = None -@pytest.fixture(params=[WebView, WebEngineView]) +@pytest.fixture(params=[QWebView, QWebEngineView]) def view(qtbot, config_stub, request): config_stub.data = { 'input': {