From 157c25bb13e439219e239264efaedcf8832443a4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 22 Mar 2015 22:39:56 +0100 Subject: [PATCH] Queue messages for 'current' window if unfocused. Fixes #512. --- qutebrowser/app.py | 1 + qutebrowser/browser/adblock.py | 21 ++--- qutebrowser/test/utils/test_utils.py | 32 ++++++++ qutebrowser/utils/message.py | 110 ++++++++++++++++++--------- qutebrowser/utils/utils.py | 15 ++++ 5 files changed, 129 insertions(+), 50 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index f30dbf078..02320493a 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -467,6 +467,7 @@ class Application(QApplication): self.lastWindowClosed.connect(self.on_last_window_closed) config_obj.style_changed.connect(style.get_stylesheet.cache_clear) self.focusChanged.connect(self.on_focus_changed) + self.focusChanged.connect(message.on_focus_changed) def _get_widgets(self): """Get a string list of all widgets.""" diff --git a/qutebrowser/browser/adblock.py b/qutebrowser/browser/adblock.py index 5815a6c38..467175ece 100644 --- a/qutebrowser/browser/adblock.py +++ b/qutebrowser/browser/adblock.py @@ -25,8 +25,6 @@ import functools import posixpath import zipfile -from PyQt5.QtCore import QTimer - from qutebrowser.config import config from qutebrowser.utils import objreg, standarddir, log, message from qutebrowser.commands import cmdutils @@ -107,9 +105,8 @@ class HostBlocker: log.misc.exception("Failed to read host blocklist!") else: if config.get('content', 'host-block-lists') is not None: - QTimer.singleShot(500, functools.partial( - message.info, 'last-focused', - "Run :adblock-update to get adblock lists.")) + message.info('current', + "Run :adblock-update to get adblock lists.") @cmdutils.register(instance='host-blocker') def adblock_update(self, win_id: {'special': 'win_id'}): @@ -156,9 +153,8 @@ class HostBlocker: f = get_fileobj(byte_io) except (OSError, UnicodeDecodeError, zipfile.BadZipFile, zipfile.LargeZipFile) as e: - message.error('last-focused', "adblock: Error while reading {}: " - "{} - {}".format( - byte_io.name, e.__class__.__name__, e)) + message.error('current', "adblock: Error while reading {}: {} - " + "{}".format(byte_io.name, e.__class__.__name__, e)) return for line in f: line_count += 1 @@ -186,17 +182,16 @@ class HostBlocker: self.blocked_hosts.add(host) log.misc.debug("{}: read {} lines".format(byte_io.name, line_count)) if error_count > 0: - message.error('last-focused', "adblock: {} read errors for " - "{}".format(error_count, byte_io.name)) + message.error('current', "adblock: {} read errors for {}".format( + error_count, byte_io.name)) def on_lists_downloaded(self): """Install block lists after files have been downloaded.""" with open(self._hosts_file, 'w', encoding='utf-8') as f: for host in sorted(self.blocked_hosts): f.write(host + '\n') - message.info('last-focused', "adblock: Read {} hosts from {} " - "sources.".format(len(self.blocked_hosts), - self._done_count)) + message.info('current', "adblock: Read {} hosts from {} sources." + .format(len(self.blocked_hosts), self._done_count)) @config.change_filter('content', 'host-block-lists') def on_config_changed(self): diff --git a/qutebrowser/test/utils/test_utils.py b/qutebrowser/test/utils/test_utils.py index b3176059f..7a143724a 100644 --- a/qutebrowser/test/utils/test_utils.py +++ b/qutebrowser/test/utils/test_utils.py @@ -22,6 +22,7 @@ import sys import enum import unittest +import datetime import os.path from PyQt5.QtCore import Qt @@ -192,6 +193,37 @@ class FormatSecondsTests(unittest.TestCase): self.assertEqual(utils.format_seconds(seconds), out) +class FormatTimedeltaTests(unittest.TestCase): + + """Tests for format_timedelta. + + Class attributes: + TESTS: A list of (input, output) tuples. + """ + + TESTS = [ + (datetime.timedelta(seconds=-1), '-1s'), + (datetime.timedelta(seconds=0), '0s'), + (datetime.timedelta(seconds=59), '59s'), + (datetime.timedelta(seconds=120), '2m'), + (datetime.timedelta(seconds=60.4), '1m'), + (datetime.timedelta(seconds=63), '1m 3s'), + (datetime.timedelta(seconds=-64), '-1m 4s'), + (datetime.timedelta(seconds=3599), '59m 59s'), + (datetime.timedelta(seconds=3600), '1h'), + (datetime.timedelta(seconds=3605), '1h 5s'), + (datetime.timedelta(seconds=3723), '1h 2m 3s'), + (datetime.timedelta(seconds=3780), '1h 3m'), + (datetime.timedelta(seconds=36000), '10h'), + ] + + def test_format_seconds(self): + """Test format_seconds with several tests.""" + for td, out in self.TESTS: + with self.subTest(td=td): + self.assertEqual(utils.format_timedelta(td), out) + + class FormatSizeTests(unittest.TestCase): """Tests for format_size. diff --git a/qutebrowser/utils/message.py b/qutebrowser/utils/message.py index 55a7f7e2e..ef98af646 100644 --- a/qutebrowser/utils/message.py +++ b/qutebrowser/utils/message.py @@ -19,16 +19,73 @@ """Message singleton so we don't have to define unneeded signals.""" -from PyQt5.QtCore import pyqtSignal, QObject, QTimer +import datetime +import collections + +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject from qutebrowser.utils import usertypes, log, objreg, utils +_QUEUED = [] +QueuedMsg = collections.namedtuple( + 'QueuedMsg', ['time', 'win_id', 'method_name', 'text', 'args', 'kwargs']) + + +def _wrapper(win_id, method_name, text, *args, **kwargs): + """A wrapper which executes the action if possible, and queues it if not. + + It tries to get the message bridge for the given window, and if it's + unavailable, it queues it. + + Args: + win_id: The window ID to execute the action in,. + method_name: The name of the MessageBridge method to call. + text: The text do display. + *args/**kwargs: Arguments to pass to the method. + """ + try: + bridge = _get_bridge(win_id) + except objreg.RegistryUnavailableError: + if win_id == 'current': + log.misc.debug("Queueing {} for window {}".format(method_name, + win_id)) + msg = QueuedMsg(time=datetime.datetime.now(), win_id=win_id, + method_name=method_name, text=text, args=args, + kwargs=kwargs) + _QUEUED.append(msg) + else: + raise + else: + getattr(bridge, method_name)(text, *args, **kwargs) + + def _get_bridge(win_id): """Get the correct MessageBridge instance for a window.""" + try: + int(win_id) + except ValueError: + if win_id == 'current': + pass + else: + raise ValueError("Invalid window id {} - needs to be 'current' or " + "a valid integer!".format(win_id)) return objreg.get('message-bridge', scope='window', window=win_id) +@pyqtSlot() +def on_focus_changed(): + """Gets called when a new window has been focused.""" + while _QUEUED: + msg = _QUEUED.pop() + delta = datetime.datetime.now() - msg.time + log.misc.debug("Handling queued {} for window {}, delta {}".format( + msg.method_name, msg.win_id, delta)) + bridge = _get_bridge(msg.win_id) + text = '[{} ago] {}'.format(utils.format_timedelta(delta), msg.text) + getattr(bridge, msg.method_name)(text, *msg.args, **msg.kwargs) + + def error(win_id, message, immediately=False): """Convienience function to display an error message in the statusbar. @@ -36,8 +93,7 @@ def error(win_id, message, immediately=False): win_id: The ID of the window which is calling this function. others: See MessageBridge.error. """ - QTimer.singleShot( - 0, lambda: _get_bridge(win_id).error(message, immediately)) + _wrapper(win_id, 'error', message, immediately) def warning(win_id, message, immediately=False): @@ -47,8 +103,7 @@ def warning(win_id, message, immediately=False): win_id: The ID of the window which is calling this function. others: See MessageBridge.warning. """ - QTimer.singleShot( - 0, lambda: _get_bridge(win_id).warning(message, immediately)) + _wrapper(win_id, 'warning', message, immediately) def info(win_id, message, immediately=True): @@ -58,13 +113,12 @@ def info(win_id, message, immediately=True): win_id: The ID of the window which is calling this function. others: See MessageBridge.info. """ - QTimer.singleShot( - 0, lambda: _get_bridge(win_id).info(message, immediately)) + _wrapper(win_id, 'info', message, immediately) def set_cmd_text(win_id, txt): """Convienience function to Set the statusbar command line to a text.""" - _get_bridge(win_id).set_cmd_text(txt) + _wrapper(win_id, 'set_cmd_text', txt) def ask(win_id, message, mode, default=None): @@ -98,7 +152,7 @@ def alert(win_id, message): q = usertypes.Question() q.text = message q.mode = usertypes.PromptMode.alert - _get_bridge(win_id).ask(q, blocking=True) + _wrapper(win_id, 'ask', q, blocking=True) q.deleteLater() @@ -114,14 +168,13 @@ def ask_async(win_id, message, mode, handler, default=None): """ if not isinstance(mode, usertypes.PromptMode): raise TypeError("Mode {} is no PromptMode member!".format(mode)) - bridge = _get_bridge(win_id) - q = usertypes.Question(bridge) + q = usertypes.Question() q.text = message q.mode = mode q.default = default q.answered.connect(handler) q.completed.connect(q.deleteLater) - bridge.ask(q, blocking=False) + _wrapper(win_id, 'ask', q, blocking=False) def confirm_async(win_id, message, yes_action, no_action=None, default=None): @@ -134,8 +187,7 @@ def confirm_async(win_id, message, yes_action, no_action=None, default=None): no_action: Callable to be called when the user answered no. default: True/False to set a default value, or None. """ - bridge = _get_bridge(win_id) - q = usertypes.Question(bridge) + q = usertypes.Question() q.text = message q.mode = usertypes.PromptMode.yesno q.default = default @@ -143,7 +195,7 @@ def confirm_async(win_id, message, yes_action, no_action=None, default=None): if no_action is not None: q.answered_no.connect(no_action) q.completed.connect(q.deleteLater) - bridge.ask(q, blocking=False) + _wrapper(win_id, 'ask', q, blocking=False) class MessageBridge(QObject): @@ -185,19 +237,6 @@ class MessageBridge(QObject): def __repr__(self): return utils.get_repr(self) - def _emit_later(self, signal, *args): - """Emit a message later when the mainloop is not busy anymore. - - This is especially useful when messages are sent during init, before - the messagebridge signals are connected - messages would get lost if we - did normally emit them. - - Args: - signal: The signal to be emitted. - *args: Args to be passed to the signal. - """ - QTimer.singleShot(0, lambda: signal.emit(*args)) - def error(self, msg, immediately=False): """Display an error in the statusbar. @@ -211,7 +250,7 @@ class MessageBridge(QObject): """ msg = str(msg) log.misc.error(msg) - self._emit_later(self.s_error, msg, immediately) + self.s_error.emit(msg, immediately) def warning(self, msg, immediately=False): """Display an warning in the statusbar. @@ -226,7 +265,7 @@ class MessageBridge(QObject): """ msg = str(msg) log.misc.warning(msg) - self._emit_later(self.s_warning, msg, immediately) + self.s_warning.emit(msg, immediately) def info(self, msg, immediately=True): """Display an info text in the statusbar. @@ -237,7 +276,7 @@ class MessageBridge(QObject): """ msg = str(msg) log.misc.info(msg) - self._emit_later(self.s_info, msg, immediately) + self.s_info.emit(msg, immediately) def set_cmd_text(self, text): """Set the command text of the statusbar. @@ -247,7 +286,7 @@ class MessageBridge(QObject): """ text = str(text) log.misc.debug(text) - self._emit_later(self.s_set_cmd_text, text) + self.s_set_cmd_text.emit(text) def set_text(self, text): """Set the normal text of the statusbar. @@ -257,7 +296,7 @@ class MessageBridge(QObject): """ text = str(text) log.misc.debug(text) - self._emit_later(self.s_set_text, text) + self.s_set_text.emit(text) def maybe_reset_text(self, text): """Reset the text in the statusbar if it matches an expected text. @@ -265,7 +304,7 @@ class MessageBridge(QObject): Args: text: The expected text. """ - self._emit_later(self.s_maybe_reset_text, str(text)) + self.s_maybe_reset_text.emit(str(text)) def ask(self, question, blocking): """Ask a question to the user. @@ -273,9 +312,6 @@ class MessageBridge(QObject): Note this method doesn't return the answer, it only blocks. The caller needs to construct a Question object and get the answer. - We don't use _emit_later here as this makes no sense with a blocking - question. - Args: question: A Question object. blocking: Whether to return immediately or wait until the diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index 11016b788..ae491c852 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -201,6 +201,21 @@ def format_seconds(total_seconds): return prefix + ':'.join(chunks) +def format_timedelta(td): + """Format a timedelta to get a "1h 5m 1s" string.""" + prefix = '-' if td.total_seconds() < 0 else '' + hours, rem = divmod(abs(round(td.total_seconds())), 3600) + minutes, seconds = divmod(rem, 60) + chunks = [] + if hours: + chunks.append('{}h'.format(hours)) + if minutes: + chunks.append('{}m'.format(minutes)) + if seconds or not chunks: + chunks.append('{}s'.format(seconds)) + return prefix + ' '.join(chunks) + + def format_size(size, base=1024, suffix=''): """Format a byte size so it's human readable.