From 341708f54394c68b61c081de84e7d48359c4d0e0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 19 May 2015 12:29:53 +0200 Subject: [PATCH 01/51] Refactor readline tests. They now use a real QLineEdit and verify a lot more. See #660, #678. --- tests/misc/test_readline.py | 338 ++++++++++++++++++++++++------------ 1 file changed, 225 insertions(+), 113 deletions(-) diff --git a/tests/misc/test_readline.py b/tests/misc/test_readline.py index 523c6f579..da2d05821 100644 --- a/tests/misc/test_readline.py +++ b/tests/misc/test_readline.py @@ -21,144 +21,256 @@ # pylint: disable=protected-access +import re import inspect -from unittest import mock -from PyQt5.QtWidgets import QLineEdit +from PyQt5.QtWidgets import QLineEdit, QApplication import pytest from qutebrowser.misc import readline +# Some functions aren't 100% readline compatible: +# https://github.com/The-Compiler/qutebrowser/issues/678 +# Those are marked with fixme and have another value marked with '# wrong' +# which marks the current behavior. + +fixme = pytest.mark.xfail(reason='readline compatibility - see #678') + + +class LineEdit(QLineEdit): + + """QLineEdit with some methods to make testing easier.""" + + def _get_index(self, haystack, needle): + """Get the index of a char (needle) in a string (haystack). + + Return: + The position where needle was found, or None if it wasn't found. + """ + try: + return haystack.index(needle) + except ValueError: + return None + + def set_aug_text(self, text): + """Set a text with markers for selected text and | as cursor.""" + real_text = re.sub('[<>|]', '', text) + self.setText(real_text) + + cursor_pos = self._get_index(text, '|') + sel_start_pos = self._get_index(text, '<') + sel_end_pos = self._get_index(text, '>') + + if sel_start_pos is not None and sel_end_pos is None: + raise ValueError("< given without >!") + if sel_start_pos is None and sel_end_pos is not None: + raise ValueError("> given without !") + self.setCursorPosition(cursor_pos) + elif sel_start_pos is not None: + if sel_start_pos > sel_end_pos: + raise ValueError("< given after >!") + sel_len = sel_end_pos - sel_start_pos - 1 + self.setSelection(sel_start_pos, sel_len) + + def aug_text(self): + """Get a text with markers for selected text and | as cursor.""" + text = self.text() + chars = list(text) + cur_pos = self.cursorPosition() + assert cur_pos >= 0 + chars.insert(cur_pos, '|') + if self.hasSelectedText(): + selected_text = self.selectedText() + sel_start = self.selectionStart() + sel_end = sel_start + len(selected_text) + assert sel_start > 0 + assert sel_end > 0 + assert sel_end > sel_start + assert cur_pos == sel_end + assert text[sel_start:sel_end] == selected_text + chars.insert(sel_start, '<') + chars.insert(sel_end + 1, '>') + return ''.join(chars) + + @pytest.fixture -def mocked_qapp(monkeypatch, stubs): - """Fixture that mocks readline.QApplication and returns it.""" - stub = stubs.FakeQApplication() - monkeypatch.setattr('qutebrowser.misc.readline.QApplication', stub) - return stub +def lineedit(qtbot, monkeypatch): + """Fixture providing a LineEdit.""" + le = LineEdit() + qtbot.add_widget(le) + monkeypatch.setattr(QApplication.instance(), 'focusWidget', lambda: le) + return le -class TestNoneWidget: - - """Test if there are no exceptions when the widget is None.""" - - def test_none(self, mocked_qapp): - """Call each rl_* method with a None focusWidget.""" - self.bridge = readline.ReadlineBridge() - mocked_qapp.focusWidget = mock.Mock(return_value=None) - for name, method in inspect.getmembers(self.bridge, inspect.ismethod): - if name.startswith('rl_'): - method() +@pytest.fixture +def bridge(): + """Fixture providing a ReadlineBridge.""" + return readline.ReadlineBridge() -class TestReadlineBridgeTest: +def test_none(bridge, qtbot): + """Call each rl_* method with a None focusWidget.""" + assert QApplication.instance().focusWidget() is None + for name, method in inspect.getmembers(bridge, inspect.ismethod): + if name.startswith('rl_'): + method() - """Tests for readline bridge.""" - @pytest.fixture(autouse=True) - def setup(self): - self.qle = mock.Mock() - self.qle.__class__ = QLineEdit - self.bridge = readline.ReadlineBridge() +@pytest.mark.parametrize('text, expected', [('fbar', 'fo|obar'), + ('|foobar', '|foobar')]) +def test_rl_backward_char(text, expected, lineedit, bridge): + """Test rl_backward_char.""" + lineedit.set_aug_text(text) + bridge.rl_backward_char() + assert lineedit.aug_text() == expected - def _set_selected_text(self, text): - """Set the value the fake QLineEdit should return for selectedText.""" - self.qle.configure_mock(**{'selectedText.return_value': text}) - def test_rl_backward_char(self, mocked_qapp): - """Test rl_backward_char.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self.bridge.rl_backward_char() - self.qle.cursorBackward.assert_called_with(False) +@pytest.mark.parametrize('text, expected', [('fbar', 'foob|ar'), + ('foobar|', 'foobar|')]) +def test_rl_forward_char(text, expected, lineedit, bridge): + """Test rl_forward_char.""" + lineedit.set_aug_text(text) + bridge.rl_forward_char() + assert lineedit.aug_text() == expected - def test_rl_forward_char(self, mocked_qapp): - """Test rl_forward_char.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self.bridge.rl_forward_char() - self.qle.cursorForward.assert_called_with(False) - def test_rl_backward_word(self, mocked_qapp): - """Test rl_backward_word.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self.bridge.rl_backward_word() - self.qle.cursorWordBackward.assert_called_with(False) +@pytest.mark.parametrize('text, expected', [('one o', 'one |two'), + ('two', '|one two'), + ('|one two', '|one two')]) +def test_rl_backward_word(text, expected, lineedit, bridge): + """Test rl_backward_word.""" + lineedit.set_aug_text(text) + bridge.rl_backward_word() + assert lineedit.aug_text() == expected - def test_rl_forward_word(self, mocked_qapp): - """Test rl_forward_word.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self.bridge.rl_forward_word() - self.qle.cursorWordForward.assert_called_with(False) - def test_rl_beginning_of_line(self, mocked_qapp): - """Test rl_beginning_of_line.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self.bridge.rl_beginning_of_line() - self.qle.home.assert_called_with(False) +@pytest.mark.parametrize('text, expected', [ + fixme(('ne two', 'one| two')), + ('ne two', 'one |two'), # wrong + fixme((' two', 'one two|')), + (' two', 'one |two'), # wrong + ('one t', 'one two|') +]) +def test_rl_forward_word(text, expected, lineedit, bridge): + """Test rl_forward_word.""" + lineedit.set_aug_text(text) + bridge.rl_forward_word() + assert lineedit.aug_text() == expected - def test_rl_end_of_line(self, mocked_qapp): - """Test rl_end_of_line.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self.bridge.rl_end_of_line() - self.qle.end.assert_called_with(False) - def test_rl_delete_char(self, mocked_qapp): - """Test rl_delete_char.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self.bridge.rl_delete_char() - self.qle.del_.assert_called_with() +def test_rl_beginning_of_line(lineedit, bridge): + """Test rl_beginning_of_line.""" + lineedit.set_aug_text('fbar') + bridge.rl_beginning_of_line() + assert lineedit.aug_text() == '|foobar' - def test_rl_backward_delete_char(self, mocked_qapp): - """Test rl_backward_delete_char.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self.bridge.rl_backward_delete_char() - self.qle.backspace.assert_called_with() - def test_rl_unix_line_discard(self, mocked_qapp): - """Set a selected text, delete it, see if it comes back with yank.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self._set_selected_text("delete test") - self.bridge.rl_unix_line_discard() - self.qle.home.assert_called_with(True) - assert self.bridge._deleted[self.qle] == "delete test" - self.qle.del_.assert_called_with() - self.bridge.rl_yank() - self.qle.insert.assert_called_with("delete test") +def test_rl_end_of_line(lineedit, bridge): + """Test rl_end_of_line.""" + lineedit.set_aug_text('fbar') + bridge.rl_end_of_line() + assert lineedit.aug_text() == 'foobar|' - def test_rl_kill_line(self, mocked_qapp): - """Set a selected text, delete it, see if it comes back with yank.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self._set_selected_text("delete test") - self.bridge.rl_kill_line() - self.qle.end.assert_called_with(True) - assert self.bridge._deleted[self.qle] == "delete test" - self.qle.del_.assert_called_with() - self.bridge.rl_yank() - self.qle.insert.assert_called_with("delete test") - def test_rl_unix_word_rubout(self, mocked_qapp): - """Set a selected text, delete it, see if it comes back with yank.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self._set_selected_text("delete test") - self.bridge.rl_unix_word_rubout() - self.qle.cursorWordBackward.assert_called_with(True) - assert self.bridge._deleted[self.qle] == "delete test" - self.qle.del_.assert_called_with() - self.bridge.rl_yank() - self.qle.insert.assert_called_with("delete test") +@pytest.mark.parametrize('text, expected', [('foo|bar', 'foo|ar'), + ('foobar|', 'foobar|'), + ('|foobar', '|oobar'), + ('fbar', 'f|bar')]) +def test_rl_delete_char(text, expected, lineedit, bridge): + """Test rl_delete_char.""" + lineedit.set_aug_text(text) + bridge.rl_delete_char() + assert lineedit.aug_text() == expected - def test_rl_kill_word(self, mocked_qapp): - """Set a selected text, delete it, see if it comes back with yank.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self._set_selected_text("delete test") - self.bridge.rl_kill_word() - self.qle.cursorWordForward.assert_called_with(True) - assert self.bridge._deleted[self.qle] == "delete test" - self.qle.del_.assert_called_with() - self.bridge.rl_yank() - self.qle.insert.assert_called_with("delete test") - def test_rl_yank_no_text(self, mocked_qapp): - """Test yank without having deleted anything.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self.bridge.rl_yank() - assert not self.qle.insert.called +@pytest.mark.parametrize('text, expected', [('foo|bar', 'fo|bar'), + ('foobar|', 'fooba|'), + ('|foobar', '|foobar'), + ('fbar', 'f|bar')]) +def test_rl_backward_delete_char(text, expected, lineedit, bridge): + """Test rl_backward_delete_char.""" + lineedit.set_aug_text(text) + bridge.rl_backward_delete_char() + assert lineedit.aug_text() == expected + + +@pytest.mark.parametrize('text, deleted, rest', [ + ('delete this| test', 'delete this', '| test'), + fixme(('delete test', 'delete this', '| test')), + ('delete test', 'delete ', '|this test'), # wrong + fixme(('fbar', 'foo', '|bar')), + ('fbar', 'f', '|oobar'), # wrong +]) +def test_rl_unix_line_discard(lineedit, bridge, text, deleted, rest): + """Delete from the cursor to the beginning of the line and yank back.""" + lineedit.set_aug_text(text) + bridge.rl_unix_line_discard() + assert bridge._deleted[lineedit] == deleted + assert lineedit.aug_text() == rest + lineedit.clear() + bridge.rl_yank() + assert lineedit.aug_text() == deleted + '|' + + +@pytest.mark.parametrize('text, deleted, rest', [ + ('test |delete this', 'delete this', 'test |'), + fixme(('delete this', 'test delete this', 'test |')), + ('delete this', 'test delete this', '|'), # wrong +]) +def test_rl_kill_line(lineedit, bridge, text, deleted, rest): + """Delete from the cursor to the end of line and yank back.""" + lineedit.set_aug_text(text) + bridge.rl_kill_line() + assert bridge._deleted[lineedit] == deleted + assert lineedit.aug_text() == rest + lineedit.clear() + bridge.rl_yank() + assert lineedit.aug_text() == deleted + '|' + + +@pytest.mark.parametrize('text, deleted, rest', [ + ('test delete|foobar', 'delete', 'test |foobar'), + ('test delete |foobar', 'delete ', 'test |foobar'), + fixme(('test delfoobar', 'delete', 'test |foobar')), + ('test delfoobar', 'del', 'test |ete foobar'), # wrong +]) +def test_rl_unix_word_rubout(lineedit, bridge, text, deleted, rest): + """Delete to word beginning and see if it comes back with yank.""" + lineedit.set_aug_text(text) + bridge.rl_unix_word_rubout() + assert bridge._deleted[lineedit] == deleted + assert lineedit.aug_text() == rest + lineedit.clear() + bridge.rl_yank() + assert lineedit.aug_text() == deleted + '|' + + +@pytest.mark.parametrize('text, deleted, rest', [ + fixme(('test foobar| delete', ' delete', 'test foobar|')), + ('test foobar| delete', ' ', 'test foobar|delete'), # wrong + fixme(('test foo|delete bar', 'delete', 'test foo| bar')), + ('test foo|delete bar', 'delete ', 'test foo|bar'), # wrong + fixme(('test foo delete', ' delete', 'test foobar|')), + ('test foodelete', 'bardelete', 'test foo|'), # wrong +]) +def test_rl_kill_word(lineedit, bridge, text, deleted, rest): + """Delete to word end and see if it comes back with yank.""" + lineedit.set_aug_text(text) + bridge.rl_kill_word() + assert bridge._deleted[lineedit] == deleted + assert lineedit.aug_text() == rest + lineedit.clear() + bridge.rl_yank() + assert lineedit.aug_text() == deleted + '|' + + +def test_rl_yank_no_text(lineedit, bridge): + """Test yank without having deleted anything.""" + lineedit.clear() + bridge.rl_yank() + assert lineedit.aug_text() == '|' From c7dcaff025f98aa72d428ee0c3a7844e0498d001 Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Fri, 29 May 2015 18:35:15 +0200 Subject: [PATCH 02/51] Add navigate option to scroll_page() So you can scroll down & navigate when you're at the bottom. To bind this to space: scroll-page 0 1 next Not sure if it's a good idea to bind this by default? May surprise some people... See #696 --- qutebrowser/browser/commands.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 0c01260d7..8c29ad317 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -643,14 +643,23 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', hide=True, scope='window', count='count') - def scroll_page(self, x: {'type': float}, y: {'type': float}, count=1): + def scroll_page(self, x: {'type': float}, y: {'type': float}, + navigate=None, count=1): """Scroll the frame page-wise. Args: x: How many pages to scroll to the right. y: How many pages to scroll down. + navigate: :navigate to the next page on bottom count: multiplier """ + frame = self._current_widget().page().currentFrame() + if (navigate is not None and + frame.scrollPosition().y() >= + frame.scrollBarMaximum(Qt.Vertical)): + self.navigate(navigate) + return + mult_x = count * x mult_y = count * y if mult_y.is_integer(): @@ -663,7 +672,6 @@ class CommandDispatcher: mult_y = 0 if mult_x == 0 and mult_y == 0: return - frame = self._current_widget().page().currentFrame() size = frame.geometry() dx = mult_x * size.width() dy = mult_y * size.height() From 8c80f99a32363ebc2b139e7c0074e4c165d992b3 Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Fri, 29 May 2015 21:18:44 +0200 Subject: [PATCH 03/51] Improve navigate option to scroll_page() --- qutebrowser/browser/commands.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 8c29ad317..735deba47 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -643,21 +643,27 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', hide=True, scope='window', count='count') - def scroll_page(self, x: {'type': float}, y: {'type': float}, - navigate=None, count=1): + def scroll_page(self, x: {'type': float}, y: {'type': float}, *, + top_navigate: {'type': ('prev', 'decrement')}=None, + bottom_navigate: {'type': ('next', 'increment')}=None, + count=1): """Scroll the frame page-wise. Args: x: How many pages to scroll to the right. y: How many pages to scroll down. - navigate: :navigate to the next page on bottom + bottom_navigate: :navigate to the next page on bottom + top_navigate: :navigate to the previous page on top count: multiplier """ frame = self._current_widget().page().currentFrame() - if (navigate is not None and + if (bottom_navigate is not None and frame.scrollPosition().y() >= frame.scrollBarMaximum(Qt.Vertical)): - self.navigate(navigate) + self.navigate(bottom_navigate) + return + elif top_navigate is not None and frame.scrollPosition().y() == 0: + self.navigate(top_navigate) return mult_x = count * x From 9c99c22f1b4edee2d43efe2365beca34a275fbd9 Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Fri, 29 May 2015 23:49:48 +0200 Subject: [PATCH 04/51] Fix issue #701 --- qutebrowser/browser/commands.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 735deba47..337823122 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -657,6 +657,9 @@ class CommandDispatcher: count: multiplier """ frame = self._current_widget().page().currentFrame() + if not frame.url().isValid() == '': # Issue 701 + return + if (bottom_navigate is not None and frame.scrollPosition().y() >= frame.scrollBarMaximum(Qt.Vertical)): From 70956aaeca3df4bf0c9842fe443202370f683ff8 Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Fri, 29 May 2015 23:57:57 +0200 Subject: [PATCH 05/51] oops --- qutebrowser/browser/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 337823122..5318bdc7d 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -657,7 +657,7 @@ class CommandDispatcher: count: multiplier """ frame = self._current_widget().page().currentFrame() - if not frame.url().isValid() == '': # Issue 701 + if not frame.url().isValid(): # Issue 701 return if (bottom_navigate is not None and From 6ca541d359b5d1d3cd088efc65683cce6bbb35cc Mon Sep 17 00:00:00 2001 From: Antoni Boucher Date: Sat, 30 May 2015 10:37:25 -0400 Subject: [PATCH 06/51] Fixed issue #401. --- qutebrowser/browser/commands.py | 6 ++++++ qutebrowser/config/configdata.py | 1 + 2 files changed, 7 insertions(+) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 0c01260d7..9f836d36a 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -985,6 +985,12 @@ class CommandDispatcher: url = objreg.get('quickmark-manager').get(name) self._open(url, tab, bg, window) + @cmdutils.register(instance='command-dispatcher', name='select-follow', scope='window') + def select_follow(self): + """Follow the selected text.""" + widget = self._current_widget() + widget.page().currentFrame().evaluateJavaScript('window.getSelection().anchorNode.parentNode.click()') + @cmdutils.register(instance='command-dispatcher', name='inspector', scope='window') def toggle_inspector(self): diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 02b8c6008..135e91e81 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1239,6 +1239,7 @@ KEY_DATA = collections.OrderedDict([ ('stop', ['']), ('print', ['']), ('open qute:settings', ['Ss']), + ('select-follow', ['', '']), ])), ('insert', collections.OrderedDict([ From a56a14fb705bf77f35f0e360fc7b8159672c158a Mon Sep 17 00:00:00 2001 From: Antoni Boucher Date: Sat, 30 May 2015 13:15:53 -0400 Subject: [PATCH 07/51] Added the possibility to open a selected link in a new tab. --- qutebrowser/browser/commands.py | 10 ++++++++-- qutebrowser/config/configdata.py | 3 ++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 9f836d36a..bc3931a65 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -986,9 +986,15 @@ class CommandDispatcher: self._open(url, tab, bg, window) @cmdutils.register(instance='command-dispatcher', name='select-follow', scope='window') - def select_follow(self): - """Follow the selected text.""" + def select_follow(self, tab=False): + """Follow the selected text. + + Args: + tab: Load the selected link in a new tab. + """ widget = self._current_widget() + if tab: + widget.page().open_target = usertypes.ClickTarget.tab widget.page().currentFrame().evaluateJavaScript('window.getSelection().anchorNode.parentNode.click()') @cmdutils.register(instance='command-dispatcher', name='inspector', diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 135e91e81..dec02b069 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1239,7 +1239,8 @@ KEY_DATA = collections.OrderedDict([ ('stop', ['']), ('print', ['']), ('open qute:settings', ['Ss']), - ('select-follow', ['', '']), + ('select-follow', ['']), + ('select-follow -t', ['']), ])), ('insert', collections.OrderedDict([ From 989e3b7291f34ec7f560a1293124aab71ba9aa62 Mon Sep 17 00:00:00 2001 From: Antoni Boucher Date: Sat, 30 May 2015 13:56:36 -0400 Subject: [PATCH 08/51] Added a fallback for when JavaScript is disabled. --- qutebrowser/browser/commands.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index bc3931a65..17ef07196 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -25,7 +25,9 @@ import shlex import subprocess import posixpath import functools +from xml.etree import ElementTree +from PyQt5.QtWebKit import QWebSettings from PyQt5.QtWidgets import QApplication, QTabBar from PyQt5.QtCore import Qt, QUrl, QEvent from PyQt5.QtGui import QClipboard, QKeyEvent @@ -985,7 +987,8 @@ class CommandDispatcher: url = objreg.get('quickmark-manager').get(name) self._open(url, tab, bg, window) - @cmdutils.register(instance='command-dispatcher', name='select-follow', scope='window') + @cmdutils.register(instance='command-dispatcher', name='select-follow', + scope='window') def select_follow(self, tab=False): """Follow the selected text. @@ -993,9 +996,19 @@ class CommandDispatcher: tab: Load the selected link in a new tab. """ widget = self._current_widget() - if tab: - widget.page().open_target = usertypes.ClickTarget.tab - widget.page().currentFrame().evaluateJavaScript('window.getSelection().anchorNode.parentNode.click()') + if QWebSettings.globalSettings().testAttribute( + QWebSettings.JavascriptEnabled): + if tab: + widget.page().open_target = usertypes.ClickTarget.tab + widget.page().currentFrame().evaluateJavaScript( + 'window.getSelection().anchorNode.parentNode.click()') + else: + selected_element = ElementTree.fromstring( + '' + widget.selectedHtml() + '').find('a') + if selected_element is not None: + url = selected_element.attrib['href'] + if url: + self._open(QUrl(url), tab) @cmdutils.register(instance='command-dispatcher', name='inspector', scope='window') From b1f8a70c02c5af72abff64d00b0a1715d24f378e Mon Sep 17 00:00:00 2001 From: Antoni Boucher Date: Sat, 30 May 2015 18:03:39 -0400 Subject: [PATCH 09/51] Added try/except for parse error. --- qutebrowser/browser/commands.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 17ef07196..7a4345176 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1003,12 +1003,15 @@ class CommandDispatcher: widget.page().currentFrame().evaluateJavaScript( 'window.getSelection().anchorNode.parentNode.click()') else: - selected_element = ElementTree.fromstring( - '' + widget.selectedHtml() + '').find('a') - if selected_element is not None: - url = selected_element.attrib['href'] - if url: - self._open(QUrl(url), tab) + try: + selected_element = ElementTree.fromstring( + '' + widget.selectedHtml() + '').find('a') + if selected_element is not None: + url = selected_element.attrib['href'] + if url: + self._open(QUrl(url), tab) + except ElementTree.ParseError: + pass @cmdutils.register(instance='command-dispatcher', name='inspector', scope='window') From 11b258568d7d29a722ae9b57cefbcce2add11db8 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 31 May 2015 15:02:09 +0200 Subject: [PATCH 10/51] Improve docstring. --- 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 5318bdc7d..8f54ed76b 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -652,8 +652,10 @@ class CommandDispatcher: Args: x: How many pages to scroll to the right. y: How many pages to scroll down. - bottom_navigate: :navigate to the next page on bottom - top_navigate: :navigate to the previous page on top + bottom_navigate: :navigate action (next, increment) to run when + scrolling down at the bottom of the page. + top_navigate: :navigate action (prev, decrement) to run when + scrolling up at the top of the page. count: multiplier """ frame = self._current_widget().page().currentFrame() From cdde1d7dfcb8a07b6d289d932269437f53a4435a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 31 May 2015 15:10:12 +0200 Subject: [PATCH 11/51] command: Add support for custom metavar for docs. --- qutebrowser/commands/command.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 998c4a892..d55597d9d 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -61,7 +61,8 @@ class Command: """ AnnotationInfo = collections.namedtuple('AnnotationInfo', - ['kwargs', 'type', 'flag', 'hide']) + ['kwargs', 'type', 'flag', 'hide', + 'metavar']) def __init__(self, *, handler, name, instance=None, maxsplit=None, hide=False, completion=None, modes=None, not_modes=None, @@ -257,10 +258,10 @@ class Command: pass if isinstance(typ, tuple): - pass + kwargs['metavar'] = annotation_info.metavar or param.name elif utils.is_enum(typ): kwargs['choices'] = [e.name.replace('_', '-') for e in typ] - kwargs['metavar'] = param.name + kwargs['metavar'] = annotation_info.metavar or param.name elif typ is bool: kwargs['action'] = 'store_true' elif typ is not None: @@ -322,11 +323,12 @@ class Command: flag: The short name/flag if overridden. name: The long name if overridden. """ - info = {'kwargs': {}, 'type': None, 'flag': None, 'hide': False} + info = {'kwargs': {}, 'type': None, 'flag': None, 'hide': False, + 'metavar': None} if param.annotation is not inspect.Parameter.empty: log.commands.vdebug("Parsing annotation {}".format( param.annotation)) - for field in ('type', 'flag', 'name', 'hide'): + for field in ('type', 'flag', 'name', 'hide', 'metavar'): if field in param.annotation: info[field] = param.annotation[field] if 'nargs' in param.annotation: From 6b550defaee70d7817b4d58c2a864486856bfe3e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 31 May 2015 15:10:35 +0200 Subject: [PATCH 12/51] scroll-page: Add custom metavar for navigate-*. --- 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 8f54ed76b..664cd752f 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -644,8 +644,10 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', hide=True, scope='window', count='count') def scroll_page(self, x: {'type': float}, y: {'type': float}, *, - top_navigate: {'type': ('prev', 'decrement')}=None, - bottom_navigate: {'type': ('next', 'increment')}=None, + top_navigate: {'type': ('prev', 'decrement'), + 'metavar': 'ACTION'}=None, + bottom_navigate: {'type': ('next', 'increment'), + 'metavar': 'ACTION'}=None, count=1): """Scroll the frame page-wise. From 1814b672d7e6e2504a24eab0df3f091abec18a1e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 31 May 2015 15:11:04 +0200 Subject: [PATCH 13/51] Regenerate docs. --- README.asciidoc | 2 +- doc/help/commands.asciidoc | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/README.asciidoc b/README.asciidoc index c651ffc33..437ab8734 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -141,10 +141,10 @@ Contributors, sorted by the number of commits in descending order: * Artur Shaik * ZDarian * Peter Vilim +* Martin Tournoij * John ShaggyTwoDope Jenkins * Jimmy * Zach-Button -* Martin Tournoij * rikn00 * Patric Schmitz * Martin Zimmermann diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index c33bccaaf..fa835f53a 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -1009,7 +1009,7 @@ multiplier [[scroll-page]] === scroll-page -Syntax: +:scroll-page 'x' 'y'+ +Syntax: +:scroll-page [*--top-navigate* 'ACTION'] [*--bottom-navigate* 'ACTION'] 'x' 'y'+ Scroll the frame page-wise. @@ -1017,6 +1017,12 @@ Scroll the frame page-wise. * +'x'+: How many pages to scroll to the right. * +'y'+: How many pages to scroll down. +==== optional arguments +* +*-t*+, +*--top-navigate*+: :navigate action (prev, decrement) to run when scrolling up at the top of the page. + +* +*-b*+, +*--bottom-navigate*+: :navigate action (next, increment) to run when scrolling down at the bottom of the page. + + ==== count multiplier From 54c1cd7c05e5780460a117c83a9e8c9e77e71e7b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 31 May 2015 15:11:37 +0200 Subject: [PATCH 14/51] Add link to issue. --- qutebrowser/browser/commands.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 664cd752f..2c008fd9e 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -661,7 +661,8 @@ class CommandDispatcher: count: multiplier """ frame = self._current_widget().page().currentFrame() - if not frame.url().isValid(): # Issue 701 + if not frame.url().isValid(): + # See https://github.com/The-Compiler/qutebrowser/issues/701 return if (bottom_navigate is not None and From 4ff9d585ea77d5438fa398e9269afb4ead623e8e Mon Sep 17 00:00:00 2001 From: Antoni Boucher Date: Sun, 31 May 2015 11:56:27 -0400 Subject: [PATCH 15/51] Fixed to use qualified import. --- qutebrowser/browser/commands.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 7a4345176..deafedb75 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -25,7 +25,7 @@ import shlex import subprocess import posixpath import functools -from xml.etree import ElementTree +import xml.etree.ElementTree from PyQt5.QtWebKit import QWebSettings from PyQt5.QtWidgets import QApplication, QTabBar @@ -1004,13 +1004,13 @@ class CommandDispatcher: 'window.getSelection().anchorNode.parentNode.click()') else: try: - selected_element = ElementTree.fromstring( + selected_element = xml.etree.ElementTree.fromstring( '' + widget.selectedHtml() + '').find('a') if selected_element is not None: url = selected_element.attrib['href'] if url: self._open(QUrl(url), tab) - except ElementTree.ParseError: + except xml.etree.ElementTree.ParseError: pass @cmdutils.register(instance='command-dispatcher', name='inspector', From c5c145320cf48804a8636037d2c2c52da0b8780c Mon Sep 17 00:00:00 2001 From: Antoni Boucher Date: Sun, 31 May 2015 12:02:15 -0400 Subject: [PATCH 16/51] Fixed exception handling in select_follow command. --- qutebrowser/browser/commands.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index deafedb75..d2738560e 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1006,12 +1006,13 @@ class CommandDispatcher: try: selected_element = xml.etree.ElementTree.fromstring( '' + widget.selectedHtml() + '').find('a') - if selected_element is not None: - url = selected_element.attrib['href'] - if url: - self._open(QUrl(url), tab) except xml.etree.ElementTree.ParseError: - pass + raise cmdexc.CommandError('Parse error') + + if selected_element is not None: + url = selected_element.attrib['href'] + if url: + self._open(QUrl(url), tab) @cmdutils.register(instance='command-dispatcher', name='inspector', scope='window') From 87e988816777dcaad807d732d438aab63f08bb11 Mon Sep 17 00:00:00 2001 From: Antoni Boucher Date: Sun, 31 May 2015 12:07:08 -0400 Subject: [PATCH 17/51] Added exception handling for href attribute. --- qutebrowser/browser/commands.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index d2738560e..748fe7d3a 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1007,12 +1007,14 @@ class CommandDispatcher: selected_element = xml.etree.ElementTree.fromstring( '' + widget.selectedHtml() + '').find('a') except xml.etree.ElementTree.ParseError: - raise cmdexc.CommandError('Parse error') + raise cmdexc.CommandError('Parse error!') if selected_element is not None: - url = selected_element.attrib['href'] - if url: - self._open(QUrl(url), tab) + try: + url = selected_element.attrib['href'] + except KeyError: + raise cmdexc.CommandError('Anchor elment without href!') + self._open(QUrl(url), tab) @cmdutils.register(instance='command-dispatcher', name='inspector', scope='window') From 1cd64481de2db6f381204beecc3ca8eab01f976c Mon Sep 17 00:00:00 2001 From: Antoni Boucher Date: Sun, 31 May 2015 12:13:37 -0400 Subject: [PATCH 18/51] Fixed for relative url. --- qutebrowser/browser/commands.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 748fe7d3a..ff7a02aad 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1014,7 +1014,8 @@ class CommandDispatcher: url = selected_element.attrib['href'] except KeyError: raise cmdexc.CommandError('Anchor elment without href!') - self._open(QUrl(url), tab) + url = self._current_url().resolved(QUrl(url)) + self._open(url, tab) @cmdutils.register(instance='command-dispatcher', name='inspector', scope='window') From d0eda3336cd9665623a14e0729f19bb86f9840e8 Mon Sep 17 00:00:00 2001 From: Antoni Boucher Date: Sun, 31 May 2015 12:18:27 -0400 Subject: [PATCH 19/51] Added a page variable. --- qutebrowser/browser/commands.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index ff7a02aad..36ef67b74 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -996,11 +996,12 @@ class CommandDispatcher: tab: Load the selected link in a new tab. """ widget = self._current_widget() + page = widget.page() if QWebSettings.globalSettings().testAttribute( QWebSettings.JavascriptEnabled): if tab: - widget.page().open_target = usertypes.ClickTarget.tab - widget.page().currentFrame().evaluateJavaScript( + page.open_target = usertypes.ClickTarget.tab + page.currentFrame().evaluateJavaScript( 'window.getSelection().anchorNode.parentNode.click()') else: try: From c0b6aef7748a2aff2f64cf4c7b6b8ac83e434ea1 Mon Sep 17 00:00:00 2001 From: Antoni Boucher Date: Sun, 31 May 2015 12:50:28 -0400 Subject: [PATCH 20/51] Fixed command name. --- qutebrowser/browser/commands.py | 4 ++-- qutebrowser/config/configdata.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 36ef67b74..18164cf31 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -987,9 +987,9 @@ class CommandDispatcher: url = objreg.get('quickmark-manager').get(name) self._open(url, tab, bg, window) - @cmdutils.register(instance='command-dispatcher', name='select-follow', + @cmdutils.register(instance='command-dispatcher', hide=True, scope='window') - def select_follow(self, tab=False): + def follow_selected(self, tab=False): """Follow the selected text. Args: diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index dec02b069..1480ddb2e 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1239,8 +1239,8 @@ KEY_DATA = collections.OrderedDict([ ('stop', ['']), ('print', ['']), ('open qute:settings', ['Ss']), - ('select-follow', ['']), - ('select-follow -t', ['']), + ('follow-selected', ['']), + ('follow-selected -t', ['']), ])), ('insert', collections.OrderedDict([ From 27cbe618f0c8f4b6e74e0abd5ddc3b857239a550 Mon Sep 17 00:00:00 2001 From: Antoni Boucher Date: Sun, 31 May 2015 12:53:14 -0400 Subject: [PATCH 21/51] Added hasSelection check before trying to click on a selected link. --- qutebrowser/browser/commands.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 18164cf31..a221ab44e 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -997,6 +997,8 @@ class CommandDispatcher: """ widget = self._current_widget() page = widget.page() + if not page.hasSelection(): + return if QWebSettings.globalSettings().testAttribute( QWebSettings.JavascriptEnabled): if tab: From 3d0721afea3747287e33c6a5351c8fc3b6a441e5 Mon Sep 17 00:00:00 2001 From: Antoni Boucher Date: Sun, 31 May 2015 12:56:08 -0400 Subject: [PATCH 22/51] Fixed error messages. --- qutebrowser/browser/commands.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index a221ab44e..0f09baddf 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1010,13 +1010,13 @@ class CommandDispatcher: selected_element = xml.etree.ElementTree.fromstring( '' + widget.selectedHtml() + '').find('a') except xml.etree.ElementTree.ParseError: - raise cmdexc.CommandError('Parse error!') + raise cmdexc.CommandError('Could not parse selected element!') if selected_element is not None: try: url = selected_element.attrib['href'] except KeyError: - raise cmdexc.CommandError('Anchor elment without href!') + raise cmdexc.CommandError('Anchor element without href!') url = self._current_url().resolved(QUrl(url)) self._open(url, tab) From 172d0c3ca222f116ad58ad88b161b8cbef77174f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 31 May 2015 21:16:53 +0200 Subject: [PATCH 23/51] Regenerate docs. --- README.asciidoc | 1 + doc/help/commands.asciidoc | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/README.asciidoc b/README.asciidoc index 437ab8734..a1d4c2128 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -139,6 +139,7 @@ Contributors, sorted by the number of commits in descending order: * Joel Torstensson * Claude * Artur Shaik +* Antoni Boucher * ZDarian * Peter Vilim * Martin Tournoij diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index fa835f53a..6cc2ee9db 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -692,6 +692,7 @@ How many steps to zoom out. |<>|Drop selection and keep selection mode enabled. |<>|Enter a key mode. |<>|Follow the currently selected hint. +|<>|Follow the selected text. |<>|Leave the mode we're currently in. |<>|Show an error message in the statusbar. |<>|Show an info message in the statusbar. @@ -774,6 +775,15 @@ Enter a key mode. === follow-hint Follow the currently selected hint. +[[follow-selected]] +=== follow-selected +Syntax: +:follow-selected [*--tab*]+ + +Follow the selected text. + +==== optional arguments +* +*-t*+, +*--tab*+: Load the selected link in a new tab. + [[leave-mode]] === leave-mode Leave the mode we're currently in. From 122f0a7edc3b805ded089548d389e6667b7e75a2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 31 May 2015 21:25:16 +0200 Subject: [PATCH 24/51] Update changelog --- CHANGELOG.asciidoc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 2981578e6..8c994280f 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -33,6 +33,9 @@ Added - New argument `--no-err-windows` to suppress all error windows. - New visual/caret mode (bound to `v`) to select text by keyboard. - New setting `tabs -> mousewheel-tab-switching` to control mousewheel behavior on the tab bar. +- New arguments `--top-navigate` and `--bottom-navigate` (`-t`/`-b`) for `:scroll-page` to specify a navigation action (e.g. automatically go to the next page when arriving at the bottom). +- New flag `-d`/`--detach` for `:spawn` to detach the spawned process so it's not closed when qutebrowser is. +- New (hidden) command `:follow-selected` (bound to `Enter`/`Ctrl-Enter` by default) to follow the link which is currently selected (e.g. after searching via `/`). Changed ~~~~~~~ From 4204a8de9a19fefb8098422a291dcbe6495e6580 Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Sun, 31 May 2015 21:32:16 +0200 Subject: [PATCH 25/51] Add ui.modal-js-dialog to restore the default JS dialogs --- doc/help/settings.asciidoc | 1 + qutebrowser/browser/webpage.py | 6 ++++++ qutebrowser/config/configdata.py | 4 ++++ 3 files changed, 11 insertions(+) diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index d3c571aa3..4886d9dcd 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -45,6 +45,7 @@ |<>|Whether to hide the statusbar unless a message is shown. |<>|The format to use for the window title. The following placeholders are defined: |<>|Whether to hide the mouse cursor. +|<>|Use standard JavaScript modal dialog for alert() and confirm() |============== .Quick reference for section ``network'' diff --git a/qutebrowser/browser/webpage.py b/qutebrowser/browser/webpage.py index 005a6e300..fd6e7a7c9 100644 --- a/qutebrowser/browser/webpage.py +++ b/qutebrowser/browser/webpage.py @@ -481,6 +481,9 @@ class BrowserPage(QWebPage): def javaScriptAlert(self, _frame, msg): """Override javaScriptAlert to use the statusbar.""" log.js.debug("alert: {}".format(msg)) + if config.get('ui', 'modal-js-dialog'): + return super().javaScriptAlert(_frame, msg) + if (self._is_shutting_down or config.get('content', 'ignore-javascript-alert')): return @@ -489,6 +492,9 @@ class BrowserPage(QWebPage): def javaScriptConfirm(self, _frame, msg): """Override javaScriptConfirm to use the statusbar.""" log.js.debug("confirm: {}".format(msg)) + if config.get('ui', 'modal-js-dialog'): + return super().javaScriptConfirm(_frame, msg) + if self._is_shutting_down: return False ans = self._ask("[js confirm] {}".format(msg), diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 02b8c6008..f2c68c708 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -305,6 +305,10 @@ def data(readonly=False): SettingValue(typ.Bool(), 'false'), "Whether to hide the mouse cursor."), + ('modal-js-dialog', + SettingValue(typ.Bool(), 'false'), + "Use standard JavaScript modal dialog for alert() and confirm()"), + readonly=readonly )), From e7619477cd341fdefaf22af29095b2fa9cef7de3 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 31 May 2015 21:40:19 +0200 Subject: [PATCH 26/51] Rename _frame argument to frame. _foo is used to denote unused arguments, so renaming this as it's now used. --- qutebrowser/browser/webpage.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qutebrowser/browser/webpage.py b/qutebrowser/browser/webpage.py index fd6e7a7c9..8e430efcb 100644 --- a/qutebrowser/browser/webpage.py +++ b/qutebrowser/browser/webpage.py @@ -478,22 +478,22 @@ class BrowserPage(QWebPage): return super().extension(ext, opt, out) return handler(opt, out) - def javaScriptAlert(self, _frame, msg): + def javaScriptAlert(self, frame, msg): """Override javaScriptAlert to use the statusbar.""" log.js.debug("alert: {}".format(msg)) if config.get('ui', 'modal-js-dialog'): - return super().javaScriptAlert(_frame, msg) + return super().javaScriptAlert(frame, msg) if (self._is_shutting_down or config.get('content', 'ignore-javascript-alert')): return self._ask("[js alert] {}".format(msg), usertypes.PromptMode.alert) - def javaScriptConfirm(self, _frame, msg): + def javaScriptConfirm(self, frame, msg): """Override javaScriptConfirm to use the statusbar.""" log.js.debug("confirm: {}".format(msg)) if config.get('ui', 'modal-js-dialog'): - return super().javaScriptConfirm(_frame, msg) + return super().javaScriptConfirm(frame, msg) if self._is_shutting_down: return False From f52f3db1f21a55509d06b17677dfff2d5b2ad379 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 31 May 2015 21:41:32 +0200 Subject: [PATCH 27/51] Regenerate docs. --- README.asciidoc | 2 +- doc/help/settings.asciidoc | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/README.asciidoc b/README.asciidoc index a1d4c2128..63d803d8f 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -141,8 +141,8 @@ Contributors, sorted by the number of commits in descending order: * Artur Shaik * Antoni Boucher * ZDarian -* Peter Vilim * Martin Tournoij +* Peter Vilim * John ShaggyTwoDope Jenkins * Jimmy * Zach-Button diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 4886d9dcd..073ad2d63 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -45,7 +45,7 @@ |<>|Whether to hide the statusbar unless a message is shown. |<>|The format to use for the window title. The following placeholders are defined: |<>|Whether to hide the mouse cursor. -|<>|Use standard JavaScript modal dialog for alert() and confirm() +|<>|Use standard JavaScript modal dialog for alert() and confirm() |============== .Quick reference for section ``network'' @@ -595,6 +595,17 @@ Valid values: Default: +pass:[false]+ +[[ui-modal-js-dialog]] +=== modal-js-dialog +Use standard JavaScript modal dialog for alert() and confirm() + +Valid values: + + * +true+ + * +false+ + +Default: +pass:[false]+ + == network Settings related to the network. From a14685be3de0609de196bdb47856a22a7da4097e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 31 May 2015 21:42:25 +0200 Subject: [PATCH 28/51] Update changelog. --- CHANGELOG.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 8c994280f..96e6482ea 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -36,6 +36,7 @@ Added - New arguments `--top-navigate` and `--bottom-navigate` (`-t`/`-b`) for `:scroll-page` to specify a navigation action (e.g. automatically go to the next page when arriving at the bottom). - New flag `-d`/`--detach` for `:spawn` to detach the spawned process so it's not closed when qutebrowser is. - New (hidden) command `:follow-selected` (bound to `Enter`/`Ctrl-Enter` by default) to follow the link which is currently selected (e.g. after searching via `/`). +- New setting `ui -> modal-js-dialog` to use the standard modal dialogs for javascript questions instead of using the statusbar. Changed ~~~~~~~ From f1c0781a4c648cf16dcc4b3e8acc508741fe56cd Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 1 Jun 2015 09:09:10 +0200 Subject: [PATCH 29/51] Use sip.SIP_VERSION_STR to get sip version. --- qutebrowser/utils/version.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 827762af4..19dae311a 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -127,18 +127,8 @@ def _module_versions(): A list of lines with version info. """ lines = [] - try: - import sipconfig # pylint: disable=import-error,unused-variable - except ImportError: - lines.append('SIP: ?') - else: - try: - lines.append('SIP: {}'.format( - sipconfig.Configuration().sip_version_str)) - except (AttributeError, TypeError): - log.misc.exception("Error while getting SIP version") - lines.append('SIP: ?') modules = collections.OrderedDict([ + ('sip', ['SIP_VERSION_STR']), ('colorlog', []), ('colorama', ['VERSION', '__version__']), ('pypeg2', ['__version__']), From 6c2fe3417ef79c3c3a1f0fc8e554a80518d7aa48 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 1 Jun 2015 13:38:13 +0200 Subject: [PATCH 30/51] Accept numpad-enter as return in default bindings. See https://bbs.archlinux.org/viewtopic.php?pid=1523326#p1523326 --- CHANGELOG.asciidoc | 2 +- qutebrowser/config/configdata.py | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 96e6482ea..30bd50cee 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -45,7 +45,7 @@ Changed - `:spawn` now shows the command being executed in the statusbar, use `-q`/`--quiet` for the old behavior. - The `content -> geolocation` and `notifications` settings now support a `true` value to always allow those. However, this is *not recommended*. - New bindings `` (rapid), `` (foreground) and `` (background) to switch hint modes while hinting. -- `` is now accepted as an additional alias for ``/`` +- `` and numpad-enter are now bound by default for bindings where `` was bound. - `:hint tab` and `F` now respect the `background-tabs` setting. To enforce a foreground tab (what `F` did before), use `:hint tab-fg` or `;f`. - `:scroll` now takes a direction argument (`up`/`down`/`left`/`right`/`top`/`bottom`/`page-up`/`page-down`) instead of two pixel arguments (`dx`/`dy`). The old form still works but is deprecated. diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 9df909676..9deadc1bd 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1125,6 +1125,12 @@ KEY_SECTION_DESC = { ""), } +# Keys which are similiar to Return and should be bound by default where Return +# is bound. + +RETURN_KEYS = ['', '', '', '', '', + ''] + KEY_DATA = collections.OrderedDict([ ('!normal', collections.OrderedDict([ @@ -1243,8 +1249,8 @@ KEY_DATA = collections.OrderedDict([ ('stop', ['']), ('print', ['']), ('open qute:settings', ['Ss']), - ('follow-selected', ['']), - ('follow-selected -t', ['']), + ('follow-selected', RETURN_KEYS), + ('follow-selected -t', ['', '']), ])), ('insert', collections.OrderedDict([ @@ -1252,7 +1258,7 @@ KEY_DATA = collections.OrderedDict([ ])), ('hint', collections.OrderedDict([ - ('follow-hint', ['', '', '']), + ('follow-hint', RETURN_KEYS), ('hint --rapid links tab-bg', ['']), ('hint links', ['']), ('hint all tab-bg', ['']), @@ -1265,13 +1271,11 @@ KEY_DATA = collections.OrderedDict([ ('command-history-next', ['']), ('completion-item-prev', ['', '']), ('completion-item-next', ['', '']), - ('command-accept', ['', '', '', - '']), + ('command-accept', RETURN_KEYS), ])), ('prompt', collections.OrderedDict([ - ('prompt-accept', ['', '', '', - '']), + ('prompt-accept', RETURN_KEYS), ('prompt-yes', ['y']), ('prompt-no', ['n']), ])), @@ -1312,7 +1316,7 @@ KEY_DATA = collections.OrderedDict([ ('move-to-start-of-document', ['gg']), ('move-to-end-of-document', ['G']), ('yank-selected -p', ['Y']), - ('yank-selected', ['y', '', '']), + ('yank-selected', ['y'] + RETURN_KEYS), ('scroll left', ['H']), ('scroll down', ['J']), ('scroll up', ['K']), From b5a70dbdec4916c21493e558aaf618eca3668c61 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 1 Jun 2015 13:43:40 +0200 Subject: [PATCH 31/51] Spelling fix. --- qutebrowser/config/configdata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 9deadc1bd..e95a54ea8 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1125,7 +1125,7 @@ KEY_SECTION_DESC = { ""), } -# Keys which are similiar to Return and should be bound by default where Return +# Keys which are similar to Return and should be bound by default where Return # is bound. RETURN_KEYS = ['', '', '', '', '', From e22ef776f9609af9dcff966b8ca2f695cf910ca3 Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Mon, 1 Jun 2015 19:02:07 +0200 Subject: [PATCH 32/51] Fix crash when executing "qutebrowser :set". Fixes #720. See #721. --- qutebrowser/mainwindow/tabbedbrowser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index ba5a5c725..d54b22d0c 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -296,7 +296,7 @@ class TabbedBrowser(tabwidget.TabWidget): newtab: True to open URL in a new tab, False otherwise. """ qtutils.ensure_valid(url) - if newtab: + if newtab or self.currentWidget() is None: self.tabopen(url, background=False) else: self.currentWidget().openurl(url) From dc59ed4d73604a7683c84cdee0b262889ee6d31c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 1 Jun 2015 19:03:28 +0200 Subject: [PATCH 33/51] Regenerate authors --- README.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.asciidoc b/README.asciidoc index 63d803d8f..d42a895ef 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -140,8 +140,8 @@ Contributors, sorted by the number of commits in descending order: * Claude * Artur Shaik * Antoni Boucher -* ZDarian * Martin Tournoij +* ZDarian * Peter Vilim * John ShaggyTwoDope Jenkins * Jimmy From 131f3450072528041a6b7b1dcde210940ad37bba Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 1 Jun 2015 19:04:07 +0200 Subject: [PATCH 34/51] Update changelog. --- CHANGELOG.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 30bd50cee..aaf69e22a 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -79,6 +79,7 @@ Fixed - Workaround for qutebrowser not starting when there are NUL-bytes in the history (because of a currently unknown bug) - Fixed handling of keybindings containing Ctrl/Meta on OS X. - Fixed crash when downloading an URL without filename (e.g. magnet links) via "Save as...". +- Fixed exception when starting qutebrowser with `:set` as argument. https://github.com/The-Compiler/qutebrowser/releases/tag/v0.2.1[v0.2.1] ----------------------------------------------------------------------- From 1bf036d1bad7549a482c1bdec43a46beabcba398 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 1 Jun 2015 22:27:15 +0200 Subject: [PATCH 35/51] Add setting for the webpage bg color to use. Fixes #719. --- CHANGELOG.asciidoc | 1 + qutebrowser/browser/webview.py | 13 +++++++++++++ qutebrowser/config/configdata.py | 5 +++++ 3 files changed, 19 insertions(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index aaf69e22a..92129b3e1 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -37,6 +37,7 @@ Added - New flag `-d`/`--detach` for `:spawn` to detach the spawned process so it's not closed when qutebrowser is. - New (hidden) command `:follow-selected` (bound to `Enter`/`Ctrl-Enter` by default) to follow the link which is currently selected (e.g. after searching via `/`). - New setting `ui -> modal-js-dialog` to use the standard modal dialogs for javascript questions instead of using the statusbar. +- New setting `colors -> webpage.bg` to set the background color to use for websites which don't set one. Changed ~~~~~~~ diff --git a/qutebrowser/browser/webview.py b/qutebrowser/browser/webview.py index 06176064d..584c7f268 100644 --- a/qutebrowser/browser/webview.py +++ b/qutebrowser/browser/webview.py @@ -24,6 +24,7 @@ import itertools import functools from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QTimer, QUrl +from PyQt5.QtGui import QPalette from PyQt5.QtWidgets import QApplication, QStyleFactory from PyQt5.QtWebKit import QWebSettings from PyQt5.QtWebKitWidgets import QWebView, QWebPage @@ -108,6 +109,7 @@ class WebView(QWebView): self.search_flags = 0 self.selection_enabled = False self.init_neighborlist() + self._set_bg_color() cfg = objreg.get('config') cfg.changed.connect(self.init_neighborlist) # For some reason, this signal doesn't get disconnected automatically @@ -181,6 +183,15 @@ class WebView(QWebView): self.load_status = val self.load_status_changed.emit(val.name) + def _set_bg_color(self): + """Set the webpage background color as configured.""" + col = config.get('colors', 'webpage.bg') + palette = self.palette() + if col is None: + col = self.style().standardPalette().color(QPalette.Base) + palette.setColor(QPalette.Base, col) + self.setPalette(palette) + @pyqtSlot(str, str) def on_config_changed(self, section, option): """Reinitialize the zoom neighborlist if related config changed.""" @@ -195,6 +206,8 @@ class WebView(QWebView): self.setContextMenuPolicy(Qt.PreventContextMenu) else: self.setContextMenuPolicy(Qt.DefaultContextMenu) + elif section == 'colors' and option == 'webpage.bg': + self._set_bg_color() def init_neighborlist(self): """Initialize the _zoom neighborlist.""" diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index e95a54ea8..a3bbe2d48 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -959,6 +959,11 @@ def data(readonly=False): SettingValue(typ.QtColor(), 'red'), "Background color for downloads with errors."), + ('webpage.bg', + SettingValue(typ.QtColor(none_ok=True), 'white'), + "Background color for webpages if unset (or empty to use the " + "theme's color)"), + readonly=readonly )), From e767f7c0b81cc5b86d945243fed3c2a16391881d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 1 Jun 2015 22:29:49 +0200 Subject: [PATCH 36/51] Regenerate docs. --- doc/help/settings.asciidoc | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 073ad2d63..fc4802232 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -221,6 +221,7 @@ |<>|Color gradient end for downloads. |<>|Color gradient interpolation system for downloads. |<>|Background color for downloads with errors. +|<>|Background color for webpages if unset (or empty to use the theme's color) |============== .Quick reference for section ``fonts'' @@ -1752,6 +1753,12 @@ Background color for downloads with errors. Default: +pass:[red]+ +[[colors-webpage.bg]] +=== webpage.bg +Background color for webpages if unset (or empty to use the theme's color) + +Default: +pass:[white]+ + == fonts Fonts used for the UI, with optional style/weight/size. From 592ace18d4aca6928fef2c269e46ee15fd492882 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 1 Jun 2015 22:32:11 +0200 Subject: [PATCH 37/51] Skip test which might be responsible for segfaults. --- tests/mainwindow/statusbar/test_progress.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/mainwindow/statusbar/test_progress.py b/tests/mainwindow/statusbar/test_progress.py index 07e93e0e5..a0d066808 100644 --- a/tests/mainwindow/statusbar/test_progress.py +++ b/tests/mainwindow/statusbar/test_progress.py @@ -44,6 +44,9 @@ def progress_widget(qtbot, monkeypatch, config_stub): return widget +@pytest.mark.xfail( + reason='Blacklisted because it could cause random segfaults - see ' + 'https://github.com/hackebrot/qutebrowser/issues/22', run=False) def test_load_started(progress_widget): """Ensure the Progress widget reacts properly when the page starts loading. From d8e58b58869f696fafd0b314740afc94af918e2b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 1 Jun 2015 22:45:40 +0200 Subject: [PATCH 38/51] Fix some typos. --- qutebrowser/commands/userscripts.py | 4 ++-- qutebrowser/config/configtypes.py | 2 +- qutebrowser/misc/crashdialog.py | 2 +- qutebrowser/misc/miscwidgets.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/qutebrowser/commands/userscripts.py b/qutebrowser/commands/userscripts.py index 85c511165..76e9f94a6 100644 --- a/qutebrowser/commands/userscripts.py +++ b/qutebrowser/commands/userscripts.py @@ -148,7 +148,7 @@ class _BaseUserscriptRunner(QObject): def run(self, cmd, *args, env=None): """Run the userscript given. - Needs to be overridden by superclasses. + Needs to be overridden by subclasses. Args: cmd: The command to be started. @@ -160,7 +160,7 @@ class _BaseUserscriptRunner(QObject): def on_proc_finished(self): """Called when the process has finished. - Needs to be overridden by superclasses. + Needs to be overridden by subclasses. """ raise NotImplementedError diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index b9c760116..55d782202 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -693,7 +693,7 @@ class FontFamily(Font): class QtFont(Font): - """A Font which gets converted to q QFont.""" + """A Font which gets converted to a QFont.""" def transform(self, value): if not value: diff --git a/qutebrowser/misc/crashdialog.py b/qutebrowser/misc/crashdialog.py index 79a425fa9..dbb473b4e 100644 --- a/qutebrowser/misc/crashdialog.py +++ b/qutebrowser/misc/crashdialog.py @@ -183,7 +183,7 @@ class _CrashDialog(QDialog): def _init_text(self): """Initialize the main text to be displayed on an exception. - Should be extended by superclass to set the actual text.""" + Should be extended by subclasses to set the actual text.""" self._lbl = QLabel(wordWrap=True, openExternalLinks=True, textInteractionFlags=Qt.LinksAccessibleByMouse) self._vbox.addWidget(self._lbl) diff --git a/qutebrowser/misc/miscwidgets.py b/qutebrowser/misc/miscwidgets.py index 74dd8f92e..04ced01c2 100644 --- a/qutebrowser/misc/miscwidgets.py +++ b/qutebrowser/misc/miscwidgets.py @@ -77,7 +77,7 @@ class CommandLineEdit(QLineEdit): def __on_cursor_position_changed(self, _old, new): """Prevent the cursor moving to the prompt. - We use __ here to avoid accidentally overriding it in superclasses. + We use __ here to avoid accidentally overriding it in subclasses. """ if new < self._promptlen: self.setCursorPosition(self._promptlen) From 36803cba06f18d6dfe79eca7846f656af208f03a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 2 Jun 2015 20:51:06 +0200 Subject: [PATCH 39/51] Switch from flake8 to pytest-{mccabe,flakes,pep8}. --- .flake8 | 13 ----- .gitignore | 1 + CONTRIBUTING.asciidoc | 4 +- MANIFEST.in | 1 - qutebrowser/app.py | 2 +- qutebrowser/browser/network/networkmanager.py | 2 +- qutebrowser/commands/command.py | 2 +- qutebrowser/misc/crashsignal.py | 2 +- qutebrowser/misc/split.py | 2 +- tox.ini | 50 ++++++++++++++++--- 10 files changed, 52 insertions(+), 27 deletions(-) delete mode 100644 .flake8 diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 29d9c8be4..000000000 --- a/.flake8 +++ /dev/null @@ -1,13 +0,0 @@ -# vim: ft=dosini fileencoding=utf-8: - -[flake8] -# E265: Block comment should start with '#' -# E501: Line too long -# F841: unused variable -# F401: Unused import -# E402: module level import not at top of file -# E266: too many leading '#' for block comment -# W503: line break before binary operator -ignore=E265,E501,F841,F401,E402,E266,W503 -max_complexity = 12 -exclude=resources.py diff --git a/.gitignore b/.gitignore index f3ff3652a..a4c699d38 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ __pycache__ /htmlcov /.tox /testresults.html +/.cache diff --git a/CONTRIBUTING.asciidoc b/CONTRIBUTING.asciidoc index 7b0fe6087..ed76f5152 100644 --- a/CONTRIBUTING.asciidoc +++ b/CONTRIBUTING.asciidoc @@ -93,7 +93,9 @@ Currently, the following tools will be invoked when you run `tox`: * Unit tests using the Python https://docs.python.org/3.4/library/unittest.html[unittest] framework -* https://pypi.python.org/pypi/flake8/[flake8] +* https://pypi.python.org/pypi/pyflakes[pyflakes] via https://pypi.python.org/pypi/pytest-flakes[pytest-flakes] +* https://pypi.python.org/pypi/pep8[pep8] via https://pypi.python.org/pypi/pytest-pep8[pytest-pep8] +* https://pypi.python.org/pypi/mccabe[mccabe] via https://pypi.python.org/pypi/pytest-mccabe[pytest-mccabe] * https://github.com/GreenSteam/pep257/[pep257] * http://pylint.org/[pylint] * https://pypi.python.org/pypi/pyroma/[pyroma] diff --git a/MANIFEST.in b/MANIFEST.in index 7ecd44de2..4092f81c5 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -28,7 +28,6 @@ include doc/qutebrowser.1.asciidoc prune tests exclude qutebrowser.rcc exclude .coveragerc -exclude .flake8 exclude .pylintrc exclude .eslintrc exclude doc/help diff --git a/qutebrowser/app.py b/qutebrowser/app.py index d125d1d1a..2e8f7ea6a 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -606,7 +606,7 @@ class Quitter: # event loop, so we can shut down immediately. self._shutdown(status) - def _shutdown(self, status): # noqa + def _shutdown(self, status): """Second stage of shutdown.""" log.destroy.debug("Stage 2 of shutting down...") if qApp is None: diff --git a/qutebrowser/browser/network/networkmanager.py b/qutebrowser/browser/network/networkmanager.py index 3b4c71ed0..a3fc76baf 100644 --- a/qutebrowser/browser/network/networkmanager.py +++ b/qutebrowser/browser/network/networkmanager.py @@ -181,7 +181,7 @@ class NetworkManager(QNetworkAccessManager): request.deleteLater() self.shutting_down.emit() - if SSL_AVAILABLE: # noqa + if SSL_AVAILABLE: # pragma: no mccabe @pyqtSlot('QNetworkReply*', 'QList') def on_ssl_errors(self, reply, errors): """Decide if SSL errors should be ignored or not. diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index d55597d9d..4bda15f8a 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -420,7 +420,7 @@ class Command: value = self._type_conv[param.name](value) return name, value - def _get_call_args(self, win_id): # noqa + def _get_call_args(self, win_id): """Get arguments for a function call. Args: diff --git a/qutebrowser/misc/crashsignal.py b/qutebrowser/misc/crashsignal.py index 7598f3312..2e336bc1e 100644 --- a/qutebrowser/misc/crashsignal.py +++ b/qutebrowser/misc/crashsignal.py @@ -190,7 +190,7 @@ class CrashHandler(QObject): objects = "" return ExceptionInfo(pages, cmd_history, objects) - def exception_hook(self, exctype, excvalue, tb): # noqa + def exception_hook(self, exctype, excvalue, tb): """Handle uncaught python exceptions. It'll try very hard to write all open tabs to a file, and then exit diff --git a/qutebrowser/misc/split.py b/qutebrowser/misc/split.py index b763d8246..a7bbeea6e 100644 --- a/qutebrowser/misc/split.py +++ b/qutebrowser/misc/split.py @@ -55,7 +55,7 @@ class ShellLexer: self.token = '' self.state = ' ' - def __iter__(self): # noqa + def __iter__(self): # pragma: no mccabe """Read a raw token from the input stream.""" # pylint: disable=too-many-branches,too-many-statements self.reset() diff --git a/tox.ini b/tox.ini index f39af4ee3..0b14e4e74 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = unittests,misc,pep257,flake8,pylint,pyroma,check-manifest +envlist = unittests,misc,pep257,pyflakes,pep8,mccabe,pylint,pyroma,check-manifest [testenv] basepython = python3 @@ -74,16 +74,40 @@ passenv = LANG # D402: First line should not be function's signature (false-positives) commands = {envpython} -m pep257 scripts tests qutebrowser --ignore=D102,D103,D209,D402 '--match=(?!resources|test_content_disposition).*\.py' -[testenv:flake8] -skip_install = true +[testenv:pyflakes] +# https://github.com/fschulze/pytest-flakes/issues/6 +setenv = LANG=en_US.UTF-8 deps = -r{toxinidir}/requirements.txt - pyflakes==0.8.1 - pep8==1.5.7 # rq.filter: <1.6.0 - flake8==2.4.0 + py==1.4.27 + pytest==2.7.1 + pyflakes==0.9.0 + pytest-flakes==0.2 commands = {[testenv:mkvenv]commands} - {envdir}/bin/flake8 scripts tests qutebrowser --config=.flake8 + {envpython} -m py.test -q --flakes -m flakes + +[testenv:pep8] +deps = + -r{toxinidir}/requirements.txt + py==1.4.27 + pytest==2.7.1 + pep8==1.6.2 + pytest-pep8==1.0.6 +commands = + {[testenv:mkvenv]commands} + {envpython} -m py.test -q --pep8 -m pep8 + +[testenv:mccabe] +deps = + -r{toxinidir}/requirements.txt + py==1.4.27 + pytest==2.7.1 + mccabe==0.3 + pytest-mccabe==0.1 +commands = + {[testenv:mkvenv]commands} + {envpython} -m py.test -q --mccabe -m mccabe [testenv:pyroma] skip_install = true @@ -129,3 +153,15 @@ commands = norecursedirs = .tox .venv markers = gui: Tests using the GUI (e.g. spawning widgets) +flakes-ignore = + UnusedImport + UnusedVariable + resources.py ALL +pep8ignore = + E265 # Block comment should start with '#' + E501 # Line too long + E402 # module level import not at top of file + E266 # too many leading '#' for block comment + W503 # line break before binary operator + resources.py ALL +mccabe-complexity = 12 From f0c58b58dd2dbbd14025bbaa5e74ab91c2f6e0d7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 2 Jun 2015 20:51:48 +0200 Subject: [PATCH 40/51] Mention pytest in CONTRIBUTING. --- CONTRIBUTING.asciidoc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CONTRIBUTING.asciidoc b/CONTRIBUTING.asciidoc index ed76f5152..1975a9d7c 100644 --- a/CONTRIBUTING.asciidoc +++ b/CONTRIBUTING.asciidoc @@ -91,8 +91,7 @@ unittests and several linters/checkers. Currently, the following tools will be invoked when you run `tox`: -* Unit tests using the Python -https://docs.python.org/3.4/library/unittest.html[unittest] framework +* Unit tests using https://www.pytest.org[pytest]. * https://pypi.python.org/pypi/pyflakes[pyflakes] via https://pypi.python.org/pypi/pytest-flakes[pytest-flakes] * https://pypi.python.org/pypi/pep8[pep8] via https://pypi.python.org/pypi/pytest-pep8[pytest-pep8] * https://pypi.python.org/pypi/mccabe[mccabe] via https://pypi.python.org/pypi/pytest-mccabe[pytest-mccabe] From e780efb3d97aa901e05e1226c8f21de9f0f53cc5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 3 Jun 2015 15:03:04 +0200 Subject: [PATCH 41/51] Handle javascript in qute:settings more gracefully. Fixes #727. --- qutebrowser/browser/network/qutescheme.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/qutebrowser/browser/network/qutescheme.py b/qutebrowser/browser/network/qutescheme.py index 0432fa6fc..48b3dbd5f 100644 --- a/qutebrowser/browser/network/qutescheme.py +++ b/qutebrowser/browser/network/qutescheme.py @@ -34,6 +34,7 @@ import configparser from PyQt5.QtCore import pyqtSlot, QObject from PyQt5.QtNetwork import QNetworkReply +from PyQt5.QtWebKit import QWebSettings import qutebrowser from qutebrowser.browser.network import schemehandler, networkreply @@ -96,6 +97,12 @@ class JSBridge(QObject): @pyqtSlot(int, str, str, str) def set(self, win_id, sectname, optname, value): """Slot to set a setting from qute:settings.""" + # https://github.com/The-Compiler/qutebrowser/issues/727 + if (sectname, optname == 'content', 'allow-javascript' and + value == 'false'): + message.error(win_id, "Refusing to disable javascript via " + "qute:settings as it needs javascript support.") + return try: objreg.get('config').set('conf', sectname, optname, value) except (configexc.Error, configparser.Error) as e: @@ -172,10 +179,18 @@ def qute_help(win_id, request): def qute_settings(win_id, _request): """Handler for qute:settings. View/change qute configuration.""" - config_getter = functools.partial(objreg.get('config').get, raw=True) - html = jinja.env.get_template('settings.html').render( - win_id=win_id, title='settings', config=configdata, - confget=config_getter) + if not QWebSettings.globalSettings().testAttribute( + QWebSettings.JavascriptEnabled): + # https://github.com/The-Compiler/qutebrowser/issues/727 + template = jinja.env.get_template('pre.html') + html = template.render( + title='Failed to open qute:settings.', + content="qute:settings needs javascript enabled to work.") + else: + config_getter = functools.partial(objreg.get('config').get, raw=True) + html = jinja.env.get_template('settings.html').render( + win_id=win_id, title='settings', config=configdata, + confget=config_getter) return html.encode('UTF-8', errors='xmlcharrefreplace') From 0132bea42bb023139b773c331115ac00ad93f081 Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Thu, 4 Jun 2015 12:20:43 +0200 Subject: [PATCH 42/51] Add --domain to yank to yank only the domain ... As I want to copy only the domain fairly frequently. I also changed the message in the statusline to show the actual text being copied, which I find helpful. But if you disagree, then just undo it (it's not that important or anything). --- doc/help/commands.asciidoc | 3 ++- qutebrowser/browser/commands.py | 13 ++++++++++--- qutebrowser/config/configdata.py | 2 ++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 6cc2ee9db..e3dd81e62 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -642,13 +642,14 @@ Save open pages and quit. [[yank]] === yank -Syntax: +:yank [*--title*] [*--sel*]+ +Syntax: +:yank [*--title*] [*--sel*] [*--domain*]+ Yank the current URL/title to the clipboard or primary selection. ==== optional arguments * +*-t*+, +*--title*+: Yank the title instead of the URL. * +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard. +* +*-d*+, +*--domain*+: Yank only the scheme & domain. [[zoom]] === zoom diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 6585abed6..9cb2a9991 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -696,19 +696,26 @@ class CommandDispatcher: frame.scroll(dx, dy) @cmdutils.register(instance='command-dispatcher', scope='window') - def yank(self, title=False, sel=False): + def yank(self, title=False, sel=False, domain=False): """Yank the current URL/title to the clipboard or primary selection. Args: sel: Use the primary selection instead of the clipboard. title: Yank the title instead of the URL. + domain: Yank only the scheme & domain. """ clipboard = QApplication.clipboard() if title: s = self._tabbed_browser.page_title(self._current_index()) + what = 'title' + elif domain: + s = '{}://{}'.format(self._current_url().scheme(), + self._current_url().host()) + what = 'domain' else: s = self._current_url().toString( QUrl.FullyEncoded | QUrl.RemovePassword) + what = 'URL' if sel and clipboard.supportsSelection(): mode = QClipboard.Selection target = "primary selection" @@ -717,8 +724,8 @@ class CommandDispatcher: target = "clipboard" log.misc.debug("Yanking to {}: '{}'".format(target, s)) clipboard.setText(s, mode) - what = 'Title' if title else 'URL' - message.info(self._win_id, "{} yanked to {}".format(what, target)) + message.info(self._win_id, "Yanked {} to {}: {}".format( + what, target, s)) @cmdutils.register(instance='command-dispatcher', scope='window', count='count') diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index a3bbe2d48..ad269d212 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1204,6 +1204,8 @@ KEY_DATA = collections.OrderedDict([ ('yank -s', ['yY']), ('yank -t', ['yt']), ('yank -ts', ['yT']), + ('yank -d', ['yd']), + ('yank -ds', ['yD']), ('paste', ['pp']), ('paste -s', ['pP']), ('paste -t', ['Pp']), From d60d4d756ce4e5cb45908d9940372e771d0b37dd Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Thu, 4 Jun 2015 13:20:39 +0200 Subject: [PATCH 43/51] Also yank port number --- doc/help/commands.asciidoc | 2 +- qutebrowser/browser/commands.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index e3dd81e62..104478c10 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -649,7 +649,7 @@ Yank the current URL/title to the clipboard or primary selection. ==== optional arguments * +*-t*+, +*--title*+: Yank the title instead of the URL. * +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard. -* +*-d*+, +*--domain*+: Yank only the scheme & domain. +* +*-d*+, +*--domain*+: Yank only the scheme, domain, and port number. [[zoom]] === zoom diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 9cb2a9991..9bfe81a10 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -702,15 +702,17 @@ class CommandDispatcher: Args: sel: Use the primary selection instead of the clipboard. title: Yank the title instead of the URL. - domain: Yank only the scheme & domain. + domain: Yank only the scheme, domain, and port number. """ clipboard = QApplication.clipboard() if title: s = self._tabbed_browser.page_title(self._current_index()) what = 'title' elif domain: - s = '{}://{}'.format(self._current_url().scheme(), - self._current_url().host()) + port = self._current_url().port() + s = '{}://{}{}'.format(self._current_url().scheme(), + self._current_url().host(), + ':' + str(port) if port > -1 else '') what = 'domain' else: s = self._current_url().toString( From 9ec6e6da80eeacd6c127768bd3a41a965d7ab21c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 4 Jun 2015 15:13:20 +0200 Subject: [PATCH 44/51] Fix exit status codes to be 0-based. --- CHANGELOG.asciidoc | 1 + qutebrowser/utils/usertypes.py | 2 +- tests/utils/usertypes/test_enum.py | 6 ++++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 92129b3e1..6bd22ced2 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -81,6 +81,7 @@ Fixed - Fixed handling of keybindings containing Ctrl/Meta on OS X. - Fixed crash when downloading an URL without filename (e.g. magnet links) via "Save as...". - Fixed exception when starting qutebrowser with `:set` as argument. +- Fixed exit status codes (successful exit was 1 instead of 0). https://github.com/The-Compiler/qutebrowser/releases/tag/v0.2.1[v0.2.1] ----------------------------------------------------------------------- diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index 5d19ad515..c82a54596 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -242,7 +242,7 @@ Completion = enum('Completion', ['command', 'section', 'option', 'value', # Exit statuses for errors. Needs to be an int for sys.exit. Exit = enum('Exit', ['ok', 'reserved', 'exception', 'err_ipc', 'err_init', - 'err_config', 'err_key_config'], is_int=True) + 'err_config', 'err_key_config'], is_int=True, start=0) class Question(QObject): diff --git a/tests/utils/usertypes/test_enum.py b/tests/utils/usertypes/test_enum.py index e1443b7be..7298b2861 100644 --- a/tests/utils/usertypes/test_enum.py +++ b/tests/utils/usertypes/test_enum.py @@ -54,3 +54,9 @@ def test_start(): e = usertypes.enum('Enum', ['three', 'four'], start=3) assert e.three.value == 3 assert e.four.value == 4 + + +def test_exit(): + """Make sure the exit status enum is correct.""" + assert usertypes.Exit.ok == 0 + assert usertypes.Exit.reserved == 1 From f41acc8fb5c00e9ba5c14dc3a3ec9fc335528c7e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 4 Jun 2015 15:14:56 +0200 Subject: [PATCH 45/51] Remove changelog entry as it's a recent regression --- CHANGELOG.asciidoc | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 6bd22ced2..92129b3e1 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -81,7 +81,6 @@ Fixed - Fixed handling of keybindings containing Ctrl/Meta on OS X. - Fixed crash when downloading an URL without filename (e.g. magnet links) via "Save as...". - Fixed exception when starting qutebrowser with `:set` as argument. -- Fixed exit status codes (successful exit was 1 instead of 0). https://github.com/The-Compiler/qutebrowser/releases/tag/v0.2.1[v0.2.1] ----------------------------------------------------------------------- From 4a909aa028bdc79424b48b24d4a908f8186058f1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 4 Jun 2015 15:25:36 +0200 Subject: [PATCH 46/51] Use pylint's built-in checker to check for CRLF. --- .pylintrc | 1 - scripts/pylint_checkers/crlf.py | 45 --------------------------------- tox.ini | 4 +-- 3 files changed, 2 insertions(+), 48 deletions(-) delete mode 100644 scripts/pylint_checkers/crlf.py diff --git a/.pylintrc b/.pylintrc index 2cc56909d..a4abb32a0 100644 --- a/.pylintrc +++ b/.pylintrc @@ -4,7 +4,6 @@ ignore=resources.py extension-pkg-whitelist=PyQt5,sip load-plugins=pylint_checkers.config, - pylint_checkers.crlf, pylint_checkers.modeline, pylint_checkers.openencoding, pylint_checkers.settrace diff --git a/scripts/pylint_checkers/crlf.py b/scripts/pylint_checkers/crlf.py deleted file mode 100644 index a77f8b9e0..000000000 --- a/scripts/pylint_checkers/crlf.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2014-2015 Florian Bruhin (The Compiler) -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: - -# This file is part of qutebrowser. -# -# qutebrowser is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# qutebrowser is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with qutebrowser. If not, see . - -"""Checker for CRLF in files.""" - -from pylint import interfaces, checkers - - -class CrlfChecker(checkers.BaseChecker): - - """Check for CRLF in files.""" - - __implements__ = interfaces.IRawChecker - - name = 'crlf' - msgs = {'W9001': ('Uses CRLFs', 'crlf', None)} - options = () - priority = -1 - - def process_module(self, node): - """Process the module.""" - for (lineno, line) in enumerate(node.file_stream): - if b'\r\n' in line: - self.add_message('crlf', line=lineno) - return - - -def register(linter): - """Register the checker.""" - linter.register_checker(CrlfChecker(linter)) diff --git a/tox.ini b/tox.ini index 0b14e4e74..cff331d21 100644 --- a/tox.ini +++ b/tox.ini @@ -61,8 +61,8 @@ deps = six==1.9.0 commands = {[testenv:mkvenv]commands} - {envdir}/bin/pylint scripts qutebrowser --rcfile=.pylintrc --output-format=colorized --reports=no - {envpython} scripts/run_pylint_on_tests.py --rcfile=.pylintrc --output-format=colorized --reports=no + {envdir}/bin/pylint scripts qutebrowser --rcfile=.pylintrc --output-format=colorized --reports=no --expected-line-ending-format=LF + {envpython} scripts/run_pylint_on_tests.py --rcfile=.pylintrc --output-format=colorized --reports=no --expected-line-ending-format=LF [testenv:pep257] skip_install = true From 05fe68ccab1a6f64819c903a6edb424be758e3fc Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 5 Jun 2015 06:39:37 +0200 Subject: [PATCH 47/51] Regenerate authors --- README.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.asciidoc b/README.asciidoc index d42a895ef..3a701fd76 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -138,9 +138,9 @@ Contributors, sorted by the number of commits in descending order: * Raphael Pierzina * Joel Torstensson * Claude +* Martin Tournoij * Artur Shaik * Antoni Boucher -* Martin Tournoij * ZDarian * Peter Vilim * John ShaggyTwoDope Jenkins From 622938e3d3e39fb5222d6c3271b654165f404130 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 5 Jun 2015 07:16:33 +0200 Subject: [PATCH 48/51] Fix completion performance with shrink=True. Before, the completion was shrinked every time any item was removed/added to the completion (rowsRemoved/rowsInserted signals), which was >3000 times when completing history. Also, the signals got connected multiple times if setting the same model, which made the situation worse. Fixes #734. --- CHANGELOG.asciidoc | 1 + qutebrowser/completion/completer.py | 2 +- qutebrowser/completion/completionwidget.py | 13 +++++++++++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 92129b3e1..c2daf1efe 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -81,6 +81,7 @@ Fixed - Fixed handling of keybindings containing Ctrl/Meta on OS X. - Fixed crash when downloading an URL without filename (e.g. magnet links) via "Save as...". - Fixed exception when starting qutebrowser with `:set` as argument. +- Fixed horrible completion performance when the `shrink` option was set. https://github.com/The-Compiler/qutebrowser/releases/tag/v0.2.1[v0.2.1] ----------------------------------------------------------------------- diff --git a/qutebrowser/completion/completer.py b/qutebrowser/completion/completer.py index 2fd1858ca..197c62ce1 100644 --- a/qutebrowser/completion/completer.py +++ b/qutebrowser/completion/completer.py @@ -272,7 +272,7 @@ class Completer(QObject): pattern = parts[self._cursor_part].strip() except IndexError: pattern = '' - self._model().set_pattern(pattern) + completion.set_pattern(pattern) log.completion.debug( "New completion for {}: {}, with pattern '{}'".format( diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py index aa2fd31da..0bd6b04c9 100644 --- a/qutebrowser/completion/completionwidget.py +++ b/qutebrowser/completion/completionwidget.py @@ -201,8 +201,17 @@ class CompletionView(QTreeView): for i in range(model.rowCount()): self.expand(model.index(i, 0)) self._resize_columns() - model.rowsRemoved.connect(self.maybe_resize_completion) - model.rowsInserted.connect(self.maybe_resize_completion) + self.maybe_resize_completion() + + def set_pattern(self, pattern): + """Set the completion pattern for the current model. + + Called from on_update_completion(). + + Args: + pattern: The filter pattern to set (what the user entered). + """ + self.model().set_pattern(pattern) self.maybe_resize_completion() @pyqtSlot() From 7102459c8148f1f356b47231638d48c4d90161e8 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 5 Jun 2015 11:15:18 +0200 Subject: [PATCH 49/51] Rename _get_modeman() to instance(). --- qutebrowser/keyinput/modeman.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index fc70ac76b..e64522204 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -97,25 +97,25 @@ def init(win_id, parent): return modeman -def _get_modeman(win_id): +def instance(win_id): """Get a modemanager object.""" return objreg.get('mode-manager', scope='window', window=win_id) def enter(win_id, mode, reason=None, only_if_normal=False): """Enter the mode 'mode'.""" - _get_modeman(win_id).enter(mode, reason, only_if_normal) + instance(win_id).enter(mode, reason, only_if_normal) def leave(win_id, mode, reason=None): """Leave the mode 'mode'.""" - _get_modeman(win_id).leave(mode, reason) + instance(win_id).leave(mode, reason) def maybe_leave(win_id, mode, reason=None): """Convenience method to leave 'mode' without exceptions.""" try: - _get_modeman(win_id).leave(mode, reason) + instance(win_id).leave(mode, reason) except NotInModeError as e: # This is rather likely to happen, so we only log to debug log. log.modes.debug("{} (leave reason: {})".format(e, reason)) From 728f06e797d9c9cf002979b2b365748913bb3c32 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 5 Jun 2015 11:15:22 +0200 Subject: [PATCH 50/51] Close context menu if another mode was entered. Fixes #735. --- qutebrowser/browser/webview.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qutebrowser/browser/webview.py b/qutebrowser/browser/webview.py index 584c7f268..ec82d2567 100644 --- a/qutebrowser/browser/webview.py +++ b/qutebrowser/browser/webview.py @@ -620,6 +620,7 @@ class WebView(QWebView): """Save a reference to the context menu so we can close it.""" menu = self.page().createStandardContextMenu() self.shutting_down.connect(menu.close) + modeman.instance(self.win_id).entered.connect(menu.close) menu.exec_(e.globalPos()) def wheelEvent(self, e): From b55e22b5c335104b80266bc761461bd0285aa201 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 5 Jun 2015 15:29:09 +0200 Subject: [PATCH 51/51] Refactor key mode/parser handling in modeman. --- qutebrowser/keyinput/basekeyparser.py | 3 ++ qutebrowser/keyinput/keyparser.py | 1 + qutebrowser/keyinput/modeman.py | 53 +++++++++---------------- qutebrowser/keyinput/modeparsers.py | 2 + qutebrowser/mainwindow/statusbar/bar.py | 14 +++---- 5 files changed, 32 insertions(+), 41 deletions(-) diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index b52a39824..1c2c9ab86 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -49,6 +49,8 @@ class BaseKeyParser(QObject): special: execute() was called via a special key binding do_log: Whether to log keypresses or not. + passthrough: Whether unbound keys should be passed through with this + handler. Attributes: bindings: Bound key bindings @@ -69,6 +71,7 @@ class BaseKeyParser(QObject): keystring_updated = pyqtSignal(str) do_log = True + passthrough = False Match = usertypes.enum('Match', ['partial', 'definitive', 'ambiguous', 'other', 'none']) diff --git a/qutebrowser/keyinput/keyparser.py b/qutebrowser/keyinput/keyparser.py index bef364e66..46f179fdb 100644 --- a/qutebrowser/keyinput/keyparser.py +++ b/qutebrowser/keyinput/keyparser.py @@ -55,6 +55,7 @@ class PassthroughKeyParser(CommandKeyParser): """ do_log = False + passthrough = True def __init__(self, win_id, mode, parent=None, warn=True): """Constructor. diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index e64522204..357a5ffc9 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -84,16 +84,8 @@ def init(win_id, parent): modeman.destroyed.connect( functools.partial(objreg.delete, 'keyparsers', scope='window', window=win_id)) - modeman.register(KM.normal, keyparsers[KM.normal].handle) - modeman.register(KM.hint, keyparsers[KM.hint].handle) - modeman.register(KM.insert, keyparsers[KM.insert].handle, passthrough=True) - modeman.register(KM.passthrough, keyparsers[KM.passthrough].handle, - passthrough=True) - modeman.register(KM.command, keyparsers[KM.command].handle, - 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) + for mode, parser in keyparsers.items(): + modeman.register(mode, parser) return modeman @@ -126,10 +118,9 @@ class ModeManager(QObject): """Manager for keyboard modes. Attributes: - passthrough: A list of modes in which to pass through events. mode: The mode we're currently in. _win_id: The window ID of this ModeManager - _handlers: A dictionary of modes and their handlers. + _parsers: A dictionary of modes and their keyparsers. _forward_unbound_keys: If we should forward unbound keys. _releaseevents_to_pass: A set of KeyEvents where the keyPressEvent was passed through, so the release event should as @@ -151,8 +142,7 @@ class ModeManager(QObject): def __init__(self, win_id, parent=None): super().__init__(parent) self._win_id = win_id - self._handlers = {} - self.passthrough = [] + self._parsers = {} self.mode = usertypes.KeyMode.normal self._releaseevents_to_pass = set() self._forward_unbound_keys = config.get( @@ -160,8 +150,7 @@ class ModeManager(QObject): objreg.get('config').changed.connect(self.set_forward_unbound_keys) def __repr__(self): - return utils.get_repr(self, mode=self.mode, - passthrough=self.passthrough) + return utils.get_repr(self, mode=self.mode) def _eventFilter_keypress(self, event): """Handle filtering of KeyPress events. @@ -173,11 +162,11 @@ class ModeManager(QObject): True if event should be filtered, False otherwise. """ curmode = self.mode - handler = self._handlers[curmode] + parser = self._parsers[curmode] if curmode != usertypes.KeyMode.insert: - log.modes.debug("got keypress in mode {} - calling handler " - "{}".format(curmode, utils.qualname(handler))) - handled = handler(event) if handler is not None else False + log.modes.debug("got keypress in mode {} - delegating to " + "{}".format(curmode, utils.qualname(parser))) + handled = parser.handle(event) is_non_alnum = bool(event.modifiers()) or not event.text().strip() focus_widget = QApplication.instance().focusWidget() @@ -187,7 +176,7 @@ class ModeManager(QObject): filter_this = True elif is_tab and not isinstance(focus_widget, QWebView): filter_this = True - elif (curmode in self.passthrough or + elif (parser.passthrough or self._forward_unbound_keys == 'all' or (self._forward_unbound_keys == 'auto' and is_non_alnum)): filter_this = False @@ -202,8 +191,8 @@ class ModeManager(QObject): "passthrough: {}, is_non_alnum: {}, is_tab {} --> " "filter: {} (focused: {!r})".format( handled, self._forward_unbound_keys, - curmode in self.passthrough, is_non_alnum, - is_tab, filter_this, focus_widget)) + parser.passthrough, is_non_alnum, is_tab, + filter_this, focus_widget)) return filter_this def _eventFilter_keyrelease(self, event): @@ -226,20 +215,16 @@ class ModeManager(QObject): log.modes.debug("filter: {}".format(filter_this)) return filter_this - def register(self, mode, handler, passthrough=False): + def register(self, mode, parser): """Register a new mode. Args: mode: The name of the mode. - handler: Handler for keyPressEvents. - passthrough: Whether to pass key bindings in this mode through to - the widgets. + parser: The KeyParser which should be used. """ - if not isinstance(mode, usertypes.KeyMode): - raise TypeError("Mode {} is no KeyMode member!".format(mode)) - self._handlers[mode] = handler - if passthrough: - self.passthrough.append(mode) + assert isinstance(mode, usertypes.KeyMode) + assert parser is not None + self._parsers[mode] = parser def enter(self, mode, reason=None, only_if_normal=False): """Enter a new mode. @@ -253,8 +238,8 @@ class ModeManager(QObject): raise TypeError("Mode {} is no KeyMode member!".format(mode)) log.modes.debug("Entering mode {}{}".format( mode, '' if reason is None else ' (reason: {})'.format(reason))) - if mode not in self._handlers: - raise ValueError("No handler for mode {}".format(mode)) + if mode not in self._parsers: + raise ValueError("No keyparser for mode {}".format(mode)) prompt_modes = (usertypes.KeyMode.prompt, usertypes.KeyMode.yesno) if self.mode == mode or (self.mode in prompt_modes and mode in prompt_modes): diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 8d47de0c1..d16734ed0 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -224,6 +224,8 @@ class CaretKeyParser(keyparser.CommandKeyParser): """KeyParser for caret mode.""" + passthrough = True + def __init__(self, win_id, parent=None): super().__init__(win_id, parent, supports_count=True, supports_chains=True) diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index 5f633eaaa..bc828a261 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -469,9 +469,9 @@ class StatusBar(QWidget): @pyqtSlot(usertypes.KeyMode) def on_mode_entered(self, mode): """Mark certain modes in the commandline.""" - mode_manager = objreg.get('mode-manager', scope='window', - window=self._win_id) - if mode in mode_manager.passthrough: + keyparsers = objreg.get('keyparsers', scope='window', + window=self._win_id) + if keyparsers[mode].passthrough: self._set_mode_text(mode.name) if mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret): self.set_mode_active(mode, True) @@ -479,10 +479,10 @@ class StatusBar(QWidget): @pyqtSlot(usertypes.KeyMode, usertypes.KeyMode) def on_mode_left(self, old_mode, new_mode): """Clear marked mode.""" - mode_manager = objreg.get('mode-manager', scope='window', - window=self._win_id) - if old_mode in mode_manager.passthrough: - if new_mode in mode_manager.passthrough: + keyparsers = objreg.get('keyparsers', scope='window', + window=self._win_id) + if keyparsers[old_mode].passthrough: + if keyparsers[new_mode].passthrough: self._set_mode_text(new_mode.name) else: self.txt.set_text(self.txt.Text.normal, '')