qutebrowser/qutebrowser/utils/message.py
2015-03-22 22:39:56 +01:00

321 lines
11 KiB
Python

# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# 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 <http://www.gnu.org/licenses/>.
"""Message singleton so we don't have to define unneeded signals."""
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.
Args:
win_id: The ID of the window which is calling this function.
others: See MessageBridge.error.
"""
_wrapper(win_id, 'error', message, immediately)
def warning(win_id, message, immediately=False):
"""Convienience function to display a warning message in the statusbar.
Args:
win_id: The ID of the window which is calling this function.
others: See MessageBridge.warning.
"""
_wrapper(win_id, 'warning', message, immediately)
def info(win_id, message, immediately=True):
"""Convienience function to display an info message in the statusbar.
Args:
win_id: The ID of the window which is calling this function.
others: See MessageBridge.info.
"""
_wrapper(win_id, 'info', message, immediately)
def set_cmd_text(win_id, txt):
"""Convienience function to Set the statusbar command line to a text."""
_wrapper(win_id, 'set_cmd_text', txt)
def ask(win_id, message, mode, default=None):
"""Ask a modular question in the statusbar (blocking).
Args:
win_id: The ID of the window which is calling this function.
message: The message to display to the user.
mode: A PromptMode.
default: The default value to display.
Return:
The answer the user gave or None if the prompt was cancelled.
"""
q = usertypes.Question()
q.text = message
q.mode = mode
q.default = default
_get_bridge(win_id).ask(q, blocking=True)
q.deleteLater()
return q.answer
def alert(win_id, message):
"""Display an alert which needs to be confirmed.
Args:
win_id: The ID of the window which is calling this function.
message: The message to show.
"""
q = usertypes.Question()
q.text = message
q.mode = usertypes.PromptMode.alert
_wrapper(win_id, 'ask', q, blocking=True)
q.deleteLater()
def ask_async(win_id, message, mode, handler, default=None):
"""Ask an async question in the statusbar.
Args:
win_id: The ID of the window which is calling this function.
message: The message to display to the user.
mode: A PromptMode.
handler: The function to get called with the answer as argument.
default: The default value to display.
"""
if not isinstance(mode, usertypes.PromptMode):
raise TypeError("Mode {} is no PromptMode member!".format(mode))
q = usertypes.Question()
q.text = message
q.mode = mode
q.default = default
q.answered.connect(handler)
q.completed.connect(q.deleteLater)
_wrapper(win_id, 'ask', q, blocking=False)
def confirm_async(win_id, message, yes_action, no_action=None, default=None):
"""Ask a yes/no question to the user and execute the given actions.
Args:
win_id: The ID of the window which is calling this function.
message: The message to display to the user.
yes_action: Callable to be called when the user answered yes.
no_action: Callable to be called when the user answered no.
default: True/False to set a default value, or None.
"""
q = usertypes.Question()
q.text = message
q.mode = usertypes.PromptMode.yesno
q.default = default
q.answered_yes.connect(yes_action)
if no_action is not None:
q.answered_no.connect(no_action)
q.completed.connect(q.deleteLater)
_wrapper(win_id, 'ask', q, blocking=False)
class MessageBridge(QObject):
"""Bridge for messages to be shown in the statusbar.
Signals:
s_error: Display an error message.
arg 0: The error message to show.
arg 1: Whether to show it immediately (True) or queue it
(False).
s_info: Display an info message.
args: See s_error.
s_warning: Display a warning message.
args: See s_error.
s_set_text: Set a persistent text in the statusbar.
arg: The text to set.
s_maybe_reset_text: Reset the text if it hasn't been changed yet.
arg: The expected text.
s_set_cmd_text: Pre-set a text for the commandline prompt.
arg: The text to set.
s_question: Ask a question to the user in the statusbar.
arg 0: The Question object to ask.
arg 1: Whether to block (True) or ask async (False).
IMPORTANT: Slots need to be connected to this signal via a
Qt.DirectConnection!
"""
s_error = pyqtSignal(str, bool)
s_warning = pyqtSignal(str, bool)
s_info = pyqtSignal(str, bool)
s_set_text = pyqtSignal(str)
s_maybe_reset_text = pyqtSignal(str)
s_set_cmd_text = pyqtSignal(str)
s_question = pyqtSignal(usertypes.Question, bool)
def __repr__(self):
return utils.get_repr(self)
def error(self, msg, immediately=False):
"""Display an error in the statusbar.
Args:
msg: The message to show.
immediately: Whether to display the message immediately (True) or
queue it for displaying when all other messages are
displayed (False). Messages resulting from direct user
input should be displayed immediately, all other
messages should be queued.
"""
msg = str(msg)
log.misc.error(msg)
self.s_error.emit(msg, immediately)
def warning(self, msg, immediately=False):
"""Display an warning in the statusbar.
Args:
msg: The message to show.
immediately: Whether to display the message immediately (True) or
queue it for displaying when all other messages are
displayed (False). Messages resulting from direct user
input should be displayed immediately, all other
messages should be queued.
"""
msg = str(msg)
log.misc.warning(msg)
self.s_warning.emit(msg, immediately)
def info(self, msg, immediately=True):
"""Display an info text in the statusbar.
Args:
See error(). Note immediately is True by default, because messages
do rarely happen without user interaction.
"""
msg = str(msg)
log.misc.info(msg)
self.s_info.emit(msg, immediately)
def set_cmd_text(self, text):
"""Set the command text of the statusbar.
Args:
text: The text to set.
"""
text = str(text)
log.misc.debug(text)
self.s_set_cmd_text.emit(text)
def set_text(self, text):
"""Set the normal text of the statusbar.
Args:
text: The text to set.
"""
text = str(text)
log.misc.debug(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.
Args:
text: The expected text.
"""
self.s_maybe_reset_text.emit(str(text))
def ask(self, question, blocking):
"""Ask a question to the user.
Note this method doesn't return the answer, it only blocks. The caller
needs to construct a Question object and get the answer.
Args:
question: A Question object.
blocking: Whether to return immediately or wait until the
question is answered.
"""
self.s_question.emit(question, blocking)