From e8fa8fadceb40866c8f16c775ec5d80f5af325e2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 15 Oct 2014 13:00:56 +0200 Subject: [PATCH 01/15] Fix completion tabbing. I accidentally broke this in fb3682f5fa9ff702488277521e60c5425cafc64d because the variable gets reset before the slot is executed now. See #189. --- qutebrowser/utils/completer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/utils/completer.py b/qutebrowser/utils/completer.py index b3202bcf4..4c2c4fe93 100644 --- a/qutebrowser/utils/completer.py +++ b/qutebrowser/utils/completer.py @@ -179,7 +179,6 @@ class Completer(QObject): else: self._ignore_change = True self.change_completed_part.emit(data, False) - self._ignore_change = False @pyqtSlot(str, list, int) def on_update_completion(self, prefix, parts, cursor_part): @@ -202,6 +201,7 @@ class Completer(QObject): cursor_part: The part the cursor is currently over. """ if self._ignore_change: + self._ignore_change = False log.completion.debug("Ignoring completion update") return From 74839d7affdaabd4899311c64e9f50a430a6ea3d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 15 Oct 2014 16:55:55 +0200 Subject: [PATCH 02/15] Use a QObject for quickmarks and add a changed signal. See #189. --- qutebrowser/app.py | 12 ++- qutebrowser/browser/commands.py | 7 +- qutebrowser/browser/quickmarks.py | 164 ++++++++++++++++-------------- 3 files changed, 99 insertions(+), 84 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index bf8380921..f94d37ef0 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -151,7 +151,8 @@ class Application(QApplication): log.init.debug("Initializing websettings...") websettings.init() log.init.debug("Initializing quickmarks...") - quickmarks.init() + quickmark_manager = quickmarks.QuickmarkManager() + objreg.register('quickmark-manager', quickmark_manager) log.init.debug("Initializing proxy...") proxy.init() log.init.debug("Initializing cookies...") @@ -663,14 +664,19 @@ class Application(QApplication): pass else: to_save.append(("keyconfig", key_config.save)) - to_save += [("window geometry", self._save_geometry), - ("quickmarks", quickmarks.save)] + to_save += [("window geometry", self._save_geometry)] try: command_history = objreg.get('command-history') except KeyError: pass else: to_save.append(("command history", command_history.save)) + try: + quickmark_manager = objreg.get('quickmark-manager') + except KeyError: + pass + else: + to_save.append(("command history", quickmark_manager.save)) try: state_config = objreg.get('state-config') except KeyError: diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 333195d6e..420580197 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -36,7 +36,7 @@ import pygments.formatters from qutebrowser.commands import userscripts, cmdexc, cmdutils from qutebrowser.config import config -from qutebrowser.browser import quickmarks, webelem +from qutebrowser.browser import webelem from qutebrowser.utils import (message, editor, usertypes, log, qtutils, urlutils, objreg, utils) @@ -815,7 +815,8 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window') def quickmark_save(self): """Save the current page as a quickmark.""" - quickmarks.prompt_save(self._win_id, self._current_url()) + quickmark_manager = objreg.get('quickmark-manager') + quickmark_manager.prompt_save(self._win_id, self._current_url()) @cmdutils.register(instance='command-dispatcher', scope='window') def quickmark_load(self, name, tab=False, bg=False, window=False): @@ -827,7 +828,7 @@ class CommandDispatcher: bg: Load the quickmark in a new background tab. window: Load the quickmark in a new window. """ - url = quickmarks.get(name) + url = objreg.get('quickmark-manager').get(name) self._open(url, tab, bg, window) @cmdutils.register(instance='command-dispatcher', name='inspector', diff --git a/qutebrowser/browser/quickmarks.py b/qutebrowser/browser/quickmarks.py index e6b4f52a5..fb7249efb 100644 --- a/qutebrowser/browser/quickmarks.py +++ b/qutebrowser/browser/quickmarks.py @@ -27,91 +27,99 @@ to a file on shutdown, so it makes semse to keep them as strings here. import functools import collections -from PyQt5.QtCore import QStandardPaths, QUrl +from PyQt5.QtCore import pyqtSignal, QStandardPaths, QUrl, QObject from qutebrowser.utils import message, usertypes, urlutils, standarddir from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.config import lineparser -marks = collections.OrderedDict() -linecp = None +class QuickmarkManager(QObject): + """Manager for quickmarks. -def init(): - """Read quickmarks from the config file.""" - global linecp - confdir = standarddir.get(QStandardPaths.ConfigLocation) - linecp = lineparser.LineConfigParser(confdir, 'quickmarks') - for line in linecp: - try: - key, url = line.rsplit(maxsplit=1) - except ValueError: - message.error(0, "Invalid quickmark '{}'".format(line)) + Attributes: + marks: An OrderedDict of all quickmarks. + _linecp: The LineConfigParser used for the quickmarks. + """ + + changed = pyqtSignal() + + def __init__(self, parent=None): + """Initialize and read quickmarks.""" + super().__init__(parent) + + self.marks = collections.OrderedDict() + + confdir = standarddir.get(QStandardPaths.ConfigLocation) + self._linecp = lineparser.LineConfigParser(confdir, 'quickmarks') + for line in self._linecp: + try: + key, url = line.rsplit(maxsplit=1) + except ValueError: + message.error(0, "Invalid quickmark '{}'".format(line)) + else: + self.marks[key] = url + + def save(self): + """Save the quickmarks to disk.""" + self._linecp.data = [' '.join(tpl) for tpl in self.marks.items()] + self._linecp.save() + + def prompt_save(self, win_id, url): + """Prompt for a new quickmark name to be added and add it. + + Args: + win_id: The current window ID. + url: The quickmark url as a QUrl. + """ + if not url.isValid(): + urlutils.invalid_url_error(win_id, url, "save quickmark") + return + urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded) + message.ask_async( + win_id, "Add quickmark:", usertypes.PromptMode.text, + functools.partial(self.quickmark_add, win_id, urlstr)) + + @cmdutils.register(instance='quickmark-manager') + def quickmark_add(self, win_id: {'special': 'win_id'}, url, name): + """Add a new quickmark. + + Args: + win_id: The window ID to display the errors in. + url: The url to add as quickmark. + name: The name for the new quickmark. + """ + # We don't raise cmdexc.CommandError here as this can be called async + # via prompt_save. + if not name: + message.error(win_id, "Can't set mark with empty name!") + return + if not url: + message.error(win_id, "Can't set mark with empty URL!") + return + + def set_mark(): + """Really set the quickmark.""" + self.marks[name] = url + self.changed.emit() + + if name in self.marks: + message.confirm_async( + win_id, "Override existing quickmark?", set_mark, default=True) else: - marks[key] = url + set_mark() - -def save(): - """Save the quickmarks to disk.""" - linecp.data = [' '.join(tpl) for tpl in marks.items()] - linecp.save() - - -def prompt_save(win_id, url): - """Prompt for a new quickmark name to be added and add it. - - Args: - win_id: The current window ID. - url: The quickmark url as a QUrl. - """ - if not url.isValid(): - urlutils.invalid_url_error(win_id, url, "save quickmark") - return - urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded) - message.ask_async(win_id, "Add quickmark:", usertypes.PromptMode.text, - functools.partial(quickmark_add, win_id, urlstr)) - - -@cmdutils.register() -def quickmark_add(win_id: {'special': 'win_id'}, url, name): - """Add a new quickmark. - - Args: - win_id: The window ID to display the errors in. - url: The url to add as quickmark. - name: The name for the new quickmark. - """ - # We don't raise cmdexc.CommandError here as this can be called async via - # prompt_save. - if not name: - message.error(win_id, "Can't set mark with empty name!") - return - if not url: - message.error(win_id, "Can't set mark with empty URL!") - return - - def set_mark(): - """Really set the quickmark.""" - marks[name] = url - - if name in marks: - message.confirm_async(win_id, "Override existing quickmark?", set_mark, - default=True) - else: - set_mark() - - -def get(name): - """Get the URL of the quickmark named name as a QUrl.""" - if name not in marks: - raise cmdexc.CommandError( - "Quickmark '{}' does not exist!".format(name)) - urlstr = marks[name] - try: - url = urlutils.fuzzy_url(urlstr) - except urlutils.FuzzyUrlError: - raise cmdexc.CommandError( - "Invalid URL for quickmark {}: {} ({})".format(name, urlstr, - url.errorString())) - return url + def get(self, name): + """Get the URL of the quickmark named name as a QUrl.""" + if name not in self.marks: + raise cmdexc.CommandError( + "Quickmark '{}' does not exist!".format(name)) + urlstr = self.marks[name] + try: + url = urlutils.fuzzy_url(urlstr) + except urlutils.FuzzyUrlError: + raise cmdexc.CommandError( + "Invalid URL for quickmark {}: {} ({})".format( + name, urlstr, url.errorString())) + return url From 348bc7147fa1a79a0dbc4f6f6b326b32f7205c66 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 15 Oct 2014 18:03:48 +0200 Subject: [PATCH 03/15] Don't clear page in WebView:shutdown. Fixes #99. It seems Qt still wants to access the page (for the mousePressEvent) and segfaults when we clear the page before that's finished. We now try it inside __del__ like done in the link mentioned in the comment. --- qutebrowser/widgets/webview.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/qutebrowser/widgets/webview.py b/qutebrowser/widgets/webview.py index 09faf7e38..5d623885a 100644 --- a/qutebrowser/widgets/webview.py +++ b/qutebrowser/widgets/webview.py @@ -127,6 +127,13 @@ class WebView(QWebView): url = utils.elide(self.url().toDisplayString(), 50) return utils.get_repr(self, tab_id=self.tab_id, url=url) + def __del__(self): + # Explicitely releasing the page here seems to prevent some segfaults + # when quitting. + # Copied from: + # https://code.google.com/p/webscraping/source/browse/webkit.py#325 + self.setPage(None) + def _set_load_status(self, val): """Setter for load_status.""" if not isinstance(val, LoadStatus): @@ -261,11 +268,6 @@ class WebView(QWebView): settings.setAttribute(QWebSettings.JavascriptEnabled, False) self.stop() self.page().networkAccessManager().shutdown() - # Explicitely releasing the page here seems to prevent some segfaults - # when quitting. - # Copied from: - # https://code.google.com/p/webscraping/source/browse/webkit.py#325 - self.setPage(None) def openurl(self, url): """Open a URL in the browser. From b54151f206473ad0a007d753faaf60a8a6ecef10 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 15 Oct 2014 20:40:18 +0200 Subject: [PATCH 04/15] Use last focused window for download errors and other stuff. When the event happens, it's possible we don't have any window focused yet, so we display it in the window which was last focused. Fixes #191. --- qutebrowser/app.py | 19 +++++++++++++++++-- qutebrowser/browser/downloads.py | 6 +++--- qutebrowser/commands/argparser.py | 2 +- qutebrowser/utils/objreg.py | 9 +++++++++ 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index f94d37ef0..1a5d56bd0 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -314,7 +314,7 @@ class Application(QApplication): quickstart_done = False if not quickstart_done: tabbed_browser = objreg.get('tabbed-browser', scope='window', - window='current') + window='last-focused') tabbed_browser.tabopen( QUrl('http://www.qutebrowser.org/quickstart.html')) try: @@ -342,6 +342,7 @@ class Application(QApplication): config_obj = objreg.get('config') self.lastWindowClosed.connect(self.shutdown) config_obj.style_changed.connect(style.get_stylesheet.cache_clear) + self.focusChanged.connect(self.on_focus_changed) def _get_widgets(self): """Get a string list of all widgets.""" @@ -542,7 +543,7 @@ class Application(QApplication): out = traceback.format_exc() qutescheme.pyeval_output = out tabbed_browser = objreg.get('tabbed-browser', scope='window', - window='current') + window='last-focused') tabbed_browser.openurl(QUrl('qute:pyeval'), newtab=True) @cmdutils.register(instance='app') @@ -706,6 +707,20 @@ class Application(QApplication): # segfaults. QTimer.singleShot(0, functools.partial(self.exit, status)) + def on_focus_changed(self, _old, new): + """Register currently focused main window in the object registry.""" + if new is None: + window = None + else: + window = new.window() + if window is None or not isinstance(window, mainwindow.MainWindow): + try: + objreg.delete('last-focused-main-window') + except KeyError: + pass + else: + objreg.register('last-focused-main-window', window, update=True) + def exit(self, status): """Extend QApplication::exit to log the event.""" log.destroy.debug("Now calling QApplication::exit.") diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index d47ea020a..e59c3cd83 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -352,7 +352,7 @@ class DownloadManager(QAbstractListModel): page: The QWebPage to get the download from. """ if not url.isValid(): - urlutils.invalid_url_error('current', url, "start download") + urlutils.invalid_url_error('last-focused', url, "start download") return req = QNetworkRequest(url) reply = page.networkAccessManager().get(req) @@ -407,7 +407,7 @@ class DownloadManager(QAbstractListModel): self.questions.append(q) download.cancelled.connect(q.abort) message_bridge = objreg.get('message-bridge', scope='window', - window='current') + window='last-focused') message_bridge.ask(q, blocking=False) @pyqtSlot(DownloadItem) @@ -431,7 +431,7 @@ class DownloadManager(QAbstractListModel): @pyqtSlot(str) def on_error(self, msg): """Display error message on download errors.""" - message.error('current', "Download error: {}".format(msg)) + message.error('last-focused', "Download error: {}".format(msg)) def last_index(self): """Get the last index in the model. diff --git a/qutebrowser/commands/argparser.py b/qutebrowser/commands/argparser.py index 684361698..1d21c2ffa 100644 --- a/qutebrowser/commands/argparser.py +++ b/qutebrowser/commands/argparser.py @@ -59,7 +59,7 @@ class HelpAction(argparse.Action): def __call__(self, parser, _namespace, _values, _option_string=None): tabbed_browser = objreg.get('tabbed-browser', scope='window', - window='current') + window='last-focused') tabbed_browser.tabopen( QUrl('qute://help/commands.html#{}'.format(parser.name))) parser.exit() diff --git a/qutebrowser/utils/objreg.py b/qutebrowser/utils/objreg.py index 7b9fd5332..8018a1867 100644 --- a/qutebrowser/utils/objreg.py +++ b/qutebrowser/utils/objreg.py @@ -136,6 +136,15 @@ def _get_window_registry(window): win = app.activeWindow() if win is None or not hasattr(win, 'win_id'): raise RegistryUnavailableError('window') + elif window == 'last-focused': + try: + win = get('last-focused-main-window') + except KeyError: + try: + win = get('last-main-window') + except KeyError: + raise RegistryUnavailableError('window') + assert hasattr(win, 'registry') else: try: win = window_registry[window] From dc8f156c2108727266ef2be4059012382117e47c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 15 Oct 2014 20:43:20 +0200 Subject: [PATCH 05/15] Make srcmodel public in CompletionFilterModel. --- qutebrowser/models/completionfilter.py | 22 +++++++++++----------- qutebrowser/utils/completer.py | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/qutebrowser/models/completionfilter.py b/qutebrowser/models/completionfilter.py index 44ab2098f..1a5bbd36a 100644 --- a/qutebrowser/models/completionfilter.py +++ b/qutebrowser/models/completionfilter.py @@ -35,7 +35,7 @@ class CompletionFilterModel(QSortFilterProxyModel): Attributes: _pattern: The pattern to filter with. - _srcmodel: The current source model. + srcmodel: The current source model. Kept as attribute because calling `sourceModel` takes quite a long time for some reason. """ @@ -43,7 +43,7 @@ class CompletionFilterModel(QSortFilterProxyModel): def __init__(self, source, parent=None): super().__init__(parent) super().setSourceModel(source) - self._srcmodel = source + self.srcmodel = source self._pattern = '' def set_pattern(self, val): @@ -61,7 +61,7 @@ class CompletionFilterModel(QSortFilterProxyModel): self.invalidateFilter() sortcol = 0 try: - self._srcmodel.sort(sortcol) + self.srcmodel.sort(sortcol) except NotImplementedError: self.sort(sortcol) self.invalidate() @@ -111,14 +111,14 @@ class CompletionFilterModel(QSortFilterProxyModel): qtutils.ensure_valid(index) index = self.mapToSource(index) qtutils.ensure_valid(index) - self._srcmodel.mark_item(index, text) + self.srcmodel.mark_item(index, text) def setSourceModel(self, model): """Override QSortFilterProxyModel's setSourceModel to clear pattern.""" log.completion.debug("Setting source model: {}".format(model)) self.set_pattern('') super().setSourceModel(model) - self._srcmodel = model + self.srcmodel = model def filterAcceptsRow(self, row, parent): """Custom filter implementation. @@ -135,9 +135,9 @@ class CompletionFilterModel(QSortFilterProxyModel): """ if parent == QModelIndex(): return True - idx = self._srcmodel.index(row, 0, parent) + idx = self.srcmodel.index(row, 0, parent) qtutils.ensure_valid(idx) - data = self._srcmodel.data(idx) + data = self.srcmodel.data(idx) # TODO more sophisticated filtering if not self._pattern: return True @@ -159,14 +159,14 @@ class CompletionFilterModel(QSortFilterProxyModel): qtutils.ensure_valid(lindex) qtutils.ensure_valid(rindex) - left_sort = self._srcmodel.data(lindex, role=completion.Role.sort) - right_sort = self._srcmodel.data(rindex, role=completion.Role.sort) + left_sort = self.srcmodel.data(lindex, role=completion.Role.sort) + right_sort = self.srcmodel.data(rindex, role=completion.Role.sort) if left_sort is not None and right_sort is not None: return left_sort < right_sort - left = self._srcmodel.data(lindex) - right = self._srcmodel.data(rindex) + left = self.srcmodel.data(lindex) + right = self.srcmodel.data(rindex) leftstart = left.startswith(self._pattern) rightstart = right.startswith(self._pattern) diff --git a/qutebrowser/utils/completer.py b/qutebrowser/utils/completer.py index 4c2c4fe93..09760361f 100644 --- a/qutebrowser/utils/completer.py +++ b/qutebrowser/utils/completer.py @@ -233,7 +233,7 @@ class Completer(QObject): log.completion.debug( "New completion for {}: {}, with pattern '{}'".format( - parts, model._srcmodel.__class__.__name__, pattern)) + parts, model.srcmodel.__class__.__name__, pattern)) if self._model().count() == 0: completion.hide() From 17816bdab2c69fedb71c0aa445fe9d6026fe126f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 15 Oct 2014 20:43:47 +0200 Subject: [PATCH 06/15] importer: Add vim modeline --- scripts/importer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/importer.py b/scripts/importer.py index 80133411f..403e9a52b 100755 --- a/scripts/importer.py +++ b/scripts/importer.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: # Copyright 2014 Claude (longneck) From 0a7ff8db093f81100798ed502c43e43fc9979a67 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 15 Oct 2014 21:06:52 +0200 Subject: [PATCH 07/15] importer: Use with-block to open file. --- scripts/importer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/importer.py b/scripts/importer.py index 403e9a52b..67ae83885 100755 --- a/scripts/importer.py +++ b/scripts/importer.py @@ -50,8 +50,8 @@ def get_args(): def import_chromium(bookmarks_file): """Import bookmarks from a HTML file generated by Chromium.""" import bs4 - - soup = bs4.BeautifulSoup(open(bookmarks_file, encoding='utf-8')) + with open(bookmarks_file, encoding='utf-8') as f: + soup = bs4.BeautifulSoup(f) html_tags = soup.findAll('a') From 971e4f4372d26566410234e2fa283bedb0cf22bd Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 15 Oct 2014 21:07:00 +0200 Subject: [PATCH 08/15] pylint_checkers: Fix getting encoding argument. --- scripts/pylint_checkers/openencoding.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/pylint_checkers/openencoding.py b/scripts/pylint_checkers/openencoding.py index caee996c8..07e97d70a 100644 --- a/scripts/pylint_checkers/openencoding.py +++ b/scripts/pylint_checkers/openencoding.py @@ -52,10 +52,16 @@ class OpenEncodingChecker(checkers.BaseChecker): keyword='mode') except utils.NoSuchArgumentError: mode_arg = None + _encoding = None try: - _encoding = utils.get_argument_from_call(node, position=2, - keyword='encoding') + _encoding = utils.get_argument_from_call(node, position=2) except utils.NoSuchArgumentError: + try: + _encoding = utils.get_argument_from_call(node, + keyword='encoding') + except utils.NoSuchArgumentError: + pass + if _encoding is None: if mode_arg is not None: mode = utils.safe_infer(mode_arg) if (mode_arg is not None and isinstance(mode, astroid.Const) and From 7e820a0e82327d11a1ed8f6f7dc4ddcdc26c9778 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 15 Oct 2014 21:22:53 +0200 Subject: [PATCH 09/15] Show error messages in downloads. --- qutebrowser/browser/downloads.py | 42 +++++++++++++++++++++++--------- qutebrowser/config/configdata.py | 4 +++ 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index e59c3cd83..566ee0e88 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -62,6 +62,7 @@ class DownloadItem(QObject): _reply: The QNetworkReply associated with this download. _last_done: The count of bytes which where downloaded when calculating the speed the last time. + _error: The current error message, or None Signals: data_changed: The downloads metadata changed. @@ -88,6 +89,7 @@ class DownloadItem(QObject): self._reply = reply self._bytes_total = None self._speed = 0 + self._error = None self.basename = '???' samples = int(self.SPEED_AVG_WINDOW * (1000 / self.SPEED_REFRESH_INTERVAL)) @@ -127,9 +129,13 @@ class DownloadItem(QObject): down = utils.format_size(self._bytes_done, suffix='B') perc = self._percentage() remaining = self._remaining_time() + if self._error is None: + errmsg = "" + else: + errmsg = " - {}".format(self._error) if all(e is None for e in (perc, remaining, self._bytes_total)): - return ('{name} [{speed:>10}|{down}]'.format( - name=self.basename, speed=speed, down=down)) + return ('{name} [{speed:>10}|{down}]{errmsg}'.format( + name=self.basename, speed=speed, down=down, errmsg=errmsg)) if perc is None: perc = '??' else: @@ -140,9 +146,9 @@ class DownloadItem(QObject): remaining = utils.format_seconds(remaining) total = utils.format_size(self._bytes_total, suffix='B') return ('{name} [{speed:>10}|{remaining:>5}|{perc:>2}%|' - '{down}/{total}]'.format(name=self.basename, speed=speed, - remaining=remaining, perc=perc, - down=down, total=total)) + '{down}/{total}]{errmsg}'.format( + name=self.basename, speed=speed, remaining=remaining, + perc=perc, down=down, total=total, errmsg=errmsg)) def _die(self, msg): """Abort the download and emit an error.""" @@ -150,17 +156,19 @@ class DownloadItem(QObject): self._reply.finished.disconnect() self._reply.error.disconnect() self._reply.readyRead.disconnect() + self._error = msg self._bytes_done = self._bytes_total self.timer.stop() self.error.emit(msg) self._reply.abort() self._reply.deleteLater() + self._reply = None if self._fileobj is not None: try: self._fileobj.close() except OSError as e: self.error.emit(e.strerror) - self.finished.emit() + self.data_changed.emit() def _percentage(self): """The current download percentage, or None if unknown.""" @@ -187,7 +195,10 @@ class DownloadItem(QObject): start = config.get('colors', 'downloads.bg.start') stop = config.get('colors', 'downloads.bg.stop') system = config.get('colors', 'downloads.bg.system') - if self._percentage() is None: + error = config.get('colors', 'downloads.bg.error') + if self._error is not None: + return error + elif self._percentage() is None: return start else: return utils.interpolate_color(start, stop, self._percentage(), @@ -198,8 +209,9 @@ class DownloadItem(QObject): log.downloads.debug("cancelled") self.cancelled.emit() self._is_cancelled = True - self._reply.abort() - self._reply.deleteLater() + if self._reply is not None: + self._reply.abort() + self._reply.deleteLater() if self._fileobj is not None: self._fileobj.close() if self._filename is not None and os.path.exists(self._filename): @@ -389,7 +401,8 @@ class DownloadManager(QAbstractListModel): functools.partial(self.on_finished, download)) download.data_changed.connect( functools.partial(self.on_data_changed, download)) - download.error.connect(self.on_error) + download.error.connect( + functools.partial(self.on_error, download)) download.basename = suggested_filename idx = len(self.downloads) + 1 self.beginInsertRows(QModelIndex(), idx, idx) @@ -428,8 +441,8 @@ class DownloadManager(QAbstractListModel): qtutils.ensure_valid(model_idx) self.dataChanged.emit(model_idx, model_idx) - @pyqtSlot(str) - def on_error(self, msg): + @pyqtSlot(DownloadItem, str) + def on_error(self, download, msg): """Display error message on download errors.""" message.error('last-focused', "Download error: {}".format(msg)) @@ -465,6 +478,11 @@ class DownloadManager(QAbstractListModel): data = item.bg_color() elif role == ModelRole.item: data = item + elif role == Qt.ToolTipRole: + if item._error is None: + data = QVariant() + else: + return item._error else: data = QVariant() return data diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 11f51d691..257d4aaef 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -714,6 +714,10 @@ DATA = collections.OrderedDict([ ('downloads.bg.system', SettingValue(typ.ColorSystem(), 'rgb'), "Color gradient interpolation system for downloads."), + + ('downloads.bg.error', + SettingValue(typ.QtColor(), 'red'), + "Background color for downloads with errors."), )), ('fonts', sect.KeyValue( From 0209382df4373bbb8bf9335f51d9efee9e3fb7e1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 15 Oct 2014 21:30:04 +0200 Subject: [PATCH 10/15] Fix redrawing of downloadview if there are downloads added. --- qutebrowser/widgets/downloads.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/qutebrowser/widgets/downloads.py b/qutebrowser/widgets/downloads.py index ad1c6215f..d0e1541f8 100644 --- a/qutebrowser/widgets/downloads.py +++ b/qutebrowser/widgets/downloads.py @@ -22,7 +22,7 @@ import functools import sip -from PyQt5.QtCore import pyqtSlot, QSize, Qt +from PyQt5.QtCore import pyqtSlot, QSize, Qt, QTimer from PyQt5.QtWidgets import QListView, QSizePolicy, QMenu from qutebrowser.browser import downloads @@ -44,9 +44,16 @@ def update_geometry(obj): Original bug: https://github.com/The-Compiler/qutebrowser/issues/167 Workaround bug: https://github.com/The-Compiler/qutebrowser/issues/171 """ - if sip.isdeleted(obj): - return - obj.updateGeometry() + + def _update_geometry(): + """Actually update the geometry if the object still exists.""" + if sip.isdeleted(obj): + return + obj.updateGeometry() + + # If we don't use a singleShot QTimer, the geometry isn't updated correctly + # and won't include the new item. + QTimer.singleShot(0, _update_geometry) class DownloadView(QListView): From b4c7669e64ed535c90b9dd4752bd3ab17aeb912e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 15 Oct 2014 23:15:27 +0200 Subject: [PATCH 11/15] Shut down TabbedBrowser in MainWindow closeEvent. Hopefully fixes #197. --- qutebrowser/app.py | 8 ++------ qutebrowser/widgets/mainwindow.py | 1 + qutebrowser/widgets/webview.py | 1 + 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 1a5d56bd0..6d10d6b6b 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -638,12 +638,8 @@ class Application(QApplication): self.removeEventFilter(self._event_filter) except AttributeError: pass - # Close all tabs - for win_id in objreg.window_registry: - log.destroy.debug("Closing tabs in window {}...".format(win_id)) - tabbed_browser = objreg.get('tabbed-browser', scope='window', - window=win_id) - tabbed_browser.shutdown() + # Close all windows + QApplication.closeAllWindows() # Shut down IPC try: objreg.get('ipc-server').shutdown() diff --git a/qutebrowser/widgets/mainwindow.py b/qutebrowser/widgets/mainwindow.py index 1ed38ef18..1510861d3 100644 --- a/qutebrowser/widgets/mainwindow.py +++ b/qutebrowser/widgets/mainwindow.py @@ -333,3 +333,4 @@ class MainWindow(QWidget): e.accept() objreg.get('app').geometry = bytes(self.saveGeometry()) log.destroy.debug("Closing window {}".format(self.win_id)) + self._tabbed_browser.shutdown() diff --git a/qutebrowser/widgets/webview.py b/qutebrowser/widgets/webview.py index 5d623885a..87b504cc4 100644 --- a/qutebrowser/widgets/webview.py +++ b/qutebrowser/widgets/webview.py @@ -264,6 +264,7 @@ class WebView(QWebView): """Shut down the webview.""" # We disable javascript because that prevents some segfaults when # quitting it seems. + log.destroy.debug("Shutting down {!r}.".format(self)) settings = self.settings() settings.setAttribute(QWebSettings.JavascriptEnabled, False) self.stop() From 999474c751bcef4d4dfe0bf03eba4652674cbb03 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 15 Oct 2014 23:24:09 +0200 Subject: [PATCH 12/15] Set title directly instead of using signals. See #198, but this didn't fix it. --- qutebrowser/widgets/mainwindow.py | 1 - qutebrowser/widgets/tabbedbrowser.py | 8 +++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/qutebrowser/widgets/mainwindow.py b/qutebrowser/widgets/mainwindow.py index 1510861d3..c565cbec9 100644 --- a/qutebrowser/widgets/mainwindow.py +++ b/qutebrowser/widgets/mainwindow.py @@ -87,7 +87,6 @@ class MainWindow(QWidget): self._downloadview.show() self._tabbed_browser = tabbedbrowser.TabbedBrowser(win_id) - self._tabbed_browser.title_changed.connect(self.setWindowTitle) objreg.register('tabbed-browser', self._tabbed_browser, scope='window', window=win_id) self._vbox.addWidget(self._tabbed_browser) diff --git a/qutebrowser/widgets/tabbedbrowser.py b/qutebrowser/widgets/tabbedbrowser.py index e5d5b1708..c53090c33 100644 --- a/qutebrowser/widgets/tabbedbrowser.py +++ b/qutebrowser/widgets/tabbedbrowser.py @@ -79,8 +79,6 @@ class TabbedBrowser(tabwidget.TabWidget): start_download: Emitted when any tab wants to start downloading something. current_tab_changed: The current tab changed to the emitted WebView. - title_changed: Emitted when the application title should be changed. - arg: The new title as string. """ cur_progress = pyqtSignal(int) @@ -96,7 +94,6 @@ class TabbedBrowser(tabwidget.TabWidget): resized = pyqtSignal('QRect') got_cmd = pyqtSignal(str) current_tab_changed = pyqtSignal(webview.WebView) - title_changed = pyqtSignal(str) def __init__(self, win_id, parent=None): super().__init__(win_id, parent) @@ -138,9 +135,10 @@ class TabbedBrowser(tabwidget.TabWidget): def _change_app_title(self, text): """Change the window title based on the tab text.""" if not text: - self.title_changed.emit('qutebrowser') + title = 'qutebrowser' else: - self.title_changed.emit('{} - qutebrowser'.format(text)) + title = '{} - qutebrowser'.format(text) + self.window().setWindowTitle(title) def _connect_tab_signals(self, tab): """Set up the needed signals for tab.""" From ee02f339d740dd7bce27b884ef950bdc2f728a29 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 17 Oct 2014 11:32:41 +0200 Subject: [PATCH 13/15] Draw hints correctly when page is zoomed. Fixes #199. --- qutebrowser/browser/hints.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 1809d92bf..0a03df1c8 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -274,9 +274,14 @@ class HintManager(QObject): else: display = 'none' rect = elem.geometry() + left = rect.x() + top = rect.y() + if not config.get('ui', 'zoom-text-only'): + zoom = elem.webFrame().zoomFactor() + left /= zoom + top /= zoom return self.HINT_CSS.format( - left=rect.x(), top=rect.y(), config=objreg.get('config'), - display=display) + left=left, top=top, config=objreg.get('config'), display=display) def _draw_label(self, elem, string): """Draw a hint label over an element. From 5a5ff70703497b3f03a2da4eea2641e2c1081065 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 17 Oct 2014 11:39:44 +0200 Subject: [PATCH 14/15] hints: fix replacing of {hint-url}. Fixes #200. --- qutebrowser/browser/hints.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 0a03df1c8..c97db8464 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -82,10 +82,8 @@ class HintContext: """Get the arguments, with {hint-url} replaced by the given URL.""" args = [] for arg in self.args: - if arg == '{hint-url}': - args.append(urlstr) - else: - args.append(arg) + arg = arg.replace('{hint-url}', urlstr) + args.append(arg) return args From bff0efb4a4a8381152ba8a479662bb71e5e7ed70 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 17 Oct 2014 15:01:08 +0200 Subject: [PATCH 15/15] Paste primary selection on Shift+Insert --- qutebrowser/widgets/misc.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/qutebrowser/widgets/misc.py b/qutebrowser/widgets/misc.py index 6232848b0..b212ffa78 100644 --- a/qutebrowser/widgets/misc.py +++ b/qutebrowser/widgets/misc.py @@ -20,8 +20,8 @@ """Misc. widgets used at different places.""" from PyQt5.QtCore import pyqtSlot, Qt -from PyQt5.QtWidgets import QLineEdit -from PyQt5.QtGui import QValidator +from PyQt5.QtWidgets import QLineEdit, QApplication +from PyQt5.QtGui import QValidator, QClipboard from qutebrowser.models import cmdhistory from qutebrowser.utils import utils @@ -64,6 +64,9 @@ class CommandLineEdit(QLineEdit): self.cursorPositionChanged.connect(self.__on_cursor_position_changed) self._promptlen = 0 + def __repr__(self): + return utils.get_repr(self, text=self.text()) + @pyqtSlot(str) def on_text_edited(self, _text): """Slot for textEdited. Stop history browsing.""" @@ -94,8 +97,16 @@ class CommandLineEdit(QLineEdit): if mark: self.setSelection(self._promptlen, oldpos - self._promptlen) - def __repr__(self): - return utils.get_repr(self, text=self.text()) + def keyPressEvent(self, e): + """Override keyPressEvent to paste primary selection on Shift + Ins.""" + if e.key() == Qt.Key_Insert and e.modifiers() == Qt.ShiftModifier: + clipboard = QApplication.clipboard() + if clipboard.supportsSelection(): + e.accept() + text = clipboard.text(QClipboard.Selection) + self.insert(text) + return + super().keyPressEvent(e) class _CommandValidator(QValidator):