From 92ff957543b13fb8cfe7659c448be75c9a3863d4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 26 Jun 2014 07:58:00 +0200 Subject: [PATCH] Clean up message API --- doc/TODO | 2 +- qutebrowser/app.py | 12 +- qutebrowser/browser/downloads.py | 2 +- qutebrowser/browser/hints.py | 7 +- qutebrowser/browser/quickmarks.py | 10 +- qutebrowser/browser/webpage.py | 13 +- qutebrowser/commands/managers.py | 4 +- qutebrowser/config/config.py | 3 +- qutebrowser/keyinput/keyparser.py | 2 +- qutebrowser/network/networkmanager.py | 13 +- qutebrowser/utils/message.py | 168 +++++++++++++++++--------- qutebrowser/widgets/mainwindow.py | 3 +- qutebrowser/widgets/statusbar/bar.py | 24 ++-- qutebrowser/widgets/webview.py | 4 +- 14 files changed, 160 insertions(+), 107 deletions(-) diff --git a/doc/TODO b/doc/TODO index 1c2aa2571..eaf626006 100644 --- a/doc/TODO +++ b/doc/TODO @@ -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? diff --git a/qutebrowser/app.py b/qutebrowser/app.py index ace945891..585f18680 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -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) diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index 48804262f..c1b3b7139 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -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)) diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index ffe395966..93d0d0600 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -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('') diff --git a/qutebrowser/browser/quickmarks.py b/qutebrowser/browser/quickmarks.py index 33b9e6b27..5ce9368ac 100644 --- a/qutebrowser/browser/quickmarks.py +++ b/qutebrowser/browser/quickmarks.py @@ -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() diff --git a/qutebrowser/browser/webpage.py b/qutebrowser/browser/webpage.py index 6c86541ec..0216e2926 100644 --- a/qutebrowser/browser/webpage.py +++ b/qutebrowser/browser/webpage.py @@ -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 diff --git a/qutebrowser/commands/managers.py b/qutebrowser/commands/managers.py index 702ab7a64..37ab9b036 100644 --- a/qutebrowser/commands/managers.py +++ b/qutebrowser/commands/managers.py @@ -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) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 408053a7b..562bebae1 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -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. diff --git a/qutebrowser/keyinput/keyparser.py b/qutebrowser/keyinput/keyparser.py index fc2b61fed..6da3eb632 100644 --- a/qutebrowser/keyinput/keyparser.py +++ b/qutebrowser/keyinput/keyparser.py @@ -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) diff --git a/qutebrowser/network/networkmanager.py b/qutebrowser/network/networkmanager.py index bb9925161..66e87f16b 100644 --- a/qutebrowser/network/networkmanager.py +++ b/qutebrowser/network/networkmanager.py @@ -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): diff --git a/qutebrowser/utils/message.py b/qutebrowser/utils/message.py index 10ed2f00b..55fc3368a 100644 --- a/qutebrowser/utils/message.py +++ b/qutebrowser/utils/message.py @@ -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) diff --git a/qutebrowser/widgets/mainwindow.py b/qutebrowser/widgets/mainwindow.py index 9c64efe79..28d00798a 100644 --- a/qutebrowser/widgets/mainwindow.py +++ b/qutebrowser/widgets/mainwindow.py @@ -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: diff --git a/qutebrowser/widgets/statusbar/bar.py b/qutebrowser/widgets/statusbar/bar.py index b7886f06e..2e4d382b7 100644 --- a/qutebrowser/widgets/statusbar/bar.py +++ b/qutebrowser/widgets/statusbar/bar.py @@ -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): diff --git a/qutebrowser/widgets/webview.py b/qutebrowser/widgets/webview.py index fc781d65c..7b9a7f3cf 100644 --- a/qutebrowser/widgets/webview.py +++ b/qutebrowser/widgets/webview.py @@ -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.