diff --git a/qutebrowser/app.py b/qutebrowser/app.py
index 70588cfed..a4e11b6d6 100644
--- a/qutebrowser/app.py
+++ b/qutebrowser/app.py
@@ -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
diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py
index 18c6505c5..93d5ee98b 100644
--- a/qutebrowser/mainwindow/mainwindow.py
+++ b/qutebrowser/mainwindow/mainwindow.py
@@ -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)
diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py
index 6a3288648..869164833 100644
--- a/qutebrowser/mainwindow/prompt.py
+++ b/qutebrowser/mainwindow/prompt.py
@@ -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('{}'.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
diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py
index 3ca947f02..fd88cf4ca 100644
--- a/qutebrowser/mainwindow/statusbar/bar.py
+++ b/qutebrowser/mainwindow/statusbar/bar.py
@@ -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
diff --git a/qutebrowser/mainwindow/statusbar/prompt.py b/qutebrowser/mainwindow/statusbar/prompt.py
deleted file mode 100644
index 2015ac599..000000000
--- a/qutebrowser/mainwindow/statusbar/prompt.py
+++ /dev/null
@@ -1,84 +0,0 @@
-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
-
-# Copyright 2014-2016 Florian Bruhin (The Compiler)
-#
-# 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 .
-
-"""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)
diff --git a/qutebrowser/mainwindow/statusbar/prompter.py b/qutebrowser/mainwindow/statusbar/prompter.py
deleted file mode 100644
index 62e4eaf02..000000000
--- a/qutebrowser/mainwindow/statusbar/prompter.py
+++ /dev/null
@@ -1,293 +0,0 @@
-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
-
-# Copyright 2014-2016 Florian Bruhin (The Compiler)
-#
-# 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 .
-
-"""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)
diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py
index 924929707..a40ed9046 100644
--- a/qutebrowser/utils/log.py
+++ b/qutebrowser/utils/log.py
@@ -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