First working prototype of global prompts

This commit is contained in:
Florian Bruhin 2016-10-27 22:54:51 +02:00
parent fdd1147620
commit dba29e518a
3 changed files with 121 additions and 123 deletions

View File

@ -155,9 +155,7 @@ def ask_for_filename(suggested_filename, win_id, *, parent=None,
q.completed.connect(q.deleteLater) q.completed.connect(q.deleteLater)
q.default = _path_suggestion(suggested_filename) q.default = _path_suggestion(suggested_filename)
message_bridge = objreg.get('message-bridge', scope='window', q.ask = lambda: message.global_bridge.ask(q, blocking=False)
window=win_id)
q.ask = lambda: message_bridge.ask(q, blocking=False)
return _DownloadPath(filename=None, question=q) return _DownloadPath(filename=None, question=q)
@ -385,7 +383,7 @@ class DownloadItem(QObject):
def _ask_confirm_question(self, title, msg): def _ask_confirm_question(self, title, msg):
"""Create a Question object to be asked.""" """Create a Question object to be asked."""
no_action = functools.partial(self.cancel, remove_data=False) no_action = functools.partial(self.cancel, remove_data=False)
message.confirm_async(self._win_id, title=title, text=msg, message.confirm_async(title=title, text=msg,
yes_action=self._create_fileobj, yes_action=self._create_fileobj,
no_action=no_action, cancel_action=no_action, no_action=no_action, cancel_action=no_action,
abort_on=[self.cancelled, self.error]) abort_on=[self.cancelled, self.error])

View File

@ -332,8 +332,7 @@ class BrowserPage(QWebPage):
self.setFeaturePermission, frame, feature, self.setFeaturePermission, frame, feature,
QWebPage.PermissionDeniedByUser) QWebPage.PermissionDeniedByUser)
question = message.confirm_async(self._win_id, question = message.confirm_async(yes_action=yes_action,
yes_action=yes_action,
no_action=no_action, no_action=no_action,
cancel_action=no_action, cancel_action=no_action,
abort_on=[self.shutting_down, abort_on=[self.shutting_down,

View File

@ -61,8 +61,8 @@ class PromptQueue(QObject):
If a question is blocking, we *need* to ask it immediately, and can't wait If a question is blocking, we *need* to ask it immediately, and can't wait
for previous questions to finish. We could theoretically ask a blocking for previous questions to finish. We could theoretically ask a blocking
question inside of another blocking one, so in ask_question we simply save question inside of another blocking one, so in ask_question we simply save
the current prompt state on the stack, let the user answer the *most the current question on the stack, let the user answer the *most recent*
recent* question, and then restore the previous state. question, and then restore the previous state.
With a non-blocking question, things are a bit easier. We simply add it to With a non-blocking question, things are a bit easier. We simply add it to
self._queue if we're still busy handling another question, since it can be self._queue if we're still busy handling another question, since it can be
@ -79,24 +79,24 @@ class PromptQueue(QObject):
should ignore future questions to avoid segfaults. should ignore future questions to avoid segfaults.
_loops: A list of local EventLoops to spin in when blocking. _loops: A list of local EventLoops to spin in when blocking.
_queue: A deque of waiting questions. _queue: A deque of waiting questions.
_prompt: The current prompt object if we're handling a question. _question: The current Question object if we're handling a question.
Signals: Signals:
show_prompt: Emitted when a prompt should be shown. show_prompts: Emitted with a Question object when prompts should be shown.
""" """
show_prompt = pyqtSignal(object) show_prompts = pyqtSignal(usertypes.Question)
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self._prompt = None self._question = None
self._shutting_down = False self._shutting_down = False
self._loops = [] self._loops = []
self._queue = collections.deque() self._queue = collections.deque()
def __repr__(self): def __repr__(self):
return utils.get_repr(self, loops=len(self._loops), return utils.get_repr(self, loops=len(self._loops),
queue=len(self._queue), prompt=self._prompt) queue=len(self._queue), question=self._question)
def _pop_later(self): def _pop_later(self):
"""Helper to call self._pop as soon as everything else is done.""" """Helper to call self._pop as soon as everything else is done."""
@ -131,77 +131,6 @@ class PromptQueue(QObject):
else: else:
return False return False
@cmdutils.register(instance='prompt-queue', hide=True,
modes=[usertypes.KeyMode.prompt,
usertypes.KeyMode.yesno])
def prompt_accept(self, value=None):
"""Accept the current prompt.
//
This executes the next action depending on the question mode, e.g. asks
for the password or leaves the mode.
Args:
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:
message.global_bridge.prompt_done.emit(self._prompt.KEY_MODE)
question.done()
@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-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')
@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.
If no specific command is given, this will use the system's default
application to open the file.
Args:
cmdline: The command which should be used to open the file. A `{}`
is expanded to the temporary file name. If no `{}` is
present, the filename is automatically appended to the
cmdline.
"""
try:
self._prompt.download_open(cmdline)
except UnsupportedOperationError:
pass
@cmdutils.register(instance='prompt-queue', hide=True,
modes=[usertypes.KeyMode.prompt])
@cmdutils.argument('which', choices=['next', 'prev'])
def prompt_item_focus(self, which):
"""Shift the focus of the prompt file completion menu to another item.
Args:
which: 'next', 'prev'
"""
try:
self._prompt.item_focus(which)
except UnsupportedOperationError:
pass
@pyqtSlot(usertypes.Question, bool) @pyqtSlot(usertypes.Question, bool)
def ask_question(self, question, blocking): def ask_question(self, question, blocking):
"""Display a prompt for a given question. """Display a prompt for a given question.
@ -226,7 +155,7 @@ class PromptQueue(QObject):
question.abort() question.abort()
return None return None
if self._prompt is not None and not blocking: if self._question is not None and not blocking:
# We got an async question, but we're already busy with one, so we # We got an async question, but we're already busy with one, so we
# just queue it up for later. # just queue it up for later.
log.prompt.debug("Adding {} to queue.".format(question)) log.prompt.debug("Adding {} to queue.".format(question))
@ -234,22 +163,12 @@ class PromptQueue(QObject):
return return
if blocking: if blocking:
# If we're blocking we save the old state on the stack, so we can # If we're blocking we save the old question on the stack, so we
# restore it after exec, if exec gets called multiple times. # can restore it after exec, if exec gets called multiple times.
old_prompt = self._prompt old_question = self._question
classes = { self._question = question
usertypes.PromptMode.yesno: YesNoPrompt, self.show_prompts.emit(question)
usertypes.PromptMode.text: LineEditPrompt,
usertypes.PromptMode.user_pwd: AuthenticationPrompt,
usertypes.PromptMode.download: DownloadFilenamePrompt,
usertypes.PromptMode.alert: AlertPrompt,
}
klass = classes[question.mode]
prompt = klass(question)
self._prompt = prompt
self.show_prompt.emit(prompt)
if blocking: if blocking:
loop = qtutils.EventLoop() loop = qtutils.EventLoop()
@ -258,10 +177,9 @@ class PromptQueue(QObject):
question.completed.connect(loop.quit) question.completed.connect(loop.quit)
question.completed.connect(loop.deleteLater) question.completed.connect(loop.deleteLater)
loop.exec_() loop.exec_()
self._prompt = prompt
# FIXME don't we end up connecting modeman signals twice here now? # FIXME don't we end up connecting modeman signals twice here now?
self.show_prompt.emit(old_prompt) self.show_prompts.emit(old_question)
if old_prompt is None: if old_question is None:
# Nothing left to restore, so we can go back to popping async # Nothing left to restore, so we can go back to popping async
# questions. # questions.
if self._queue: if self._queue:
@ -273,15 +191,12 @@ class PromptQueue(QObject):
@pyqtSlot(usertypes.KeyMode) @pyqtSlot(usertypes.KeyMode)
def on_mode_left(self, mode): def on_mode_left(self, mode):
"""Clear and reset input when the mode was left.""" """Clear and reset input when the mode was left."""
# FIXME when is this not the case? if self._question is not None:
if (self._prompt is not None and self.show_prompts.emit(None)
mode == self._prompt.KEY_MODE):
question = self._prompt.question
self._prompt = None
self.show_prompt.emit(None)
# FIXME move this somewhere else? # FIXME move this somewhere else?
if question.answer is None and not question.is_aborted: if self._question.answer is None and not self._question.is_aborted:
question.cancel() self._question.cancel()
self._question = None
class PromptContainer(QWidget): class PromptContainer(QWidget):
@ -323,35 +238,49 @@ class PromptContainer(QWidget):
self._layout = QVBoxLayout(self) self._layout = QVBoxLayout(self)
self._layout.setContentsMargins(10, 10, 10, 10) self._layout.setContentsMargins(10, 10, 10, 10)
self._win_id = win_id self._win_id = win_id
self._prompt = None
self.setObjectName('PromptContainer') self.setObjectName('PromptContainer')
self.setAttribute(Qt.WA_StyledBackground, True) self.setAttribute(Qt.WA_StyledBackground, True)
style.set_register_stylesheet(self) style.set_register_stylesheet(self)
message.global_bridge.prompt_done.connect(self._on_prompt_done) message.global_bridge.prompt_done.connect(self._on_prompt_done)
prompt_queue.show_prompt.connect(self._on_show_prompt) prompt_queue.show_prompts.connect(self._on_show_prompts)
def __repr__(self): def __repr__(self):
return utils.get_repr(self, win_id=self._win_id) return utils.get_repr(self, win_id=self._win_id)
@pyqtSlot(object) @pyqtSlot(usertypes.Question)
def _on_show_prompt(self, prompt): def _on_show_prompts(self, question):
"""Show the given prompt object. """Show a prompt for the given question.
Args: Args:
prompt: A Prompt object or None. question: A Question object or None.
""" """
# Note that we don't delete the old prompt here, as we might be in the item = self._layout.takeAt(0)
# middle of saving/restoring an old prompt object.
# FIXME where is it deleted?
self._layout.takeAt(0)
assert self._layout.count() == 0 assert self._layout.count() == 0
log.prompt.debug("Displaying prompt {}".format(prompt)) if item is not None:
if prompt is None: item.widget().deleteLater()
if question is None:
self._prompt = None
self.hide() self.hide()
return return
prompt.question.aborted.connect( classes = {
usertypes.PromptMode.yesno: YesNoPrompt,
usertypes.PromptMode.text: LineEditPrompt,
usertypes.PromptMode.user_pwd: AuthenticationPrompt,
usertypes.PromptMode.download: DownloadFilenamePrompt,
usertypes.PromptMode.alert: AlertPrompt,
}
klass = classes[question.mode]
prompt = klass(question)
log.prompt.debug("Displaying prompt {}".format(prompt))
self._prompt = prompt
question.aborted.connect(
lambda: modeman.maybe_leave(self._win_id, prompt.KEY_MODE, lambda: modeman.maybe_leave(self._win_id, prompt.KEY_MODE,
'aborted')) 'aborted'))
modeman.enter(self._win_id, prompt.KEY_MODE, 'question asked') modeman.enter(self._win_id, prompt.KEY_MODE, 'question asked')
@ -368,6 +297,78 @@ class PromptContainer(QWidget):
"""Leave the prompt mode in this window if a question was answered.""" """Leave the prompt mode in this window if a question was answered."""
modeman.maybe_leave(self._win_id, key_mode, ':prompt-accept') modeman.maybe_leave(self._win_id, key_mode, ':prompt-accept')
@cmdutils.register(instance='prompt-container', hide=True, scope='window',
modes=[usertypes.KeyMode.prompt,
usertypes.KeyMode.yesno])
def prompt_accept(self, value=None):
"""Accept the current prompt.
//
This executes the next action depending on the question mode, e.g. asks
for the password or leaves the mode.
Args:
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:
message.global_bridge.prompt_done.emit(self._prompt.KEY_MODE)
question.done()
@cmdutils.register(instance='prompt-container', hide=True, scope='window',
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',
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')
@cmdutils.register(instance='prompt-container', hide=True, scope='window',
modes=[usertypes.KeyMode.prompt], maxsplit=0)
def prompt_open_download(self, cmdline: str=None):
"""Immediately open a download.
If no specific command is given, this will use the system's default
application to open the file.
Args:
cmdline: The command which should be used to open the file. A `{}`
is expanded to the temporary file name. If no `{}` is
present, the filename is automatically appended to the
cmdline.
"""
try:
self._prompt.download_open(cmdline)
except UnsupportedOperationError:
pass
@cmdutils.register(instance='prompt-container', hide=True, scope='window',
modes=[usertypes.KeyMode.prompt])
@cmdutils.argument('which', choices=['next', 'prev'])
def prompt_item_focus(self, which):
"""Shift the focus of the prompt file completion menu to another item.
Args:
which: 'next', 'prev'
"""
try:
self._prompt.item_focus(which)
except UnsupportedOperationError:
pass
class LineEdit(QLineEdit): class LineEdit(QLineEdit):