First working prototype of global prompts
This commit is contained in:
parent
fdd1147620
commit
dba29e518a
@ -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])
|
||||||
|
@ -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,
|
||||||
|
@ -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):
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user