Clean up message API

This commit is contained in:
Florian Bruhin 2014-06-26 07:58:00 +02:00
parent 297c2bcad1
commit 92ff957543
14 changed files with 160 additions and 107 deletions

View File

@ -42,6 +42,7 @@ New big features
Improvements / minor features
=============================
- Make sure Question objects are deleteLater'ed correctly.
- qutebrowser local_file.foo should open that file in $PWD
- Downloading: Download to default filename if only path is given
- Downloading: Remember last path for prompt, if explicitely set.
@ -102,7 +103,6 @@ style
=====
- Clean up the package checking mess in earlyinit.py
- Clean up the message API (sync. vs. async.? queue or not?)
- Use list models for completion and a proxy model which converts them to a
tree?

View File

@ -380,12 +380,12 @@ class Application(QApplication):
tabs.hint_strings_updated.connect(kp['hint'].on_hint_strings_updated)
# messages
self.messagebridge.error.connect(status.disp_error)
self.messagebridge.info.connect(status.disp_temp_text)
self.messagebridge.text.connect(status.set_text)
self.messagebridge.set_cmd_text.connect(cmd.set_cmd_text)
self.messagebridge.question.connect(status.prompt.ask_question,
Qt.DirectConnection)
self.messagebridge.s_error.connect(status.disp_error)
self.messagebridge.s_info.connect(status.disp_temp_text)
self.messagebridge.s_set_text.connect(status.set_text)
self.messagebridge.s_set_cmd_text.connect(cmd.set_cmd_text)
self.messagebridge.s_question.connect(status.prompt.ask_question,
Qt.DirectConnection)
# config
self.config.style_changed.connect(style.invalidate_caches)

View File

@ -415,4 +415,4 @@ class DownloadManager(QObject):
@pyqtSlot(str)
def on_error(self, msg):
"""Display error message on download errors."""
message.error("Download error: {}".format(msg), queue=True)
message.error("Download error: {}".format(msg))

View File

@ -444,7 +444,7 @@ class HintManager(QObject):
raise CommandError("No elements found.")
ctx.target = target
ctx.baseurl = baseurl
message.text(self.HINT_TEXTS[target])
message.instance().set_text(self.HINT_TEXTS[target])
strings = self._hint_strings(visible_elems)
for e, string in zip(visible_elems, strings):
label = self._draw_label(e, string)
@ -532,7 +532,8 @@ class HintManager(QObject):
elif self._context.target in url_handlers:
url = self._resolve_url(elem)
if url is None:
message.error("No suitable link found for this element.")
message.error("No suitable link found for this element.",
immediately=True)
return
url_handlers[self._context.target](url)
else:
@ -577,4 +578,4 @@ class HintManager(QObject):
if not elem.label.isNull():
elem.label.removeFromDocument()
self._context = None
message.clear()
message.instance().set_text('')

View File

@ -51,7 +51,7 @@ def init():
try:
key, url = line.split(maxsplit=1)
except ValueError:
message.error("Invalid quickmark '{}'".format(line), queue=True)
message.error("Invalid quickmark '{}'".format(line))
else:
marks[key] = url
@ -70,8 +70,8 @@ def prompt_save(url):
"""
qt_ensure_valid(url)
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
message.question("Add quickmark:", PromptMode.text,
partial(quickmark_add, urlstr))
message.ask_async("Add quickmark:", PromptMode.text,
partial(quickmark_add, urlstr))
@cmdutils.register()
@ -92,8 +92,8 @@ def quickmark_add(urlstr, name):
marks[name] = urlstr
if name in marks:
message.confirm_action("Override existing quickmark?", set_mark,
default=True)
message.confirm_async("Override existing quickmark?", set_mark,
default=True)
else:
set_mark()

View File

@ -76,8 +76,7 @@ class BrowserPage(QWebPage):
http://www.riverbankcomputing.com/pipermail/pyqt/2014-June/034385.html
"""
answer = message.modular_question(
"js: {}".format(msg), PromptMode.text, default)
answer = message.ask("js: {}".format(msg), PromptMode.text, default)
if answer is None:
return (False, "")
else:
@ -149,7 +148,7 @@ class BrowserPage(QWebPage):
"""Handle printing when requested via javascript."""
if not check_print_compat():
message.error("Printing on Qt < 5.3.0 on Windows is broken, "
"please upgrade!")
"please upgrade!", immediately=True)
return
printdiag = QPrintDialog()
printdiag.setAttribute(Qt.WA_DeleteOnClose)
@ -205,11 +204,11 @@ class BrowserPage(QWebPage):
def javaScriptAlert(self, _frame, msg):
"""Override javaScriptAlert to use the statusbar."""
message.modular_question("js: {}".format(msg), PromptMode.alert)
message.ask("js: {}".format(msg), PromptMode.alert)
def javaScriptConfirm(self, _frame, msg):
"""Override javaScriptConfirm to use the statusbar."""
ans = message.modular_question("js: {}".format(msg), PromptMode.yesno)
ans = message.ask("js: {}".format(msg), PromptMode.yesno)
return bool(ans)
def javaScriptConsoleMessage(self, msg, line, source):
@ -228,8 +227,8 @@ class BrowserPage(QWebPage):
def shouldInterruptJavaScript(self):
"""Override shouldInterruptJavaScript to use the statusbar."""
answer = message.modular_question("Interrupt long-running javascript?",
PromptMode.yesno)
answer = message.ask("Interrupt long-running javascript?",
PromptMode.yesno)
if answer is None:
answer = True
return answer

View File

@ -258,7 +258,7 @@ class CommandManager:
try:
self.run(text, count)
except (CommandMetaError, CommandError) as e:
message.error(e)
message.error(e, immediately=True)
@pyqtSlot(str, int)
def run_safely_init(self, text, count=None):
@ -269,4 +269,4 @@ class CommandManager:
try:
self.run(text, count)
except (CommandMetaError, CommandError) as e:
message.error(e, queue=True)
message.error(e)

View File

@ -270,7 +270,8 @@ class ConfigManager(QObject):
except (NoOptionError, NoSectionError) as e:
raise CommandError("get: {} - {}".format(e.__class__.__name__, e))
else:
message.info("{} {} = {}".format(sectname, optname, val))
message.info("{} {} = {}".format(sectname, optname, val),
immediately=True)
def get(self, sectname, optname, raw=False, transformed=True):
"""Get the value from a section/option.

View File

@ -54,7 +54,7 @@ class CommandKeyParser(BaseKeyParser):
cmdstr))
message.set_cmd_text(':{} '.format(cmdstr))
except (CommandMetaError, CommandError) as e:
message.error(e)
message.error(e, immediately=True)
def execute(self, cmdstr, _keytype, count=None):
self._run_or_fill(cmdstr, count)

View File

@ -92,24 +92,21 @@ class NetworkManager(QNetworkAccessManager):
return
for err in errors:
# FIXME we might want to use warn here (non-fatal error)
message.error('SSL error: {}'.format(err.errorString()),
queue=True)
message.error('SSL error: {}'.format(err.errorString()))
reply.ignoreSslErrors()
@pyqtSlot('QNetworkReply', 'QAuthenticator')
def on_authentication_required(self, _reply, authenticator):
"""Called when a website needs authentication."""
answer = message.modular_question(
"Username ({}):".format(authenticator.realm()),
mode=PromptMode.user_pwd)
answer = message.ask("Username ({}):".format(authenticator.realm()),
mode=PromptMode.user_pwd)
self._fill_authenticator(authenticator, answer)
@pyqtSlot('QNetworkProxy', 'QAuthenticator')
def on_proxy_authentication_required(self, _proxy, authenticator):
"""Called when a proxy needs authentication."""
answer = message.modular_question(
"Proxy username ({}):".format(authenticator.realm()),
mode=PromptMode.user_pwd)
answer = message.ask("Proxy username ({}):".format(
authenticator.realm()), mode=PromptMode.user_pwd)
self._fill_authenticator(authenticator, answer)
def createRequest(self, op, req, outgoing_data):

View File

@ -30,44 +30,31 @@ def instance():
return QCoreApplication.instance().messagebridge
def error(message, queue=False):
"""Display an error message in the statusbar.
def error(message, immediately=False):
"""Convienience function to display an error message in the statusbar.
Args:
message: The message to display.
queue: If set, message gets queued rather than being displayed
immediately.
See MessageBridge.error.
"""
message = str(message)
logger.error(message)
bridge = instance()
bridge.queue(bridge.error, message, queue)
instance().error(message, immediately)
def info(message, queue=False):
"""Display a temporary info message in the statusbar.
def info(message, immediately=True):
"""Convienience function to display an info message in the statusbar.
Args:
message: The message to display.
queue: If set, message gets queued rather than being displayed
immediately.
See MessageBridge.info.
"""
message = str(message)
logger.info(message)
bridge = instance()
bridge.queue(bridge.info, message, queue)
instance().info(message, immediately)
def text(message):
"""Display a persistent message in the statusbar."""
message = str(message)
logger.debug(message)
bridge = instance()
bridge.queue(bridge.text, message)
def set_cmd_text(txt):
"""Convienience function to Set the statusbar command line to a text."""
instance().set_cmd_text(txt)
def modular_question(message, mode, default=None):
"""Ask a modular question in the statusbar.
def ask(message, mode, default=None):
"""Ask a modular question in the statusbar (blocking).
Args:
message: The message to display to the user.
@ -95,29 +82,25 @@ def alert(message):
q.deleteLater()
def question(message, mode, handler, cancelled_handler=None, default=None):
def ask_async(message, mode, handler, default=None):
"""Ask an async question in the statusbar.
Args:
message: The message to display to the user.
mode: A PromptMode.
handler: The function to get called with the answer as argument.
cancelled_handler: The function to get called when the prompt was
cancelled by the user, or None.
default: The default value to display.
"""
q = Question(instance())
bridge = instance()
q = Question(bridge)
q.text = message
q.mode = mode
q.default = default
q.answered.connect(handler)
if cancelled_handler is not None:
q.cancelled.connect(cancelled_handler)
bridge = instance()
bridge.queue(bridge.question, q, False)
def confirm_action(message, yes_action, no_action=None, default=None):
def confirm_async(message, yes_action, no_action=None, default=None):
"""Ask a yes/no question to the user and execute the given actions.
Args:
@ -126,47 +109,120 @@ def confirm_action(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.
"""
q = Question(instance())
bridge = instance()
q = Question(bridge)
q.text = message
q.mode = PromptMode.yesno
q.default = default
q.answered_yes.connect(yes_action)
if no_action is not None:
q.answered_no.connect(no_action)
bridge = instance()
bridge.queue(bridge.question, q, False)
def clear():
"""Clear a persistent message in the statusbar."""
bridge = instance()
bridge.queue(bridge.text, '')
def set_cmd_text(txt):
"""Set the statusbar command line to a preset text."""
bridge = instance()
bridge.queue(bridge.set_cmd_text, txt)
class MessageBridge(QObject):
"""Bridge for messages to be shown in the statusbar."""
"""Bridge for messages to be shown in the statusbar.
error = pyqtSignal(str, bool)
info = pyqtSignal(str, bool)
text = pyqtSignal(str)
set_cmd_text = pyqtSignal(str)
question = pyqtSignal(Question, bool)
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_set_text: Set a persistent text in the statusbar.
arg: The text to set.
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_info = pyqtSignal(str, bool)
s_set_text = pyqtSignal(str)
s_set_cmd_text = pyqtSignal(str)
s_question = pyqtSignal(Question, bool)
def __repr__(self):
return '<{}>'.format(self.__class__.__name__)
def queue(self, signal, *args):
"""Queue a message to be emitted.
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.
Args:
msg: The message to show.
queue: Whether to queue the message (True) or display it
immediately (False). Messages resulting from direct user
input should be displayed immediately, all other messages
should be queued.
"""
msg = str(msg)
logger.error(msg)
self._emit_later(self.s_error, 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)
logger.info(msg)
self._emit_later(self.s_info, msg, immediately)
def set_cmd_text(self, text):
"""Set the command text of the statusbar.
Args:
text: The text to set.
"""
text = str(text)
logger.debug(text)
self._emit_later(self.s_set_cmd_text, text)
def set_text(self, text):
"""Set the normal text of the statusbar.
Args:
text: The text to set.
"""
text = str(text)
logger.debug(text)
self._emit_later(self.s_set_text, 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.
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
question is answered.
"""
self.question.emit(question, blocking)

View File

@ -177,8 +177,7 @@ class MainWindow(QWidget):
else:
text = "Close {} {}?".format(
count, "tab" if count == 1 else "tabs")
confirmed = message.modular_question(text, PromptMode.yesno,
default=True)
confirmed = message.ask(text, PromptMode.yesno, default=True)
if confirmed:
e.accept()
else:

View File

@ -279,14 +279,14 @@ class StatusBar(QWidget):
self._timer_was_active = False
self._stack.setCurrentWidget(self.txt)
def _disp_text(self, text, error, queue=False):
def _disp_text(self, text, error, immediately=False):
"""Inner logic for disp_error and disp_temp_text.
Args:
text: The message to display.
error: Whether it's an error message (True) or normal text (False)
queue: If set, message gets queued rather than being displayed
immediately.
immediately: If set, message gets displayed immediately instead of
queued.
"""
# FIXME probably using a QTime here would be easier.
logger.debug("Displaying text: {} (error={})".format(text, error))
@ -310,7 +310,7 @@ class StatusBar(QWidget):
# If we get the same message multiple times in a row and we're
# still displaying it *anyways* we ignore the new one
logger.debug("ignoring")
elif not queue:
elif immediately:
# This message is a reaction to a keypress and should be displayed
# immediately, temporarely interrupting the message queue.
# We display this immediately and restart the timer.to clear it and
@ -327,26 +327,26 @@ class StatusBar(QWidget):
self._text_pop_timer.start()
@pyqtSlot(str, bool)
def disp_error(self, text, queue=False):
def disp_error(self, text, immediately=False):
"""Display an error in the statusbar.
Args:
text: The message to display.
queue: If set, message gets queued rather than being displayed
immediately.
immediately: If set, message gets displayed immediately instead of
queued.
"""
self._disp_text(text, True, queue)
self._disp_text(text, True, immediately)
@pyqtSlot(str, bool)
def disp_temp_text(self, text, queue):
def disp_temp_text(self, text, immediately):
"""Display a temporary text in the statusbar.
Args:
text: The message to display.
queue: If set, message gets queued rather than being displayed
immediately.
immediately: If set, message gets displayed immediately instead of
queued.
"""
self._disp_text(text, False, queue)
self._disp_text(text, False, immediately)
@pyqtSlot(str)
def set_text(self, val):

View File

@ -190,13 +190,13 @@ class WebView(QWebView):
try:
self.go_back()
except CommandError as ex:
message.error(ex)
message.error(ex, immediately=True)
elif e.button() == Qt.XButton2:
# Forward button on mice which have it.
try:
self.go_forward()
except CommandError as ex:
message.error(ex)
message.error(ex, immediately=True)
def _mousepress_insertmode(self, e):
"""Switch to insert mode when an editable element was clicked.