First attempt at showing prompts in all windows

This commit is contained in:
Florian Bruhin 2016-10-27 13:40:34 +02:00
parent d5a1f6d6b5
commit 9ce1180b31
8 changed files with 198 additions and 176 deletions

View File

@ -48,7 +48,7 @@ from qutebrowser.config import style, config, websettings, configexc
from qutebrowser.browser import urlmarks, adblock, history, browsertab
from qutebrowser.browser.webkit import cookies, cache, downloads
from qutebrowser.browser.webkit.network import networkmanager
from qutebrowser.mainwindow import mainwindow
from qutebrowser.mainwindow import mainwindow, prompt
from qutebrowser.misc import (readline, ipc, savemanager, sessions,
crashsignal, earlyinit)
from qutebrowser.misc import utilcmds # pylint: disable=unused-import
@ -372,6 +372,9 @@ def _init_modules(args, crash_handler):
crash_handler: The CrashHandler instance.
"""
# pylint: disable=too-many-statements
log.init.debug("Initializing prompts...")
prompt.init()
log.init.debug("Initializing save manager...")
save_manager = savemanager.SaveManager(qApp)
objreg.register('save-manager', save_manager)
@ -644,11 +647,8 @@ class Quitter:
load_next_time=True)
deferrer = False
for win_id in objreg.window_registry:
prompt_container = objreg.get('prompt-container', None,
scope='window', window=win_id)
if prompt_container is not None and prompt_container.shutdown():
deferrer = True
if prompt.prompt_queue.shutdown():
deferrer = True
if deferrer:
# If shutdown was called while we were asking a question, we're in
# a still sub-eventloop (which gets quit now) and not in the main

View File

@ -1137,7 +1137,7 @@ class CommandDispatcher:
def quickmark_save(self):
"""Save the current page as a quickmark."""
quickmark_manager = objreg.get('quickmark-manager')
quickmark_manager.prompt_save(self._win_id, self._current_url())
quickmark_manager.prompt_save(self._current_url())
@cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0)

View File

@ -159,11 +159,10 @@ class QuickmarkManager(UrlMarkManager):
else:
self.marks[key] = url
def prompt_save(self, win_id, url):
def prompt_save(self, url):
"""Prompt for a new quickmark name to be added and add it.
Args:
win_id: The current window ID.
url: The quickmark url as a QUrl.
"""
if not url.isValid():
@ -171,19 +170,17 @@ class QuickmarkManager(UrlMarkManager):
return
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
message.ask_async(
win_id, "Add quickmark:", usertypes.PromptMode.text,
functools.partial(self.quickmark_add, win_id, urlstr))
"Add quickmark:", usertypes.PromptMode.text,
functools.partial(self.quickmark_add, urlstr))
@cmdutils.register(instance='quickmark-manager')
@cmdutils.argument('win_id', win_id=True)
def quickmark_add(self, win_id, url, name):
def quickmark_add(self, url, name):
"""Add a new quickmark.
You can view all saved quickmarks on the
link:qute://bookmarks[bookmarks page].
Args:
win_id: The window ID to display the errors in.
url: The url to add as quickmark.
name: The name for the new quickmark.
"""
@ -205,7 +202,7 @@ class QuickmarkManager(UrlMarkManager):
if name in self.marks:
message.confirm_async(
win_id, title="Override existing quickmark?",
title="Override existing quickmark?",
yes_action=set_mark, default=True)
else:
set_mark()

View File

@ -231,8 +231,8 @@ class NetworkManager(QNetworkAccessManager):
tab=self._tab_id)
abort_on.append(tab.load_started)
return message.ask(win_id=self._win_id, title=title, text=text,
mode=mode, abort_on=abort_on, default=default)
return message.ask(title=title, text=text, mode=mode,
abort_on=abort_on, default=default)
def shutdown(self):
"""Abort all running requests."""

View File

@ -98,7 +98,7 @@ class BrowserPage(QWebPage):
if (self._is_shutting_down or
config.get('content', 'ignore-javascript-prompt')):
return (False, "")
answer = message.ask(self._win_id, 'Javascript prompt', msg,
answer = message.ask('Javascript prompt', msg,
mode=usertypes.PromptMode.text,
default=default,
abort_on=[self.loadStarted,
@ -139,7 +139,6 @@ class BrowserPage(QWebPage):
url = QUrl(info.url)
scheme = url.scheme()
message.confirm_async(
self._win_id,
title="Open external application for {}-link?".format(scheme),
text="URL: {}".format(url.toDisplayString()),
yes_action=functools.partial(QDesktopServices.openUrl, url))
@ -453,8 +452,7 @@ class BrowserPage(QWebPage):
if (self._is_shutting_down or
config.get('content', 'ignore-javascript-alert')):
return
message.ask(self._win_id, 'Javascript alert', msg,
mode=usertypes.PromptMode.alert,
message.ask('Javascript alert', msg, mode=usertypes.PromptMode.alert,
abort_on=[self.loadStarted, self.shutting_down])
def javaScriptConfirm(self, frame, msg):
@ -465,7 +463,7 @@ class BrowserPage(QWebPage):
if self._is_shutting_down:
return False
ans = message.ask(self._win_id, 'Javascript confirm', msg,
ans = message.ask('Javascript confirm', msg,
mode=usertypes.PromptMode.yesno,
abort_on=[self.loadStarted, self.shutting_down])
return bool(ans)

View File

@ -397,7 +397,7 @@ class MainWindow(QWidget):
mode_manager.entered.connect(status.on_mode_entered)
mode_manager.left.connect(status.on_mode_left)
mode_manager.left.connect(cmd.on_mode_left)
mode_manager.left.connect(self._prompt_container.on_mode_left)
mode_manager.left.connect(prompt.prompt_queue.on_mode_left)
# commands
keyparsers[usertypes.KeyMode.normal].keystring_updated.connect(
@ -420,8 +420,6 @@ class MainWindow(QWidget):
message_bridge.s_set_text.connect(status.set_text)
message_bridge.s_maybe_reset_text.connect(status.txt.maybe_reset_text)
message_bridge.s_question.connect(self._prompt_container.ask_question,
Qt.DirectConnection)
# statusbar
tabs.current_tab_changed.connect(status.prog.on_tab_changed)
@ -520,8 +518,7 @@ class MainWindow(QWidget):
# Process all quit messages that user must confirm
if quit_texts or 'always' in confirm_quit:
text = '\n'.join(['Really quit?'] + quit_texts)
confirmed = message.ask(self.win_id, text,
mode=usertypes.PromptMode.yesno,
confirmed = message.ask(text, mode=usertypes.PromptMode.yesno,
default=True)
# Stop asking if the user cancels
if not confirmed:

View File

@ -25,17 +25,20 @@ import collections
import sip
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QTimer, QDir, QModelIndex,
QItemSelectionModel)
QItemSelectionModel, QObject)
from PyQt5.QtWidgets import (QWidget, QGridLayout, QVBoxLayout, QLineEdit,
QLabel, QWidgetItem, QFileSystemModel, QTreeView,
QSizePolicy)
from qutebrowser.config import style
from qutebrowser.utils import usertypes, log, utils, qtutils, objreg
from qutebrowser.utils import usertypes, log, utils, qtutils, objreg, message
from qutebrowser.keyinput import modeman
from qutebrowser.commands import cmdutils, cmdexc
prompt_queue = None
AuthTuple = collections.namedtuple('AuthTuple', ['user', 'password'])
@ -49,9 +52,9 @@ class UnsupportedOperationError(Exception):
"""Raised when the prompt class doesn't support the requested operation."""
class PromptContainer(QWidget):
class PromptQueue(QObject):
"""Container for prompts to be shown above the statusbar.
"""Global manager and queue for upcoming prompts.
The way in which multiple questions are handled deserves some explanation.
@ -77,43 +80,19 @@ class PromptContainer(QWidget):
_loops: A list of local EventLoops to spin in when blocking.
_queue: A deque of waiting questions.
_prompt: The current prompt object if we're handling a question.
_layout: The layout used to show prompts in.
_win_id: The window ID this object is associated with.
Signals:
show_prompt: Emitted when a prompt should be shown.
"""
STYLESHEET = """
{% set prompt_radius = config.get('ui', 'prompt-radius') %}
QWidget#Prompt {
{% if config.get('ui', 'status-position') == 'top' %}
border-bottom-left-radius: {{ prompt_radius }}px;
border-bottom-right-radius: {{ prompt_radius }}px;
{% else %}
border-top-left-radius: {{ prompt_radius }}px;
border-top-right-radius: {{ prompt_radius }}px;
{% endif %}
}
show_prompt = pyqtSignal(object)
QWidget {
font: {{ font['prompts'] }};
color: {{ color['prompts.fg'] }};
background-color: {{ color['prompts.bg'] }};
}
"""
update_geometry = pyqtSignal()
def __init__(self, win_id, parent=None):
def __init__(self, parent=None):
super().__init__(parent)
self._layout = QVBoxLayout(self)
self._layout.setContentsMargins(10, 10, 10, 10)
self._prompt = None
self._shutting_down = False
self._loops = []
self._queue = collections.deque()
self._win_id = win_id
self.setObjectName('Prompt')
self.setAttribute(Qt.WA_StyledBackground, True)
style.set_register_stylesheet(self)
def __repr__(self):
return utils.get_repr(self, loops=len(self._loops),
@ -134,50 +113,6 @@ class PromptContainer(QWidget):
# https://github.com/The-Compiler/qutebrowser/issues/415
self.ask_question(question, blocking=False)
def _show_prompt(self, prompt):
"""SHow the given prompt object.
Args:
prompt: A Prompt object or None.
Return: True if a prompt was shown, False otherwise.
"""
# Before we set a new prompt, make sure the old one is what we expect
# This will also work if self._prompt is None and verify nothing is
# displayed.
#
# Note that we don't delete the old prompt here, as we might be in the
# middle of saving/restoring an old prompt object.
assert self._layout.count() in [0, 1], self._layout.count()
item = self._layout.takeAt(0)
if item is None:
assert self._prompt is None, self._prompt
else:
if (not isinstance(item, QWidgetItem) or
item.widget() is not self._prompt):
raise AssertionError("Expected {} to be in layout but got "
"{}!".format(self._prompt, item))
item.widget().hide()
log.prompt.debug("Displaying prompt {}".format(prompt))
self._prompt = prompt
if prompt is None:
self.hide()
return False
prompt.question.aborted.connect(
lambda: modeman.maybe_leave(self._win_id, prompt.KEY_MODE,
'aborted'))
modeman.enter(self._win_id, prompt.KEY_MODE, 'question asked')
self._prompt = prompt
self.setSizePolicy(self._prompt.sizePolicy())
self._layout.addWidget(self._prompt)
self._prompt.show()
self.show()
self._prompt.setFocus()
self.update_geometry.emit()
return True
def shutdown(self):
"""Cancel all blocking questions.
@ -196,7 +131,7 @@ class PromptContainer(QWidget):
else:
return False
@cmdutils.register(instance='prompt-container', hide=True, scope='window',
@cmdutils.register(instance='prompt-queue', hide=True,
modes=[usertypes.KeyMode.prompt,
usertypes.KeyMode.yesno])
def prompt_accept(self, value=None):
@ -211,42 +146,30 @@ class PromptContainer(QWidget):
value: If given, uses this value instead of the entered one.
For boolean prompts, "yes"/"no" are accepted as value.
"""
question = self._prompt.question
try:
done = self._prompt.accept(value)
except Error as e:
raise cmdexc.CommandError(str(e))
if done:
key_mode = self._prompt.KEY_MODE
self._prompt.question.done()
modeman.maybe_leave(self._win_id, key_mode, ':prompt-accept')
message.global_bridge.prompt_done.emit(self._prompt.KEY_MODE)
question.done()
@cmdutils.register(instance='prompt-container', hide=True, scope='window',
@cmdutils.register(instance='prompt-queue', hide=True,
modes=[usertypes.KeyMode.yesno],
deprecated='Use :prompt-accept yes instead!')
def prompt_yes(self):
"""Answer yes to a yes/no prompt."""
self.prompt_accept('yes')
@cmdutils.register(instance='prompt-container', hide=True, scope='window',
@cmdutils.register(instance='prompt-queue', hide=True,
modes=[usertypes.KeyMode.yesno],
deprecated='Use :prompt-accept no instead!')
def prompt_no(self):
"""Answer no to a yes/no prompt."""
self.prompt_accept('no')
@pyqtSlot(usertypes.KeyMode)
def on_mode_left(self, mode):
"""Clear and reset input when the mode was left."""
# FIXME when is this not the case?
if (self._prompt is not None and
mode == self._prompt.KEY_MODE):
question = self._prompt.question
self._show_prompt(None)
# FIXME move this somewhere else?
if question.answer is None and not question.is_aborted:
question.cancel()
@cmdutils.register(instance='prompt-container', hide=True, scope='window',
@cmdutils.register(instance='prompt-queue', hide=True,
modes=[usertypes.KeyMode.prompt], maxsplit=0)
def prompt_open_download(self, cmdline: str=None):
"""Immediately open a download.
@ -265,7 +188,7 @@ class PromptContainer(QWidget):
except UnsupportedOperationError:
pass
@cmdutils.register(instance='prompt-container', hide=True, scope='window',
@cmdutils.register(instance='prompt-queue', hide=True,
modes=[usertypes.KeyMode.prompt])
@cmdutils.argument('which', choices=['next', 'prev'])
def prompt_item_focus(self, which):
@ -323,7 +246,11 @@ class PromptContainer(QWidget):
usertypes.PromptMode.alert: AlertPrompt,
}
klass = classes[question.mode]
self._show_prompt(klass(question, self._win_id))
prompt = klass(question)
self._prompt = prompt
self.show_prompt.emit(prompt)
if blocking:
loop = qtutils.EventLoop()
self._loops.append(loop)
@ -331,8 +258,10 @@ class PromptContainer(QWidget):
question.completed.connect(loop.quit)
question.completed.connect(loop.deleteLater)
loop.exec_()
self._prompt = prompt
# FIXME don't we end up connecting modeman signals twice here now?
if not self._show_prompt(old_prompt):
self.show_prompt.emit(old_prompt)
if old_prompt is None:
# Nothing left to restore, so we can go back to popping async
# questions.
if self._queue:
@ -341,6 +270,104 @@ class PromptContainer(QWidget):
else:
question.completed.connect(self._pop_later)
@pyqtSlot(usertypes.KeyMode)
def on_mode_left(self, mode):
"""Clear and reset input when the mode was left."""
# FIXME when is this not the case?
if (self._prompt is not None and
mode == self._prompt.KEY_MODE):
question = self._prompt.question
self._prompt = None
self.show_prompt.emit(None)
# FIXME move this somewhere else?
if question.answer is None and not question.is_aborted:
question.cancel()
class PromptContainer(QWidget):
"""Container for prompts to be shown above the statusbar.
This is a per-window object, however each window shows the same prompt.
Attributes:
_layout: The layout used to show prompts in.
_win_id: The window ID this object is associated with.
Signals:
update_geometry: Emitted when the geometry should be updated.
"""
STYLESHEET = """
{% set prompt_radius = config.get('ui', 'prompt-radius') %}
QWidget#PromptContainer {
{% if config.get('ui', 'status-position') == 'top' %}
border-bottom-left-radius: {{ prompt_radius }}px;
border-bottom-right-radius: {{ prompt_radius }}px;
{% else %}
border-top-left-radius: {{ prompt_radius }}px;
border-top-right-radius: {{ prompt_radius }}px;
{% endif %}
}
QWidget {
font: {{ font['prompts'] }};
color: {{ color['prompts.fg'] }};
background-color: {{ color['prompts.bg'] }};
}
"""
update_geometry = pyqtSignal()
def __init__(self, win_id, parent=None):
super().__init__(parent)
self._layout = QVBoxLayout(self)
self._layout.setContentsMargins(10, 10, 10, 10)
self._win_id = win_id
self.setObjectName('PromptContainer')
self.setAttribute(Qt.WA_StyledBackground, True)
style.set_register_stylesheet(self)
message.global_bridge.prompt_done.connect(self._on_prompt_done)
prompt_queue.show_prompt.connect(self._on_show_prompt)
def __repr__(self):
return utils.get_repr(self, win_id=self._win_id)
@pyqtSlot(object)
def _on_show_prompt(self, prompt):
"""Show the given prompt object.
Args:
prompt: A Prompt object or None.
"""
# Note that we don't delete the old prompt here, as we might be in the
# middle of saving/restoring an old prompt object.
# FIXME where is it deleted?
self._layout.takeAt(0)
assert self._layout.count() == 0
log.prompt.debug("Displaying prompt {}".format(prompt))
if prompt is None:
self.hide()
return
prompt.question.aborted.connect(
lambda: modeman.maybe_leave(self._win_id, prompt.KEY_MODE,
'aborted'))
modeman.enter(self._win_id, prompt.KEY_MODE, 'question asked')
self.setSizePolicy(prompt.sizePolicy())
self._layout.addWidget(prompt)
prompt.show()
self.show()
prompt.setFocus()
self.update_geometry.emit()
@pyqtSlot(usertypes.KeyMode)
def _on_prompt_done(self, key_mode):
"""Leave the prompt mode in this window if a question was answered."""
modeman.maybe_leave(self._win_id, key_mode, ':prompt-accept')
class LineEdit(QLineEdit):
@ -379,10 +406,9 @@ class _BasePrompt(QWidget):
KEY_MODE = usertypes.KeyMode.prompt
def __init__(self, question, win_id, parent=None):
def __init__(self, question, parent=None):
super().__init__(parent)
self.question = question
self._win_id = win_id
self._vbox = QVBoxLayout(self)
self._vbox.setSpacing(15)
self._key_grid = None
@ -448,8 +474,8 @@ class LineEditPrompt(_BasePrompt):
"""A prompt for a single text value."""
def __init__(self, question, win_id, parent=None):
super().__init__(question, win_id, parent)
def __init__(self, question, parent=None):
super().__init__(question, parent)
self._lineedit = LineEdit(self)
self._init_title(question)
self._vbox.addWidget(self._lineedit)
@ -472,8 +498,8 @@ class FilenamePrompt(_BasePrompt):
"""A prompt for a filename."""
def __init__(self, question, win_id, parent=None):
super().__init__(question, win_id, parent)
def __init__(self, question, parent=None):
super().__init__(question, parent)
self._init_title(question)
self._init_fileview()
self._set_fileview_root(question.default)
@ -584,8 +610,8 @@ class DownloadFilenamePrompt(FilenamePrompt):
"""A prompt for a filename for downloads."""
def __init__(self, question, win_id, parent=None):
super().__init__(question, win_id, parent)
def __init__(self, question, parent=None):
super().__init__(question, parent)
self._file_model.setFilter(QDir.AllDirs | QDir.Drives | QDir.NoDot)
def accept(self, value=None):
@ -595,8 +621,9 @@ class DownloadFilenamePrompt(FilenamePrompt):
def download_open(self, cmdline):
self.question.answer = usertypes.OpenFileDownloadTarget(cmdline)
modeman.maybe_leave(self._win_id, usertypes.KeyMode.prompt,
'download open')
# FIXME now we don't have a window ID here...
# modeman.maybe_leave(self._win_id, usertypes.KeyMode.prompt,
# 'download open')
self.question.done()
def _allowed_commands(self):
@ -612,8 +639,8 @@ class AuthenticationPrompt(_BasePrompt):
"""A prompt for username/password."""
def __init__(self, question, win_id, parent=None):
super().__init__(question, win_id, parent)
def __init__(self, question, parent=None):
super().__init__(question, parent)
self._init_title(question)
user_label = QLabel("Username:", self)
@ -672,8 +699,8 @@ class YesNoPrompt(_BasePrompt):
KEY_MODE = usertypes.KeyMode.yesno
def __init__(self, question, win_id, parent=None):
super().__init__(question, win_id, parent)
def __init__(self, question, parent=None):
super().__init__(question, parent)
self._init_title(question)
self._init_key_label()
@ -709,8 +736,8 @@ class AlertPrompt(_BasePrompt):
"""A prompt without any answer possibility."""
def __init__(self, question, win_id, parent=None):
super().__init__(question, win_id, parent)
def __init__(self, question, parent=None):
super().__init__(question, parent)
self._init_title(question)
self._init_key_label()
@ -722,3 +749,11 @@ class AlertPrompt(_BasePrompt):
def _allowed_commands(self):
return [('prompt-accept', "Hide")]
def init():
global prompt_queue
prompt_queue = PromptQueue()
objreg.register('prompt-queue', prompt_queue) # for commands
message.global_bridge.ask_question.connect(
prompt_queue.ask_question, Qt.DirectConnection)

View File

@ -90,11 +90,10 @@ def _build_question(title, text=None, *, mode, default=None, abort_on=()):
return question
def ask(win_id, *args, **kwargs):
def ask(*args, **kwargs):
"""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.
@ -105,18 +104,16 @@ def ask(win_id, *args, **kwargs):
The answer the user gave or None if the prompt was cancelled.
"""
question = _build_question(*args, **kwargs) # pylint: disable=missing-kwoa
bridge = objreg.get('message-bridge', scope='window', window=win_id)
bridge.ask(question, blocking=True)
global_bridge.ask(question, blocking=True)
answer = question.answer
question.deleteLater()
return answer
def ask_async(win_id, text, mode, handler, **kwargs):
def ask_async(text, mode, handler, **kwargs):
"""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.
@ -126,16 +123,14 @@ def ask_async(win_id, text, mode, handler, **kwargs):
question = _build_question(text, mode=mode, **kwargs)
question.answered.connect(handler)
question.completed.connect(question.deleteLater)
bridge = objreg.get('message-bridge', scope='window', window=win_id)
bridge.ask(question, blocking=False)
global_bridge.ask(question, blocking=False)
def confirm_async(win_id, yes_action, no_action=None, cancel_action=None,
def confirm_async(yes_action, no_action=None, cancel_action=None,
*args, **kwargs):
"""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.
@ -156,8 +151,7 @@ def confirm_async(win_id, yes_action, no_action=None, cancel_action=None,
question.cancelled.connect(cancel_action)
question.completed.connect(question.deleteLater)
bridge = objreg.get('message-bridge', scope='window', window=win_id)
bridge.ask(question, blocking=False)
global_bridge.ask(question, blocking=False)
return question
@ -169,9 +163,32 @@ class GlobalMessageBridge(QObject):
show_message: Show a message
arg 0: A MessageLevel member
arg 1: The text to show
prompt_done: Emitted when a prompt was answered somewhere.
ask_question: Ask a question to the user.
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!
"""
show_message = pyqtSignal(usertypes.MessageLevel, str)
prompt_done = pyqtSignal(usertypes.KeyMode)
ask_question = pyqtSignal(usertypes.Question, bool)
def ask(self, question, blocking, *, log_stack=False):
"""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.
log_stack: ignored
"""
self.ask_question.emit(question, blocking)
class MessageBridge(QObject):
@ -183,18 +200,10 @@ class MessageBridge(QObject):
arg: The text to set.
s_maybe_reset_text: Reset the text if it hasn't been changed yet.
arg: The expected text.
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_set_text = pyqtSignal(str)
s_maybe_reset_text = pyqtSignal(str)
s_question = pyqtSignal(usertypes.Question, bool)
def __repr__(self):
return utils.get_repr(self)
@ -219,19 +228,5 @@ class MessageBridge(QObject):
"""
self.s_maybe_reset_text.emit(str(text))
def ask(self, question, blocking, *, log_stack=False):
"""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.
log_stack: ignored
"""
self.s_question.emit(question, blocking)
global_bridge = GlobalMessageBridge()