half-working auth prompts
This commit is contained in:
parent
903e31efa4
commit
33088588d9
@ -645,9 +645,9 @@ class Quitter:
|
||||
|
||||
deferrer = False
|
||||
for win_id in objreg.window_registry:
|
||||
prompter = objreg.get('prompter', None, scope='window',
|
||||
window=win_id)
|
||||
if prompter is not None and prompter.shutdown():
|
||||
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 deferrer:
|
||||
# If shutdown was called while we were asking a question, we're in
|
||||
|
@ -182,11 +182,14 @@ class MainWindow(QWidget):
|
||||
self._add_overlay(self._keyhint, self._keyhint.update_geometry)
|
||||
self._messageview = messageview.MessageView(parent=self)
|
||||
self._add_overlay(self._messageview, self._messageview.update_geometry)
|
||||
self._promptcontainer = prompt.PromptContainer(self)
|
||||
self._add_overlay(self._promptcontainer,
|
||||
self._promptcontainer.update_geometry,
|
||||
|
||||
self._prompt_container = prompt.PromptContainer(self.win_id, self)
|
||||
self._add_overlay(self._prompt_container,
|
||||
self._prompt_container.update_geometry,
|
||||
centered=True)
|
||||
self._promptcontainer.hide()
|
||||
objreg.register('prompt-container', self._prompt_container,
|
||||
scope='window', window=self.win_id)
|
||||
self._prompt_container.hide()
|
||||
|
||||
log.init.debug("Initializing modes...")
|
||||
modeman.init(self.win_id, self)
|
||||
@ -385,7 +388,6 @@ class MainWindow(QWidget):
|
||||
cmd = self._get_object('status-command')
|
||||
message_bridge = self._get_object('message-bridge')
|
||||
mode_manager = self._get_object('mode-manager')
|
||||
#prompter = self._get_object('prompter')
|
||||
|
||||
# misc
|
||||
self.tabbed_browser.close_window.connect(self.close)
|
||||
@ -395,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(prompter.on_mode_left)
|
||||
mode_manager.left.connect(self._prompt_container.on_mode_left)
|
||||
|
||||
# commands
|
||||
keyparsers[usertypes.KeyMode.normal].keystring_updated.connect(
|
||||
@ -419,8 +421,8 @@ 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_set_cmd_text.connect(cmd.set_cmd_text)
|
||||
#message_bridge.s_question.connect(prompter.ask_question,
|
||||
# Qt.DirectConnection)
|
||||
message_bridge.s_question.connect(self._prompt_container.ask_question,
|
||||
Qt.DirectConnection)
|
||||
|
||||
# statusbar
|
||||
tabs.current_tab_changed.connect(status.prog.on_tab_changed)
|
||||
|
@ -19,13 +19,17 @@
|
||||
|
||||
"""Showing prompts above the statusbar."""
|
||||
|
||||
import sip
|
||||
import collections
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, Qt
|
||||
from PyQt5.QtWidgets import QWidget, QGridLayout, QVBoxLayout, QLineEdit, QLabel, QSpacerItem
|
||||
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QTimer
|
||||
from PyQt5.QtWidgets import (QWidget, QGridLayout, QVBoxLayout, QLineEdit,
|
||||
QLabel, QSpacerItem, QWidgetItem)
|
||||
|
||||
from qutebrowser.config import style, config
|
||||
from qutebrowser.utils import usertypes
|
||||
from qutebrowser.utils import usertypes, log, utils, qtutils
|
||||
from qutebrowser.keyinput import modeman
|
||||
from qutebrowser.commands import cmdutils
|
||||
|
||||
|
||||
AuthTuple = collections.namedtuple('AuthTuple', ['user', 'password'])
|
||||
@ -36,19 +40,65 @@ class Error(Exception):
|
||||
"""Base class for errors in this module."""
|
||||
|
||||
|
||||
class UnsupportedOperationError(Exception):
|
||||
|
||||
"""Raised when the prompt class doesn't support the requested operation."""
|
||||
|
||||
|
||||
class PromptContainer(QWidget):
|
||||
|
||||
"""Container for prompts to be shown above the statusbar.
|
||||
|
||||
The way in which multiple questions are handled deserves some explanation.
|
||||
|
||||
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
|
||||
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
|
||||
recent* question, and then restore the previous state.
|
||||
|
||||
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
|
||||
answered at any time.
|
||||
|
||||
In either case, as soon as we finished handling a question, we call
|
||||
_pop_later() which schedules a _pop to ask the next question in _queue. We
|
||||
schedule it rather than doing it immediately because then the order of how
|
||||
things happen is clear, e.g. on_mode_left can't happen after we already set
|
||||
up the *new* question.
|
||||
|
||||
Attributes:
|
||||
_shutting_down: Whether we're currently shutting down the prompter and
|
||||
should ignore future questions to avoid segfaults.
|
||||
_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.
|
||||
"""
|
||||
|
||||
update_geometry = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setObjectName('Prompt')
|
||||
self.setAttribute(Qt.WA_StyledBackground, True)
|
||||
self._layout = QVBoxLayout(self)
|
||||
self._layout.setContentsMargins(10, 10, 10, 10)
|
||||
self._prompt = None
|
||||
style.set_register_stylesheet(self,
|
||||
generator=self._generate_stylesheet)
|
||||
|
||||
# FIXME review this
|
||||
self._shutting_down = False
|
||||
self._loops = []
|
||||
self._queue = collections.deque()
|
||||
self._win_id = win_id
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, loops=len(self._loops),
|
||||
queue=len(self._queue), prompt=self._prompt)
|
||||
|
||||
def _generate_stylesheet(self):
|
||||
"""Generate a stylesheet with the right edge rounded."""
|
||||
stylesheet = """
|
||||
@ -78,28 +128,228 @@ class PromptContainer(QWidget):
|
||||
else:
|
||||
raise ValueError("Invalid position {}!".format(position))
|
||||
|
||||
def _show_prompt(self, prompt):
|
||||
while True:
|
||||
# FIXME do we really want to delete children?
|
||||
child = self._layout.takeAt(0)
|
||||
if child is None:
|
||||
break
|
||||
child.deleteLater()
|
||||
def _pop_later(self):
|
||||
"""Helper to call self._pop as soon as everything else is done."""
|
||||
QTimer.singleShot(0, self._pop)
|
||||
|
||||
self._layout.addWidget(prompt)
|
||||
def _pop(self):
|
||||
"""Pop a question from the queue and ask it, if there are any."""
|
||||
log.prompt.debug("Popping from queue {}".format(self._queue))
|
||||
if self._queue:
|
||||
question = self._queue.popleft()
|
||||
if not sip.isdeleted(question):
|
||||
# the question could already be deleted, e.g. by a cancelled
|
||||
# download. See
|
||||
# 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._layout.addWidget(self._prompt)
|
||||
self._prompt.show()
|
||||
self.show()
|
||||
self._prompt.setFocus()
|
||||
self.setFocus()
|
||||
self.update_geometry.emit()
|
||||
return True
|
||||
|
||||
def shutdown(self):
|
||||
"""Cancel all blocking questions.
|
||||
|
||||
Quits and removes all running event loops.
|
||||
|
||||
Return:
|
||||
True if loops needed to be aborted,
|
||||
False otherwise.
|
||||
"""
|
||||
self._shutting_down = True
|
||||
if self._loops:
|
||||
for loop in self._loops:
|
||||
loop.quit()
|
||||
loop.deleteLater()
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
@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.
|
||||
"""
|
||||
done = self._prompt.accept(value)
|
||||
if done:
|
||||
self._prompt.question.done()
|
||||
modeman.maybe_leave(self._win_id, self._prompt.KEY_MODE,
|
||||
':prompt-accept')
|
||||
|
||||
@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')
|
||||
|
||||
@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='prompter', 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
|
||||
|
||||
@pyqtSlot(usertypes.Question, bool)
|
||||
def ask_question(self, question, blocking):
|
||||
"""Display a prompt for a given question.
|
||||
|
||||
Args:
|
||||
question: The Question object to ask.
|
||||
blocking: If True, this function blocks and returns the result.
|
||||
|
||||
Return:
|
||||
The answer of the user when blocking=True.
|
||||
None if blocking=False.
|
||||
"""
|
||||
log.prompt.debug("Asking question {}, blocking {}, loops {}, queue "
|
||||
"{}".format(question, blocking, self._loops,
|
||||
self._queue))
|
||||
|
||||
if self._shutting_down:
|
||||
# If we're currently shutting down we have to ignore this question
|
||||
# to avoid segfaults - see
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/95
|
||||
log.prompt.debug("Ignoring question because we're shutting down.")
|
||||
question.abort()
|
||||
return None
|
||||
|
||||
if self._prompt is not None and not blocking:
|
||||
# We got an async question, but we're already busy with one, so we
|
||||
# just queue it up for later.
|
||||
log.prompt.debug("Adding {} to queue.".format(question))
|
||||
self._queue.append(question)
|
||||
return
|
||||
|
||||
if blocking:
|
||||
# If we're blocking we save the old state on the stack, so we can
|
||||
# restore it after exec, if exec gets called multiple times.
|
||||
old_prompt = self._prompt
|
||||
|
||||
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]
|
||||
self._show_prompt(klass(question))
|
||||
if blocking:
|
||||
loop = qtutils.EventLoop()
|
||||
self._loops.append(loop)
|
||||
loop.destroyed.connect(lambda: self._loops.remove(loop))
|
||||
question.completed.connect(loop.quit)
|
||||
question.completed.connect(loop.deleteLater)
|
||||
loop.exec_()
|
||||
# FIXME don't we end up connecting modeman signals twice here now?
|
||||
if not self._show_prompt(old_prompt):
|
||||
# Nothing left to restore, so we can go back to popping async
|
||||
# questions.
|
||||
if self._queue:
|
||||
self._pop_later()
|
||||
return question.answer
|
||||
else:
|
||||
question.completed.connect(self._pop_later)
|
||||
|
||||
|
||||
class _BasePrompt(QWidget):
|
||||
|
||||
"""Base class for all prompts."""
|
||||
|
||||
KEY_MODE = usertypes.KeyMode.prompt
|
||||
|
||||
def __init__(self, question, parent=None):
|
||||
super().__init__(parent)
|
||||
self._question = question
|
||||
self.question = question
|
||||
self._layout = QGridLayout(self)
|
||||
self._layout.setVerticalSpacing(15)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, question=self.question, constructor=True)
|
||||
|
||||
def _init_title(self, title, *, span=1):
|
||||
label = QLabel('<b>{}</b>'.format(title), self)
|
||||
self._layout.addWidget(label, 0, 0, 1, span)
|
||||
@ -107,6 +357,9 @@ class _BasePrompt(QWidget):
|
||||
def accept(self, value=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def open_download(self, _cmdline):
|
||||
raise UnsupportedOperationError
|
||||
|
||||
|
||||
class LineEditPrompt(_BasePrompt):
|
||||
|
||||
@ -120,7 +373,8 @@ class LineEditPrompt(_BasePrompt):
|
||||
|
||||
def accept(self, value=None):
|
||||
text = value if value is not None else self._lineedit.text()
|
||||
self._question.answer = text
|
||||
self.question.answer = text
|
||||
return True
|
||||
|
||||
|
||||
class DownloadFilenamePrompt(LineEditPrompt):
|
||||
@ -130,7 +384,7 @@ class DownloadFilenamePrompt(LineEditPrompt):
|
||||
def __init__(self, question, parent=None):
|
||||
super().__init__(question, parent)
|
||||
# FIXME show :prompt-open-download keybinding
|
||||
# key_mode = self.KEY_MODES[self._question.mode]
|
||||
# key_mode = self.KEY_MODES[self.question.mode]
|
||||
# key_config = objreg.get('key-config')
|
||||
# all_bindings = key_config.get_reverse_bindings_for(key_mode.name)
|
||||
# bindings = all_bindings.get('prompt-open-download', [])
|
||||
@ -140,7 +394,14 @@ class DownloadFilenamePrompt(LineEditPrompt):
|
||||
|
||||
def accept(self, value=None):
|
||||
text = value if value is not None else self._lineedit.text()
|
||||
self._question.answer = usertypes.FileDownloadTarget(text)
|
||||
self.question.answer = usertypes.FileDownloadTarget(text)
|
||||
return True
|
||||
|
||||
def download_open(self, cmdline):
|
||||
self.question.answer = usertypes.OpenFileDownloadTarget(cmdline)
|
||||
modeman.maybe_leave(self._win_id, usertypes.KeyMode.prompt,
|
||||
'download open')
|
||||
self.question.done()
|
||||
|
||||
|
||||
class AuthenticationPrompt(_BasePrompt):
|
||||
@ -167,6 +428,9 @@ class AuthenticationPrompt(_BasePrompt):
|
||||
self._layout.addWidget(help_1, 4, 0)
|
||||
self._layout.addWidget(help_2, 5, 0)
|
||||
|
||||
# FIXME needed?
|
||||
self._user_lineedit.setFocus()
|
||||
|
||||
def accept(self, value=None):
|
||||
if value is not None:
|
||||
if ':' not in value:
|
||||
@ -174,14 +438,23 @@ class AuthenticationPrompt(_BasePrompt):
|
||||
"username:password, but {} was given".format(
|
||||
value))
|
||||
username, password = value.split(':', maxsplit=1)
|
||||
self._question.answer = AuthTuple(username, password)
|
||||
self.question.answer = AuthTuple(username, password)
|
||||
return True
|
||||
elif self._user_lineedit.hasFocus():
|
||||
# Earlier, tab was bound to :prompt-accept, so to still support that
|
||||
# we simply switch the focus when tab was pressed.
|
||||
self._password_lineedit.setFocus()
|
||||
return False
|
||||
else:
|
||||
self._question.answer = AuthTuple(self._user_lineedit.text(),
|
||||
self._password_lineedit.text())
|
||||
self.question.answer = AuthTuple(self._user_lineedit.text(),
|
||||
self._password_lineedit.text())
|
||||
return True
|
||||
|
||||
|
||||
class YesNoPrompt(_BasePrompt):
|
||||
|
||||
KEY_MODE = usertypes.KeyMode.yesno
|
||||
|
||||
def __init__(self, question, parent=None):
|
||||
super().__init__(question, parent)
|
||||
self._init_title(question.text)
|
||||
@ -192,13 +465,14 @@ class YesNoPrompt(_BasePrompt):
|
||||
|
||||
def accept(self, value=None):
|
||||
if value is None:
|
||||
self._question.answer = self._question.default
|
||||
self.question.answer = self.question.default
|
||||
elif value == 'yes':
|
||||
self._question.answer = True
|
||||
self.question.answer = True
|
||||
elif value == 'no':
|
||||
self._question.answer = False
|
||||
self.question.answer = False
|
||||
else:
|
||||
raise Error("Invalid value {} - expected yes/no!".format(value))
|
||||
return True
|
||||
|
||||
|
||||
class AlertPrompt(_BasePrompt):
|
||||
@ -213,3 +487,4 @@ class AlertPrompt(_BasePrompt):
|
||||
if value is not None:
|
||||
raise Error("No value is permitted with alert prompts!")
|
||||
# Doing nothing otherwise
|
||||
return True
|
||||
|
@ -25,8 +25,7 @@ from PyQt5.QtWidgets import QWidget, QHBoxLayout, QStackedLayout, QSizePolicy
|
||||
from qutebrowser.config import config, style
|
||||
from qutebrowser.utils import usertypes, log, objreg, utils
|
||||
from qutebrowser.mainwindow.statusbar import (command, progress, keystring,
|
||||
percentage, url, prompt,
|
||||
tabindex)
|
||||
percentage, url, tabindex)
|
||||
from qutebrowser.mainwindow.statusbar import text as textwidget
|
||||
|
||||
|
||||
|
@ -1,84 +0,0 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014-2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Prompt shown in the statusbar."""
|
||||
|
||||
import functools
|
||||
|
||||
from PyQt5.QtCore import QSize
|
||||
from PyQt5.QtWidgets import QHBoxLayout, QWidget, QLineEdit, QSizePolicy
|
||||
|
||||
from qutebrowser.mainwindow.statusbar import textbase, prompter
|
||||
from qutebrowser.utils import objreg, utils
|
||||
from qutebrowser.misc import miscwidgets as misc
|
||||
|
||||
|
||||
class PromptLineEdit(misc.MinimalLineEditMixin, QLineEdit):
|
||||
|
||||
"""QLineEdit with a minimal stylesheet."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QLineEdit.__init__(self, parent)
|
||||
misc.MinimalLineEditMixin.__init__(self)
|
||||
self.textChanged.connect(self.updateGeometry)
|
||||
|
||||
def sizeHint(self):
|
||||
"""Dynamically calculate the needed size."""
|
||||
height = super().sizeHint().height()
|
||||
text = self.text()
|
||||
if not text:
|
||||
text = 'x'
|
||||
width = self.fontMetrics().width(text)
|
||||
return QSize(width, height)
|
||||
|
||||
|
||||
class Prompt(QWidget):
|
||||
|
||||
"""The prompt widget shown in the statusbar.
|
||||
|
||||
Attributes:
|
||||
txt: The TextBase instance (QLabel) used to display the prompt text.
|
||||
lineedit: The MinimalLineEdit instance (QLineEdit) used for the input.
|
||||
_hbox: The QHBoxLayout used to display the text and prompt.
|
||||
"""
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
objreg.register('prompt', self, scope='window', window=win_id)
|
||||
self._hbox = QHBoxLayout(self)
|
||||
self._hbox.setContentsMargins(0, 0, 0, 0)
|
||||
self._hbox.setSpacing(5)
|
||||
|
||||
self.txt = textbase.TextBase()
|
||||
self._hbox.addWidget(self.txt)
|
||||
|
||||
self.lineedit = PromptLineEdit()
|
||||
self.lineedit.setSizePolicy(QSizePolicy.MinimumExpanding,
|
||||
QSizePolicy.Fixed)
|
||||
self._hbox.addWidget(self.lineedit)
|
||||
|
||||
prompter_obj = prompter.Prompter(win_id)
|
||||
objreg.register('prompter', prompter_obj, scope='window',
|
||||
window=win_id)
|
||||
self.destroyed.connect(
|
||||
functools.partial(objreg.delete, 'prompter', scope='window',
|
||||
window=win_id))
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self)
|
@ -1,293 +0,0 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014-2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Manager for questions to be shown in the statusbar."""
|
||||
|
||||
import sip
|
||||
import collections
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer, QObject
|
||||
from PyQt5.QtWidgets import QLineEdit
|
||||
|
||||
from qutebrowser.keyinput import modeman
|
||||
from qutebrowser.commands import cmdutils, cmdexc
|
||||
from qutebrowser.utils import usertypes, log, qtutils, objreg, utils
|
||||
|
||||
|
||||
class Prompter(QObject):
|
||||
|
||||
"""Manager for questions to be shown in the statusbar.
|
||||
|
||||
The way in which multiple questions are handled deserves some explanation.
|
||||
|
||||
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
|
||||
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
|
||||
recent* question, and then restore the previous state.
|
||||
|
||||
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
|
||||
answered at any time.
|
||||
|
||||
In either case, as soon as we finished handling a question, we call
|
||||
_pop_later() which schedules a _pop to ask the next question in _queue. We
|
||||
schedule it rather than doing it immediately because then the order of how
|
||||
things happen is clear, e.g. on_mode_left can't happen after we already set
|
||||
up the *new* question.
|
||||
|
||||
Class Attributes:
|
||||
KEY_MODES: A mapping of PromptModes to KeyModes.
|
||||
|
||||
Attributes:
|
||||
_shutting_down: Whether we're currently shutting down the prompter and
|
||||
should ignore future questions to avoid segfaults.
|
||||
_question: A Question object with the question to be asked to the user.
|
||||
_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.
|
||||
_win_id: The window ID this object is associated with.
|
||||
"""
|
||||
|
||||
KEY_MODES = {
|
||||
usertypes.PromptMode.yesno: usertypes.KeyMode.yesno,
|
||||
usertypes.PromptMode.text: usertypes.KeyMode.prompt,
|
||||
usertypes.PromptMode.user_pwd: usertypes.KeyMode.prompt,
|
||||
usertypes.PromptMode.alert: usertypes.KeyMode.prompt,
|
||||
usertypes.PromptMode.download: usertypes.KeyMode.prompt,
|
||||
}
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
self._shutting_down = False
|
||||
self._question = None
|
||||
self._loops = []
|
||||
self._queue = collections.deque()
|
||||
self._prompt = None
|
||||
self._win_id = win_id
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, loops=len(self._loops),
|
||||
question=self._question, queue=len(self._queue),
|
||||
prompt=self._prompt)
|
||||
|
||||
def _pop_later(self):
|
||||
"""Helper to call self._pop as soon as everything else is done."""
|
||||
QTimer.singleShot(0, self._pop)
|
||||
|
||||
def _pop(self):
|
||||
"""Pop a question from the queue and ask it, if there are any."""
|
||||
log.statusbar.debug("Popping from queue {}".format(self._queue))
|
||||
if self._queue:
|
||||
question = self._queue.popleft()
|
||||
if not sip.isdeleted(question):
|
||||
# the question could already be deleted, e.g. by a cancelled
|
||||
# download. See
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/415
|
||||
self.ask_question(question, blocking=False)
|
||||
|
||||
def _restore_prompt(self, prompt):
|
||||
"""Restore an old prompt which was interrupted.
|
||||
|
||||
Args:
|
||||
prompt: A Prompt object or None.
|
||||
|
||||
Return: True if a prompt was restored, False otherwise.
|
||||
"""
|
||||
log.statusbar.debug("Restoring prompt {}".format(prompt))
|
||||
if prompt is None:
|
||||
self._prompt.hide() # FIXME
|
||||
self._prompt = None
|
||||
return False
|
||||
self._question = ctx.question
|
||||
self._prompt = prompt
|
||||
# FIXME do promptcintainer stuff here??
|
||||
prompt.show()
|
||||
mode = self.KEY_MODES[ctx.question.mode]
|
||||
ctx.question.aborted.connect(
|
||||
lambda: modeman.maybe_leave(self._win_id, mode, 'aborted'))
|
||||
modeman.enter(self._win_id, mode, 'question asked')
|
||||
return True
|
||||
|
||||
def _display_question(self):
|
||||
"""Display the question saved in self._question."""
|
||||
handlers = {
|
||||
usertypes.PromptMode.yesno: self._display_question_yesno,
|
||||
usertypes.PromptMode.text: self._display_question_input,
|
||||
usertypes.PromptMode.user_pwd: self._display_question_input,
|
||||
usertypes.PromptMode.download: self._display_question_input,
|
||||
usertypes.PromptMode.alert: self._display_question_alert,
|
||||
}
|
||||
handler = handlers[self._question.mode]
|
||||
handler(prompt)
|
||||
log.modes.debug("Question asked, focusing {!r}".format(
|
||||
prompt.lineedit))
|
||||
prompt.lineedit.setFocus()
|
||||
prompt.show()
|
||||
# FIXME
|
||||
self._prompt = prompt
|
||||
|
||||
def shutdown(self):
|
||||
"""Cancel all blocking questions.
|
||||
|
||||
Quits and removes all running event loops.
|
||||
|
||||
Return:
|
||||
True if loops needed to be aborted,
|
||||
False otherwise.
|
||||
"""
|
||||
self._shutting_down = True
|
||||
if self._loops:
|
||||
for loop in self._loops:
|
||||
loop.quit()
|
||||
loop.deleteLater()
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def on_mode_left(self, mode):
|
||||
"""Clear and reset input when the mode was left."""
|
||||
prompt = objreg.get('prompt', scope='window', window=self._win_id)
|
||||
if mode in [usertypes.KeyMode.prompt, usertypes.KeyMode.yesno]:
|
||||
prompt.txt.setText('')
|
||||
prompt.lineedit.clear()
|
||||
prompt.lineedit.setEchoMode(QLineEdit.Normal)
|
||||
self._prompt.hide() # FIXME
|
||||
self._prompt = None
|
||||
if self._question.answer is None and not self._question.is_aborted:
|
||||
self._question.cancel()
|
||||
|
||||
@cmdutils.register(instance='prompter', 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.
|
||||
"""
|
||||
prompt = objreg.get('prompt', scope='window', window=self._win_id)
|
||||
text = value if value is not None else prompt.lineedit.text()
|
||||
|
||||
self._prompt.accept(text)
|
||||
modeman.maybe_leave(self._win_id, self._question.mode,
|
||||
':prompt-accept')
|
||||
self._question.done()
|
||||
|
||||
@cmdutils.register(instance='prompter', 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='prompter', 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='prompter', 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.
|
||||
"""
|
||||
if self._question.mode != usertypes.PromptMode.download:
|
||||
# We just ignore this if we don't have a download question.
|
||||
return
|
||||
self._question.answer = usertypes.OpenFileDownloadTarget(cmdline)
|
||||
modeman.maybe_leave(self._win_id, usertypes.KeyMode.prompt,
|
||||
'download open')
|
||||
self._question.done()
|
||||
|
||||
@pyqtSlot(usertypes.Question, bool)
|
||||
def ask_question(self, question, blocking):
|
||||
"""Display a question in the statusbar.
|
||||
|
||||
Args:
|
||||
question: The Question object to ask.
|
||||
blocking: If True, this function blocks and returns the result.
|
||||
|
||||
Return:
|
||||
The answer of the user when blocking=True.
|
||||
None if blocking=False.
|
||||
"""
|
||||
log.statusbar.debug("Asking question {}, blocking {}, loops {}, queue "
|
||||
"{}".format(question, blocking, self._loops,
|
||||
self._queue))
|
||||
|
||||
if self._shutting_down:
|
||||
# If we're currently shutting down we have to ignore this question
|
||||
# to avoid segfaults - see
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/95
|
||||
log.statusbar.debug("Ignoring question because we're shutting "
|
||||
"down.")
|
||||
question.abort()
|
||||
return None
|
||||
|
||||
if self._prompt is not None and not blocking:
|
||||
# We got an async question, but we're already busy with one, so we
|
||||
# just queue it up for later.
|
||||
log.statusbar.debug("Adding {} to queue.".format(question))
|
||||
self._queue.append(question)
|
||||
return
|
||||
|
||||
if blocking:
|
||||
# If we're blocking we save the old state on the stack, so we can
|
||||
# restore it after exec, if exec gets called multiple times.
|
||||
old_prompt = self._prompt
|
||||
|
||||
self._question = question
|
||||
self._display_question()
|
||||
mode = self.KEY_MODES[self._question.mode]
|
||||
question.aborted.connect(
|
||||
lambda: modeman.maybe_leave(self._win_id, mode, 'aborted'))
|
||||
modeman.enter(self._win_id, mode, 'question asked')
|
||||
if blocking:
|
||||
loop = qtutils.EventLoop()
|
||||
self._loops.append(loop)
|
||||
loop.destroyed.connect(lambda: self._loops.remove(loop))
|
||||
question.completed.connect(loop.quit)
|
||||
question.completed.connect(loop.deleteLater)
|
||||
loop.exec_()
|
||||
if not self._restore_prompt(old_prompt):
|
||||
# Nothing left to restore, so we can go back to popping async
|
||||
# questions.
|
||||
if self._queue:
|
||||
self._pop_later()
|
||||
return self._question.answer
|
||||
else:
|
||||
question.completed.connect(self._pop_later)
|
@ -94,7 +94,7 @@ LOGGER_NAMES = [
|
||||
'commands', 'signals', 'downloads',
|
||||
'js', 'qt', 'rfc6266', 'ipc', 'shlexer',
|
||||
'save', 'message', 'config', 'sessions',
|
||||
'webelem'
|
||||
'webelem', 'prompt'
|
||||
]
|
||||
|
||||
|
||||
@ -139,6 +139,7 @@ message = logging.getLogger('message')
|
||||
config = logging.getLogger('config')
|
||||
sessions = logging.getLogger('sessions')
|
||||
webelem = logging.getLogger('webelem')
|
||||
prompt = logging.getLogger('prompt')
|
||||
|
||||
|
||||
ram_handler = None
|
||||
|
Loading…
Reference in New Issue
Block a user