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