From 33dbed5624bc808c51e1e3b478eaaa93129bbb87 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 9 Apr 2015 06:35:41 +0200 Subject: [PATCH 01/16] Update authors. --- README.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/README.asciidoc b/README.asciidoc index e686ea522..0c0bdad0e 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -134,6 +134,7 @@ Contributors, sorted by the number of commits in descending order: // QUTE_AUTHORS_START * Florian Bruhin +* Bruno Oliveira * Joel Torstensson * Raphael Pierzina * Claude From 425cffc2f7001030c97f7b671255afc3ea7df15e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 9 Apr 2015 07:43:47 +0200 Subject: [PATCH 02/16] pylint: Ignore 'undefined-variable' for tests. It's less than optimal, but disabling it selectively because of https://bitbucket.org/logilab/pylint/issue/511/ is too annoying. --- scripts/run_pylint_on_tests.py | 9 +++++++-- tests/config/test_configtypes.py | 15 --------------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/scripts/run_pylint_on_tests.py b/scripts/run_pylint_on_tests.py index 94ba89c7d..792eaf7a6 100644 --- a/scripts/run_pylint_on_tests.py +++ b/scripts/run_pylint_on_tests.py @@ -46,8 +46,13 @@ def main(): for fn in filenames: if os.path.splitext(fn)[1] == '.py': files.append(os.path.join(dirpath, fn)) - disabled = ['attribute-defined-outside-init', 'redefined-outer-name', - 'unused-argument'] + disabled = [ + 'attribute-defined-outside-init', + 'redefined-outer-name', + 'unused-argument', + # https://bitbucket.org/logilab/pylint/issue/511/ + 'undefined-variable', + ] no_docstring_rgx = ['^__.*__$', '^setup$'] args = (['--disable={}'.format(','.join(disabled)), '--no-docstring-rgx=({})'.format('|'.join(no_docstring_rgx))] + diff --git a/tests/config/test_configtypes.py b/tests/config/test_configtypes.py index 0b6ea21b7..08a095177 100644 --- a/tests/config/test_configtypes.py +++ b/tests/config/test_configtypes.py @@ -328,9 +328,6 @@ class TestBool: """Test Bool.""" - # https://bitbucket.org/logilab/pylint/issue/511/ - # pylint: disable=undefined-variable - TESTS = {True: ['1', 'yes', 'YES', 'true', 'TrUe', 'on'], False: ['0', 'no', 'NO', 'false', 'FaLsE', 'off']} @@ -942,9 +939,6 @@ class TestColorSystem: """Test ColorSystem.""" - # https://bitbucket.org/logilab/pylint/issue/511/ - # pylint: disable=undefined-variable - TESTS = { 'RGB': QColor.Rgb, 'rgb': QColor.Rgb, @@ -1109,9 +1103,6 @@ class TestFont: """Test Font/QtFont.""" - # https://bitbucket.org/logilab/pylint/issue/511/ - # pylint: disable=undefined-variable - TESTS = { # (style, weight, pointsize, pixelsize, family '"Foobar Neue"': @@ -1960,9 +1951,6 @@ class TestAutoSearch: """Test AutoSearch.""" - # https://bitbucket.org/logilab/pylint/issue/511/ - # pylint: disable=undefined-variable - TESTS = { 'naive': ['naive', 'NAIVE'] + TestBool.TESTS[True], 'dns': ['dns', 'DNS'], @@ -2012,9 +2000,6 @@ class TestIgnoreCase: """Test IgnoreCase.""" - # https://bitbucket.org/logilab/pylint/issue/511/ - # pylint: disable=undefined-variable - TESTS = { 'smart': ['smart', 'SMART'], True: TestBool.TESTS[True], From 2fa66ba2500b1a4a12049b2072dcde938cf66831 Mon Sep 17 00:00:00 2001 From: Joel Torstensson Date: Wed, 25 Mar 2015 00:09:45 +0100 Subject: [PATCH 03/16] Added option for downloadview placement. --- qutebrowser/config/configdata.py | 5 ++++ qutebrowser/mainwindow/mainwindow.py | 35 +++++++++++++++++----------- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 1d332e62a..b03a4f52d 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -235,6 +235,11 @@ def data(readonly=False): SettingValue(typ.Perc(), '100%'), "The default zoom level."), + ('downloads-at-top', + SettingValue(typ.Bool(), 'true'), + "Whether to show downloaded files at top, " + "false will show at bottom."), + ('message-timeout', SettingValue(typ.Int(), '2000'), "Time (in ms) to show messages in the statusbar for."), diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index dbcead867..8f48c9155 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -90,19 +90,12 @@ class MainWindow(QWidget): self._vbox.setContentsMargins(0, 0, 0, 0) self._vbox.setSpacing(0) - log.init.debug("Initializing downloads...") - download_manager = downloads.DownloadManager(self.win_id, self) - objreg.register('download-manager', download_manager, scope='window', - window=self.win_id) - - self._downloadview = downloadview.DownloadView(self.win_id) - self._vbox.addWidget(self._downloadview) - self._downloadview.show() - - self._tabbed_browser = tabbedbrowser.TabbedBrowser(self.win_id) - objreg.register('tabbed-browser', self._tabbed_browser, scope='window', - window=self.win_id) - self._vbox.addWidget(self._tabbed_browser) + if config.get('ui', 'downloads-at-top'): + self._init_downloadview() + self._init_tabbed_browser() + else: + self._init_tabbed_browser() + self._init_downloadview() # We need to set an explicit parent for StatusBar because it does some # show/hide magic immediately which would mean it'd show up as a @@ -142,6 +135,22 @@ class MainWindow(QWidget): if section == 'completion' and option in ('height', 'shrink'): self.resize_completion() + def _init_downloadview(self): + log.init.debug("Initializing downloads...") + download_manager = downloads.DownloadManager(self.win_id, self) + objreg.register('download-manager', download_manager, scope='window', + window=self.win_id) + + self._downloadview = downloadview.DownloadView(self.win_id) + self._vbox.addWidget(self._downloadview) + self._downloadview.show() + + def _init_tabbed_browser(self): + self._tabbed_browser = tabbedbrowser.TabbedBrowser(self.win_id) + objreg.register('tabbed-browser', self._tabbed_browser, scope='window', + window=self.win_id) + self._vbox.addWidget(self._tabbed_browser) + def _load_state_geometry(self): """Load the geometry from the state file.""" state_config = objreg.get('state-config') From cc2c7c09ea60ea2ff3eed1b0a42ba82356f1749e Mon Sep 17 00:00:00 2001 From: Joel Torstensson Date: Thu, 9 Apr 2015 11:39:41 +0200 Subject: [PATCH 04/16] Changing position without restart now possible. --- qutebrowser/config/configdata.py | 8 ++--- qutebrowser/config/configtypes.py | 7 ++++ qutebrowser/mainwindow/mainwindow.py | 50 ++++++++++++++++------------ 3 files changed, 39 insertions(+), 26 deletions(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index b03a4f52d..35c7dbaea 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -235,10 +235,9 @@ def data(readonly=False): SettingValue(typ.Perc(), '100%'), "The default zoom level."), - ('downloads-at-top', - SettingValue(typ.Bool(), 'true'), - "Whether to show downloaded files at top, " - "false will show at bottom."), + ('downloads-position', + SettingValue(typ.VerticalPosition(), 'north'), + "Where to show the downloaded files."), ('message-timeout', SettingValue(typ.Int(), '2000'), @@ -1011,7 +1010,6 @@ def data(readonly=False): DATA = data(readonly=True) - KEY_FIRST_COMMENT = """ # vim: ft=conf # diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index b0523f5f4..55159d538 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -1264,6 +1264,13 @@ class Position(BaseType): return self.MAPPING[value] +class VerticalPosition(BaseType): + + """The position of the download bar.""" + + valid_values = ValidValues('north', 'south') + + class UrlList(List): """A list of URLs.""" diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index 8f48c9155..ff50ab354 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -90,18 +90,24 @@ class MainWindow(QWidget): self._vbox.setContentsMargins(0, 0, 0, 0) self._vbox.setSpacing(0) - if config.get('ui', 'downloads-at-top'): - self._init_downloadview() - self._init_tabbed_browser() - else: - self._init_tabbed_browser() - self._init_downloadview() + log.init.debug("Initializing downloads...") + download_manager = downloads.DownloadManager(self.win_id, self) + objreg.register('download-manager', download_manager, scope='window', + window=self.win_id) + + self._downloadview = downloadview.DownloadView(self.win_id) + self._downloadview.show() + + self._tabbed_browser = tabbedbrowser.TabbedBrowser(self.win_id) + objreg.register('tabbed-browser', self._tabbed_browser, scope='window', + window=self.win_id) # We need to set an explicit parent for StatusBar because it does some # show/hide magic immediately which would mean it'd show up as a # window. self.status = bar.StatusBar(self.win_id, parent=self) - self._vbox.addWidget(self.status) + + self._add_widgets() self._completion = completionwidget.CompletionView(self.win_id, self) @@ -134,22 +140,24 @@ class MainWindow(QWidget): """Resize the completion if related config options changed.""" if section == 'completion' and option in ('height', 'shrink'): self.resize_completion() + elif section == 'ui' and option == 'downloads-position': + self._add_widgets() - def _init_downloadview(self): - log.init.debug("Initializing downloads...") - download_manager = downloads.DownloadManager(self.win_id, self) - objreg.register('download-manager', download_manager, scope='window', - window=self.win_id) + def _add_widgets(self): + self._vbox.removeWidget(self._tabbed_browser) + self._vbox.removeWidget(self._downloadview) + self._vbox.removeWidget(self.status) + position = config.get('ui', 'downloads-position') + if position == 'north': + self._vbox.addWidget(self._downloadview) + self._vbox.addWidget(self._tabbed_browser) + elif position == 'south': + self._vbox.addWidget(self._tabbed_browser) + self._vbox.addWidget(self._downloadview) + else: + raise ValueError("Invalid position {}!".format(position)) + self._vbox.addWidget(self.status) - self._downloadview = downloadview.DownloadView(self.win_id) - self._vbox.addWidget(self._downloadview) - self._downloadview.show() - - def _init_tabbed_browser(self): - self._tabbed_browser = tabbedbrowser.TabbedBrowser(self.win_id) - objreg.register('tabbed-browser', self._tabbed_browser, scope='window', - window=self.win_id) - self._vbox.addWidget(self._tabbed_browser) def _load_state_geometry(self): """Load the geometry from the state file.""" From 12c83b721fabc365113f0dd656cb7070895dcfc1 Mon Sep 17 00:00:00 2001 From: Joel Torstensson Date: Thu, 9 Apr 2015 12:49:32 +0200 Subject: [PATCH 05/16] Fixed some style errors. --- qutebrowser/config/configdata.py | 5 +++-- qutebrowser/mainwindow/mainwindow.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 35c7dbaea..96e146ada 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -236,8 +236,8 @@ def data(readonly=False): "The default zoom level."), ('downloads-position', - SettingValue(typ.VerticalPosition(), 'north'), - "Where to show the downloaded files."), + SettingValue(typ.VerticalPosition(), 'north'), + "Where to show the downloaded files."), ('message-timeout', SettingValue(typ.Int(), '2000'), @@ -1010,6 +1010,7 @@ def data(readonly=False): DATA = data(readonly=True) + KEY_FIRST_COMMENT = """ # vim: ft=conf # diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index ff50ab354..c619a6f47 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -144,6 +144,7 @@ class MainWindow(QWidget): self._add_widgets() def _add_widgets(self): + """Add or readd all widgets to the VBox.""" self._vbox.removeWidget(self._tabbed_browser) self._vbox.removeWidget(self._downloadview) self._vbox.removeWidget(self.status) @@ -158,7 +159,6 @@ class MainWindow(QWidget): raise ValueError("Invalid position {}!".format(position)) self._vbox.addWidget(self.status) - def _load_state_geometry(self): """Load the geometry from the state file.""" state_config = objreg.get('state-config') From 2a796d9aa432bbaf5401a501b1854ee615009be6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 9 Apr 2015 12:54:59 +0200 Subject: [PATCH 06/16] Regenerate docs. --- doc/help/settings.asciidoc | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 93d290e34..b774461b2 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -31,6 +31,7 @@ |Setting|Description |<>|The available zoom levels, separated by commas. |<>|The default zoom level. +|<>|Where to show the downloaded files. |<>|Time (in ms) to show messages in the statusbar for. |<>|Whether to show messages in unfocused windows. |<>|Whether to confirm quitting the application. @@ -441,6 +442,17 @@ The default zoom level. Default: +pass:[100%]+ +[[ui-downloads-position]] +=== downloads-position +Where to show the downloaded files. + +Valid values: + + * +north+ + * +south+ + +Default: +pass:[north]+ + [[ui-message-timeout]] === message-timeout Time (in ms) to show messages in the statusbar for. From 9111ae7b3c3d12b729b7fda1fb76c1f9b9d3891c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 9 Apr 2015 13:17:57 +0200 Subject: [PATCH 07/16] tox: Update pytest-mock to 0.4.3. Upstream changelog: - mocker and the backward compatible mock fixture now return the same object. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 60a480388..101d2b02c 100644 --- a/tox.ini +++ b/tox.ini @@ -21,7 +21,7 @@ deps = pytest==2.7.0 pytest-capturelog==0.7 pytest-qt==1.3.0 - pytest-mock==0.4.2 + pytest-mock==0.4.3 # We don't use {[testenv:mkvenv]commands} here because that seems to be broken # on Ubuntu Trusty. commands = From 9e0d65c21995d088b6bf16bde741818c55fb9cb2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 9 Apr 2015 13:22:16 +0200 Subject: [PATCH 08/16] manpage: Mention ":help". Closes #618. --- doc/qutebrowser.1.asciidoc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/qutebrowser.1.asciidoc b/doc/qutebrowser.1.asciidoc index 68e6e1d14..c603aeace 100644 --- a/doc/qutebrowser.1.asciidoc +++ b/doc/qutebrowser.1.asciidoc @@ -21,6 +21,10 @@ on Python, PyQt5 and QtWebKit and free software, licensed under the GPL. It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl. +Note the commands and settings of qutebrowser are not described in this +manpage, but in the help integrated in qutebrowser - use the ":help" command to +show it. + == OPTIONS // QUTE_OPTIONS_START === positional arguments From ecb0a4e2f8b7cfcddfa944b6aeface2082907a4b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 9 Apr 2015 13:24:52 +0200 Subject: [PATCH 09/16] manpage: Mention XDG_*_HOME in the FILES section. Closes #619. --- doc/qutebrowser.1.asciidoc | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/qutebrowser.1.asciidoc b/doc/qutebrowser.1.asciidoc index c603aeace..f559fb96a 100644 --- a/doc/qutebrowser.1.asciidoc +++ b/doc/qutebrowser.1.asciidoc @@ -114,6 +114,11 @@ show it. - '~/.local/share/qutebrowser/': Various state information. - '~/.cache/qutebrowser/': Temporary data. +Note qutebrowser conforms to the XDG basedir specification - if +'XDG_CONFIG_HOME', 'XDG_DATA_HOME' or 'XDG_CACHE_HOME' are set in the +environment, the directories configured there are used instead of the above +defaults. + == BUGS Bugs are tracked in the Github issue tracker at https://github.com/The-Compiler/qutebrowser/issues. From 2d8df76609743b4dd1c4b55863df437b6166d894 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 9 Apr 2015 17:45:16 +0200 Subject: [PATCH 10/16] Add $QUTE_HTML and $QUTE_TEXT for userscripts. --- doc/userscripts.asciidoc | 2 ++ qutebrowser/browser/commands.py | 11 ++++++++--- qutebrowser/browser/hints.py | 3 +++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/doc/userscripts.asciidoc b/doc/userscripts.asciidoc index 0e34f5d4d..e9ae01cf8 100644 --- a/doc/userscripts.asciidoc +++ b/doc/userscripts.asciidoc @@ -24,6 +24,8 @@ The following environment variables will be set when an userscript is launched: command or key binding). - `QUTE_USER_AGENT`: The currently set user agent. - `QUTE_FIFO`: The FIFO or file to write commands to. +- `QUTE_HTML`: The HTML source of the current page. +- `QUTE_TEXT`: The plaintext of the current page. In `command` mode: diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 7dc0a35fd..93bed2b49 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -876,9 +876,14 @@ class CommandDispatcher: env['QUTE_TITLE'] = tabbed_browser.page_title(idx) webview = tabbed_browser.currentWidget() - if webview is not None and webview.hasSelection(): - env['QUTE_SELECTED_TEXT'] = webview.selectedText() - env['QUTE_SELECTED_HTML'] = webview.selectedHtml() + if webview is not None: + if webview.hasSelection(): + env['QUTE_SELECTED_TEXT'] = webview.selectedText() + env['QUTE_SELECTED_HTML'] = webview.selectedHtml() + mainframe = webview.page().mainFrame() + if mainframe is not None: + env['QUTE_HTML'] = mainframe.toHtml() + env['QUTE_TEXT'] = mainframe.toPlainText() try: url = tabbed_browser.current_url() diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index debe5658b..d4fe4bcc6 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -518,10 +518,13 @@ class HintManager(QObject): """ cmd = context.args[0] args = context.args[1:] + frame = context.mainframe env = { 'QUTE_MODE': 'hints', 'QUTE_SELECTED_TEXT': str(elem), 'QUTE_SELECTED_HTML': elem.toOuterXml(), + 'QUTE_HTML': mainframe.toHtml(), + 'QUTE_TEXT': mainframe.toPlainText(), } url = self._resolve_url(elem, context.baseurl) if url is not None: From 7160a89cb91ff6965861ca085e9a39a63f5dcb9c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 9 Apr 2015 17:47:09 +0200 Subject: [PATCH 11/16] Fix NameError in hints.py. --- qutebrowser/browser/hints.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index d4fe4bcc6..2a6341b03 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -523,8 +523,8 @@ class HintManager(QObject): 'QUTE_MODE': 'hints', 'QUTE_SELECTED_TEXT': str(elem), 'QUTE_SELECTED_HTML': elem.toOuterXml(), - 'QUTE_HTML': mainframe.toHtml(), - 'QUTE_TEXT': mainframe.toPlainText(), + 'QUTE_HTML': frame.toHtml(), + 'QUTE_TEXT': frame.toPlainText(), } url = self._resolve_url(elem, context.baseurl) if url is not None: From f77ba5744b4d08b7dff34b1f7d3a9886f3a69bb9 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 9 Apr 2015 19:51:50 +0200 Subject: [PATCH 12/16] Add a ui -> hide-mouse-cursor option. --- doc/help/settings.asciidoc | 12 ++++++++++++ qutebrowser/app.py | 17 ++++++++++++++++- qutebrowser/config/configdata.py | 4 ++++ qutebrowser/keyinput/modeman.py | 16 +++++++++++++--- qutebrowser/mainwindow/mainwindow.py | 4 ++++ 5 files changed, 49 insertions(+), 4 deletions(-) diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index b774461b2..35a22db40 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -43,6 +43,7 @@ |<>|Whether to remove finished downloads automatically. |<>|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. |============== .Quick reference for section ``network'' @@ -564,6 +565,17 @@ The format to use for the window title. The following placeholders are defined: Default: +pass:[{perc}{title}{title_sep}qutebrowser]+ +[[ui-hide-mouse-cursor]] +=== hide-mouse-cursor +Whether to hide the mouse cursor. + +Valid values: + + * +true+ + * +false+ + +Default: +pass:[false]+ + == network Settings related to the network. diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 359f7cd3d..6b05f369a 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -33,7 +33,7 @@ import faulthandler import json from PyQt5.QtWidgets import QApplication, QDialog, QMessageBox -from PyQt5.QtGui import QDesktopServices, QPixmap, QIcon +from PyQt5.QtGui import QDesktopServices, QPixmap, QIcon, QCursor from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QTimer, QUrl, QObject, Qt, QSocketNotifier) try: @@ -210,6 +210,19 @@ class Application(QApplication): objreg.register('cache', diskcache) log.init.debug("Initializing completions...") completionmodels.init() + log.init.debug("Misc initialization...") + self.maybe_hide_mouse_cursor() + objreg.get('config').changed.connect(self.maybe_hide_mouse_cursor) + + @config.change_filter('ui', 'hide-mouse-cursor') + def maybe_hide_mouse_cursor(self): + """Hide the mouse cursor if it isn't yet and it's configured.""" + if config.get('ui', 'hide-mouse-cursor'): + if self.overrideCursor() is not None: + return + self.setOverrideCursor(QCursor(Qt.BlankCursor)) + else: + self.restoreOverrideCursor() def _init_icon(self): """Initialize the icon of qutebrowser.""" @@ -940,8 +953,10 @@ class Application(QApplication): objreg.delete('last-focused-main-window') except KeyError: pass + self.restoreOverrideCursor() else: objreg.register('last-focused-main-window', window, update=True) + self.maybe_hide_mouse_cursor() @pyqtSlot(QUrl) def open_desktopservices_url(self, url): diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 96e146ada..fec9c93fe 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -297,6 +297,10 @@ def data(readonly=False): "otherwise.\n" "* `{id}`: The internal window ID of this window."), + ('hide-mouse-cursor', + SettingValue(typ.Bool(), 'false'), + "Whether to hide the mouse cursor."), + readonly=readonly )), diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index 4699de8e9..00c0a4d73 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -131,9 +131,20 @@ class EventFilter(QObject): def eventFilter(self, obj, event): """Forward events to the correct modeman.""" try: + qapp = QApplication.instance() if not self._activated: return False - if event.type() not in [QEvent.KeyPress, QEvent.KeyRelease]: + if event.type() in [QEvent.MouseButtonDblClick, + QEvent.MouseButtonPress, + QEvent.MouseButtonRelease, + QEvent.MouseMove]: + if qapp.overrideCursor() is None: + # Mouse cursor shown -> don't filter event + return False + else: + # Mouse cursor hidden -> filter event + return True + elif event.type() not in [QEvent.KeyPress, QEvent.KeyRelease]: # We're not interested in non-key-events so we pass them # through. return False @@ -141,8 +152,7 @@ class EventFilter(QObject): # We already handled this same event at some point earlier, so # we're not interested in it anymore. return False - if (QApplication.instance().activeWindow() not in - objreg.window_registry.values()): + if qapp.activeWindow() not in objreg.window_registry.values(): # Some other window (print dialog, etc.) is focused so we pass # the event through. return False diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index c619a6f47..b9e456b42 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -128,6 +128,10 @@ class MainWindow(QWidget): # we defer this until everything else is initialized. QTimer.singleShot(0, self._connect_resize_completion) objreg.get('config').changed.connect(self.on_config_changed) + + if config.get('ui', 'hide-mouse-cursor'): + self.setCursor(Qt.BlankCursor) + #self.retranslateUi(MainWindow) #self.tabWidget.setCurrentIndex(0) #QtCore.QMetaObject.connectSlotsByName(MainWindow) From 83dbe4846916ebeecd14d18e6db60e7e76f09ce1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 9 Apr 2015 20:18:23 +0200 Subject: [PATCH 13/16] Refactor EventFilter. --- qutebrowser/app.py | 96 +++++++++++++++++++++++++++++++-- qutebrowser/keyinput/modeman.py | 51 ------------------ 2 files changed, 92 insertions(+), 55 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 6b05f369a..db50f0425 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -33,9 +33,9 @@ import faulthandler import json from PyQt5.QtWidgets import QApplication, QDialog, QMessageBox -from PyQt5.QtGui import QDesktopServices, QPixmap, QIcon, QCursor +from PyQt5.QtGui import QDesktopServices, QPixmap, QIcon, QCursor, QWindow from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QTimer, QUrl, - QObject, Qt, QSocketNotifier) + QObject, Qt, QSocketNotifier, QEvent) try: import hunter except ImportError: @@ -52,7 +52,6 @@ from qutebrowser.mainwindow import mainwindow from qutebrowser.misc import (crashdialog, readline, ipc, earlyinit, savemanager, sessions) from qutebrowser.misc import utilcmds # pylint: disable=unused-import -from qutebrowser.keyinput import modeman from qutebrowser.utils import (log, version, message, utils, qtutils, urlutils, objreg, usertypes, standarddir) # We import utilcmds to run the cmdutils.register decorators. @@ -143,7 +142,7 @@ class Application(QApplication): QTimer.singleShot(0, self._process_args) log.init.debug("Initializing eventfilter...") - self._event_filter = modeman.EventFilter(self) + self._event_filter = EventFilter(self) self.installEventFilter(self._event_filter) log.init.debug("Connecting signals...") @@ -977,3 +976,92 @@ class Application(QApplication): print("Now logging late shutdown.", file=sys.stderr) hunter.trace() super().exit(status) + + +class EventFilter(QObject): + + """Global Qt event filter. + + Attributes: + _activated: Whether the EventFilter is currently active. + _handlers; A {QEvent.Type: callable} dict with the handlers for an + event. + """ + + def __init__(self, parent=None): + super().__init__(parent) + self._activated = True + self._handlers = { + QEvent.MouseButtonDblClick: self._handle_mouse_event, + QEvent.MouseButtonPress: self._handle_mouse_event, + QEvent.MouseButtonRelease: self._handle_mouse_event, + QEvent.MouseMove: self._handle_mouse_event, + QEvent.KeyPress: self._handle_key_event, + QEvent.KeyRelease: self._handle_key_event, + } + + def _handle_key_event(self, event): + """Handle a key press/release event. + + Args: + event: The QEvent which is about to be delivered. + + Return: + True if the event should be filtered, False if it's passed through. + """ + qapp = QApplication.instance() + if qapp.activeWindow() not in objreg.window_registry.values(): + # Some other window (print dialog, etc.) is focused so we pass the + # event through. + return False + try: + man = objreg.get('mode-manager', scope='window', window='current') + return man.eventFilter(event) + except objreg.RegistryUnavailableError: + # No window available yet, or not a MainWindow + return False + + def _handle_mouse_event(self, _event): + """Handle a mouse event. + + Args: + _event: The QEvent which is about to be delivered. + + Return: + True if the event should be filtered, False if it's passed through. + """ + if QApplication.instance().overrideCursor() is None: + # Mouse cursor shown -> don't filter event + return False + else: + # Mouse cursor hidden -> filter event + return True + + def eventFilter(self, obj, event): + """Handle an event. + + Args: + obj: The object which will get the event. + event: The QEvent which is about to be delivered. + + Return: + True if the event should be filtered, False if it's passed through. + """ + try: + if not self._activated: + return False + if not isinstance(obj, QWindow): + # We already handled this same event at some point earlier, so + # we're not interested in it anymore. + return False + try: + handler = self._handlers[event.type()] + except KeyError: + return False + else: + return handler(event) + except: + # If there is an exception in here and we leave the eventfilter + # activated, we'll get an infinite loop and a stack overflow. + self._activated = False + raise diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index 00c0a4d73..7d77f872c 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -21,7 +21,6 @@ import functools -from PyQt5.QtGui import QWindow from PyQt5.QtCore import pyqtSignal, Qt, QObject, QEvent from PyQt5.QtWidgets import QApplication from PyQt5.QtWebKitWidgets import QWebView @@ -120,56 +119,6 @@ def maybe_leave(win_id, mode, reason=None): log.modes.debug("{} (leave reason: {})".format(e, reason)) -class EventFilter(QObject): - - """Event filter which passes the event to the current ModeManager.""" - - def __init__(self, parent=None): - super().__init__(parent) - self._activated = True - - def eventFilter(self, obj, event): - """Forward events to the correct modeman.""" - try: - qapp = QApplication.instance() - if not self._activated: - return False - if event.type() in [QEvent.MouseButtonDblClick, - QEvent.MouseButtonPress, - QEvent.MouseButtonRelease, - QEvent.MouseMove]: - if qapp.overrideCursor() is None: - # Mouse cursor shown -> don't filter event - return False - else: - # Mouse cursor hidden -> filter event - return True - elif event.type() not in [QEvent.KeyPress, QEvent.KeyRelease]: - # We're not interested in non-key-events so we pass them - # through. - return False - if not isinstance(obj, QWindow): - # We already handled this same event at some point earlier, so - # we're not interested in it anymore. - return False - if qapp.activeWindow() not in objreg.window_registry.values(): - # Some other window (print dialog, etc.) is focused so we pass - # the event through. - return False - try: - modeman = objreg.get('mode-manager', scope='window', - window='current') - return modeman.eventFilter(event) - except objreg.RegistryUnavailableError: - # No window available yet, or not a MainWindow - return False - except: - # If there is an exception in here and we leave the eventfilter - # activated, we'll get an infinite loop and a stack overflow. - self._activated = False - raise - - class ModeManager(QObject): """Manager for keyboard modes. From 8d98868ccdc153631f83e2b500742f9cf6dea104 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 9 Apr 2015 20:36:11 +0200 Subject: [PATCH 14/16] Fix deprecated default keybindings. Those were auto-corrected with the next run, but still are bad... --- qutebrowser/config/configdata.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index fec9c93fe..af0f5ac17 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1163,13 +1163,13 @@ KEY_DATA = collections.OrderedDict([ ('paste -w', ['wp']), ('paste -ws', ['wP']), ('quickmark-save', ['m']), - ('set-cmd-text ":quickmark-load "', ['b']), - ('set-cmd-text ":quickmark-load -t "', ['B']), - ('set-cmd-text ":quickmark-load -w"', ['wb']), + ('set-cmd-text -s :quickmark-load', ['b']), + ('set-cmd-text -s :quickmark-load -t', ['B']), + ('set-cmd-text -s :quickmark-load -w', ['wb']), ('save', ['sf']), - ('set-cmd-text ":set "', ['ss']), - ('set-cmd-text ":set -t "', ['sl']), - ('set-cmd-text ":set keybind "', ['sk']), + ('set-cmd-text -s :set', ['ss']), + ('set-cmd-text -s :set -t', ['sl']), + ('set-cmd-text -s :set keybind', ['sk']), ('zoom-out', ['-']), ('zoom-in', ['+']), ('zoom', ['=']), From e294e325f0ddd8f3dc59f7aa348dc12a1704056f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 10 Apr 2015 06:40:48 +0200 Subject: [PATCH 15/16] Ignore invalid history entries on start. --- qutebrowser/browser/history.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/qutebrowser/browser/history.py b/qutebrowser/browser/history.py index 6b4e0cdd9..90f2e5567 100644 --- a/qutebrowser/browser/history.py +++ b/qutebrowser/browser/history.py @@ -25,7 +25,7 @@ import collections from PyQt5.QtCore import pyqtSignal, QUrl from PyQt5.QtWebKit import QWebHistoryInterface -from qutebrowser.utils import utils, objreg, standarddir +from qutebrowser.utils import utils, objreg, standarddir, log from qutebrowser.config import config from qutebrowser.misc import lineparser @@ -89,6 +89,11 @@ class WebHistory(QWebHistoryInterface): if not data: # empty line continue + elif len(data) != 2: + # other malformed line + log.init.warning("Invalid history entry {!r}!".format( + line)) + continue atime, url = data # This de-duplicates history entries; only the latest # entry for each URL is kept. If you want to keep From f865b87a749e185088921dffda03aebad5886493 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 10 Apr 2015 07:52:06 +0200 Subject: [PATCH 16/16] Show a message and update notifier on reports. Fixes #340. Fixes #447. See #429. --- qutebrowser/misc/autoupdate.py | 101 ++++++++++++++++++++++++++++++++ qutebrowser/misc/crashdialog.py | 53 ++++++++++++++++- qutebrowser/misc/msgbox.py | 68 +++++++++++++++++++++ 3 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 qutebrowser/misc/autoupdate.py create mode 100644 qutebrowser/misc/msgbox.py diff --git a/qutebrowser/misc/autoupdate.py b/qutebrowser/misc/autoupdate.py new file mode 100644 index 000000000..370de0f06 --- /dev/null +++ b/qutebrowser/misc/autoupdate.py @@ -0,0 +1,101 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2014-2015 Florian Bruhin (The Compiler) +# +# 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 . + +"""Classes related to auto-updating and getting the latest version.""" + +import json +import functools + +from PyQt5.QtCore import pyqtSignal, QObject, QUrl +from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkRequest, + QNetworkReply) + + +class PyPIVersionClient(QObject): + + """A client for the PyPI API using QNetworkAccessManager. + + It gets the latest version of qutebrowser from PyPI. + + Attributes: + _nam: The QNetworkAccessManager used. + + Class attributes: + API_URL: The base API URL. + + Signals: + success: Emitted when getting the version info succeeded. + arg: The newest version. + error: Emitted when getting the version info failed. + arg: The error message, as string. + """ + + API_URL = 'https://pypi.python.org/pypi/{}/json' + success = pyqtSignal(str) + error = pyqtSignal(str) + + def __init__(self, parent=None): + super().__init__(parent) + self._nam = QNetworkAccessManager(self) + + def get_version(self, package='qutebrowser'): + """Get the newest version of a given package. + + Emits success/error when done. + + Args: + package: The name of the package to check. + """ + url = QUrl(self.API_URL.format(package)) + request = QNetworkRequest(url) + reply = self._nam.get(request) + if reply.isFinished(): + self.on_reply_finished(reply) + else: + reply.finished.connect(functools.partial( + self.on_reply_finished, reply)) + + def on_reply_finished(self, reply): + """When the reply finished, load and parse the json data. + + Then emits error/success. + + Args: + reply: The QNetworkReply which finished. + """ + if reply.error() != QNetworkReply.NoError: + self.error.emit(reply.errorString()) + return + try: + data = bytes(reply.readAll()).decode('utf-8') + except UnicodeDecodeError as e: + self.error.emit("Invalid UTF-8 data received in reply: " + "{}!".format(e)) + return + try: + json_data = json.loads(data) + except ValueError as e: + self.error.emit("Invalid JSON received in reply: {}!".format(e)) + return + try: + self.success.emit(json_data['info']['version']) + except KeyError as e: + self.error.emit("Malformed data recieved in reply " + "({!r} not found)!".format(e)) + return diff --git a/qutebrowser/misc/crashdialog.py b/qutebrowser/misc/crashdialog.py index 4a5a4f0ea..bc00bce06 100644 --- a/qutebrowser/misc/crashdialog.py +++ b/qutebrowser/misc/crashdialog.py @@ -24,6 +24,8 @@ import sys import html import getpass import traceback +import distutils.version # pylint: disable=no-name-in-module,import-error +# https://bitbucket.org/logilab/pylint/issue/73/ from PyQt5.QtCore import pyqtSlot, Qt, QSize, qVersion from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton, @@ -32,7 +34,7 @@ from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton, import qutebrowser from qutebrowser.utils import version, log, utils, objreg, qtutils -from qutebrowser.misc import miscwidgets +from qutebrowser.misc import miscwidgets, autoupdate, msgbox from qutebrowser.browser.network import pastebin from qutebrowser.config import config @@ -103,6 +105,7 @@ class _CrashDialog(QDialog): _url: Pastebin URL QLabel. _crash_info: A list of tuples with title and crash information. _paste_client: A PastebinClient instance to use. + _pypi_client: A PyPIVersionClient instance to use. _paste_text: The text to pastebin. """ @@ -125,6 +128,7 @@ class _CrashDialog(QDialog): self.resize(QSize(640, 600)) self._vbox = QVBoxLayout(self) self._paste_client = pastebin.PastebinClient(self) + self._pypi_client = autoupdate.PyPIVersionClient(self) self._init_text() contact = QLabel("I'd like to be able to follow up with you, to keep " @@ -293,10 +297,17 @@ class _CrashDialog(QDialog): self._btn_report.setEnabled(False) self._btn_cancel.setEnabled(False) self._btn_report.setText("Reporting...") - self._paste_client.success.connect(self.finish) + self._paste_client.success.connect(self.on_paste_success) self._paste_client.error.connect(self.show_error) self.report() + @pyqtSlot() + def on_paste_success(self): + """Get the newest version from PyPI when the paste is done.""" + self._pypi_client.success.connect(self.on_version_success) + self._pypi_client.error.connect(self.on_version_error) + self._pypi_client.get_version() + @pyqtSlot(str) def show_error(self, text): """Show a paste error dialog. @@ -308,6 +319,44 @@ class _CrashDialog(QDialog): error_dlg.finished.connect(self.finish) error_dlg.show() + @pyqtSlot(str) + def on_version_success(self, newest): + """Called when the version was obtained from self._pypi_client. + + Args: + newest: The newest version as a string. + """ + # pylint: disable=no-member + # https://bitbucket.org/logilab/pylint/issue/73/ + new_version = distutils.version.StrictVersion(newest) + cur_version = distutils.version.StrictVersion(qutebrowser.__version__) + lines = ['The report has been sent successfully. Thanks!'] + if new_version > cur_version: + lines.append("Note: The newest available version is v{}, " + "but you're currently running v{} - please " + "update!".format(newest, qutebrowser.__version__)) + text = '

'.join(lines) + self.hide() + msgbox.information(self, "Report successfully sent!", text, + on_finished=self.finish, plain_text=False) + + @pyqtSlot(str) + def on_version_error(self, msg): + """Called when the version was not obtained from self._pypi_client. + + Args: + msg: The error message to show. + """ + lines = ['The report has been sent successfully. Thanks!'] + lines.append("There was an error while getting the newest version: " + "{}. Please check for a new version on " + "qutebrowser.org " + "by yourself.".format(msg)) + text = '

'.join(lines) + self.hide() + msgbox.information(self, "Report successfully sent!", text, + on_finished=self.finish, plain_text=False) + @pyqtSlot() def finish(self): """Save contact info and close the dialog.""" diff --git a/qutebrowser/misc/msgbox.py b/qutebrowser/misc/msgbox.py new file mode 100644 index 000000000..406e4e0bf --- /dev/null +++ b/qutebrowser/misc/msgbox.py @@ -0,0 +1,68 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2015 Florian Bruhin (The Compiler) +# +# 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 . + +"""Convenience functions to show message boxes.""" + + +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QMessageBox + + +def msgbox(parent, title, text, *, icon, buttons=QMessageBox.Ok, + on_finished=None, plain_text=None): + """Display an QMessageBox with the given icon. + + Args: + parent: The parent to set for the message box. + title: The title to set. + text: The text to set. + buttons: The buttons to set (QMessageBox::StandardButtons) + on_finished: A slot to connect to the 'finished' signal. + plain_text: Whether to force plain text (True) or rich text (False). + None (the default) uses Qt's auto detection. + + Return: + A new QMessageBox. + """ + box = QMessageBox(parent) + box.setIcon(icon) + box.setStandardButtons(buttons) + if on_finished is not None: + box.finished.connect(on_finished) + if plain_text: + box.setTextFormat(Qt.PlainText) + elif plain_text is not None: + box.setTextFormat(Qt.RichText) + box.setWindowTitle(title) + box.setText(text) + box.show() + return box + + +def information(*args, **kwargs): + """Display an information box. + + Args: + *args: Passed to msgbox. + **kwargs: Passed to msgbox. + + Return: + A new QMessageBox. + """ + return msgbox(*args, icon=QMessageBox.Information, **kwargs)