Queue messages for 'current' window if unfocused.

Fixes #512.
This commit is contained in:
Florian Bruhin 2015-03-22 22:39:56 +01:00
parent 19d369377e
commit 157c25bb13
5 changed files with 129 additions and 50 deletions

View File

@ -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."""

View File

@ -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):

View File

@ -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.

View File

@ -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

View File

@ -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.