From 903e31efa4f21f97d77ffb1154181cb39ff88dfa Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 23 Sep 2016 06:18:48 +0200 Subject: [PATCH 01/87] First prototype of new prompts --- qutebrowser/mainwindow/mainwindow.py | 49 +++-- qutebrowser/mainwindow/prompt.py | 215 +++++++++++++++++++ qutebrowser/mainwindow/statusbar/bar.py | 22 -- qutebrowser/mainwindow/statusbar/prompter.py | 174 +++------------ qutebrowser/utils/usertypes.py | 2 - 5 files changed, 273 insertions(+), 189 deletions(-) create mode 100644 qutebrowser/mainwindow/prompt.py diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index ebc4f28ad..18c6505c5 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -30,7 +30,7 @@ from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QSizePolicy from qutebrowser.commands import runners, cmdutils from qutebrowser.config import config from qutebrowser.utils import message, log, usertypes, qtutils, objreg, utils -from qutebrowser.mainwindow import tabbedbrowser, messageview +from qutebrowser.mainwindow import tabbedbrowser, messageview, prompt from qutebrowser.mainwindow.statusbar import bar from qutebrowser.completion import completionwidget, completer from qutebrowser.keyinput import modeman @@ -179,10 +179,14 @@ class MainWindow(QWidget): partial_match=True) self._keyhint = keyhintwidget.KeyHintView(self.win_id, self) - self._overlays.append((self._keyhint, self._keyhint.update_geometry)) + self._add_overlay(self._keyhint, self._keyhint.update_geometry) self._messageview = messageview.MessageView(parent=self) - self._overlays.append((self._messageview, - self._messageview.update_geometry)) + self._add_overlay(self._messageview, self._messageview.update_geometry) + self._promptcontainer = prompt.PromptContainer(self) + self._add_overlay(self._promptcontainer, + self._promptcontainer.update_geometry, + centered=True) + self._promptcontainer.hide() log.init.debug("Initializing modes...") modeman.init(self.win_id, self) @@ -206,33 +210,40 @@ class MainWindow(QWidget): objreg.get("app").new_window.emit(self) - def _update_overlay_geometry(self, widget=None): + def _add_overlay(self, widget, signal, *, centered=False): + self._overlays.append((widget, signal, centered)) + + def _update_overlay_geometry(self, widget=None, centered=None): """Reposition/resize the given overlay. If no widget is given, reposition/resize all overlays. """ if widget is None: - for w, _signal in self._overlays: - self._update_overlay_geometry(w) + for w, _signal, centered in self._overlays: + self._update_overlay_geometry(w, centered) return + assert centered is not None + if not widget.isVisible(): return size_hint = widget.sizeHint() if widget.sizePolicy().horizontalPolicy() == QSizePolicy.Expanding: width = self.width() + left = 0 else: width = size_hint.width() + left = (self.width() - size_hint.width()) / 2 if centered else 0 status_position = config.get('ui', 'status-position') if status_position == 'bottom': top = self.height() - self.status.height() - size_hint.height() top = qtutils.check_overflow(top, 'int', fatal=False) - topleft = QPoint(0, top) - bottomright = QPoint(width, self.status.geometry().top()) + topleft = QPoint(left, top) + bottomright = QPoint(left + width, self.status.geometry().top()) elif status_position == 'top': - topleft = self.status.geometry().bottomLeft() + topleft = QPoint(left, self.status.geometry().bottom()) bottom = self.status.height() + size_hint.height() bottom = qtutils.check_overflow(bottom, 'int', fatal=False) bottomright = QPoint(width, bottom) @@ -261,8 +272,7 @@ class MainWindow(QWidget): completer_obj.on_selection_changed) objreg.register('completion', self._completion, scope='window', window=self.win_id) - self._overlays.append((self._completion, - self._completion.update_geometry)) + self._add_overlay(self._completion, self._completion.update_geometry) def _init_command_dispatcher(self): dispatcher = commands.CommandDispatcher(self.win_id, @@ -350,10 +360,11 @@ class MainWindow(QWidget): def _connect_overlay_signals(self): """Connect the resize signal and resize everything once.""" - for widget, signal in self._overlays: + for widget, signal, centered in self._overlays: signal.connect( - functools.partial(self._update_overlay_geometry, widget)) - self._update_overlay_geometry(widget) + functools.partial(self._update_overlay_geometry, widget, + centered)) + self._update_overlay_geometry(widget, centered) def _set_default_geometry(self): """Set some sensible default geometry.""" @@ -374,7 +385,7 @@ 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') + #prompter = self._get_object('prompter') # misc self.tabbed_browser.close_window.connect(self.close) @@ -384,7 +395,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(prompter.on_mode_left) # commands keyparsers[usertypes.KeyMode.normal].keystring_updated.connect( @@ -408,8 +419,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(prompter.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 new file mode 100644 index 000000000..6a3288648 --- /dev/null +++ b/qutebrowser/mainwindow/prompt.py @@ -0,0 +1,215 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 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 . + +"""Showing prompts above the statusbar.""" + +import collections + +from PyQt5.QtCore import pyqtSignal, Qt +from PyQt5.QtWidgets import QWidget, QGridLayout, QVBoxLayout, QLineEdit, QLabel, QSpacerItem + +from qutebrowser.config import style, config +from qutebrowser.utils import usertypes + + +AuthTuple = collections.namedtuple('AuthTuple', ['user', 'password']) + + +class Error(Exception): + + """Base class for errors in this module.""" + + +class PromptContainer(QWidget): + + update_geometry = pyqtSignal() + + def __init__(self, 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) + style.set_register_stylesheet(self, + generator=self._generate_stylesheet) + + def _generate_stylesheet(self): + """Generate a stylesheet with the right edge rounded.""" + stylesheet = """ + QWidget#Prompt { + border-POSITION-left-radius: 10px; + border-POSITION-right-radius: 10px; + } + + QWidget { + /* FIXME + font: {{ font['keyhint'] }}; + FIXME + */ + color: {{ color['statusbar.fg.prompt'] }}; + background-color: {{ color['statusbar.bg.prompt'] }}; + } + + QLineEdit { + border: 1px solid grey; + } + """ + position = config.get('ui', 'status-position') + if position == 'top': + return stylesheet.replace('POSITION', 'bottom') + elif position == 'bottom': + return stylesheet.replace('POSITION', 'top') + 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() + + self._layout.addWidget(prompt) + self.update_geometry.emit() + + +class _BasePrompt(QWidget): + + """Base class for all prompts.""" + + def __init__(self, question, parent=None): + super().__init__(parent) + self._question = question + self._layout = QGridLayout(self) + self._layout.setVerticalSpacing(15) + + def _init_title(self, title, *, span=1): + label = QLabel('{}'.format(title), self) + self._layout.addWidget(label, 0, 0, 1, span) + + def accept(self, value=None): + raise NotImplementedError + + +class LineEditPrompt(_BasePrompt): + + def __init__(self, question, parent=None): + super().__init__(parent) + self._lineedit = QLineEdit(self) + self._layout.addWidget(self._lineedit, 1, 0) + self._init_title(question.text) + if question.default: + self._lineedit.setText(question.default) + + def accept(self, value=None): + text = value if value is not None else self._lineedit.text() + self._question.answer = text + + +class DownloadFilenamePrompt(LineEditPrompt): + + # FIXME have a FilenamePrompt + + 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_config = objreg.get('key-config') +# all_bindings = key_config.get_reverse_bindings_for(key_mode.name) +# bindings = all_bindings.get('prompt-open-download', []) +# if bindings: +# text += ' ({} to open)'.format(bindings[0]) + + + def accept(self, value=None): + text = value if value is not None else self._lineedit.text() + self._question.answer = usertypes.FileDownloadTarget(text) + + +class AuthenticationPrompt(_BasePrompt): + + def __init__(self, question, parent=None): + super().__init__(question, parent) + self._init_title(question.text, span=2) + user_label = QLabel("Username:", self) + self._user_lineedit = QLineEdit(self) + password_label = QLabel("Password:", self) + self._password_lineedit = QLineEdit(self) + self._password_lineedit.setEchoMode(QLineEdit.Password) + self._layout.addWidget(user_label, 1, 0) + self._layout.addWidget(self._user_lineedit, 1, 1) + self._layout.addWidget(password_label, 2, 0) + self._layout.addWidget(self._password_lineedit, 2, 1) + assert not question.default, question.default + + spacer = QSpacerItem(0, 10) + self._layout.addItem(spacer, 3, 0) + + help_1 = QLabel("Accept: Enter") + help_2 = QLabel("Abort: Escape") + self._layout.addWidget(help_1, 4, 0) + self._layout.addWidget(help_2, 5, 0) + + def accept(self, value=None): + if value is not None: + if ':' not in value: + raise Error("Value needs to be in the format " + "username:password, but {} was given".format( + value)) + username, password = value.split(':', maxsplit=1) + self._question.answer = AuthTuple(username, password) + else: + self._question.answer = AuthTuple(self._user_lineedit.text(), + self._password_lineedit.text()) + + +class YesNoPrompt(_BasePrompt): + + def __init__(self, question, parent=None): + super().__init__(question, parent) + self._init_title(question.text) + # FIXME + # "Enter/y: yes" + # "n: no" + # (depending on default) + + def accept(self, value=None): + if value is None: + self._question.answer = self._question.default + elif value == 'yes': + self._question.answer = True + elif value == 'no': + self._question.answer = False + else: + raise Error("Invalid value {} - expected yes/no!".format(value)) + + +class AlertPrompt(_BasePrompt): + + def __init__(self, question, parent=None): + super().__init__(question, parent) + self._init_title(question.text) + # FIXME + # Enter: acknowledge + + def accept(self, value=None): + if value is not None: + raise Error("No value is permitted with alert prompts!") + # Doing nothing otherwise diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index 1645ce583..3ca947f02 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -162,16 +162,9 @@ class StatusBar(QWidget): self.txt = textwidget.Text() self._stack.addWidget(self.txt) - self.prompt = prompt.Prompt(win_id) - self._stack.addWidget(self.prompt) - self.cmd.show_cmd.connect(self._show_cmd_widget) self.cmd.hide_cmd.connect(self._hide_cmd_widget) self._hide_cmd_widget() - prompter = objreg.get('prompter', scope='window', window=self._win_id) - prompter.show_prompt.connect(self._show_prompt_widget) - prompter.hide_prompt.connect(self._hide_prompt_widget) - self._hide_prompt_widget() self.keystring = keystring.KeyString() self._hbox.addWidget(self.keystring) @@ -285,21 +278,6 @@ class StatusBar(QWidget): self._stack.setCurrentWidget(self.txt) self.maybe_hide() - def _show_prompt_widget(self): - """Show prompt widget instead of temporary text.""" - if self._stack.currentWidget() is self.prompt: - return - self._set_prompt_active(True) - self._stack.setCurrentWidget(self.prompt) - self.show() - - def _hide_prompt_widget(self): - """Show temporary text instead of prompt widget.""" - self._set_prompt_active(False) - log.statusbar.debug("Hiding prompt widget") - self._stack.setCurrentWidget(self.txt) - self.maybe_hide() - @pyqtSlot(str) def set_text(self, val): """Set a normal (persistent) text in the status bar.""" diff --git a/qutebrowser/mainwindow/statusbar/prompter.py b/qutebrowser/mainwindow/statusbar/prompter.py index c93f5912a..62e4eaf02 100644 --- a/qutebrowser/mainwindow/statusbar/prompter.py +++ b/qutebrowser/mainwindow/statusbar/prompter.py @@ -30,12 +30,6 @@ from qutebrowser.commands import cmdutils, cmdexc from qutebrowser.utils import usertypes, log, qtutils, objreg, utils -PromptContext = collections.namedtuple('PromptContext', - ['question', 'text', 'input_text', - 'echo_mode', 'input_visible']) -AuthTuple = collections.namedtuple('AuthTuple', ['user', 'password']) - - class Prompter(QObject): """Manager for questions to be shown in the statusbar. @@ -67,12 +61,8 @@ class Prompter(QObject): _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. - _busy: If we're currently busy with asking a question. + _prompt: The current prompt object if we're handling a question. _win_id: The window ID this object is associated with. - - Signals: - show_prompt: Emitted when the prompt widget should be shown. - hide_prompt: Emitted when the prompt widget should be hidden. """ KEY_MODES = { @@ -83,22 +73,19 @@ class Prompter(QObject): usertypes.PromptMode.download: usertypes.KeyMode.prompt, } - show_prompt = pyqtSignal() - hide_prompt = pyqtSignal() - def __init__(self, win_id, parent=None): super().__init__(parent) self._shutting_down = False self._question = None self._loops = [] self._queue = collections.deque() - self._busy = False + 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), - busy=self._busy) + prompt=self._prompt) def _pop_later(self): """Helper to call self._pop as soon as everything else is done.""" @@ -115,78 +102,31 @@ class Prompter(QObject): # https://github.com/The-Compiler/qutebrowser/issues/415 self.ask_question(question, blocking=False) - def _get_ctx(self): - """Get a PromptContext based on the current state.""" - if not self._busy: - return None - prompt = objreg.get('prompt', scope='window', window=self._win_id) - ctx = PromptContext(question=self._question, - text=prompt.txt.text(), - input_text=prompt.lineedit.text(), - echo_mode=prompt.lineedit.echoMode(), - input_visible=prompt.lineedit.isVisible()) - return ctx - - def _restore_ctx(self, ctx): - """Restore state from a PromptContext. + def _restore_prompt(self, prompt): + """Restore an old prompt which was interrupted. Args: - ctx: A PromptContext previously saved by _get_ctx, or None. + prompt: A Prompt object or None. - Return: True if a context was restored, False otherwise. + Return: True if a prompt was restored, False otherwise. """ - log.statusbar.debug("Restoring context {}".format(ctx)) - if ctx is None: - self.hide_prompt.emit() - self._busy = False + log.statusbar.debug("Restoring prompt {}".format(prompt)) + if prompt is None: + self._prompt.hide() # FIXME + self._prompt = None return False self._question = ctx.question - prompt = objreg.get('prompt', scope='window', window=self._win_id) - prompt.txt.setText(ctx.text) - prompt.lineedit.setText(ctx.input_text) - prompt.lineedit.setEchoMode(ctx.echo_mode) - prompt.lineedit.setVisible(ctx.input_visible) - self.show_prompt.emit() + 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_yesno(self, prompt): - """Display a yes/no question.""" - if self._question.default is None: - suffix = "" - elif self._question.default: - suffix = " (yes)" - else: - suffix = " (no)" - prompt.txt.setText(self._question.text + suffix) - prompt.lineedit.hide() - - def _display_question_input(self, prompt): - """Display a question with an input.""" - text = self._question.text - if self._question.mode == usertypes.PromptMode.download: - 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', []) - if bindings: - text += ' ({} to open)'.format(bindings[0]) - prompt.txt.setText(text) - if self._question.default: - prompt.lineedit.setText(self._question.default) - prompt.lineedit.show() - - def _display_question_alert(self, prompt): - """Display a JS alert 'question'.""" - prompt.txt.setText(self._question.text + ' (ok)') - prompt.lineedit.hide() - def _display_question(self): """Display the question saved in self._question.""" - prompt = objreg.get('prompt', scope='window', window=self._win_id) handlers = { usertypes.PromptMode.yesno: self._display_question_yesno, usertypes.PromptMode.text: self._display_question_input, @@ -199,8 +139,9 @@ class Prompter(QObject): log.modes.debug("Question asked, focusing {!r}".format( prompt.lineedit)) prompt.lineedit.setFocus() - self.show_prompt.emit() - self._busy = True + prompt.show() + # FIXME + self._prompt = prompt def shutdown(self): """Cancel all blocking questions. @@ -228,8 +169,8 @@ class Prompter(QObject): prompt.txt.setText('') prompt.lineedit.clear() prompt.lineedit.setEchoMode(QLineEdit.Normal) - self.hide_prompt.emit() - self._busy = False + self._prompt.hide() # FIXME + self._prompt = None if self._question.answer is None and not self._question.is_aborted: self._question.cancel() @@ -251,83 +192,24 @@ class Prompter(QObject): prompt = objreg.get('prompt', scope='window', window=self._win_id) text = value if value is not None else prompt.lineedit.text() - if (self._question.mode == usertypes.PromptMode.user_pwd and - self._question.user is None): - # User just entered a username - self._question.user = text - prompt.txt.setText("Password:") - prompt.lineedit.clear() - prompt.lineedit.setEchoMode(QLineEdit.Password) - elif self._question.mode == usertypes.PromptMode.user_pwd: - # User just entered a password - self._question.answer = AuthTuple(self._question.user, text) - modeman.maybe_leave(self._win_id, usertypes.KeyMode.prompt, - 'prompt accept') - self._question.done() - elif self._question.mode == usertypes.PromptMode.text: - # User just entered text. - self._question.answer = text - modeman.maybe_leave(self._win_id, usertypes.KeyMode.prompt, - 'prompt accept') - self._question.done() - elif self._question.mode == usertypes.PromptMode.download: - # User just entered a path for a download. - target = usertypes.FileDownloadTarget(text) - self._question.answer = target - modeman.maybe_leave(self._win_id, usertypes.KeyMode.prompt, - 'prompt accept') - self._question.done() - elif self._question.mode == usertypes.PromptMode.yesno: - # User wants to accept the default of a yes/no question. - if value is None: - self._question.answer = self._question.default - elif value == 'yes': - self._question.answer = True - elif value == 'no': - self._question.answer = False - else: - raise cmdexc.CommandError("Invalid value {} - expected " - "yes/no!".format(value)) - modeman.maybe_leave(self._win_id, usertypes.KeyMode.yesno, - 'yesno accept') - self._question.done() - elif self._question.mode == usertypes.PromptMode.alert: - if value is not None: - raise cmdexc.CommandError("No value is permitted with alert " - "prompts!") - # User acknowledged an alert - self._question.answer = None - modeman.maybe_leave(self._win_id, usertypes.KeyMode.prompt, - 'alert accept') - self._question.done() - else: - raise ValueError("Invalid question mode!") + 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.""" - if self._question.mode != usertypes.PromptMode.yesno: - # We just ignore this if we don't have a yes/no question. - return - self._question.answer = True - modeman.maybe_leave(self._win_id, usertypes.KeyMode.yesno, - 'yesno accept') - self._question.done() + 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.""" - if self._question.mode != usertypes.PromptMode.yesno: - # We just ignore this if we don't have a yes/no question. - return - self._question.answer = False - modeman.maybe_leave(self._win_id, usertypes.KeyMode.yesno, - 'prompt accept') - self._question.done() + self.prompt_accept('no') @cmdutils.register(instance='prompter', hide=True, scope='window', modes=[usertypes.KeyMode.prompt], maxsplit=0) @@ -376,7 +258,7 @@ class Prompter(QObject): question.abort() return None - if self._busy and not blocking: + 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)) @@ -386,7 +268,7 @@ class Prompter(QObject): 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. - context = self._get_ctx() + old_prompt = self._prompt self._question = question self._display_question() @@ -401,7 +283,7 @@ class Prompter(QObject): question.completed.connect(loop.quit) question.completed.connect(loop.deleteLater) loop.exec_() - if not self._restore_ctx(context): + if not self._restore_prompt(old_prompt): # Nothing left to restore, so we can go back to popping async # questions. if self._queue: diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index 6dd3d9674..172281ba0 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -336,7 +336,6 @@ class Question(QObject): For text, a default text as string. For user_pwd, a default username as string. text: The prompt text to display to the user. - user: The value the user entered as username. answer: The value the user entered (as password for user_pwd). is_aborted: Whether the question was aborted. @@ -365,7 +364,6 @@ class Question(QObject): self._mode = None self.default = None self.text = None - self.user = None self.answer = None self.is_aborted = False From 33088588d959b903a9c088a13d5bfad7862c72a5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 3 Oct 2016 18:39:53 +0200 Subject: [PATCH 02/87] half-working auth prompts --- qutebrowser/app.py | 6 +- qutebrowser/mainwindow/mainwindow.py | 18 +- qutebrowser/mainwindow/prompt.py | 319 +++++++++++++++++-- qutebrowser/mainwindow/statusbar/bar.py | 3 +- qutebrowser/mainwindow/statusbar/prompt.py | 84 ----- qutebrowser/mainwindow/statusbar/prompter.py | 293 ----------------- qutebrowser/utils/log.py | 3 +- 7 files changed, 313 insertions(+), 413 deletions(-) delete mode 100644 qutebrowser/mainwindow/statusbar/prompt.py delete mode 100644 qutebrowser/mainwindow/statusbar/prompter.py 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 From 1f011bdd5fb9a424bc50122e7cd2edc42cd147e5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 3 Oct 2016 19:12:43 +0200 Subject: [PATCH 03/87] Fix prompt focus handling --- qutebrowser/mainwindow/prompt.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 869164833..de949b04b 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -183,7 +183,6 @@ class PromptContainer(QWidget): self._prompt.show() self.show() self._prompt.setFocus() - self.setFocus() self.update_geometry.emit() return True @@ -370,6 +369,7 @@ class LineEditPrompt(_BasePrompt): self._init_title(question.text) if question.default: self._lineedit.setText(question.default) + self.setFocusProxy(self._lineedit) def accept(self, value=None): text = value if value is not None else self._lineedit.text() @@ -427,9 +427,7 @@ class AuthenticationPrompt(_BasePrompt): help_2 = QLabel("Abort: Escape") self._layout.addWidget(help_1, 4, 0) self._layout.addWidget(help_2, 5, 0) - - # FIXME needed? - self._user_lineedit.setFocus() + self.setFocusProxy(self._user_lineedit) def accept(self, value=None): if value is not None: From e3581a50ca94a6cc95b90de4b316068bd142080d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 3 Oct 2016 19:31:01 +0200 Subject: [PATCH 04/87] Remove generated stylesheets again We can just use jinja logic instead... --- qutebrowser/config/style.py | 15 ++++----- qutebrowser/mainwindow/prompt.py | 56 ++++++++++++++----------------- qutebrowser/misc/keyhintwidget.py | 32 ++++++++---------- tests/unit/config/test_style.py | 27 --------------- 4 files changed, 45 insertions(+), 85 deletions(-) diff --git a/qutebrowser/config/style.py b/qutebrowser/config/style.py index 92c893c45..b2697daac 100644 --- a/qutebrowser/config/style.py +++ b/qutebrowser/config/style.py @@ -46,7 +46,7 @@ def get_stylesheet(template_str): config=objreg.get('config')) -def set_register_stylesheet(obj, *, generator=None): +def set_register_stylesheet(obj): """Set the stylesheet for an object based on it's STYLESHEET attribute. Also, register an update when the config is changed. @@ -54,23 +54,20 @@ def set_register_stylesheet(obj, *, generator=None): Args: obj: The object to set the stylesheet for and register. Must have a STYLESHEET attribute. - generator: If set, call the given function to dynamically generate a - stylesheet instead. """ - stylesheet = generator() if generator is not None else obj.STYLESHEET - qss = get_stylesheet(stylesheet) + qss = get_stylesheet(obj.STYLESHEET) log.config.vdebug("stylesheet for {}: {}".format( obj.__class__.__name__, qss)) obj.setStyleSheet(qss) objreg.get('config').changed.connect( - functools.partial(_update_stylesheet, obj, generator=generator)) + functools.partial(_update_stylesheet, obj)) -def _update_stylesheet(obj, *, generator): +def _update_stylesheet(obj): """Update the stylesheet for obj.""" + get_stylesheet.cache_clear() if not sip.isdeleted(obj): - stylesheet = generator() if generator is not None else obj.STYLESHEET - obj.setStyleSheet(get_stylesheet(stylesheet)) + obj.setStyleSheet(get_stylesheet(obj.STYLESHEET)) class ColorDict(collections.UserDict): diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index de949b04b..a85b00f93 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -77,6 +77,30 @@ class PromptContainer(QWidget): _win_id: The window ID this object is associated with. """ + STYLESHEET = """ + QWidget#Prompt { + {% if config.get('ui', 'status-position') == 'top' %} + border-bottom-left-radius: 10px; + border-bottom-right-radius: 10px; + {% else %} + border-top-left-radius: 10px; + border-top-right-radius: 10px; + {% endif %} + } + + QWidget { + /* FIXME + font: {{ font['keyhint'] }}; + FIXME + */ + color: {{ color['statusbar.fg.prompt'] }}; + background-color: {{ color['statusbar.bg.prompt'] }}; + } + + QLineEdit { + border: 1px solid grey; + } + """ update_geometry = pyqtSignal() def __init__(self, win_id, parent=None): @@ -86,8 +110,7 @@ class PromptContainer(QWidget): self._layout = QVBoxLayout(self) self._layout.setContentsMargins(10, 10, 10, 10) self._prompt = None - style.set_register_stylesheet(self, - generator=self._generate_stylesheet) + style.set_register_stylesheet(self) # FIXME review this self._shutting_down = False @@ -99,35 +122,6 @@ class PromptContainer(QWidget): 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 = """ - QWidget#Prompt { - border-POSITION-left-radius: 10px; - border-POSITION-right-radius: 10px; - } - - QWidget { - /* FIXME - font: {{ font['keyhint'] }}; - FIXME - */ - color: {{ color['statusbar.fg.prompt'] }}; - background-color: {{ color['statusbar.bg.prompt'] }}; - } - - QLineEdit { - border: 1px solid grey; - } - """ - position = config.get('ui', 'status-position') - if position == 'top': - return stylesheet.replace('POSITION', 'bottom') - elif position == 'bottom': - return stylesheet.replace('POSITION', 'top') - else: - raise ValueError("Invalid position {}!".format(position)) - def _pop_later(self): """Helper to call self._pop as soon as everything else is done.""" QTimer.singleShot(0, self._pop) diff --git a/qutebrowser/misc/keyhintwidget.py b/qutebrowser/misc/keyhintwidget.py index 667edeb1c..c66712eb9 100644 --- a/qutebrowser/misc/keyhintwidget.py +++ b/qutebrowser/misc/keyhintwidget.py @@ -45,6 +45,19 @@ class KeyHintView(QLabel): update_geometry: Emitted when this widget should be resized/positioned. """ + STYLESHEET = """ + QLabel { + font: {{ font['keyhint'] }}; + color: {{ color['keyhint.fg'] }}; + background-color: {{ color['keyhint.bg'] }}; + padding: 6px; + {% if config.get('ui', 'status-position') == 'top' %} + border-bottom-right-radius: 6px; + {% else %} + border-top-right-radius: 6px; + {% endif %} + } + """ update_geometry = pyqtSignal() def __init__(self, win_id, parent=None): @@ -56,8 +69,7 @@ class KeyHintView(QLabel): self._show_timer = usertypes.Timer(self, 'keyhint_show') self._show_timer.setInterval(500) self._show_timer.timeout.connect(self.show) - style.set_register_stylesheet(self, - generator=self._generate_stylesheet) + style.set_register_stylesheet(self) def __repr__(self): return utils.get_repr(self, win_id=self._win_id) @@ -67,22 +79,6 @@ class KeyHintView(QLabel): self.update_geometry.emit() super().showEvent(e) - def _generate_stylesheet(self): - """Generate a stylesheet with the right edge rounded.""" - stylesheet = """ - QLabel { - font: {{ font['keyhint'] }}; - color: {{ color['keyhint.fg'] }}; - background-color: {{ color['keyhint.bg'] }}; - padding: 6px; - border-EDGE-radius: 6px; - } - """ - if config.get('ui', 'status-position') == 'top': - return stylesheet.replace('EDGE', 'bottom-right') - else: - return stylesheet.replace('EDGE', 'top-right') - @pyqtSlot(str) def update_keyhint(self, modename, prefix): """Show hints for the given prefix (or hide if prefix is empty). diff --git a/tests/unit/config/test_style.py b/tests/unit/config/test_style.py index bcb8a6b28..6b55aebae 100644 --- a/tests/unit/config/test_style.py +++ b/tests/unit/config/test_style.py @@ -59,24 +59,6 @@ class Obj(QObject): self.rendered_stylesheet = stylesheet -class GeneratedObj(QObject): - - def __init__(self, parent=None): - super().__init__(parent) - self.rendered_stylesheet = None - self._generated = False - - def setStyleSheet(self, stylesheet): - self.rendered_stylesheet = stylesheet - - def generate(self): - if not self._generated: - self._generated = True - return 'one' - else: - return 'two' - - @pytest.mark.parametrize('delete', [True, False]) def test_set_register_stylesheet(delete, qtbot, config_stub, caplog): config_stub.data = {'fonts': {'foo': 'bar'}, 'colors': {}} @@ -105,15 +87,6 @@ def test_set_register_stylesheet(delete, qtbot, config_stub, caplog): assert obj.rendered_stylesheet == expected -def test_set_register_stylesheet_generator(qtbot, config_stub): - config_stub.data = {'fonts': {}, 'colors': {}} - obj = GeneratedObj() - style.set_register_stylesheet(obj, generator=obj.generate) - assert obj.rendered_stylesheet == 'one' - config_stub.changed.emit('foo', 'bar') - assert obj.rendered_stylesheet == 'two' - - class TestColorDict: @pytest.mark.parametrize('key, expected', [ From 523369882aac5e38dcdbad2abec4c2bf828fab27 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 3 Oct 2016 20:12:15 +0200 Subject: [PATCH 05/87] Add title/text to questions --- .../browser/webkit/network/networkmanager.py | 11 ++-- qutebrowser/browser/webkit/webpage.py | 13 ++-- qutebrowser/mainwindow/prompt.py | 63 ++++++++++--------- qutebrowser/utils/usertypes.py | 6 +- 4 files changed, 53 insertions(+), 40 deletions(-) diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py index d28bd6be4..3a02f1745 100644 --- a/qutebrowser/browser/webkit/network/networkmanager.py +++ b/qutebrowser/browser/webkit/network/networkmanager.py @@ -207,10 +207,11 @@ class NetworkManager(QNetworkAccessManager): self.setCache(cache) cache.setParent(app) - def _ask(self, text, mode, owner=None): + def _ask(self, title, text, mode, owner=None): """Ask a blocking question in the statusbar. Args: + title: The title to display to the user. text: The text to display to the user. mode: A PromptMode. owner: An object which will abort the question if destroyed, or @@ -220,6 +221,7 @@ class NetworkManager(QNetworkAccessManager): The answer the user gave or None if the prompt was cancelled. """ q = usertypes.Question() + q.title = title q.text = text q.mode = mode self.shutting_down.connect(q.abort) @@ -284,7 +286,7 @@ class NetworkManager(QNetworkAccessManager): if ssl_strict == 'ask': err_string = '\n'.join('- ' + err.errorString() for err in errors) - answer = self._ask('SSL errors - continue?\n{}'.format(err_string), + answer = self._ask('SSL errors - continue?', err_string, mode=usertypes.PromptMode.yesno, owner=reply) log.webview.debug("Asked for SSL errors, answer {}".format(answer)) if answer: @@ -343,7 +345,8 @@ class NetworkManager(QNetworkAccessManager): if user is None: # netrc check failed - answer = self._ask("Username ({}):".format(authenticator.realm()), + answer = self._ask("Authentication required", + authenticator.realm(), mode=usertypes.PromptMode.user_pwd, owner=reply) if answer is not None: @@ -362,7 +365,7 @@ class NetworkManager(QNetworkAccessManager): authenticator.setPassword(password) else: answer = self._ask( - "Proxy username ({}):".format(authenticator.realm()), + "Proxy authentication required", authenticator.realm(), mode=usertypes.PromptMode.user_pwd) if answer is not None: authenticator.setUser(answer.user) diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py index c34bc5074..7f02fbb98 100644 --- a/qutebrowser/browser/webkit/webpage.py +++ b/qutebrowser/browser/webkit/webpage.py @@ -98,8 +98,8 @@ class BrowserPage(QWebPage): if (self._is_shutting_down or config.get('content', 'ignore-javascript-prompt')): return (False, "") - answer = self._ask("js: {}".format(msg), usertypes.PromptMode.text, - default) + answer = self._ask('Javascript prompt', msg, + usertypes.PromptMode.text, default) if answer is None: return (False, "") else: @@ -196,10 +196,11 @@ class BrowserPage(QWebPage): suggested_file) return True - def _ask(self, text, mode, default=None): + def _ask(self, title, text, mode, default=None): """Ask a blocking question in the statusbar. Args: + title: The title to display. text: The text to display to the user. mode: A PromptMode. default: The default value to display. @@ -208,6 +209,7 @@ class BrowserPage(QWebPage): The answer the user gave or None if the prompt was cancelled. """ q = usertypes.Question() + q.title = title q.text = text q.mode = mode q.default = default @@ -478,7 +480,7 @@ class BrowserPage(QWebPage): if (self._is_shutting_down or config.get('content', 'ignore-javascript-alert')): return - self._ask("[js alert] {}".format(msg), usertypes.PromptMode.alert) + self._ask('Javascript alert', msg, usertypes.PromptMode.alert) def javaScriptConfirm(self, frame, msg): """Override javaScriptConfirm to use the statusbar.""" @@ -488,8 +490,7 @@ class BrowserPage(QWebPage): if self._is_shutting_down: return False - ans = self._ask("[js confirm] {}".format(msg), - usertypes.PromptMode.yesno) + ans = self._ask('Javascript confirm', msg, usertypes.PromptMode.yesno) return bool(ans) def javaScriptConsoleMessage(self, msg, line, source): diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index a85b00f93..db2f11f4e 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -105,19 +105,18 @@ class PromptContainer(QWidget): 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) - - # FIXME review this self._shutting_down = False self._loops = [] self._queue = collections.deque() self._win_id = win_id + self.setObjectName('Prompt') + self.setAttribute(Qt.WA_StyledBackground, True) + style.set_register_stylesheet(self) + def __repr__(self): return utils.get_repr(self, loops=len(self._loops), queue=len(self._queue), prompt=self._prompt) @@ -337,15 +336,25 @@ class _BasePrompt(QWidget): def __init__(self, question, parent=None): super().__init__(parent) self.question = question - self._layout = QGridLayout(self) - self._layout.setVerticalSpacing(15) + self._vbox = QVBoxLayout(self) + self._vbox.setSpacing(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) + def _init_title(self, question): + if question.title is None: + title = question.text + text = None + else: + title = question.title + text = question.text + + title_label = QLabel('{}'.format(title), self) + self._vbox.addWidget(title_label) + if text is not None: + text_label = QLabel(text) + self._vbox.addWidget(text_label) def accept(self, value=None): raise NotImplementedError @@ -357,10 +366,10 @@ class _BasePrompt(QWidget): class LineEditPrompt(_BasePrompt): def __init__(self, question, parent=None): - super().__init__(parent) + super().__init__(question, parent) self._lineedit = QLineEdit(self) - self._layout.addWidget(self._lineedit, 1, 0) - self._init_title(question.text) + self._init_title(question) + self._vbox.addWidget(self._lineedit) if question.default: self._lineedit.setText(question.default) self.setFocusProxy(self._lineedit) @@ -402,25 +411,23 @@ class AuthenticationPrompt(_BasePrompt): def __init__(self, question, parent=None): super().__init__(question, parent) - self._init_title(question.text, span=2) + self._init_title(question) + user_label = QLabel("Username:", self) self._user_lineedit = QLineEdit(self) + password_label = QLabel("Password:", self) self._password_lineedit = QLineEdit(self) self._password_lineedit.setEchoMode(QLineEdit.Password) - self._layout.addWidget(user_label, 1, 0) - self._layout.addWidget(self._user_lineedit, 1, 1) - self._layout.addWidget(password_label, 2, 0) - self._layout.addWidget(self._password_lineedit, 2, 1) + + grid = QGridLayout() + grid.addWidget(user_label, 1, 0) + grid.addWidget(self._user_lineedit, 1, 1) + grid.addWidget(password_label, 2, 0) + grid.addWidget(self._password_lineedit, 2, 1) + self._vbox.addLayout(grid) + assert not question.default, question.default - - spacer = QSpacerItem(0, 10) - self._layout.addItem(spacer, 3, 0) - - help_1 = QLabel("Accept: Enter") - help_2 = QLabel("Abort: Escape") - self._layout.addWidget(help_1, 4, 0) - self._layout.addWidget(help_2, 5, 0) self.setFocusProxy(self._user_lineedit) def accept(self, value=None): @@ -449,7 +456,7 @@ class YesNoPrompt(_BasePrompt): def __init__(self, question, parent=None): super().__init__(question, parent) - self._init_title(question.text) + self._init_title(question) # FIXME # "Enter/y: yes" # "n: no" @@ -471,7 +478,7 @@ class AlertPrompt(_BasePrompt): def __init__(self, question, parent=None): super().__init__(question, parent) - self._init_title(question.text) + self._init_title(question) # FIXME # Enter: acknowledge diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index 172281ba0..71517136c 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -335,6 +335,7 @@ class Question(QObject): For yesno, None (no default), True or False. For text, a default text as string. For user_pwd, a default username as string. + title: The question title to show. text: The prompt text to display to the user. answer: The value the user entered (as password for user_pwd). is_aborted: Whether the question was aborted. @@ -363,13 +364,14 @@ class Question(QObject): super().__init__(parent) self._mode = None self.default = None + self.title = None self.text = None self.answer = None self.is_aborted = False def __repr__(self): - return utils.get_repr(self, text=self.text, mode=self._mode, - default=self.default) + return utils.get_repr(self, title=self.title, text=self.text, + mode=self._mode, default=self.default) @property def mode(self): From 756564ebff4cd3210f39705309846520e3915de3 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 3 Oct 2016 20:25:09 +0200 Subject: [PATCH 06/87] Get rid of message_bridge.set_cmd_text --- qutebrowser/browser/hints.py | 5 ++--- qutebrowser/mainwindow/mainwindow.py | 1 - qutebrowser/mainwindow/statusbar/command.py | 1 - qutebrowser/utils/message.py | 14 -------------- 4 files changed, 2 insertions(+), 19 deletions(-) diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index e58684289..4d9407469 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -265,9 +265,8 @@ class HintActions: if text[0] not in modeparsers.STARTCHARS: raise HintingError("Invalid command text '{}'.".format(text)) - bridge = objreg.get('message-bridge', scope='window', - window=self._win_id) - bridge.set_cmd_text(text) + cmd = objreg.get('status-command', scope='window', window=self._win_id) + cmd.set_cmd_text(text) def download(self, elem, context): """Download a hint URL. diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index 93d5ee98b..37be132ce 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -420,7 +420,6 @@ 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(self._prompt_container.ask_question, Qt.DirectConnection) diff --git a/qutebrowser/mainwindow/statusbar/command.py b/qutebrowser/mainwindow/statusbar/command.py index 79a5f0305..a5abaa290 100644 --- a/qutebrowser/mainwindow/statusbar/command.py +++ b/qutebrowser/mainwindow/statusbar/command.py @@ -77,7 +77,6 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit): else: return '' - @pyqtSlot(str) def set_cmd_text(self, text): """Preset the statusbar to some text. diff --git a/qutebrowser/utils/message.py b/qutebrowser/utils/message.py index 43c820ad1..9cec02871 100644 --- a/qutebrowser/utils/message.py +++ b/qutebrowser/utils/message.py @@ -164,8 +164,6 @@ class MessageBridge(QObject): arg: The text to set. s_maybe_reset_text: Reset the text if it hasn't been changed yet. arg: The expected text. - s_set_cmd_text: Pre-set a text for the commandline prompt. - arg: The text to set. s_question: Ask a question to the user in the statusbar. arg 0: The Question object to ask. @@ -177,23 +175,11 @@ class MessageBridge(QObject): s_set_text = pyqtSignal(str) s_maybe_reset_text = pyqtSignal(str) - s_set_cmd_text = pyqtSignal(str) s_question = pyqtSignal(usertypes.Question, bool) def __repr__(self): return utils.get_repr(self) - def set_cmd_text(self, text, *, log_stack=False): - """Set the command text of the statusbar. - - Args: - text: The text to set. - log_stack: ignored - """ - text = str(text) - log.message.debug(text) - self.s_set_cmd_text.emit(text) - def set_text(self, text, *, log_stack=False): """Set the normal text of the statusbar. From f0ed43ec203026952955882d533ecadaee4832fc Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 3 Oct 2016 20:57:33 +0200 Subject: [PATCH 07/87] Use message module as API for most questions --- qutebrowser/browser/webkit/downloads.py | 25 +++---- .../browser/webkit/network/networkmanager.py | 18 ++--- qutebrowser/browser/webkit/webpage.py | 68 ++++++------------ qutebrowser/mainwindow/prompt.py | 14 ++-- qutebrowser/utils/message.py | 72 ++++++++++++------- 5 files changed, 85 insertions(+), 112 deletions(-) diff --git a/qutebrowser/browser/webkit/downloads.py b/qutebrowser/browser/webkit/downloads.py index 164440687..05857f649 100644 --- a/qutebrowser/browser/webkit/downloads.py +++ b/qutebrowser/browser/webkit/downloads.py @@ -150,7 +150,7 @@ def ask_for_filename(suggested_filename, win_id, *, parent=None, suggested_filename = utils.force_encoding(suggested_filename, encoding) q = usertypes.Question(parent) - q.text = "Save file to:" + q.title = "Save file to:" q.mode = usertypes.PromptMode.text q.completed.connect(q.deleteLater) q.default = _path_suggestion(suggested_filename) @@ -382,20 +382,13 @@ class DownloadItem(QObject): else: self.set_fileobj(fileobj) - def _ask_confirm_question(self, msg): + def _ask_confirm_question(self, title, msg): """Create a Question object to be asked.""" - q = usertypes.Question(self) - q.text = msg - q.mode = usertypes.PromptMode.yesno - q.answered_yes.connect(self._create_fileobj) - q.answered_no.connect(functools.partial(self.cancel, - remove_data=False)) - q.cancelled.connect(functools.partial(self.cancel, remove_data=False)) - self.cancelled.connect(q.abort) - self.error.connect(q.abort) - message_bridge = objreg.get('message-bridge', scope='window', - window=self._win_id) - message_bridge.ask(q, blocking=False) + no_action = functools.partial(self.cancel, remove_data=False) + message.confirm_async(self._win_id, title=title, text=msg, + yes_action=self._create_fileobj, + no_action=no_action, cancel_action=no_action, + abort_on=[self.cancelled, self.error]) def _die(self, msg): """Abort the download and emit an error.""" @@ -615,13 +608,13 @@ class DownloadItem(QObject): # The file already exists, so ask the user if it should be # overwritten. txt = self._filename + " already exists. Overwrite?" - self._ask_confirm_question(txt) + self._ask_confirm_question("Overwrite existing file?", txt) # FIFO, device node, etc. Make sure we want to do this elif (os.path.exists(self._filename) and not os.path.isdir(self._filename)): txt = (self._filename + " already exists and is a special file. " "Write to this?") - self._ask_confirm_question(txt) + self._ask_confirm_question("Overwrite special file?", txt) else: self._create_fileobj() diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py index 3a02f1745..e1026a8ab 100644 --- a/qutebrowser/browser/webkit/network/networkmanager.py +++ b/qutebrowser/browser/webkit/network/networkmanager.py @@ -220,25 +220,19 @@ class NetworkManager(QNetworkAccessManager): Return: The answer the user gave or None if the prompt was cancelled. """ - q = usertypes.Question() - q.title = title - q.text = text - q.mode = mode - self.shutting_down.connect(q.abort) + abort_on = [self.shutting_down] if owner is not None: - owner.destroyed.connect(q.abort) + abort_on.append(owner.destroyed) # This might be a generic network manager, e.g. one belonging to a # DownloadManager. In this case, just skip the webview thing. if self._tab_id is not None: tab = objreg.get('tab', scope='tab', window=self._win_id, tab=self._tab_id) - tab.load_started.connect(q.abort) - bridge = objreg.get('message-bridge', scope='window', - window=self._win_id) - bridge.ask(q, blocking=True) - q.deleteLater() - return q.answer + abort_on.append(tab.load_started) + + return message.ask(win_id=self._win_id, title=title, text=text, mode=mode, + abort_on=abort_on) def shutdown(self): """Abort all running requests.""" diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py index 7f02fbb98..5cc0aa3e9 100644 --- a/qutebrowser/browser/webkit/webpage.py +++ b/qutebrowser/browser/webkit/webpage.py @@ -98,8 +98,10 @@ class BrowserPage(QWebPage): if (self._is_shutting_down or config.get('content', 'ignore-javascript-prompt')): return (False, "") - answer = self._ask('Javascript prompt', msg, - usertypes.PromptMode.text, default) + answer = message.ask(self._win_id, 'Javascript prompt', msg, + usertypes.PromptMode.text, default=default, + abort_on=[self.loadStarted, + self.shutting_down]) if answer is None: return (False, "") else: @@ -196,31 +198,6 @@ class BrowserPage(QWebPage): suggested_file) return True - def _ask(self, title, text, mode, default=None): - """Ask a blocking question in the statusbar. - - Args: - title: The title to display. - text: The text to display to the user. - mode: A PromptMode. - default: The default value to display. - - Return: - The answer the user gave or None if the prompt was cancelled. - """ - q = usertypes.Question() - q.title = title - q.text = text - q.mode = mode - q.default = default - self.loadStarted.connect(q.abort) - self.shutting_down.connect(q.abort) - bridge = objreg.get('message-bridge', scope='window', - window=self._win_id) - bridge.ask(q, blocking=True) - q.deleteLater() - return q.answer - def _show_pdfjs(self, reply): """Show the reply with pdfjs.""" try: @@ -335,11 +312,6 @@ class BrowserPage(QWebPage): } config_val = config.get(*options[feature]) if config_val == 'ask': - bridge = objreg.get('message-bridge', scope='window', - window=self._win_id) - q = usertypes.Question(bridge) - q.mode = usertypes.PromptMode.yesno - msgs = { QWebPage.Notifications: 'show notifications', QWebPage.Geolocation: 'access your location', @@ -347,30 +319,28 @@ class BrowserPage(QWebPage): host = frame.url().host() if host: - q.text = "Allow the website at {} to {}?".format( + text = "Allow the website at {} to {}?".format( frame.url().host(), msgs[feature]) else: - q.text = "Allow the website to {}?".format(msgs[feature]) + text = "Allow the website to {}?".format(msgs[feature]) yes_action = functools.partial( self.setFeaturePermission, frame, feature, QWebPage.PermissionGrantedByUser) - q.answered_yes.connect(yes_action) - no_action = functools.partial( self.setFeaturePermission, frame, feature, QWebPage.PermissionDeniedByUser) - q.answered_no.connect(no_action) - q.cancelled.connect(no_action) - - self.shutting_down.connect(q.abort) - q.completed.connect(q.deleteLater) + question = message.confirm_async(self._win_id, + yes_action=yes_action, + no_action=no_action, + cancel_action=cancel_action, + abort_on=[self.shutting_down, + self.loadStarted], + title='Permission request', + text=text) self.featurePermissionRequestCanceled.connect(functools.partial( - self.on_feature_permission_cancelled, q, frame, feature)) - self.loadStarted.connect(q.abort) - - bridge.ask(q, blocking=False) + self.on_feature_permission_cancelled, question, frame, feature)) elif config_val: self.setFeaturePermission(frame, feature, QWebPage.PermissionGrantedByUser) @@ -480,7 +450,9 @@ class BrowserPage(QWebPage): if (self._is_shutting_down or config.get('content', 'ignore-javascript-alert')): return - self._ask('Javascript alert', msg, usertypes.PromptMode.alert) + message.ask(self._win_id, 'Javascript alert', msg, + usertypes.PromptMode.alert, + abort_on=[self.loadStarted, self.shutting_down]) def javaScriptConfirm(self, frame, msg): """Override javaScriptConfirm to use the statusbar.""" @@ -490,7 +462,9 @@ class BrowserPage(QWebPage): if self._is_shutting_down: return False - ans = self._ask('Javascript confirm', msg, usertypes.PromptMode.yesno) + ans = message.ask(self._win_id, 'Javascript confirm', msg, + usertypes.PromptMode.yesno, + abort_on=[self.loadStarted, self.shutting_down]) return bool(ans) def javaScriptConsoleMessage(self, msg, line, source): diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index db2f11f4e..b124ec3fa 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -343,17 +343,11 @@ class _BasePrompt(QWidget): return utils.get_repr(self, question=self.question, constructor=True) def _init_title(self, question): - if question.title is None: - title = question.text - text = None - else: - title = question.title - text = question.text - - title_label = QLabel('{}'.format(title), self) + assert question.title is not None, question + title_label = QLabel('{}'.format(question.title), self) self._vbox.addWidget(title_label) - if text is not None: - text_label = QLabel(text) + if question.text is not None: + text_label = QLabel(question.text) self._vbox.addWidget(text_label) def accept(self, value=None): diff --git a/qutebrowser/utils/message.py b/qutebrowser/utils/message.py index 9cec02871..7eacbc75d 100644 --- a/qutebrowser/utils/message.py +++ b/qutebrowser/utils/message.py @@ -76,7 +76,21 @@ def info(message): global_bridge.show_message.emit(usertypes.MessageLevel.info, message) -def ask(win_id, message, mode, default=None): +def _build_question(title, mode, *, default=None, text=None, abort_on=()): + """Common function for ask/ask_async.""" + if not isinstance(mode, usertypes.PromptMode): + raise TypeError("Mode {} is no PromptMode member!".format(mode)) + question = usertypes.Question() + question.title = title + question.text = text + question.mode = mode + question.default = default + for sig in abort_on: + sig.connect(question.abort) + return question + + +def ask(win_id, *args, **kwargs): """Ask a modular question in the statusbar (blocking). Args: @@ -84,21 +98,21 @@ def ask(win_id, message, mode, default=None): message: The message to display to the user. mode: A PromptMode. default: The default value to display. + text: Additional text to show + abort_on: A list of signals which abort the question if emitted. Return: The answer the user gave or None if the prompt was cancelled. """ - q = usertypes.Question() - q.text = message - q.mode = mode - q.default = default + question = _build_question(*args, **kwargs) bridge = objreg.get('message-bridge', scope='window', window=win_id) - bridge.ask(q, blocking=True) - q.deleteLater() - return q.answer + bridge.ask(question, blocking=True) + answer = question.answer + question.deleteLater() + return answer -def ask_async(win_id, message, mode, handler, default=None): +def ask_async(win_id, text, mode, handler, **kwargs): """Ask an async question in the statusbar. Args: @@ -107,20 +121,17 @@ def ask_async(win_id, message, mode, handler, default=None): mode: A PromptMode. handler: The function to get called with the answer as argument. default: The default value to display. + text: Additional text to show. """ - if not isinstance(mode, usertypes.PromptMode): - raise TypeError("Mode {} is no PromptMode member!".format(mode)) - q = usertypes.Question() - q.text = message - q.mode = mode - q.default = default - q.answered.connect(handler) - q.completed.connect(q.deleteLater) + question = _build_question(text, mode, **kwargs) + question.answered.connect(handler) + question.completed.connect(question.deleteLater) bridge = objreg.get('message-bridge', scope='window', window=win_id) - bridge.ask(q, blocking=False) + bridge.ask(question, blocking=False) -def confirm_async(win_id, message, yes_action, no_action=None, default=None): +def confirm_async(win_id, yes_action, no_action=None, cancel_action=None, + *args, **kwargs): """Ask a yes/no question to the user and execute the given actions. Args: @@ -128,18 +139,25 @@ def confirm_async(win_id, message, yes_action, no_action=None, default=None): message: The message to display to the user. yes_action: Callable to be called when the user answered yes. no_action: Callable to be called when the user answered no. + cancel_action: Callable to be called when the user cancelled the + question. default: True/False to set a default value, or None. + text: Additional text to show. + + Return: + The question object. """ - q = usertypes.Question() - q.text = message - q.mode = usertypes.PromptMode.yesno - q.default = default - q.answered_yes.connect(yes_action) + question = self._build_question(*args, **kwargs) + question.answered_yes.connect(yes_action) if no_action is not None: - q.answered_no.connect(no_action) - q.completed.connect(q.deleteLater) + question.answered_no.connect(no_action) + if cancel_action is not None: + question.cancelled.connect(cancel_action) + + question.completed.connect(q.deleteLater) bridge = objreg.get('message-bridge', scope='window', window=win_id) - bridge.ask(q, blocking=False) + bridge.ask(question, blocking=False) + return question class GlobalMessageBridge(QObject): From d74b0109c7642ebfc83a9a2c4f6b873112907b8c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 3 Oct 2016 21:27:13 +0200 Subject: [PATCH 08/87] Add key hints --- qutebrowser/mainwindow/prompt.py | 98 +++++++++++++++++++++++++------- 1 file changed, 79 insertions(+), 19 deletions(-) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index b124ec3fa..a2ac7fd39 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -19,15 +19,16 @@ """Showing prompts above the statusbar.""" -import sip +import html import collections +import sip 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, log, utils, qtutils +from qutebrowser.utils import usertypes, log, utils, qtutils, objreg from qutebrowser.keyinput import modeman from qutebrowser.commands import cmdutils @@ -338,6 +339,7 @@ class _BasePrompt(QWidget): self.question = question self._vbox = QVBoxLayout(self) self._vbox.setSpacing(15) + self._key_grid = None def __repr__(self): return utils.get_repr(self, question=self.question, constructor=True) @@ -350,12 +352,49 @@ class _BasePrompt(QWidget): text_label = QLabel(question.text) self._vbox.addWidget(text_label) + def _init_key_label(self): + # Remove old grid + if self._key_grid is not None: + self._key_grid.hide() + self._vbox.removeWidget(self._key_grid) + + self._key_grid = QGridLayout() + self._key_grid.setVerticalSpacing(0) + + key_config = objreg.get('key-config') + all_bindings = key_config.get_reverse_bindings_for(self.KEY_MODE.name) + labels = [] + + for cmd, text in self._allowed_commands(): + bindings = all_bindings.get(cmd, []) + if bindings: + binding = None + preferred = ['', ''] + for pref in preferred: + if pref in bindings: + binding = pref + if binding is None: + binding = bindings[0] + key_label = QLabel('{}'.format(html.escape(binding))) + text_label = QLabel(text) + labels.append((key_label, text_label)) + + for i, (key_label, text_label) in enumerate(labels): + self._key_grid.addWidget(key_label, i, 0) + self._key_grid.addWidget(text_label, i, 1) + + self._vbox.addLayout(self._key_grid) + def accept(self, value=None): raise NotImplementedError def open_download(self, _cmdline): raise UnsupportedOperationError + def _allowed_commands(self): + """Get the commands we could run as response to this message.""" + raise NotImplementedError + class LineEditPrompt(_BasePrompt): @@ -367,28 +406,22 @@ class LineEditPrompt(_BasePrompt): if question.default: self._lineedit.setText(question.default) self.setFocusProxy(self._lineedit) + self._init_key_label() def accept(self, value=None): text = value if value is not None else self._lineedit.text() self.question.answer = text return True + def _allowed_commands(self): + """Get the commands we could run as response to this message.""" + return [('prompt-accept', 'Accept'), ('leave-mode', 'Abort')] + class DownloadFilenamePrompt(LineEditPrompt): # FIXME have a FilenamePrompt - 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_config = objreg.get('key-config') -# all_bindings = key_config.get_reverse_bindings_for(key_mode.name) -# bindings = all_bindings.get('prompt-open-download', []) -# if bindings: -# text += ' ({} to open)'.format(bindings[0]) - - def accept(self, value=None): text = value if value is not None else self._lineedit.text() self.question.answer = usertypes.FileDownloadTarget(text) @@ -400,6 +433,14 @@ class DownloadFilenamePrompt(LineEditPrompt): 'download open') self.question.done() + def _allowed_commands(self): + cmds = [ + ('prompt-accept', 'Accept'), + ('leave-mode', 'Abort'), + ('prompt-open-download', "Open download"), + ] + return cmds + class AuthenticationPrompt(_BasePrompt): @@ -420,6 +461,7 @@ class AuthenticationPrompt(_BasePrompt): grid.addWidget(password_label, 2, 0) grid.addWidget(self._password_lineedit, 2, 1) self._vbox.addLayout(grid) + self._init_key_label() assert not question.default, question.default self.setFocusProxy(self._user_lineedit) @@ -437,12 +479,21 @@ class AuthenticationPrompt(_BasePrompt): # 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() + self._init_key_label() return False else: self.question.answer = AuthTuple(self._user_lineedit.text(), self._password_lineedit.text()) return True + def _allowed_commands(self): + if self._user_lineedit.hasFocus(): + cmds = [('prompt-accept', "Switch to password field")] + else: + cmds = [('prompt-accept', "Accept")] + cmds.append(('leave-mode', "Abort")) + return cmds + class YesNoPrompt(_BasePrompt): @@ -451,10 +502,7 @@ class YesNoPrompt(_BasePrompt): def __init__(self, question, parent=None): super().__init__(question, parent) self._init_title(question) - # FIXME - # "Enter/y: yes" - # "n: no" - # (depending on default) + self._init_key_label() def accept(self, value=None): if value is None: @@ -467,17 +515,29 @@ class YesNoPrompt(_BasePrompt): raise Error("Invalid value {} - expected yes/no!".format(value)) return True + def _allowed_commands(self): + cmds = [ + ('prompt-accept', + "Use default ({})".format(self.question.default)), + ('prompt-accept yes', "Yes"), + ('prompt-accept no', "No"), + ('leave-mode', "Abort"), + ] + return cmds + class AlertPrompt(_BasePrompt): def __init__(self, question, parent=None): super().__init__(question, parent) self._init_title(question) - # FIXME - # Enter: acknowledge + self._init_key_label() def accept(self, value=None): if value is not None: raise Error("No value is permitted with alert prompts!") # Doing nothing otherwise return True + + def _allowed_commands(self): + return [('prompt-accept', "Hide")] From 47208eb022327f9fdab606f31b201298d453b57e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 3 Oct 2016 21:29:20 +0200 Subject: [PATCH 09/87] Fix :prompt-open-download --- qutebrowser/mainwindow/prompt.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index a2ac7fd39..774060030 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -246,7 +246,7 @@ class PromptContainer(QWidget): question.cancel() - @cmdutils.register(instance='prompter', hide=True, scope='window', + @cmdutils.register(instance='prompt-container', hide=True, scope='window', modes=[usertypes.KeyMode.prompt], maxsplit=0) def prompt_open_download(self, cmdline: str=None): """Immediately open a download. @@ -309,7 +309,7 @@ class PromptContainer(QWidget): usertypes.PromptMode.alert: AlertPrompt, } klass = classes[question.mode] - self._show_prompt(klass(question)) + self._show_prompt(klass(question, self._win_id)) if blocking: loop = qtutils.EventLoop() self._loops.append(loop) @@ -334,9 +334,10 @@ class _BasePrompt(QWidget): KEY_MODE = usertypes.KeyMode.prompt - def __init__(self, question, parent=None): + def __init__(self, question, win_id, parent=None): super().__init__(parent) self.question = question + self._win_id = win_id self._vbox = QVBoxLayout(self) self._vbox.setSpacing(15) self._key_grid = None @@ -398,8 +399,8 @@ class _BasePrompt(QWidget): class LineEditPrompt(_BasePrompt): - def __init__(self, question, parent=None): - super().__init__(question, parent) + def __init__(self, question, win_id, parent=None): + super().__init__(question, win_id, parent) self._lineedit = QLineEdit(self) self._init_title(question) self._vbox.addWidget(self._lineedit) @@ -444,8 +445,8 @@ class DownloadFilenamePrompt(LineEditPrompt): class AuthenticationPrompt(_BasePrompt): - def __init__(self, question, parent=None): - super().__init__(question, parent) + def __init__(self, question, win_id, parent=None): + super().__init__(question, win_id, parent) self._init_title(question) user_label = QLabel("Username:", self) @@ -499,8 +500,8 @@ class YesNoPrompt(_BasePrompt): KEY_MODE = usertypes.KeyMode.yesno - def __init__(self, question, parent=None): - super().__init__(question, parent) + def __init__(self, question, win_id, parent=None): + super().__init__(question, win_id, parent) self._init_title(question) self._init_key_label() @@ -528,8 +529,8 @@ class YesNoPrompt(_BasePrompt): class AlertPrompt(_BasePrompt): - def __init__(self, question, parent=None): - super().__init__(question, parent) + def __init__(self, question, win_id, parent=None): + super().__init__(question, win_id, parent) self._init_title(question) self._init_key_label() From 6d7748f05fbda84183ec017d6ee818a1ce309545 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 3 Oct 2016 22:25:51 +0200 Subject: [PATCH 10/87] Add FilenamePrompt --- qutebrowser/mainwindow/prompt.py | 62 ++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 774060030..210f533ff 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -19,13 +19,15 @@ """Showing prompts above the statusbar.""" +import os.path import html import collections import sip from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QTimer from PyQt5.QtWidgets import (QWidget, QGridLayout, QVBoxLayout, QLineEdit, - QLabel, QSpacerItem, QWidgetItem) + QLabel, QSpacerItem, QWidgetItem, + QFileSystemModel, QTreeView) from qutebrowser.config import style, config from qutebrowser.utils import usertypes, log, utils, qtutils, objreg @@ -419,7 +421,63 @@ class LineEditPrompt(_BasePrompt): return [('prompt-accept', 'Accept'), ('leave-mode', 'Abort')] -class DownloadFilenamePrompt(LineEditPrompt): +class FilenamePrompt(_BasePrompt): + + def __init__(self, question, win_id, parent=None): + super().__init__(question, win_id, parent) + self._init_title(question) + self._init_fileview() + self._set_fileview_root(question.default) + + self._lineedit = QLineEdit(self) + self._lineedit.textChanged.connect(self._set_fileview_root) + self._vbox.addWidget(self._lineedit) + + if question.default: + self._lineedit.setText(question.default) + self.setFocusProxy(self._lineedit) + self._init_key_label() + + def sizeHint(self): + orig = super().sizeHint() + orig.setWidth(orig.width() * 3) + return orig + + @pyqtSlot(str) + def _set_fileview_root(self, path): + try: + if os.path.isdir(path): + path = path + elif os.path.isdir(os.path.basename(path)): + path = os.path.basename(path) + else: + path = None + except OSError: + path = None + + if path is None: + return + + root = self._file_model.setRootPath(path) + self._file_view.setRootIndex(root) + + def _init_fileview(self): + self._file_view = QTreeView(self) + self._file_model = QFileSystemModel(self) + self._file_view.setModel(self._file_model) + self._vbox.addWidget(self._file_view) + + def accept(self, value=None): + text = value if value is not None else self._lineedit.text() + self.question.answer = text + return True + + def _allowed_commands(self): + """Get the commands we could run as response to this message.""" + return [('prompt-accept', 'Accept'), ('leave-mode', 'Abort')] + + +class DownloadFilenamePrompt(FilenamePrompt): # FIXME have a FilenamePrompt From 75bb2265be938f8076459133c43d509724674047 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 4 Oct 2016 11:12:22 +0200 Subject: [PATCH 11/87] Take text as pos. argument for _build_question --- qutebrowser/browser/webkit/webpage.py | 7 ++++--- qutebrowser/mainwindow/mainwindow.py | 2 +- qutebrowser/utils/message.py | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py index 5cc0aa3e9..7945a10e8 100644 --- a/qutebrowser/browser/webkit/webpage.py +++ b/qutebrowser/browser/webkit/webpage.py @@ -99,7 +99,8 @@ class BrowserPage(QWebPage): config.get('content', 'ignore-javascript-prompt')): return (False, "") answer = message.ask(self._win_id, 'Javascript prompt', msg, - usertypes.PromptMode.text, default=default, + mode=usertypes.PromptMode.text, + default=default, abort_on=[self.loadStarted, self.shutting_down]) if answer is None: @@ -451,7 +452,7 @@ class BrowserPage(QWebPage): config.get('content', 'ignore-javascript-alert')): return message.ask(self._win_id, 'Javascript alert', msg, - usertypes.PromptMode.alert, + mode=usertypes.PromptMode.alert, abort_on=[self.loadStarted, self.shutting_down]) def javaScriptConfirm(self, frame, msg): @@ -463,7 +464,7 @@ class BrowserPage(QWebPage): if self._is_shutting_down: return False ans = message.ask(self._win_id, 'Javascript confirm', msg, - usertypes.PromptMode.yesno, + mode=usertypes.PromptMode.yesno, abort_on=[self.loadStarted, self.shutting_down]) return bool(ans) diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index 37be132ce..c38ff1548 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -521,7 +521,7 @@ class MainWindow(QWidget): if quit_texts or 'always' in confirm_quit: text = '\n'.join(['Really quit?'] + quit_texts) confirmed = message.ask(self.win_id, text, - usertypes.PromptMode.yesno, + mode=usertypes.PromptMode.yesno, default=True) # Stop asking if the user cancels if not confirmed: diff --git a/qutebrowser/utils/message.py b/qutebrowser/utils/message.py index 7eacbc75d..60526578b 100644 --- a/qutebrowser/utils/message.py +++ b/qutebrowser/utils/message.py @@ -76,7 +76,7 @@ def info(message): global_bridge.show_message.emit(usertypes.MessageLevel.info, message) -def _build_question(title, mode, *, default=None, text=None, abort_on=()): +def _build_question(title, text=None, *, mode, default=None, abort_on=()): """Common function for ask/ask_async.""" if not isinstance(mode, usertypes.PromptMode): raise TypeError("Mode {} is no PromptMode member!".format(mode)) @@ -123,7 +123,7 @@ def ask_async(win_id, text, mode, handler, **kwargs): default: The default value to display. text: Additional text to show. """ - question = _build_question(text, mode, **kwargs) + question = _build_question(text, mode=mode, **kwargs) question.answered.connect(handler) question.completed.connect(question.deleteLater) bridge = objreg.get('message-bridge', scope='window', window=win_id) From 04d24e3d94a8a0c20e803aab6515f735aafec737 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 4 Oct 2016 11:16:12 +0200 Subject: [PATCH 12/87] Fix permission question --- qutebrowser/browser/webkit/webpage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py index 7945a10e8..5c5bef83f 100644 --- a/qutebrowser/browser/webkit/webpage.py +++ b/qutebrowser/browser/webkit/webpage.py @@ -335,7 +335,7 @@ class BrowserPage(QWebPage): question = message.confirm_async(self._win_id, yes_action=yes_action, no_action=no_action, - cancel_action=cancel_action, + cancel_action=no_action, abort_on=[self.shutting_down, self.loadStarted], title='Permission request', From 9c34a64f818ae3e2fad18b33d90a959afa242438 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 4 Oct 2016 11:17:37 +0200 Subject: [PATCH 13/87] self._build_question fail --- qutebrowser/utils/message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/utils/message.py b/qutebrowser/utils/message.py index 60526578b..2e9a23c2d 100644 --- a/qutebrowser/utils/message.py +++ b/qutebrowser/utils/message.py @@ -147,7 +147,7 @@ def confirm_async(win_id, yes_action, no_action=None, cancel_action=None, Return: The question object. """ - question = self._build_question(*args, **kwargs) + question = _build_question(*args, **kwargs) question.answered_yes.connect(yes_action) if no_action is not None: question.answered_no.connect(no_action) From 7d38430d2d3dad4069f8cf520aa82a7071d3b932 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 4 Oct 2016 11:20:21 +0200 Subject: [PATCH 14/87] Fix confirm_async --- qutebrowser/utils/message.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qutebrowser/utils/message.py b/qutebrowser/utils/message.py index 2e9a23c2d..34b5d7a67 100644 --- a/qutebrowser/utils/message.py +++ b/qutebrowser/utils/message.py @@ -147,6 +147,7 @@ def confirm_async(win_id, yes_action, no_action=None, cancel_action=None, Return: The question object. """ + kwargs['mode'] = usertypes.PromptMode.yesno question = _build_question(*args, **kwargs) question.answered_yes.connect(yes_action) if no_action is not None: @@ -154,7 +155,7 @@ def confirm_async(win_id, yes_action, no_action=None, cancel_action=None, if cancel_action is not None: question.cancelled.connect(cancel_action) - question.completed.connect(q.deleteLater) + question.completed.connect(question.deleteLater) bridge = objreg.get('message-bridge', scope='window', window=win_id) bridge.ask(question, blocking=False) return question From 051b31d1019fd4f871f456a10c5a5305373554bc Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 4 Oct 2016 11:24:20 +0200 Subject: [PATCH 15/87] Fix reinitializing of key label --- qutebrowser/mainwindow/prompt.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 210f533ff..5863055c6 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -343,6 +343,7 @@ class _BasePrompt(QWidget): self._vbox = QVBoxLayout(self) self._vbox.setSpacing(15) self._key_grid = None + self._key_grid_item = None def __repr__(self): return utils.get_repr(self, question=self.question, constructor=True) @@ -358,8 +359,7 @@ class _BasePrompt(QWidget): def _init_key_label(self): # Remove old grid if self._key_grid is not None: - self._key_grid.hide() - self._vbox.removeWidget(self._key_grid) + self._vbox.removeItem(self._key_grid_item) self._key_grid = QGridLayout() self._key_grid.setVerticalSpacing(0) @@ -387,6 +387,7 @@ class _BasePrompt(QWidget): self._key_grid.addWidget(text_label, i, 1) self._vbox.addLayout(self._key_grid) + self._key_grid_item = self._vbox.itemAt(self._vbox.count() - 1) def accept(self, value=None): raise NotImplementedError From 433afe865861fa6cf5716f8bb19024d4382ace93 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 4 Oct 2016 11:25:31 +0200 Subject: [PATCH 16/87] Handle invalid values with :prompt-accept --- qutebrowser/mainwindow/prompt.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 5863055c6..764399e70 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -32,7 +32,7 @@ from PyQt5.QtWidgets import (QWidget, QGridLayout, QVBoxLayout, QLineEdit, from qutebrowser.config import style, config from qutebrowser.utils import usertypes, log, utils, qtutils, objreg from qutebrowser.keyinput import modeman -from qutebrowser.commands import cmdutils +from qutebrowser.commands import cmdutils, cmdexc AuthTuple = collections.namedtuple('AuthTuple', ['user', 'password']) @@ -215,7 +215,10 @@ class PromptContainer(QWidget): 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) + try: + done = self._prompt.accept(value) + except Error as e: + raise cmdexc.CommandError(str(e)) if done: self._prompt.question.done() modeman.maybe_leave(self._win_id, self._prompt.KEY_MODE, From f4be35c9813b96b2f6531cccfc0524256f110fd3 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 4 Oct 2016 11:40:38 +0200 Subject: [PATCH 17/87] Add a LineEdit subclass for prompts --- qutebrowser/mainwindow/prompt.py | 41 +++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 764399e70..447e1c46d 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -99,10 +99,6 @@ class PromptContainer(QWidget): color: {{ color['statusbar.fg.prompt'] }}; background-color: {{ color['statusbar.bg.prompt'] }}; } - - QLineEdit { - border: 1px solid grey; - } """ update_geometry = pyqtSignal() @@ -333,6 +329,35 @@ class PromptContainer(QWidget): question.completed.connect(self._pop_later) +class LineEdit(QLineEdit): + + def __init__(self, parent=None): + super().__init__(parent) + self.setStyleSheet(""" + QLineEdit { + border: 1px solid grey; + background-color: transparent; + } + """) + self.setAttribute(Qt.WA_MacShowFocusRect, False) + + def keyPressEvent(self, e): + """Override keyPressEvent to paste primary selection on Shift + Ins.""" + if e.key() == Qt.Key_Insert and e.modifiers() == Qt.ShiftModifier: + try: + text = utils.get_clipboard(selection=True) + except utils.ClipboardError: + pass + else: + e.accept() + self.insert(text) + return + super().keyPressEvent(e) + + def __repr__(self): + return utils.get_repr(self) + + class _BasePrompt(QWidget): """Base class for all prompts.""" @@ -407,7 +432,7 @@ class LineEditPrompt(_BasePrompt): def __init__(self, question, win_id, parent=None): super().__init__(question, win_id, parent) - self._lineedit = QLineEdit(self) + self._lineedit = LineEdit(self) self._init_title(question) self._vbox.addWidget(self._lineedit) if question.default: @@ -433,7 +458,7 @@ class FilenamePrompt(_BasePrompt): self._init_fileview() self._set_fileview_root(question.default) - self._lineedit = QLineEdit(self) + self._lineedit = LineEdit(self) self._lineedit.textChanged.connect(self._set_fileview_root) self._vbox.addWidget(self._lineedit) @@ -512,10 +537,10 @@ class AuthenticationPrompt(_BasePrompt): self._init_title(question) user_label = QLabel("Username:", self) - self._user_lineedit = QLineEdit(self) + self._user_lineedit = LineEdit(self) password_label = QLabel("Password:", self) - self._password_lineedit = QLineEdit(self) + self._password_lineedit = LineEdit(self) self._password_lineedit.setEchoMode(QLineEdit.Password) grid = QGridLayout() From d92674dd04a4ad71efb35430f98bf7e1053c4193 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 4 Oct 2016 11:43:01 +0200 Subject: [PATCH 18/87] Remove test_prompt.py --- scripts/dev/check_coverage.py | 2 - .../unit/mainwindow/statusbar/test_prompt.py | 57 ------------------- 2 files changed, 59 deletions(-) delete mode 100644 tests/unit/mainwindow/statusbar/test_prompt.py diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py index e9bb675b0..698e04b79 100644 --- a/scripts/dev/check_coverage.py +++ b/scripts/dev/check_coverage.py @@ -115,8 +115,6 @@ PERFECT_FILES = [ 'qutebrowser/mainwindow/statusbar/tabindex.py'), ('tests/unit/mainwindow/statusbar/test_textbase.py', 'qutebrowser/mainwindow/statusbar/textbase.py'), - ('tests/unit/mainwindow/statusbar/test_prompt.py', - 'qutebrowser/mainwindow/statusbar/prompt.py'), ('tests/unit/mainwindow/statusbar/test_url.py', 'qutebrowser/mainwindow/statusbar/url.py'), ('tests/unit/mainwindow/test_messageview.py', diff --git a/tests/unit/mainwindow/statusbar/test_prompt.py b/tests/unit/mainwindow/statusbar/test_prompt.py deleted file mode 100644 index 86c6122b1..000000000 --- a/tests/unit/mainwindow/statusbar/test_prompt.py +++ /dev/null @@ -1,57 +0,0 @@ -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: - -# Copyright 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 . - -"""Test Prompt widget.""" - -import sip - -import pytest - -from qutebrowser.mainwindow.statusbar.prompt import Prompt -from qutebrowser.utils import objreg - - -@pytest.fixture -def prompt(qtbot, win_registry): - prompt = Prompt(0) - qtbot.addWidget(prompt) - - yield prompt - - # If we don't clean up here, this test will remove 'prompter' from the - # objreg at some point in the future, which will cause some other test to - # fail. - sip.delete(prompt) - - -def test_prompt(prompt): - prompt.show() - objreg.get('prompt', scope='window', window=0) - objreg.get('prompter', scope='window', window=0) - - -@pytest.mark.xfail(reason="This test is broken and I don't get why") -def test_resizing(fake_statusbar, prompt): - fake_statusbar.hbox.addWidget(prompt) - - prompt.txt.setText("Blah?") - old_width = prompt.lineedit.width() - - prompt.lineedit.setText("Hello World" * 100) - assert prompt.lineedit.width() > old_width From a8d1187ba0eda4b3641d170d72aca6ffd779a3ef Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 4 Oct 2016 18:12:54 +0200 Subject: [PATCH 19/87] Fix keyhint tests --- tests/unit/misc/test_keyhints.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/unit/misc/test_keyhints.py b/tests/unit/misc/test_keyhints.py index 005cce1a6..a4093382c 100644 --- a/tests/unit/misc/test_keyhints.py +++ b/tests/unit/misc/test_keyhints.py @@ -50,10 +50,13 @@ def keyhint(qtbot, config_stub, key_config_stub): 'colors': { 'keyhint.fg': 'white', 'keyhint.fg.suffix': 'yellow', - 'keyhint.bg': 'black' + 'keyhint.bg': 'black', }, 'fonts': {'keyhint': 'Comic Sans'}, - 'ui': {'keyhint-blacklist': '', 'status-position': 'bottom'}, + 'ui': { + 'keyhint-blacklist': '', + 'status-position': 'bottom', + }, } keyhint = KeyHintView(0, None) qtbot.add_widget(keyhint) From fe230219a9b176301ab76d2e0c398e4e22e59a6b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 4 Oct 2016 18:34:25 +0200 Subject: [PATCH 20/87] Fix lint --- .../browser/webkit/network/networkmanager.py | 4 +-- qutebrowser/browser/webkit/webpage.py | 5 ++-- qutebrowser/mainwindow/prompt.py | 29 ++++++++++++++----- qutebrowser/utils/message.py | 4 +-- 4 files changed, 29 insertions(+), 13 deletions(-) diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py index e1026a8ab..4c5a91fa4 100644 --- a/qutebrowser/browser/webkit/network/networkmanager.py +++ b/qutebrowser/browser/webkit/network/networkmanager.py @@ -231,8 +231,8 @@ class NetworkManager(QNetworkAccessManager): tab=self._tab_id) abort_on.append(tab.load_started) - return message.ask(win_id=self._win_id, title=title, text=text, mode=mode, - abort_on=abort_on) + return message.ask(win_id=self._win_id, title=title, text=text, + mode=mode, abort_on=abort_on) def shutdown(self): """Abort all running requests.""" diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py index 5c5bef83f..e5cbf8e01 100644 --- a/qutebrowser/browser/webkit/webpage.py +++ b/qutebrowser/browser/webkit/webpage.py @@ -340,8 +340,9 @@ class BrowserPage(QWebPage): self.loadStarted], title='Permission request', text=text) - self.featurePermissionRequestCanceled.connect(functools.partial( - self.on_feature_permission_cancelled, question, frame, feature)) + self.featurePermissionRequestCanceled.connect( + functools.partial(self.on_feature_permission_cancelled, + question, frame, feature)) elif config_val: self.setFeaturePermission(frame, feature, QWebPage.PermissionGrantedByUser) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 447e1c46d..88a8d6fd9 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -26,10 +26,9 @@ import collections import sip from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QTimer from PyQt5.QtWidgets import (QWidget, QGridLayout, QVBoxLayout, QLineEdit, - QLabel, QSpacerItem, QWidgetItem, - QFileSystemModel, QTreeView) + QLabel, QWidgetItem, QFileSystemModel, QTreeView) -from qutebrowser.config import style, config +from qutebrowser.config import style from qutebrowser.utils import usertypes, log, utils, qtutils, objreg from qutebrowser.keyinput import modeman from qutebrowser.commands import cmdutils, cmdexc @@ -246,7 +245,6 @@ class PromptContainer(QWidget): if question.answer is None and not question.is_aborted: question.cancel() - @cmdutils.register(instance='prompt-container', hide=True, scope='window', modes=[usertypes.KeyMode.prompt], maxsplit=0) def prompt_open_download(self, cmdline: str=None): @@ -331,6 +329,8 @@ class PromptContainer(QWidget): class LineEdit(QLineEdit): + """A line edit used in prompts.""" + def __init__(self, parent=None): super().__init__(parent) self.setStyleSheet(""" @@ -430,6 +430,8 @@ class _BasePrompt(QWidget): class LineEditPrompt(_BasePrompt): + """A prompt for a single text value.""" + def __init__(self, question, win_id, parent=None): super().__init__(question, win_id, parent) self._lineedit = LineEdit(self) @@ -452,6 +454,8 @@ class LineEditPrompt(_BasePrompt): class FilenamePrompt(_BasePrompt): + """A prompt for a filename.""" + def __init__(self, question, win_id, parent=None): super().__init__(question, win_id, parent) self._init_title(question) @@ -468,12 +472,17 @@ class FilenamePrompt(_BasePrompt): self._init_key_label() def sizeHint(self): + """Get some more width. + + FIXME do this properly... + """ orig = super().sizeHint() orig.setWidth(orig.width() * 3) return orig @pyqtSlot(str) def _set_fileview_root(self, path): + """Set the root path for the file display.""" try: if os.path.isdir(path): path = path @@ -508,7 +517,7 @@ class FilenamePrompt(_BasePrompt): class DownloadFilenamePrompt(FilenamePrompt): - # FIXME have a FilenamePrompt + """A prompt for a filename for downloads.""" def accept(self, value=None): text = value if value is not None else self._lineedit.text() @@ -532,6 +541,8 @@ class DownloadFilenamePrompt(FilenamePrompt): class AuthenticationPrompt(_BasePrompt): + """A prompt for username/password.""" + def __init__(self, question, win_id, parent=None): super().__init__(question, win_id, parent) self._init_title(question) @@ -564,8 +575,8 @@ class AuthenticationPrompt(_BasePrompt): 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. + # 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() self._init_key_label() return False @@ -585,6 +596,8 @@ class AuthenticationPrompt(_BasePrompt): class YesNoPrompt(_BasePrompt): + """A prompt with yes/no answers.""" + KEY_MODE = usertypes.KeyMode.yesno def __init__(self, question, win_id, parent=None): @@ -616,6 +629,8 @@ class YesNoPrompt(_BasePrompt): class AlertPrompt(_BasePrompt): + """A prompt without any answer possibility.""" + def __init__(self, question, win_id, parent=None): super().__init__(question, win_id, parent) self._init_title(question) diff --git a/qutebrowser/utils/message.py b/qutebrowser/utils/message.py index 34b5d7a67..287c8fb40 100644 --- a/qutebrowser/utils/message.py +++ b/qutebrowser/utils/message.py @@ -104,7 +104,7 @@ def ask(win_id, *args, **kwargs): Return: The answer the user gave or None if the prompt was cancelled. """ - question = _build_question(*args, **kwargs) + question = _build_question(*args, **kwargs) # pylint: disable=missing-kwoa bridge = objreg.get('message-bridge', scope='window', window=win_id) bridge.ask(question, blocking=True) answer = question.answer @@ -148,7 +148,7 @@ def confirm_async(win_id, yes_action, no_action=None, cancel_action=None, The question object. """ kwargs['mode'] = usertypes.PromptMode.yesno - question = _build_question(*args, **kwargs) + question = _build_question(*args, **kwargs) # pylint: disable=missing-kwoa question.answered_yes.connect(yes_action) if no_action is not None: question.answered_no.connect(no_action) From fe59e0618f615738a0e7d083c7e635ebdc4239d6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 26 Oct 2016 19:22:02 +0200 Subject: [PATCH 21/87] Rename download_open function --- qutebrowser/mainwindow/prompt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 88a8d6fd9..2a5138122 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -420,7 +420,7 @@ class _BasePrompt(QWidget): def accept(self, value=None): raise NotImplementedError - def open_download(self, _cmdline): + def download_open(self, _cmdline): raise UnsupportedOperationError def _allowed_commands(self): From e110f3b590e4d3fb8c26e0adffd05614995a17bc Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 26 Oct 2016 19:27:15 +0200 Subject: [PATCH 22/87] Get rid of StatusBar._set_prompt_active --- qutebrowser/mainwindow/statusbar/bar.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index fd88cf4ca..cca640c93 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -208,16 +208,6 @@ class StatusBar(QWidget): """Getter for self.prompt_active, so it can be used as Qt property.""" return self._prompt_active - def _set_prompt_active(self, val): - """Setter for self.prompt_active. - - Re-set the stylesheet after setting the value, so everything gets - updated by Qt properly. - """ - log.statusbar.debug("Setting prompt_active to {}".format(val)) - self._prompt_active = val - self.setStyleSheet(style.get_stylesheet(self.STYLESHEET)) - @pyqtProperty(bool) def command_active(self): """Getter for self.command_active, so it can be used as Qt property.""" @@ -245,6 +235,9 @@ class StatusBar(QWidget): if mode == usertypes.KeyMode.command: log.statusbar.debug("Setting command_active to {}".format(val)) self._command_active = val + elif mode in [usertypes.KeyMode.prompt, usertypes.KeyMode.yesno]: + log.statusbar.debug("Setting prompt_active to {}".format(val)) + self._prompt_active = val elif mode == usertypes.KeyMode.caret: tab = objreg.get('tabbed-browser', scope='window', window=self._win_id).currentWidget() @@ -291,7 +284,9 @@ class StatusBar(QWidget): self._set_mode_text(mode.name) if mode in [usertypes.KeyMode.insert, usertypes.KeyMode.command, - usertypes.KeyMode.caret]: + usertypes.KeyMode.caret, + usertypes.KeyMode.prompt, + usertypes.KeyMode.yesno]: self.set_mode_active(mode, True) @pyqtSlot(usertypes.KeyMode, usertypes.KeyMode) @@ -306,7 +301,9 @@ class StatusBar(QWidget): self.txt.set_text(self.txt.Text.normal, '') if old_mode in [usertypes.KeyMode.insert, usertypes.KeyMode.command, - usertypes.KeyMode.caret]: + usertypes.KeyMode.caret, + usertypes.KeyMode.prompt, + usertypes.KeyMode.yesno]: self.set_mode_active(old_mode, False) def resizeEvent(self, e): From 80caed464256b835f1094cfb1b42899d81ab7175 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 26 Oct 2016 19:36:08 +0200 Subject: [PATCH 23/87] Add missing docstring --- qutebrowser/mainwindow/prompt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 2a5138122..c56196db1 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -421,6 +421,7 @@ class _BasePrompt(QWidget): raise NotImplementedError def download_open(self, _cmdline): + """Open the download directly if this is a download prompt.""" raise UnsupportedOperationError def _allowed_commands(self): From 67cb3cd23e71dc7e83f9ad6ba4c5009fc2d3e711 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 26 Oct 2016 19:46:10 +0200 Subject: [PATCH 24/87] tests: Fix waiting for questions with a text --- tests/end2end/features/downloads.feature | 6 +++--- tests/end2end/features/misc.feature | 2 +- tests/end2end/features/test_downloads_bdd.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/end2end/features/downloads.feature b/tests/end2end/features/downloads.feature index 18a65dfbc..588aa9be8 100644 --- a/tests/end2end/features/downloads.feature +++ b/tests/end2end/features/downloads.feature @@ -63,7 +63,7 @@ Feature: Downloading things from a website. And I set storage -> prompt-download-directory to true And I open data/downloads/issue1243.html And I hint with args "links download" and follow a - And I wait for "Asking question text='Save file to:'>, *" in the log + And I wait for "Asking question text=None title='Save file to:'>, *" in the log Then the error "Download error: No handler found for qute://!" should be shown Scenario: Downloading a data: link (issue 1214) @@ -71,7 +71,7 @@ Feature: Downloading things from a website. And I set storage -> prompt-download-directory to true And I open data/downloads/issue1214.html And I hint with args "links download" and follow a - And I wait for "Asking question text='Save file to:'>, *" in the log + And I wait for "Asking question text=None title='Save file to:'>, *" in the log And I run :leave-mode Then no crash should happen @@ -338,7 +338,7 @@ Feature: Downloading things from a website. When I set storage -> prompt-download-directory to true And I open data/downloads/issue1725.html And I run :click-element id long-link - And I wait for "Asking question text='Save file to:'>, *" in the log + And I wait for "Asking question text=None title='Save file to:'>, *" in the log And I directly open the download And I wait until the download is finished Then "Opening * with [*python*]" should be logged diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index 50a436680..e0b60f9af 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -324,7 +324,7 @@ Feature: Various utility commands. And I open data/misc/test.pdf And I wait for "[qute://pdfjs/*] PDF * (PDF.js: *)" in the log And I run :jseval document.getElementById("download").click() - And I wait for "Asking question text='Save file to:'>, *" in the log + And I wait for "Asking question text=None title='Save file to:'>, *" in the log And I run :leave-mode Then no crash should happen diff --git a/tests/end2end/features/test_downloads_bdd.py b/tests/end2end/features/test_downloads_bdd.py index 3ce6790bb..76e1584ab 100644 --- a/tests/end2end/features/test_downloads_bdd.py +++ b/tests/end2end/features/test_downloads_bdd.py @@ -31,8 +31,8 @@ pytestmark = pytest.mark.qtwebengine_todo("Downloads not implemented yet", PROMPT_MSG = ("Asking question " - "text='Save file to:'>, *") + "default={!r} mode= text=None " + "title='Save file to:'>, *") @bdd.given("I set up a temporary download dir") From bc4e34032382d95205793a0465f58c3e3d2a671f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 26 Oct 2016 19:46:22 +0200 Subject: [PATCH 25/87] Save key mode to leave Doing self._prompt.question.done() already sets self._prompt to the next prompt or to None. --- qutebrowser/mainwindow/prompt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index c56196db1..daa61bdbf 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -215,9 +215,9 @@ class PromptContainer(QWidget): except Error as e: raise cmdexc.CommandError(str(e)) if done: + key_mode = self._prompt.KEY_MODE self._prompt.question.done() - modeman.maybe_leave(self._win_id, self._prompt.KEY_MODE, - ':prompt-accept') + modeman.maybe_leave(self._win_id, key_mode, ':prompt-accept') @cmdutils.register(instance='prompt-container', hide=True, scope='window', modes=[usertypes.KeyMode.yesno], From dff20ffe35f044f50d42ec5d470017bf29acd863 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 26 Oct 2016 19:50:11 +0200 Subject: [PATCH 26/87] Fix message.confirm_async calls --- qutebrowser/browser/urlmarks.py | 3 ++- qutebrowser/browser/webkit/webpage.py | 9 +++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/qutebrowser/browser/urlmarks.py b/qutebrowser/browser/urlmarks.py index 90b2defc0..1a5f66b05 100644 --- a/qutebrowser/browser/urlmarks.py +++ b/qutebrowser/browser/urlmarks.py @@ -204,7 +204,8 @@ class QuickmarkManager(UrlMarkManager): if name in self.marks: message.confirm_async( - win_id, "Override existing quickmark?", set_mark, default=True) + win_id, title="Override existing quickmark?", + yes_action=set_mark, default=True) else: set_mark() diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py index e5cbf8e01..7cc4ab2be 100644 --- a/qutebrowser/browser/webkit/webpage.py +++ b/qutebrowser/browser/webkit/webpage.py @@ -137,11 +137,12 @@ class BrowserPage(QWebPage): # QDesktopServices::openUrl with info.url directly - however it # works when we construct a copy of it. url = QUrl(info.url) - msg = "Open external application for {}-link?\nURL: {}".format( - url.scheme(), url.toDisplayString()) + scheme = url.scheme() message.confirm_async( - self._win_id, msg, - functools.partial(QDesktopServices.openUrl, url)) + self._win_id, + title="Open external application for {}-link?".format(scheme), + text="URL: {}".format(url.toDisplayString()), + yes_action=functools.partial(QDesktopServices.openUrl, url)) return True elif (info.domain, info.error) in ignored_errors: log.webview.debug("Ignored error on {}: {} (error domain: {}, " From a33333eb414192f30577eb004b2c0942179bc6ca Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 26 Oct 2016 20:11:41 +0200 Subject: [PATCH 27/87] Make the radius for prompt edges configurable --- qutebrowser/config/configdata.py | 4 ++++ qutebrowser/mainwindow/prompt.py | 9 +++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 4575c3f4e..bad9433f1 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -384,6 +384,10 @@ def data(readonly=False): "Globs are supported, so ';*' will blacklist all keychains" "starting with ';'. Use '*' to disable keyhints"), + ('prompt-radius', + SettingValue(typ.Int(minval=0), '8'), + "The rounding radius for the edges of prompts."), + readonly=readonly )), diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index daa61bdbf..4d7430e14 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -80,13 +80,14 @@ class PromptContainer(QWidget): """ STYLESHEET = """ + {% set prompt_radius = config.get('ui', 'prompt-radius') %} QWidget#Prompt { {% if config.get('ui', 'status-position') == 'top' %} - border-bottom-left-radius: 10px; - border-bottom-right-radius: 10px; + border-bottom-left-radius: {{ prompt_radius }}px; + border-bottom-right-radius: {{ prompt_radius }}px; {% else %} - border-top-left-radius: 10px; - border-top-right-radius: 10px; + border-top-left-radius: {{ prompt_radius }}px; + border-top-right-radius: {{ prompt_radius }}px; {% endif %} } From 7d57d884d68eb59f4979bb3791570dd5ba10cb92 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 26 Oct 2016 20:38:03 +0200 Subject: [PATCH 28/87] Add configuration options for prompts --- doc/help/settings.asciidoc | 42 ++++++++++++++++--------- qutebrowser/config/config.py | 2 ++ qutebrowser/config/configdata.py | 20 +++++++----- qutebrowser/mainwindow/prompt.py | 9 ++---- qutebrowser/mainwindow/statusbar/bar.py | 4 +-- 5 files changed, 47 insertions(+), 30 deletions(-) diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 8a25e2b31..069376be4 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -54,6 +54,7 @@ |<>|Use standard JavaScript modal dialog for alert() and confirm() |<>|Hide the window decoration when using wayland (requires restart) |<>|Keychains that shouldn't be shown in the keyhint dialog +|<>|The rounding radius for the edges of prompts. |============== .Quick reference for section ``network'' @@ -213,8 +214,6 @@ |<>|Color of the scrollbar in completion view |<>|Foreground color of the statusbar. |<>|Background color of the statusbar. -|<>|Foreground color of the statusbar if there is a prompt. -|<>|Background color of the statusbar if there is a prompt. |<>|Foreground color of the statusbar in insert mode. |<>|Background color of the statusbar in insert mode. |<>|Foreground color of the statusbar in command mode. @@ -268,6 +267,8 @@ |<>|Foreground color an info message. |<>|Background color of an info message. |<>|Border color of an info message. +|<>|Foreground color for prompts. +|<>|Background color for prompts. |============== .Quick reference for section ``fonts'' @@ -296,6 +297,7 @@ |<>|Font used for error messages. |<>|Font used for warning messages. |<>|Font used for info messages. +|<>|Font used for prompts. |============== == general @@ -706,6 +708,12 @@ Globs are supported, so ';*' will blacklist all keychainsstarting with ';'. Use Default: empty +[[ui-prompt-radius]] +=== prompt-radius +The rounding radius for the edges of prompts. + +Default: +pass:[8]+ + == network Settings related to the network. @@ -1831,18 +1839,6 @@ Background color of the statusbar. Default: +pass:[black]+ -[[colors-statusbar.fg.prompt]] -=== statusbar.fg.prompt -Foreground color of the statusbar if there is a prompt. - -Default: +pass:[${statusbar.fg}]+ - -[[colors-statusbar.bg.prompt]] -=== statusbar.bg.prompt -Background color of the statusbar if there is a prompt. - -Default: +pass:[darkblue]+ - [[colors-statusbar.fg.insert]] === statusbar.fg.insert Foreground color of the statusbar in insert mode. @@ -2184,6 +2180,18 @@ Border color of an info message. Default: +pass:[#333333]+ +[[colors-prompts.fg]] +=== prompts.fg +Foreground color for prompts. + +Default: +pass:[white]+ + +[[colors-prompts.bg]] +=== prompts.bg +Background color for prompts. + +Default: +pass:[darkblue]+ + == fonts Fonts used for the UI, with optional style/weight/size. @@ -2322,3 +2330,9 @@ Default: +pass:[8pt ${_monospace}]+ Font used for info messages. Default: +pass:[8pt ${_monospace}]+ + +[[fonts-prompts]] +=== prompts +Font used for prompts. + +Default: +pass:[8pt sans-serif]+ diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 8ee27918a..7c8ba98c5 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -391,6 +391,8 @@ class ConfigManager(QObject): ('colors', 'statusbar.bg.error'): 'messages.bg.error', ('colors', 'statusbar.fg.warning'): 'messages.fg.warning', ('colors', 'statusbar.bg.warning'): 'messages.bg.warning', + ('colors', 'statusbar.fg.prompt'): 'prompts.fg', + ('colors', 'statusbar.bg.prompt'): 'prompts.bg', } DELETED_OPTIONS = [ ('colors', 'tab.separator'), diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index bad9433f1..b97a8316a 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1079,14 +1079,6 @@ def data(readonly=False): SettingValue(typ.QssColor(), 'black'), "Background color of the statusbar."), - ('statusbar.fg.prompt', - SettingValue(typ.QssColor(), '${statusbar.fg}'), - "Foreground color of the statusbar if there is a prompt."), - - ('statusbar.bg.prompt', - SettingValue(typ.QssColor(), 'darkblue'), - "Background color of the statusbar if there is a prompt."), - ('statusbar.fg.insert', SettingValue(typ.QssColor(), '${statusbar.fg}'), "Foreground color of the statusbar in insert mode."), @@ -1310,6 +1302,14 @@ def data(readonly=False): SettingValue(typ.QssColor(), '#333333'), "Border color of an info message."), + ('prompts.fg', + SettingValue(typ.QssColor(), 'white'), + "Foreground color for prompts."), + + ('prompts.bg', + SettingValue(typ.QssColor(), 'darkblue'), + "Background color for prompts."), + readonly=readonly )), @@ -1411,6 +1411,10 @@ def data(readonly=False): SettingValue(typ.Font(), DEFAULT_FONT_SIZE + ' ${_monospace}'), "Font used for info messages."), + ('prompts', + SettingValue(typ.Font(), DEFAULT_FONT_SIZE + ' sans-serif'), + "Font used for prompts."), + readonly=readonly )), ]) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 4d7430e14..6be7eb633 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -92,12 +92,9 @@ class PromptContainer(QWidget): } QWidget { - /* FIXME - font: {{ font['keyhint'] }}; - FIXME - */ - color: {{ color['statusbar.fg.prompt'] }}; - background-color: {{ color['statusbar.bg.prompt'] }}; + font: {{ font['prompts'] }}; + color: {{ color['prompts.fg'] }}; + background-color: {{ color['prompts.bg'] }}; } """ update_geometry = pyqtSignal() diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index cca640c93..21e0d46ea 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -112,8 +112,8 @@ class StatusBar(QWidget): QWidget#StatusBar[prompt_active="true"], QWidget#StatusBar[prompt_active="true"] QLabel, QWidget#StatusBar[prompt_active="true"] QLineEdit { - color: {{ color['statusbar.fg.prompt'] }}; - background-color: {{ color['statusbar.bg.prompt'] }}; + color: {{ color['prompts.fg'] }}; + background-color: {{ color['prompts.bg'] }}; } QWidget#StatusBar[insert_active="true"], From 7d637956135a3175ae1597cda2192445c2bebf54 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 26 Oct 2016 21:05:54 +0200 Subject: [PATCH 29/87] Improve bindings output for yesno questions --- qutebrowser/mainwindow/prompt.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 6be7eb633..1c0dbcbfc 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -391,7 +391,8 @@ class _BasePrompt(QWidget): self._key_grid.setVerticalSpacing(0) key_config = objreg.get('key-config') - all_bindings = key_config.get_reverse_bindings_for(self.KEY_MODE.name) + # The bindings are all in the 'prompt' mode, even for yesno prompts + all_bindings = key_config.get_reverse_bindings_for('prompt') labels = [] for cmd, text in self._allowed_commands(): @@ -616,11 +617,11 @@ class YesNoPrompt(_BasePrompt): return True def _allowed_commands(self): + default = 'yes' if self.question.default else 'no' cmds = [ - ('prompt-accept', - "Use default ({})".format(self.question.default)), ('prompt-accept yes', "Yes"), ('prompt-accept no', "No"), + ('prompt-accept', "Use default ({})".format(default)), ('leave-mode', "Abort"), ] return cmds From 741ecac9efcf300eb36ce8a9b6dca551b17ba2cf Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 26 Oct 2016 21:09:33 +0200 Subject: [PATCH 30/87] More improvements for yesno prompt key hints --- qutebrowser/mainwindow/prompt.py | 12 +++++++++--- tests/end2end/features/prompts.feature | 9 +++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 1c0dbcbfc..ce9542d0a 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -607,6 +607,8 @@ class YesNoPrompt(_BasePrompt): def accept(self, value=None): if value is None: + if self.question.default is None: + raise Error("No default value was set for this question!") self.question.answer = self.question.default elif value == 'yes': self.question.answer = True @@ -617,13 +619,17 @@ class YesNoPrompt(_BasePrompt): return True def _allowed_commands(self): - default = 'yes' if self.question.default else 'no' cmds = [ ('prompt-accept yes', "Yes"), ('prompt-accept no', "No"), - ('prompt-accept', "Use default ({})".format(default)), - ('leave-mode', "Abort"), ] + + if self.question.default is not None: + assert self.question.default in [True, False] + default = 'yes' if self.question.default else 'no' + cmds.append(('prompt-accept', "Use default ({})".format(default))) + + cmds.append(('leave-mode', "Abort")) return cmds diff --git a/tests/end2end/features/prompts.feature b/tests/end2end/features/prompts.feature index 05324061f..61db9eb2a 100644 --- a/tests/end2end/features/prompts.feature +++ b/tests/end2end/features/prompts.feature @@ -249,3 +249,12 @@ Feature: Prompts And I run :prompt-accept yes Then the javascript message "confirm reply: true" should be logged And the error "Invalid value nope - expected yes/no!" should be shown + + Scenario: Javascript confirm with default value + When I open data/prompt/jsconfirm.html + And I run :click-element id button + And I wait for a prompt + And I run :prompt-accept + And I run :prompt-accept yes + Then the javascript message "confirm reply: true" should be logged + And the error "No default value was set for this question!" should be shown From 6da67582635476c239a2edda196017f5b5b83a23 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 26 Oct 2016 21:25:47 +0200 Subject: [PATCH 31/87] Improve FilenamePrompt widget sizing --- qutebrowser/mainwindow/mainwindow.py | 20 ++++++++++---------- qutebrowser/mainwindow/prompt.py | 14 ++++---------- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index c38ff1548..0880c3389 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -186,7 +186,7 @@ class MainWindow(QWidget): self._prompt_container = prompt.PromptContainer(self.win_id, self) self._add_overlay(self._prompt_container, self._prompt_container.update_geometry, - centered=True) + centered=True, padding=10) objreg.register('prompt-container', self._prompt_container, scope='window', window=self.win_id) self._prompt_container.hide() @@ -213,17 +213,17 @@ class MainWindow(QWidget): objreg.get("app").new_window.emit(self) - def _add_overlay(self, widget, signal, *, centered=False): - self._overlays.append((widget, signal, centered)) + def _add_overlay(self, widget, signal, *, centered=False, padding=0): + self._overlays.append((widget, signal, centered, padding)) - def _update_overlay_geometry(self, widget=None, centered=None): + def _update_overlay_geometry(self, widget=None, centered=None, padding=0): """Reposition/resize the given overlay. If no widget is given, reposition/resize all overlays. """ if widget is None: - for w, _signal, centered in self._overlays: - self._update_overlay_geometry(w, centered) + for w, _signal, centered, padding in self._overlays: + self._update_overlay_geometry(w, centered, padding) return assert centered is not None @@ -233,8 +233,8 @@ class MainWindow(QWidget): size_hint = widget.sizeHint() if widget.sizePolicy().horizontalPolicy() == QSizePolicy.Expanding: - width = self.width() - left = 0 + width = self.width() - 2 * padding + left = padding else: width = size_hint.width() left = (self.width() - size_hint.width()) / 2 if centered else 0 @@ -363,10 +363,10 @@ class MainWindow(QWidget): def _connect_overlay_signals(self): """Connect the resize signal and resize everything once.""" - for widget, signal, centered in self._overlays: + for widget, signal, centered, padding in self._overlays: signal.connect( functools.partial(self._update_overlay_geometry, widget, - centered)) + centered, padding)) self._update_overlay_geometry(widget, centered) def _set_default_geometry(self): diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index ce9542d0a..4e4b0dcc0 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -26,7 +26,8 @@ import collections import sip from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QTimer from PyQt5.QtWidgets import (QWidget, QGridLayout, QVBoxLayout, QLineEdit, - QLabel, QWidgetItem, QFileSystemModel, QTreeView) + QLabel, QWidgetItem, QFileSystemModel, QTreeView, + QSizePolicy) from qutebrowser.config import style from qutebrowser.utils import usertypes, log, utils, qtutils, objreg @@ -168,6 +169,7 @@ class PromptContainer(QWidget): 'aborted')) modeman.enter(self._win_id, prompt.KEY_MODE, 'question asked') self._prompt = prompt + self.setSizePolicy(self._prompt.sizePolicy()) self._layout.addWidget(self._prompt) self._prompt.show() self.show() @@ -470,15 +472,7 @@ class FilenamePrompt(_BasePrompt): self._lineedit.setText(question.default) self.setFocusProxy(self._lineedit) self._init_key_label() - - def sizeHint(self): - """Get some more width. - - FIXME do this properly... - """ - orig = super().sizeHint() - orig.setWidth(orig.width() * 3) - return orig + self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) @pyqtSlot(str) def _set_fileview_root(self, path): From 46b24fc64a1922661d5dc6257c52ec34a0a60874 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 26 Oct 2016 21:36:46 +0200 Subject: [PATCH 32/87] Improve how file list is shown in download prompt --- qutebrowser/mainwindow/prompt.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 4e4b0dcc0..592b1fbfb 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -477,6 +477,10 @@ class FilenamePrompt(_BasePrompt): @pyqtSlot(str) def _set_fileview_root(self, path): """Set the root path for the file display.""" + if not path.endswith('/'): + return + path.rstrip('/') + try: if os.path.isdir(path): path = path @@ -498,6 +502,10 @@ class FilenamePrompt(_BasePrompt): self._file_model = QFileSystemModel(self) self._file_view.setModel(self._file_model) self._vbox.addWidget(self._file_view) + # Only show name + self._file_view.setHeaderHidden(True) + for col in range(1, 4): + self._file_view.setColumnHidden(col, True) def accept(self, value=None): text = value if value is not None else self._lineedit.text() From acbd5107d6fe8cc469497e83b335c5442443c981 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 26 Oct 2016 22:06:29 +0200 Subject: [PATCH 33/87] Only show directories in download filename prompts --- qutebrowser/mainwindow/prompt.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 592b1fbfb..7136cf161 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -24,7 +24,7 @@ import html import collections import sip -from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QTimer +from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QTimer, QDir from PyQt5.QtWidgets import (QWidget, QGridLayout, QVBoxLayout, QLineEdit, QLabel, QWidgetItem, QFileSystemModel, QTreeView, QSizePolicy) @@ -521,6 +521,11 @@ class DownloadFilenamePrompt(FilenamePrompt): """A prompt for a filename for downloads.""" + def __init__(self, question, win_id, parent=None): + super().__init__(question, win_id, parent) + self._file_model.setFilter(QDir.AllDirs | QDir.Drives | + QDir.NoDotAndDotDot) + def accept(self, value=None): text = value if value is not None else self._lineedit.text() self.question.answer = usertypes.FileDownloadTarget(text) From 6ab51e0b7b7e317ed3dbeea89572ee9cd3628a85 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 26 Oct 2016 22:24:11 +0200 Subject: [PATCH 34/87] Handle clicking on file items in prompt --- qutebrowser/mainwindow/prompt.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 7136cf161..8544f44f7 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -24,7 +24,7 @@ import html import collections import sip -from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QTimer, QDir +from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QTimer, QDir, QModelIndex from PyQt5.QtWidgets import (QWidget, QGridLayout, QVBoxLayout, QLineEdit, QLabel, QWidgetItem, QFileSystemModel, QTreeView, QSizePolicy) @@ -497,10 +497,26 @@ class FilenamePrompt(_BasePrompt): root = self._file_model.setRootPath(path) self._file_view.setRootIndex(root) + @pyqtSlot(QModelIndex) + def _on_clicked(self, index): + """Handle a click on an element.""" + parts = [] + cur = index + while cur.isValid(): + parts.append(cur.data()) + cur = cur.parent() + path = os.path.normpath(os.path.join(*reversed(parts))) + os.sep + log.prompt.debug('Clicked {!r} -> {}'.format(parts, path)) + self._lineedit.setText(path) + self._lineedit.setFocus() + # Avoid having a ..-subtree highlighted + self._file_view.setCurrentIndex(QModelIndex()) + def _init_fileview(self): self._file_view = QTreeView(self) self._file_model = QFileSystemModel(self) self._file_view.setModel(self._file_model) + self._file_view.clicked.connect(self._on_clicked) self._vbox.addWidget(self._file_view) # Only show name self._file_view.setHeaderHidden(True) @@ -523,8 +539,7 @@ class DownloadFilenamePrompt(FilenamePrompt): def __init__(self, question, win_id, parent=None): super().__init__(question, win_id, parent) - self._file_model.setFilter(QDir.AllDirs | QDir.Drives | - QDir.NoDotAndDotDot) + self._file_model.setFilter(QDir.AllDirs | QDir.Drives | QDir.NoDot) def accept(self, value=None): text = value if value is not None else self._lineedit.text() From 9bdbb257ba2156078475e6e93d6b3f5b45abaae8 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 26 Oct 2016 22:53:14 +0200 Subject: [PATCH 35/87] Add initial filename completion --- doc/help/commands.asciidoc | 10 +++++ qutebrowser/config/configdata.py | 2 + qutebrowser/mainwindow/prompt.py | 68 ++++++++++++++++++++++++++++---- 3 files changed, 72 insertions(+), 8 deletions(-) diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 093b6f2c6..8c5471ab1 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -977,6 +977,7 @@ How many steps to zoom out. |<>|Move the cursor or selection to the start of previous block. |<>|Open an external editor with the currently selected form field. |<>|Accept the current prompt. +|<>|Shift the focus of the prompt file completion menu to another item. |<>|Immediately open a download. |<>|Repeat the last executed command. |<>|Move back a character. @@ -1229,6 +1230,15 @@ Accept the current prompt. * +'value'+: If given, uses this value instead of the entered one. For boolean prompts, "yes"/"no" are accepted as value. +[[prompt-item-focus]] +=== prompt-item-focus +Syntax: +:prompt-item-focus 'which'+ + +Shift the focus of the prompt file completion menu to another item. + +==== positional arguments +* +'which'+: 'next', 'prev' + [[prompt-open-download]] === prompt-open-download Syntax: +:prompt-open-download ['cmdline']+ diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index b97a8316a..2056cca01 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1680,6 +1680,8 @@ KEY_DATA = collections.OrderedDict([ ('prompt-accept yes', ['y']), ('prompt-accept no', ['n']), ('prompt-open-download', ['']), + ('prompt-item-focus prev', ['', '']), + ('prompt-item-focus next', ['', '']), ])), ('command,prompt', collections.OrderedDict([ diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 8544f44f7..b0fd35d59 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -24,7 +24,8 @@ import html import collections import sip -from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QTimer, QDir, QModelIndex +from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QTimer, QDir, QModelIndex, + QItemSelectionModel) from PyQt5.QtWidgets import (QWidget, QGridLayout, QVBoxLayout, QLineEdit, QLabel, QWidgetItem, QFileSystemModel, QTreeView, QSizePolicy) @@ -264,6 +265,20 @@ class PromptContainer(QWidget): except UnsupportedOperationError: pass + @cmdutils.register(instance='prompt-container', hide=True, scope='window', + modes=[usertypes.KeyMode.prompt]) + @cmdutils.argument('which', choices=['next', 'prev']) + def prompt_item_focus(self, which): + """Shift the focus of the prompt file completion menu to another item. + + Args: + which: 'next', 'prev' + """ + try: + self._prompt.item_focus(which) + except UnsupportedOperationError: + pass + @pyqtSlot(usertypes.Question, bool) def ask_question(self, question, blocking): """Display a prompt for a given question. @@ -425,6 +440,10 @@ class _BasePrompt(QWidget): """Open the download directly if this is a download prompt.""" raise UnsupportedOperationError + def item_focus(self, _which): + """Switch to next file item if this is a filename prompt..""" + raise UnsupportedOperationError + def _allowed_commands(self): """Get the commands we could run as response to this message.""" raise NotImplementedError @@ -477,7 +496,7 @@ class FilenamePrompt(_BasePrompt): @pyqtSlot(str) def _set_fileview_root(self, path): """Set the root path for the file display.""" - if not path.endswith('/'): + if not path.endswith('/') or path == '/': return path.rstrip('/') @@ -498,25 +517,33 @@ class FilenamePrompt(_BasePrompt): self._file_view.setRootIndex(root) @pyqtSlot(QModelIndex) - def _on_clicked(self, index): - """Handle a click on an element.""" + def _insert_path(self, index, *, clicked=True): + """Handle an element selection. + + Args: + index: The QModelIndex of the selected element. + clicked: Whether the element was clicked. + """ parts = [] cur = index while cur.isValid(): parts.append(cur.data()) cur = cur.parent() - path = os.path.normpath(os.path.join(*reversed(parts))) + os.sep + path = os.path.normpath(os.path.join(*reversed(parts))) + if clicked: + path += os.sep log.prompt.debug('Clicked {!r} -> {}'.format(parts, path)) self._lineedit.setText(path) self._lineedit.setFocus() - # Avoid having a ..-subtree highlighted - self._file_view.setCurrentIndex(QModelIndex()) + if clicked: + # Avoid having a ..-subtree highlighted + self._file_view.setCurrentIndex(QModelIndex()) def _init_fileview(self): self._file_view = QTreeView(self) self._file_model = QFileSystemModel(self) self._file_view.setModel(self._file_model) - self._file_view.clicked.connect(self._on_clicked) + self._file_view.clicked.connect(self._insert_path) self._vbox.addWidget(self._file_view) # Only show name self._file_view.setHeaderHidden(True) @@ -528,6 +555,31 @@ class FilenamePrompt(_BasePrompt): self.question.answer = text return True + def item_focus(self, which): + # This duplicates some completion code, but I don't see a nicer way... + assert which in ['prev', 'next'], which + selmodel = self._file_view.selectionModel() + + first_index = self._file_model.index(0, 0) + last_index = self._file_model.index(self._file_model.rowCount() - 1, 0) + + idx = selmodel.currentIndex() + if not idx.isValid(): + # No item selected yet + idx = last_index if which == 'prev' else first_index + + if which == 'prev': + idx = self._file_view.indexAbove(idx) + else: + idx = self._file_view.indexBelow(idx) + # wrap around if we arrived at beginning/end + if not idx.isValid(): + idx = last_index if which == 'prev' else first_index + + selmodel.setCurrentIndex( + idx, QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows) + self._insert_path(idx, clicked=False) + def _allowed_commands(self): """Get the commands we could run as response to this message.""" return [('prompt-accept', 'Accept'), ('leave-mode', 'Abort')] From 3ffcb011a7aa5f28c53b098bb766da846f4ab049 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 27 Oct 2016 07:23:49 +0200 Subject: [PATCH 36/87] Simplify key label for auth prompts --- qutebrowser/mainwindow/prompt.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index b0fd35d59..741a7ea50 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -652,7 +652,6 @@ class AuthenticationPrompt(_BasePrompt): # 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() - self._init_key_label() return False else: self.question.answer = AuthTuple(self._user_lineedit.text(), @@ -660,12 +659,8 @@ class AuthenticationPrompt(_BasePrompt): return True def _allowed_commands(self): - if self._user_lineedit.hasFocus(): - cmds = [('prompt-accept', "Switch to password field")] - else: - cmds = [('prompt-accept', "Accept")] - cmds.append(('leave-mode', "Abort")) - return cmds + return [('prompt-accept', "Accept"), + ('leave-mode', "Abort")] class YesNoPrompt(_BasePrompt): From 20716c137cc0f16fae8cef82ead0bf0865744f5c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 27 Oct 2016 07:26:18 +0200 Subject: [PATCH 37/87] Support tab completion for auth prompts again --- qutebrowser/mainwindow/prompt.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 741a7ea50..fe11e9425 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -658,6 +658,14 @@ class AuthenticationPrompt(_BasePrompt): self._password_lineedit.text()) return True + def item_focus(self, which): + """Support switching between fields with tab.""" + assert which in ['prev', 'next'], which + if which == 'next' and self._user_lineedit.hasFocus(): + self._password_lineedit.setFocus() + elif which == 'prev' and self._password_lineedit.hasFocus(): + self._user_lineedit.setFocus() + def _allowed_commands(self): return [('prompt-accept', "Accept"), ('leave-mode', "Abort")] From 653ca9799d3c5165723b05e2054c7f88b37b0bba Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 27 Oct 2016 07:34:01 +0200 Subject: [PATCH 38/87] Add a default for SSL questions --- qutebrowser/browser/webkit/network/networkmanager.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py index 4c5a91fa4..8814d1685 100644 --- a/qutebrowser/browser/webkit/network/networkmanager.py +++ b/qutebrowser/browser/webkit/network/networkmanager.py @@ -207,7 +207,7 @@ class NetworkManager(QNetworkAccessManager): self.setCache(cache) cache.setParent(app) - def _ask(self, title, text, mode, owner=None): + def _ask(self, title, text, mode, owner=None, default=None): """Ask a blocking question in the statusbar. Args: @@ -232,7 +232,7 @@ class NetworkManager(QNetworkAccessManager): abort_on.append(tab.load_started) return message.ask(win_id=self._win_id, title=title, text=text, - mode=mode, abort_on=abort_on) + mode=mode, abort_on=abort_on, default=default) def shutdown(self): """Abort all running requests.""" @@ -281,7 +281,8 @@ class NetworkManager(QNetworkAccessManager): if ssl_strict == 'ask': err_string = '\n'.join('- ' + err.errorString() for err in errors) answer = self._ask('SSL errors - continue?', err_string, - mode=usertypes.PromptMode.yesno, owner=reply) + mode=usertypes.PromptMode.yesno, owner=reply, + default=False) log.webview.debug("Asked for SSL errors, answer {}".format(answer)) if answer: reply.ignoreSslErrors() From fefc8c4eb2f92f26291464c2a6edfe595bc1de41 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 27 Oct 2016 09:16:19 +0200 Subject: [PATCH 39/87] Add more tests for prompts --- qutebrowser/browser/urlmarks.py | 3 +- qutebrowser/mainwindow/prompt.py | 2 +- tests/end2end/data/prompt/jsprompt.html | 9 +- tests/end2end/features/downloads.feature | 12 ++ tests/end2end/features/prompts.feature | 161 +++++++++++++++++++++ tests/end2end/features/test_prompts_bdd.py | 3 +- tests/manual/js/jsprompt.html | 4 +- 7 files changed, 184 insertions(+), 10 deletions(-) diff --git a/qutebrowser/browser/urlmarks.py b/qutebrowser/browser/urlmarks.py index 1a5f66b05..847f49564 100644 --- a/qutebrowser/browser/urlmarks.py +++ b/qutebrowser/browser/urlmarks.py @@ -33,7 +33,7 @@ import collections from PyQt5.QtCore import pyqtSignal, QUrl, QObject from qutebrowser.utils import (message, usertypes, qtutils, urlutils, - standarddir, objreg) + standarddir, objreg, log) from qutebrowser.commands import cmdutils from qutebrowser.misc import lineparser @@ -201,6 +201,7 @@ class QuickmarkManager(UrlMarkManager): self.marks[name] = url self.changed.emit() self.added.emit(name, url) + log.misc.debug("Added quickmark {} for {}".format(name, url)) if name in self.marks: message.confirm_async( diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index fe11e9425..5086e3b41 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -361,7 +361,7 @@ class LineEdit(QLineEdit): if e.key() == Qt.Key_Insert and e.modifiers() == Qt.ShiftModifier: try: text = utils.get_clipboard(selection=True) - except utils.ClipboardError: + except utils.ClipboardError: # pragma: no cover pass else: e.accept() diff --git a/tests/end2end/data/prompt/jsprompt.html b/tests/end2end/data/prompt/jsprompt.html index d8c848553..4279fc075 100644 --- a/tests/end2end/data/prompt/jsprompt.html +++ b/tests/end2end/data/prompt/jsprompt.html @@ -3,13 +3,14 @@ - + + diff --git a/tests/end2end/features/downloads.feature b/tests/end2end/features/downloads.feature index 588aa9be8..dd8419671 100644 --- a/tests/end2end/features/downloads.feature +++ b/tests/end2end/features/downloads.feature @@ -484,3 +484,15 @@ Feature: Downloading things from a website. And I run :click-element id download And I wait until the download is finished Then the downloaded file test.pdf should exist + + Scenario: Answering a question for a cancelled download (#415) + When I set storage -> prompt-download-directory to true + And I run :download http://localhost:(port)/data/downloads/download.bin + And I wait for "Asking question text=None title='Save file to:'>, *" in the log + And I run :download http://localhost:(port)/data/downloads/download2.bin + And I wait for "Asking question text=None title='Save file to:'>, *" in the log + And I run :download-cancel with count 2 + And I run :prompt-accept + And I wait until the download is finished + Then the downloaded file download.bin should exist + And the downloaded file download2.bin should not exist diff --git a/tests/end2end/features/prompts.feature b/tests/end2end/features/prompts.feature index 61db9eb2a..316f45b4c 100644 --- a/tests/end2end/features/prompts.feature +++ b/tests/end2end/features/prompts.feature @@ -49,6 +49,14 @@ Feature: Prompts And I run :prompt-accept Then the javascript message "Prompt reply: prompt test" should be logged + @pyqt>=5.3.1 + Scenario: Javascript prompt with default + When I open data/prompt/jsprompt.html + And I run :click-element id button-default + And I wait for a prompt + And I run :prompt-accept + Then the javascript message "Prompt reply: default" should be logged + @pyqt>=5.3.1 Scenario: Rejected javascript prompt When I open data/prompt/jsprompt.html @@ -58,6 +66,68 @@ Feature: Prompts And I run :leave-mode Then the javascript message "Prompt reply: null" should be logged + # Multiple prompts + + Scenario: Blocking question interrupted by blocking one + When I set content -> ignore-javascript-alert to false + And I open data/prompt/jsalert.html + And I run :click-element id button + And I wait for a prompt + And I open data/prompt/jsconfirm.html in a new tab + And I run :click-element id button + And I wait for a prompt + # JS confirm + And I run :prompt-accept yes + # JS alert + And I run :prompt-accept + Then the javascript message "confirm reply: true" should be logged + And the javascript message "Alert done" should be logged + + Scenario: Blocking question interrupted by async one + When I set content -> ignore-javascript-alert to false + And I set content -> notifications to ask + And I open data/prompt/jsalert.html + And I run :click-element id button + And I wait for a prompt + And I open data/prompt/notifications.html in a new tab + And I run :click-element id button + And I wait for a prompt + # JS alert + And I run :prompt-accept + # notification permission + And I run :prompt-accept yes + Then the javascript message "Alert done" should be logged + And the javascript message "notification permission granted" should be logged + + Scenario: Async question interrupted by async one + When I set content -> notifications to ask + And I open data/prompt/notifications.html in a new tab + And I run :click-element id button + And I wait for a prompt + And I run :quickmark-save + And I wait for a prompt + # notification permission + And I run :prompt-accept yes + # quickmark + And I run :prompt-accept test + Then the javascript message "notification permission granted" should be logged + And "Added quickmark test for *" should be logged + + Scenario: Async question interrupted by blocking one + When I set content -> notifications to ask + And I set content -> ignore-javascript-alert to false + And I open data/prompt/notifications.html in a new tab + And I run :click-element id button + And I wait for a prompt + And I open data/prompt/jsalert.html in a new tab + And I run :click-element id button + And I wait for a prompt + # JS alert + And I run :prompt-accept + # notification permission + And I run :prompt-accept yes + Then the javascript message "Alert done" should be logged + And the javascript message "notification permission granted" should be logged # Shift-Insert with prompt (issue 1299) @@ -72,6 +142,17 @@ Feature: Prompts And I run :prompt-accept Then the javascript message "Prompt reply: insert test" should be logged + @pyqt>=5.3.1 + Scenario: Pasting via shift-insert without it being supported + When selection is not supported + And I put "insert test" into the primary selection + And I open data/prompt/jsprompt.html + And I run :click-element id button + And I wait for a prompt + And I press the keys "" + And I run :prompt-accept + Then the javascript message "Prompt reply: " should be logged + @pyqt>=5.3.1 Scenario: Using content -> ignore-javascript-prompt When I set content -> ignore-javascript-prompt to true @@ -219,6 +300,44 @@ Feature: Prompts "user": "user" } + Scenario: Authentication with :prompt-accept value + When I open about:blank in a new tab + And I open basic-auth/user/password without waiting + And I wait for a prompt + And I run :prompt-accept user:password + And I wait until basic-auth/user/password is loaded + Then the json on the page should be: + { + "authenticated": true, + "user": "user" + } + + Scenario: Authentication with invalid :prompt-accept value + When I open about:blank in a new tab + And I open basic-auth/user/password without waiting + And I wait for a prompt + And I run :prompt-accept foo + And I run :prompt-accept user:password + Then the error "Value needs to be in the format username:password, but foo was given" should be shown + + Scenario: Tabbing between username and password + When I open about:blank in a new tab + And I open basic-auth/user/password without waiting + And I wait for a prompt + And I press the keys "us" + And I run :prompt-item-focus next + And I press the keys "password" + And I run :prompt-item-focus prev + And I press the keys "er" + And I run :prompt-accept + And I run :prompt-accept + And I wait until basic-auth/user/password is loaded + Then the json on the page should be: + { + "authenticated": true, + "user": "user" + } + # :prompt-accept with value argument Scenario: Javascript alert with value @@ -258,3 +377,45 @@ Feature: Prompts And I run :prompt-accept yes Then the javascript message "confirm reply: true" should be logged And the error "No default value was set for this question!" should be shown + + Scenario: Javascript confirm with deprecated :prompt-yes command + When I open data/prompt/jsconfirm.html + And I run :click-element id button + And I wait for a prompt + And I run :prompt-yes + Then the javascript message "confirm reply: true" should be logged + And the warning "prompt-yes is deprecated - Use :prompt-accept yes instead!" should be shown + + Scenario: Javascript confirm with deprecated :prompt-no command + When I open data/prompt/jsconfirm.html + And I run :click-element id button + And I wait for a prompt + And I run :prompt-no + Then the javascript message "confirm reply: false" should be logged + And the warning "prompt-no is deprecated - Use :prompt-accept no instead!" should be shown + + # Other + + Scenario: Shutting down with a question + When I open data/prompt/jsconfirm.html + And I run :click-element id button + And I wait for a prompt + And I run :quit + Then the javascript message "confirm reply: false" should be logged + And qutebrowser should quit + + Scenario: Using :prompt-open-download with a prompt which does not support it + When I open data/hello.txt + And I run :quickmark-save + And I wait for a prompt + And I run :prompt-open-download + And I run :prompt-accept test-prompt-open-download + Then "Added quickmark test-prompt-open-download for *" should be logged + + Scenario: Using :prompt-item-focus with a prompt which does not support it + When I open data/hello.txt + And I run :quickmark-save + And I wait for a prompt + And I run :prompt-item-focus next + And I run :prompt-accept test-prompt-item-focus + Then "Added quickmark test-prompt-item-focus for *" should be logged diff --git a/tests/end2end/features/test_prompts_bdd.py b/tests/end2end/features/test_prompts_bdd.py index d98acab9a..8a66880b7 100644 --- a/tests/end2end/features/test_prompts_bdd.py +++ b/tests/end2end/features/test_prompts_bdd.py @@ -39,8 +39,7 @@ def wait_ssl_page_finished_loading(quteproc, ssl_server): @bdd.when("I wait for a prompt") def wait_for_prompt(quteproc): - quteproc.wait_for(message='Entering mode KeyMode.* (reason: question ' - 'asked)') + quteproc.wait_for(message='Asking question *') @bdd.then("no prompt should be shown") diff --git a/tests/manual/js/jsprompt.html b/tests/manual/js/jsprompt.html index 782dc1622..b767fdd2f 100644 --- a/tests/manual/js/jsprompt.html +++ b/tests/manual/js/jsprompt.html @@ -1,8 +1,8 @@ From d5a1f6d6b5a96a3df7ad16fc0aa0a7baa168dfb6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 27 Oct 2016 09:16:51 +0200 Subject: [PATCH 40/87] Only support calling _init_key_label once --- qutebrowser/mainwindow/prompt.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 5086e3b41..d44aea307 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -386,7 +386,6 @@ class _BasePrompt(QWidget): self._vbox = QVBoxLayout(self) self._vbox.setSpacing(15) self._key_grid = None - self._key_grid_item = None def __repr__(self): return utils.get_repr(self, question=self.question, constructor=True) @@ -400,10 +399,7 @@ class _BasePrompt(QWidget): self._vbox.addWidget(text_label) def _init_key_label(self): - # Remove old grid - if self._key_grid is not None: - self._vbox.removeItem(self._key_grid_item) - + assert self._key_grid is None, self._key_grid self._key_grid = QGridLayout() self._key_grid.setVerticalSpacing(0) @@ -431,7 +427,6 @@ class _BasePrompt(QWidget): self._key_grid.addWidget(text_label, i, 1) self._vbox.addLayout(self._key_grid) - self._key_grid_item = self._vbox.itemAt(self._vbox.count() - 1) def accept(self, value=None): raise NotImplementedError From 9ce1180b3110a3e02cb3ff7980765cde286305b1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 27 Oct 2016 13:40:34 +0200 Subject: [PATCH 41/87] First attempt at showing prompts in all windows --- qutebrowser/app.py | 12 +- qutebrowser/browser/commands.py | 2 +- qutebrowser/browser/urlmarks.py | 13 +- .../browser/webkit/network/networkmanager.py | 4 +- qutebrowser/browser/webkit/webpage.py | 8 +- qutebrowser/mainwindow/mainwindow.py | 7 +- qutebrowser/mainwindow/prompt.py | 265 ++++++++++-------- qutebrowser/utils/message.py | 63 ++--- 8 files changed, 198 insertions(+), 176 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index a4e11b6d6..cdce0be6d 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -48,7 +48,7 @@ from qutebrowser.config import style, config, websettings, configexc from qutebrowser.browser import urlmarks, adblock, history, browsertab from qutebrowser.browser.webkit import cookies, cache, downloads from qutebrowser.browser.webkit.network import networkmanager -from qutebrowser.mainwindow import mainwindow +from qutebrowser.mainwindow import mainwindow, prompt from qutebrowser.misc import (readline, ipc, savemanager, sessions, crashsignal, earlyinit) from qutebrowser.misc import utilcmds # pylint: disable=unused-import @@ -372,6 +372,9 @@ def _init_modules(args, crash_handler): crash_handler: The CrashHandler instance. """ # pylint: disable=too-many-statements + log.init.debug("Initializing prompts...") + prompt.init() + log.init.debug("Initializing save manager...") save_manager = savemanager.SaveManager(qApp) objreg.register('save-manager', save_manager) @@ -644,11 +647,8 @@ class Quitter: load_next_time=True) deferrer = False - for win_id in objreg.window_registry: - 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 prompt.prompt_queue.shutdown(): + deferrer = True if deferrer: # If shutdown was called while we were asking a question, we're in # a still sub-eventloop (which gets quit now) and not in the main diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index f1849b0bf..55051e003 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1137,7 +1137,7 @@ class CommandDispatcher: def quickmark_save(self): """Save the current page as a quickmark.""" quickmark_manager = objreg.get('quickmark-manager') - quickmark_manager.prompt_save(self._win_id, self._current_url()) + quickmark_manager.prompt_save(self._current_url()) @cmdutils.register(instance='command-dispatcher', scope='window', maxsplit=0) diff --git a/qutebrowser/browser/urlmarks.py b/qutebrowser/browser/urlmarks.py index 847f49564..16d9f7bd6 100644 --- a/qutebrowser/browser/urlmarks.py +++ b/qutebrowser/browser/urlmarks.py @@ -159,11 +159,10 @@ class QuickmarkManager(UrlMarkManager): else: self.marks[key] = url - def prompt_save(self, win_id, url): + def prompt_save(self, url): """Prompt for a new quickmark name to be added and add it. Args: - win_id: The current window ID. url: The quickmark url as a QUrl. """ if not url.isValid(): @@ -171,19 +170,17 @@ class QuickmarkManager(UrlMarkManager): return urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded) message.ask_async( - win_id, "Add quickmark:", usertypes.PromptMode.text, - functools.partial(self.quickmark_add, win_id, urlstr)) + "Add quickmark:", usertypes.PromptMode.text, + functools.partial(self.quickmark_add, urlstr)) @cmdutils.register(instance='quickmark-manager') - @cmdutils.argument('win_id', win_id=True) - def quickmark_add(self, win_id, url, name): + def quickmark_add(self, url, name): """Add a new quickmark. You can view all saved quickmarks on the link:qute://bookmarks[bookmarks page]. Args: - win_id: The window ID to display the errors in. url: The url to add as quickmark. name: The name for the new quickmark. """ @@ -205,7 +202,7 @@ class QuickmarkManager(UrlMarkManager): if name in self.marks: message.confirm_async( - win_id, title="Override existing quickmark?", + title="Override existing quickmark?", yes_action=set_mark, default=True) else: set_mark() diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py index 8814d1685..d54eca76d 100644 --- a/qutebrowser/browser/webkit/network/networkmanager.py +++ b/qutebrowser/browser/webkit/network/networkmanager.py @@ -231,8 +231,8 @@ class NetworkManager(QNetworkAccessManager): tab=self._tab_id) abort_on.append(tab.load_started) - return message.ask(win_id=self._win_id, title=title, text=text, - mode=mode, abort_on=abort_on, default=default) + return message.ask(title=title, text=text, mode=mode, + abort_on=abort_on, default=default) def shutdown(self): """Abort all running requests.""" diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py index 7cc4ab2be..5f6e5e76b 100644 --- a/qutebrowser/browser/webkit/webpage.py +++ b/qutebrowser/browser/webkit/webpage.py @@ -98,7 +98,7 @@ class BrowserPage(QWebPage): if (self._is_shutting_down or config.get('content', 'ignore-javascript-prompt')): return (False, "") - answer = message.ask(self._win_id, 'Javascript prompt', msg, + answer = message.ask('Javascript prompt', msg, mode=usertypes.PromptMode.text, default=default, abort_on=[self.loadStarted, @@ -139,7 +139,6 @@ class BrowserPage(QWebPage): url = QUrl(info.url) scheme = url.scheme() message.confirm_async( - self._win_id, title="Open external application for {}-link?".format(scheme), text="URL: {}".format(url.toDisplayString()), yes_action=functools.partial(QDesktopServices.openUrl, url)) @@ -453,8 +452,7 @@ class BrowserPage(QWebPage): if (self._is_shutting_down or config.get('content', 'ignore-javascript-alert')): return - message.ask(self._win_id, 'Javascript alert', msg, - mode=usertypes.PromptMode.alert, + message.ask('Javascript alert', msg, mode=usertypes.PromptMode.alert, abort_on=[self.loadStarted, self.shutting_down]) def javaScriptConfirm(self, frame, msg): @@ -465,7 +463,7 @@ class BrowserPage(QWebPage): if self._is_shutting_down: return False - ans = message.ask(self._win_id, 'Javascript confirm', msg, + ans = message.ask('Javascript confirm', msg, mode=usertypes.PromptMode.yesno, abort_on=[self.loadStarted, self.shutting_down]) return bool(ans) diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index 0880c3389..490529180 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -397,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(self._prompt_container.on_mode_left) + mode_manager.left.connect(prompt.prompt_queue.on_mode_left) # commands keyparsers[usertypes.KeyMode.normal].keystring_updated.connect( @@ -420,8 +420,6 @@ 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_question.connect(self._prompt_container.ask_question, - Qt.DirectConnection) # statusbar tabs.current_tab_changed.connect(status.prog.on_tab_changed) @@ -520,8 +518,7 @@ class MainWindow(QWidget): # Process all quit messages that user must confirm if quit_texts or 'always' in confirm_quit: text = '\n'.join(['Really quit?'] + quit_texts) - confirmed = message.ask(self.win_id, text, - mode=usertypes.PromptMode.yesno, + confirmed = message.ask(text, mode=usertypes.PromptMode.yesno, default=True) # Stop asking if the user cancels if not confirmed: diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index d44aea307..6c21bb69a 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -25,17 +25,20 @@ import collections import sip from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QTimer, QDir, QModelIndex, - QItemSelectionModel) + QItemSelectionModel, QObject) from PyQt5.QtWidgets import (QWidget, QGridLayout, QVBoxLayout, QLineEdit, QLabel, QWidgetItem, QFileSystemModel, QTreeView, QSizePolicy) from qutebrowser.config import style -from qutebrowser.utils import usertypes, log, utils, qtutils, objreg +from qutebrowser.utils import usertypes, log, utils, qtutils, objreg, message from qutebrowser.keyinput import modeman from qutebrowser.commands import cmdutils, cmdexc +prompt_queue = None + + AuthTuple = collections.namedtuple('AuthTuple', ['user', 'password']) @@ -49,9 +52,9 @@ class UnsupportedOperationError(Exception): """Raised when the prompt class doesn't support the requested operation.""" -class PromptContainer(QWidget): +class PromptQueue(QObject): - """Container for prompts to be shown above the statusbar. + """Global manager and queue for upcoming prompts. The way in which multiple questions are handled deserves some explanation. @@ -77,43 +80,19 @@ class PromptContainer(QWidget): _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. + + Signals: + show_prompt: Emitted when a prompt should be shown. """ - STYLESHEET = """ - {% set prompt_radius = config.get('ui', 'prompt-radius') %} - QWidget#Prompt { - {% if config.get('ui', 'status-position') == 'top' %} - border-bottom-left-radius: {{ prompt_radius }}px; - border-bottom-right-radius: {{ prompt_radius }}px; - {% else %} - border-top-left-radius: {{ prompt_radius }}px; - border-top-right-radius: {{ prompt_radius }}px; - {% endif %} - } + show_prompt = pyqtSignal(object) - QWidget { - font: {{ font['prompts'] }}; - color: {{ color['prompts.fg'] }}; - background-color: {{ color['prompts.bg'] }}; - } - """ - update_geometry = pyqtSignal() - - def __init__(self, win_id, parent=None): + def __init__(self, parent=None): super().__init__(parent) - self._layout = QVBoxLayout(self) - self._layout.setContentsMargins(10, 10, 10, 10) self._prompt = None self._shutting_down = False self._loops = [] self._queue = collections.deque() - self._win_id = win_id - - self.setObjectName('Prompt') - self.setAttribute(Qt.WA_StyledBackground, True) - style.set_register_stylesheet(self) def __repr__(self): return utils.get_repr(self, loops=len(self._loops), @@ -134,50 +113,6 @@ class PromptContainer(QWidget): # 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.setSizePolicy(self._prompt.sizePolicy()) - self._layout.addWidget(self._prompt) - self._prompt.show() - self.show() - self._prompt.setFocus() - self.update_geometry.emit() - return True - def shutdown(self): """Cancel all blocking questions. @@ -196,7 +131,7 @@ class PromptContainer(QWidget): else: return False - @cmdutils.register(instance='prompt-container', hide=True, scope='window', + @cmdutils.register(instance='prompt-queue', hide=True, modes=[usertypes.KeyMode.prompt, usertypes.KeyMode.yesno]) def prompt_accept(self, value=None): @@ -211,42 +146,30 @@ class PromptContainer(QWidget): value: If given, uses this value instead of the entered one. For boolean prompts, "yes"/"no" are accepted as value. """ + question = self._prompt.question try: done = self._prompt.accept(value) except Error as e: raise cmdexc.CommandError(str(e)) if done: - key_mode = self._prompt.KEY_MODE - self._prompt.question.done() - modeman.maybe_leave(self._win_id, key_mode, ':prompt-accept') + message.global_bridge.prompt_done.emit(self._prompt.KEY_MODE) + question.done() - @cmdutils.register(instance='prompt-container', hide=True, scope='window', + @cmdutils.register(instance='prompt-queue', hide=True, modes=[usertypes.KeyMode.yesno], deprecated='Use :prompt-accept yes instead!') def prompt_yes(self): """Answer yes to a yes/no prompt.""" self.prompt_accept('yes') - @cmdutils.register(instance='prompt-container', hide=True, scope='window', + @cmdutils.register(instance='prompt-queue', hide=True, modes=[usertypes.KeyMode.yesno], deprecated='Use :prompt-accept no instead!') def prompt_no(self): """Answer no to a yes/no prompt.""" self.prompt_accept('no') - @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='prompt-container', hide=True, scope='window', + @cmdutils.register(instance='prompt-queue', hide=True, modes=[usertypes.KeyMode.prompt], maxsplit=0) def prompt_open_download(self, cmdline: str=None): """Immediately open a download. @@ -265,7 +188,7 @@ class PromptContainer(QWidget): except UnsupportedOperationError: pass - @cmdutils.register(instance='prompt-container', hide=True, scope='window', + @cmdutils.register(instance='prompt-queue', hide=True, modes=[usertypes.KeyMode.prompt]) @cmdutils.argument('which', choices=['next', 'prev']) def prompt_item_focus(self, which): @@ -323,7 +246,11 @@ class PromptContainer(QWidget): usertypes.PromptMode.alert: AlertPrompt, } klass = classes[question.mode] - self._show_prompt(klass(question, self._win_id)) + + prompt = klass(question) + self._prompt = prompt + self.show_prompt.emit(prompt) + if blocking: loop = qtutils.EventLoop() self._loops.append(loop) @@ -331,8 +258,10 @@ class PromptContainer(QWidget): question.completed.connect(loop.quit) question.completed.connect(loop.deleteLater) loop.exec_() + self._prompt = prompt # FIXME don't we end up connecting modeman signals twice here now? - if not self._show_prompt(old_prompt): + self.show_prompt.emit(old_prompt) + if old_prompt is None: # Nothing left to restore, so we can go back to popping async # questions. if self._queue: @@ -341,6 +270,104 @@ class PromptContainer(QWidget): else: question.completed.connect(self._pop_later) + @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._prompt = None + self.show_prompt.emit(None) + # FIXME move this somewhere else? + if question.answer is None and not question.is_aborted: + question.cancel() + + +class PromptContainer(QWidget): + + """Container for prompts to be shown above the statusbar. + + This is a per-window object, however each window shows the same prompt. + + Attributes: + _layout: The layout used to show prompts in. + _win_id: The window ID this object is associated with. + + Signals: + update_geometry: Emitted when the geometry should be updated. + """ + + STYLESHEET = """ + {% set prompt_radius = config.get('ui', 'prompt-radius') %} + QWidget#PromptContainer { + {% if config.get('ui', 'status-position') == 'top' %} + border-bottom-left-radius: {{ prompt_radius }}px; + border-bottom-right-radius: {{ prompt_radius }}px; + {% else %} + border-top-left-radius: {{ prompt_radius }}px; + border-top-right-radius: {{ prompt_radius }}px; + {% endif %} + } + + QWidget { + font: {{ font['prompts'] }}; + color: {{ color['prompts.fg'] }}; + background-color: {{ color['prompts.bg'] }}; + } + """ + update_geometry = pyqtSignal() + + def __init__(self, win_id, parent=None): + super().__init__(parent) + self._layout = QVBoxLayout(self) + self._layout.setContentsMargins(10, 10, 10, 10) + self._win_id = win_id + + self.setObjectName('PromptContainer') + self.setAttribute(Qt.WA_StyledBackground, True) + style.set_register_stylesheet(self) + + message.global_bridge.prompt_done.connect(self._on_prompt_done) + prompt_queue.show_prompt.connect(self._on_show_prompt) + + def __repr__(self): + return utils.get_repr(self, win_id=self._win_id) + + @pyqtSlot(object) + def _on_show_prompt(self, prompt): + """Show the given prompt object. + + Args: + prompt: A Prompt object or None. + """ + # Note that we don't delete the old prompt here, as we might be in the + # middle of saving/restoring an old prompt object. + # FIXME where is it deleted? + self._layout.takeAt(0) + assert self._layout.count() == 0 + log.prompt.debug("Displaying prompt {}".format(prompt)) + if prompt is None: + self.hide() + return + + 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.setSizePolicy(prompt.sizePolicy()) + self._layout.addWidget(prompt) + prompt.show() + self.show() + prompt.setFocus() + self.update_geometry.emit() + + @pyqtSlot(usertypes.KeyMode) + def _on_prompt_done(self, key_mode): + """Leave the prompt mode in this window if a question was answered.""" + modeman.maybe_leave(self._win_id, key_mode, ':prompt-accept') + class LineEdit(QLineEdit): @@ -379,10 +406,9 @@ class _BasePrompt(QWidget): KEY_MODE = usertypes.KeyMode.prompt - def __init__(self, question, win_id, parent=None): + def __init__(self, question, parent=None): super().__init__(parent) self.question = question - self._win_id = win_id self._vbox = QVBoxLayout(self) self._vbox.setSpacing(15) self._key_grid = None @@ -448,8 +474,8 @@ class LineEditPrompt(_BasePrompt): """A prompt for a single text value.""" - def __init__(self, question, win_id, parent=None): - super().__init__(question, win_id, parent) + def __init__(self, question, parent=None): + super().__init__(question, parent) self._lineedit = LineEdit(self) self._init_title(question) self._vbox.addWidget(self._lineedit) @@ -472,8 +498,8 @@ class FilenamePrompt(_BasePrompt): """A prompt for a filename.""" - def __init__(self, question, win_id, parent=None): - super().__init__(question, win_id, parent) + def __init__(self, question, parent=None): + super().__init__(question, parent) self._init_title(question) self._init_fileview() self._set_fileview_root(question.default) @@ -584,8 +610,8 @@ class DownloadFilenamePrompt(FilenamePrompt): """A prompt for a filename for downloads.""" - def __init__(self, question, win_id, parent=None): - super().__init__(question, win_id, parent) + def __init__(self, question, parent=None): + super().__init__(question, parent) self._file_model.setFilter(QDir.AllDirs | QDir.Drives | QDir.NoDot) def accept(self, value=None): @@ -595,8 +621,9 @@ class DownloadFilenamePrompt(FilenamePrompt): def download_open(self, cmdline): self.question.answer = usertypes.OpenFileDownloadTarget(cmdline) - modeman.maybe_leave(self._win_id, usertypes.KeyMode.prompt, - 'download open') + # FIXME now we don't have a window ID here... + # modeman.maybe_leave(self._win_id, usertypes.KeyMode.prompt, + # 'download open') self.question.done() def _allowed_commands(self): @@ -612,8 +639,8 @@ class AuthenticationPrompt(_BasePrompt): """A prompt for username/password.""" - def __init__(self, question, win_id, parent=None): - super().__init__(question, win_id, parent) + def __init__(self, question, parent=None): + super().__init__(question, parent) self._init_title(question) user_label = QLabel("Username:", self) @@ -672,8 +699,8 @@ class YesNoPrompt(_BasePrompt): KEY_MODE = usertypes.KeyMode.yesno - def __init__(self, question, win_id, parent=None): - super().__init__(question, win_id, parent) + def __init__(self, question, parent=None): + super().__init__(question, parent) self._init_title(question) self._init_key_label() @@ -709,8 +736,8 @@ class AlertPrompt(_BasePrompt): """A prompt without any answer possibility.""" - def __init__(self, question, win_id, parent=None): - super().__init__(question, win_id, parent) + def __init__(self, question, parent=None): + super().__init__(question, parent) self._init_title(question) self._init_key_label() @@ -722,3 +749,11 @@ class AlertPrompt(_BasePrompt): def _allowed_commands(self): return [('prompt-accept', "Hide")] + + +def init(): + global prompt_queue + prompt_queue = PromptQueue() + objreg.register('prompt-queue', prompt_queue) # for commands + message.global_bridge.ask_question.connect( + prompt_queue.ask_question, Qt.DirectConnection) diff --git a/qutebrowser/utils/message.py b/qutebrowser/utils/message.py index 287c8fb40..642ec55b0 100644 --- a/qutebrowser/utils/message.py +++ b/qutebrowser/utils/message.py @@ -90,11 +90,10 @@ def _build_question(title, text=None, *, mode, default=None, abort_on=()): return question -def ask(win_id, *args, **kwargs): +def ask(*args, **kwargs): """Ask a modular question in the statusbar (blocking). Args: - win_id: The ID of the window which is calling this function. message: The message to display to the user. mode: A PromptMode. default: The default value to display. @@ -105,18 +104,16 @@ def ask(win_id, *args, **kwargs): The answer the user gave or None if the prompt was cancelled. """ question = _build_question(*args, **kwargs) # pylint: disable=missing-kwoa - bridge = objreg.get('message-bridge', scope='window', window=win_id) - bridge.ask(question, blocking=True) + global_bridge.ask(question, blocking=True) answer = question.answer question.deleteLater() return answer -def ask_async(win_id, text, mode, handler, **kwargs): +def ask_async(text, mode, handler, **kwargs): """Ask an async question in the statusbar. Args: - win_id: The ID of the window which is calling this function. message: The message to display to the user. mode: A PromptMode. handler: The function to get called with the answer as argument. @@ -126,16 +123,14 @@ def ask_async(win_id, text, mode, handler, **kwargs): question = _build_question(text, mode=mode, **kwargs) question.answered.connect(handler) question.completed.connect(question.deleteLater) - bridge = objreg.get('message-bridge', scope='window', window=win_id) - bridge.ask(question, blocking=False) + global_bridge.ask(question, blocking=False) -def confirm_async(win_id, yes_action, no_action=None, cancel_action=None, +def confirm_async(yes_action, no_action=None, cancel_action=None, *args, **kwargs): """Ask a yes/no question to the user and execute the given actions. Args: - win_id: The ID of the window which is calling this function. message: The message to display to the user. yes_action: Callable to be called when the user answered yes. no_action: Callable to be called when the user answered no. @@ -156,8 +151,7 @@ def confirm_async(win_id, yes_action, no_action=None, cancel_action=None, question.cancelled.connect(cancel_action) question.completed.connect(question.deleteLater) - bridge = objreg.get('message-bridge', scope='window', window=win_id) - bridge.ask(question, blocking=False) + global_bridge.ask(question, blocking=False) return question @@ -169,9 +163,32 @@ class GlobalMessageBridge(QObject): show_message: Show a message arg 0: A MessageLevel member arg 1: The text to show + prompt_done: Emitted when a prompt was answered somewhere. + ask_question: Ask a question to the user. + arg 0: The Question object to ask. + arg 1: Whether to block (True) or ask async (False). + + IMPORTANT: Slots need to be connected to this signal via + a Qt.DirectConnection! """ show_message = pyqtSignal(usertypes.MessageLevel, str) + prompt_done = pyqtSignal(usertypes.KeyMode) + ask_question = pyqtSignal(usertypes.Question, bool) + + def ask(self, question, blocking, *, log_stack=False): + """Ask a question to the user. + + Note this method doesn't return the answer, it only blocks. The caller + needs to construct a Question object and get the answer. + + Args: + question: A Question object. + blocking: Whether to return immediately or wait until the + question is answered. + log_stack: ignored + """ + self.ask_question.emit(question, blocking) class MessageBridge(QObject): @@ -183,18 +200,10 @@ class MessageBridge(QObject): arg: The text to set. s_maybe_reset_text: Reset the text if it hasn't been changed yet. arg: The expected text. - - s_question: Ask a question to the user in the statusbar. - arg 0: The Question object to ask. - arg 1: Whether to block (True) or ask async (False). - - IMPORTANT: Slots need to be connected to this signal via a - Qt.DirectConnection! """ s_set_text = pyqtSignal(str) s_maybe_reset_text = pyqtSignal(str) - s_question = pyqtSignal(usertypes.Question, bool) def __repr__(self): return utils.get_repr(self) @@ -219,19 +228,5 @@ class MessageBridge(QObject): """ self.s_maybe_reset_text.emit(str(text)) - def ask(self, question, blocking, *, log_stack=False): - """Ask a question to the user. - - Note this method doesn't return the answer, it only blocks. The caller - needs to construct a Question object and get the answer. - - Args: - question: A Question object. - blocking: Whether to return immediately or wait until the - question is answered. - log_stack: ignored - """ - self.s_question.emit(question, blocking) - global_bridge = GlobalMessageBridge() From 1d6166b4747cb65fb3cddaa8c698f093926bea37 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 27 Oct 2016 22:16:23 +0200 Subject: [PATCH 42/87] Global prompt container --- qutebrowser/app.py | 6 +++--- qutebrowser/mainwindow/mainwindow.py | 8 ++------ qutebrowser/mainwindow/prompt.py | 21 +++++++++++---------- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index cdce0be6d..0f4b45b0e 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -372,9 +372,6 @@ def _init_modules(args, crash_handler): crash_handler: The CrashHandler instance. """ # pylint: disable=too-many-statements - log.init.debug("Initializing prompts...") - prompt.init() - log.init.debug("Initializing save manager...") save_manager = savemanager.SaveManager(qApp) objreg.register('save-manager', save_manager) @@ -391,6 +388,9 @@ def _init_modules(args, crash_handler): config.init(qApp) save_manager.init_autosave() + log.init.debug("Initializing prompts...") + prompt.init() + log.init.debug("Initializing web history...") history.init(qApp) diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index 490529180..f06c926b8 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -183,13 +183,9 @@ class MainWindow(QWidget): self._messageview = messageview.MessageView(parent=self) self._add_overlay(self._messageview, self._messageview.update_geometry) - self._prompt_container = prompt.PromptContainer(self.win_id, self) - self._add_overlay(self._prompt_container, - self._prompt_container.update_geometry, + self._add_overlay(prompt.prompt_container, + prompt.prompt_container.update_geometry, centered=True, padding=10) - 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) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 6c21bb69a..1b91bb0c6 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -292,7 +292,6 @@ class PromptContainer(QWidget): Attributes: _layout: The layout used to show prompts in. - _win_id: The window ID this object is associated with. Signals: update_geometry: Emitted when the geometry should be updated. @@ -318,11 +317,10 @@ class PromptContainer(QWidget): """ update_geometry = pyqtSignal() - def __init__(self, win_id, parent=None): + def __init__(self, parent=None): super().__init__(parent) self._layout = QVBoxLayout(self) self._layout.setContentsMargins(10, 10, 10, 10) - self._win_id = win_id self.setObjectName('PromptContainer') self.setAttribute(Qt.WA_StyledBackground, True) @@ -332,7 +330,7 @@ class PromptContainer(QWidget): prompt_queue.show_prompt.connect(self._on_show_prompt) def __repr__(self): - return utils.get_repr(self, win_id=self._win_id) + return utils.get_repr(self) @pyqtSlot(object) def _on_show_prompt(self, prompt): @@ -351,10 +349,11 @@ class PromptContainer(QWidget): self.hide() return - 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') + # FIXME no win-id + # 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.setSizePolicy(prompt.sizePolicy()) self._layout.addWidget(prompt) @@ -366,7 +365,8 @@ class PromptContainer(QWidget): @pyqtSlot(usertypes.KeyMode) def _on_prompt_done(self, key_mode): """Leave the prompt mode in this window if a question was answered.""" - modeman.maybe_leave(self._win_id, key_mode, ':prompt-accept') + # FIXME no win-id + #modeman.maybe_leave(self._win_id, key_mode, ':prompt-accept') class LineEdit(QLineEdit): @@ -752,8 +752,9 @@ class AlertPrompt(_BasePrompt): def init(): - global prompt_queue + global prompt_queue, prompt_container prompt_queue = PromptQueue() + prompt_container = PromptContainer() objreg.register('prompt-queue', prompt_queue) # for commands message.global_bridge.ask_question.connect( prompt_queue.ask_question, Qt.DirectConnection) From fdd11476203b8e26e4ede17e1a8821ea8317c3f5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 27 Oct 2016 22:16:26 +0200 Subject: [PATCH 43/87] Revert "Global prompt container" This reverts commit c23beee6502776dd19c0955b311e8dfb9f1c77ae. --- qutebrowser/app.py | 6 +++--- qutebrowser/mainwindow/mainwindow.py | 8 ++++++-- qutebrowser/mainwindow/prompt.py | 21 ++++++++++----------- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 0f4b45b0e..cdce0be6d 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -372,6 +372,9 @@ def _init_modules(args, crash_handler): crash_handler: The CrashHandler instance. """ # pylint: disable=too-many-statements + log.init.debug("Initializing prompts...") + prompt.init() + log.init.debug("Initializing save manager...") save_manager = savemanager.SaveManager(qApp) objreg.register('save-manager', save_manager) @@ -388,9 +391,6 @@ def _init_modules(args, crash_handler): config.init(qApp) save_manager.init_autosave() - log.init.debug("Initializing prompts...") - prompt.init() - log.init.debug("Initializing web history...") history.init(qApp) diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index f06c926b8..490529180 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -183,9 +183,13 @@ class MainWindow(QWidget): self._messageview = messageview.MessageView(parent=self) self._add_overlay(self._messageview, self._messageview.update_geometry) - self._add_overlay(prompt.prompt_container, - prompt.prompt_container.update_geometry, + self._prompt_container = prompt.PromptContainer(self.win_id, self) + self._add_overlay(self._prompt_container, + self._prompt_container.update_geometry, centered=True, padding=10) + 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) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 1b91bb0c6..6c21bb69a 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -292,6 +292,7 @@ class PromptContainer(QWidget): Attributes: _layout: The layout used to show prompts in. + _win_id: The window ID this object is associated with. Signals: update_geometry: Emitted when the geometry should be updated. @@ -317,10 +318,11 @@ class PromptContainer(QWidget): """ update_geometry = pyqtSignal() - def __init__(self, parent=None): + def __init__(self, win_id, parent=None): super().__init__(parent) self._layout = QVBoxLayout(self) self._layout.setContentsMargins(10, 10, 10, 10) + self._win_id = win_id self.setObjectName('PromptContainer') self.setAttribute(Qt.WA_StyledBackground, True) @@ -330,7 +332,7 @@ class PromptContainer(QWidget): prompt_queue.show_prompt.connect(self._on_show_prompt) def __repr__(self): - return utils.get_repr(self) + return utils.get_repr(self, win_id=self._win_id) @pyqtSlot(object) def _on_show_prompt(self, prompt): @@ -349,11 +351,10 @@ class PromptContainer(QWidget): self.hide() return - # FIXME no win-id - # 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') + 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.setSizePolicy(prompt.sizePolicy()) self._layout.addWidget(prompt) @@ -365,8 +366,7 @@ class PromptContainer(QWidget): @pyqtSlot(usertypes.KeyMode) def _on_prompt_done(self, key_mode): """Leave the prompt mode in this window if a question was answered.""" - # FIXME no win-id - #modeman.maybe_leave(self._win_id, key_mode, ':prompt-accept') + modeman.maybe_leave(self._win_id, key_mode, ':prompt-accept') class LineEdit(QLineEdit): @@ -752,9 +752,8 @@ class AlertPrompt(_BasePrompt): def init(): - global prompt_queue, prompt_container + global prompt_queue prompt_queue = PromptQueue() - prompt_container = PromptContainer() objreg.register('prompt-queue', prompt_queue) # for commands message.global_bridge.ask_question.connect( prompt_queue.ask_question, Qt.DirectConnection) From dba29e518a4a832f8fffd5f3c65fcabd42adbcc7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 27 Oct 2016 22:54:51 +0200 Subject: [PATCH 44/87] First working prototype of global prompts --- qutebrowser/browser/webkit/downloads.py | 6 +- qutebrowser/browser/webkit/webpage.py | 3 +- qutebrowser/mainwindow/prompt.py | 235 ++++++++++++------------ 3 files changed, 121 insertions(+), 123 deletions(-) diff --git a/qutebrowser/browser/webkit/downloads.py b/qutebrowser/browser/webkit/downloads.py index 05857f649..610dae610 100644 --- a/qutebrowser/browser/webkit/downloads.py +++ b/qutebrowser/browser/webkit/downloads.py @@ -155,9 +155,7 @@ def ask_for_filename(suggested_filename, win_id, *, parent=None, q.completed.connect(q.deleteLater) q.default = _path_suggestion(suggested_filename) - message_bridge = objreg.get('message-bridge', scope='window', - window=win_id) - q.ask = lambda: message_bridge.ask(q, blocking=False) + q.ask = lambda: message.global_bridge.ask(q, blocking=False) return _DownloadPath(filename=None, question=q) @@ -385,7 +383,7 @@ class DownloadItem(QObject): def _ask_confirm_question(self, title, msg): """Create a Question object to be asked.""" no_action = functools.partial(self.cancel, remove_data=False) - message.confirm_async(self._win_id, title=title, text=msg, + message.confirm_async(title=title, text=msg, yes_action=self._create_fileobj, no_action=no_action, cancel_action=no_action, abort_on=[self.cancelled, self.error]) diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py index 5f6e5e76b..92a9b9dfb 100644 --- a/qutebrowser/browser/webkit/webpage.py +++ b/qutebrowser/browser/webkit/webpage.py @@ -332,8 +332,7 @@ class BrowserPage(QWebPage): self.setFeaturePermission, frame, feature, QWebPage.PermissionDeniedByUser) - question = message.confirm_async(self._win_id, - yes_action=yes_action, + question = message.confirm_async(yes_action=yes_action, no_action=no_action, cancel_action=no_action, abort_on=[self.shutting_down, diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 6c21bb69a..5e7242a0d 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -61,8 +61,8 @@ class PromptQueue(QObject): 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. + the current question 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 @@ -79,24 +79,24 @@ class PromptQueue(QObject): 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. + _question: The current Question object if we're handling a question. Signals: - show_prompt: Emitted when a prompt should be shown. + show_prompts: Emitted with a Question object when prompts should be shown. """ - show_prompt = pyqtSignal(object) + show_prompts = pyqtSignal(usertypes.Question) def __init__(self, parent=None): super().__init__(parent) - self._prompt = None + self._question = None self._shutting_down = False self._loops = [] self._queue = collections.deque() def __repr__(self): return utils.get_repr(self, loops=len(self._loops), - queue=len(self._queue), prompt=self._prompt) + queue=len(self._queue), question=self._question) def _pop_later(self): """Helper to call self._pop as soon as everything else is done.""" @@ -131,77 +131,6 @@ class PromptQueue(QObject): else: return False - @cmdutils.register(instance='prompt-queue', hide=True, - modes=[usertypes.KeyMode.prompt, - usertypes.KeyMode.yesno]) - def prompt_accept(self, value=None): - """Accept the current prompt. - - // - - This executes the next action depending on the question mode, e.g. asks - for the password or leaves the mode. - - Args: - value: If given, uses this value instead of the entered one. - For boolean prompts, "yes"/"no" are accepted as value. - """ - question = self._prompt.question - try: - done = self._prompt.accept(value) - except Error as e: - raise cmdexc.CommandError(str(e)) - if done: - message.global_bridge.prompt_done.emit(self._prompt.KEY_MODE) - question.done() - - @cmdutils.register(instance='prompt-queue', hide=True, - modes=[usertypes.KeyMode.yesno], - deprecated='Use :prompt-accept yes instead!') - def prompt_yes(self): - """Answer yes to a yes/no prompt.""" - self.prompt_accept('yes') - - @cmdutils.register(instance='prompt-queue', hide=True, - modes=[usertypes.KeyMode.yesno], - deprecated='Use :prompt-accept no instead!') - def prompt_no(self): - """Answer no to a yes/no prompt.""" - self.prompt_accept('no') - - @cmdutils.register(instance='prompt-queue', hide=True, - modes=[usertypes.KeyMode.prompt], maxsplit=0) - def prompt_open_download(self, cmdline: str=None): - """Immediately open a download. - - If no specific command is given, this will use the system's default - application to open the file. - - Args: - cmdline: The command which should be used to open the file. A `{}` - is expanded to the temporary file name. If no `{}` is - present, the filename is automatically appended to the - cmdline. - """ - try: - self._prompt.download_open(cmdline) - except UnsupportedOperationError: - pass - - @cmdutils.register(instance='prompt-queue', hide=True, - modes=[usertypes.KeyMode.prompt]) - @cmdutils.argument('which', choices=['next', 'prev']) - def prompt_item_focus(self, which): - """Shift the focus of the prompt file completion menu to another item. - - Args: - which: 'next', 'prev' - """ - try: - self._prompt.item_focus(which) - except UnsupportedOperationError: - pass - @pyqtSlot(usertypes.Question, bool) def ask_question(self, question, blocking): """Display a prompt for a given question. @@ -226,7 +155,7 @@ class PromptQueue(QObject): question.abort() return None - if self._prompt is not None and not blocking: + if self._question is not None and not blocking: # We got an async question, but we're already busy with one, so we # just queue it up for later. log.prompt.debug("Adding {} to queue.".format(question)) @@ -234,22 +163,12 @@ class PromptQueue(QObject): 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 + # If we're blocking we save the old question on the stack, so we + # can restore it after exec, if exec gets called multiple times. + old_question = self._question - classes = { - usertypes.PromptMode.yesno: YesNoPrompt, - usertypes.PromptMode.text: LineEditPrompt, - usertypes.PromptMode.user_pwd: AuthenticationPrompt, - usertypes.PromptMode.download: DownloadFilenamePrompt, - usertypes.PromptMode.alert: AlertPrompt, - } - klass = classes[question.mode] - - prompt = klass(question) - self._prompt = prompt - self.show_prompt.emit(prompt) + self._question = question + self.show_prompts.emit(question) if blocking: loop = qtutils.EventLoop() @@ -258,10 +177,9 @@ class PromptQueue(QObject): question.completed.connect(loop.quit) question.completed.connect(loop.deleteLater) loop.exec_() - self._prompt = prompt # FIXME don't we end up connecting modeman signals twice here now? - self.show_prompt.emit(old_prompt) - if old_prompt is None: + self.show_prompts.emit(old_question) + if old_question is None: # Nothing left to restore, so we can go back to popping async # questions. if self._queue: @@ -273,15 +191,12 @@ class PromptQueue(QObject): @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._prompt = None - self.show_prompt.emit(None) + if self._question is not None: + self.show_prompts.emit(None) # FIXME move this somewhere else? - if question.answer is None and not question.is_aborted: - question.cancel() + if self._question.answer is None and not self._question.is_aborted: + self._question.cancel() + self._question = None class PromptContainer(QWidget): @@ -323,35 +238,49 @@ class PromptContainer(QWidget): self._layout = QVBoxLayout(self) self._layout.setContentsMargins(10, 10, 10, 10) self._win_id = win_id + self._prompt = None self.setObjectName('PromptContainer') self.setAttribute(Qt.WA_StyledBackground, True) style.set_register_stylesheet(self) message.global_bridge.prompt_done.connect(self._on_prompt_done) - prompt_queue.show_prompt.connect(self._on_show_prompt) + prompt_queue.show_prompts.connect(self._on_show_prompts) def __repr__(self): return utils.get_repr(self, win_id=self._win_id) - @pyqtSlot(object) - def _on_show_prompt(self, prompt): - """Show the given prompt object. + @pyqtSlot(usertypes.Question) + def _on_show_prompts(self, question): + """Show a prompt for the given question. Args: - prompt: A Prompt object or None. + question: A Question object or None. """ - # Note that we don't delete the old prompt here, as we might be in the - # middle of saving/restoring an old prompt object. - # FIXME where is it deleted? - self._layout.takeAt(0) + item = self._layout.takeAt(0) assert self._layout.count() == 0 - log.prompt.debug("Displaying prompt {}".format(prompt)) - if prompt is None: + if item is not None: + item.widget().deleteLater() + + if question is None: + self._prompt = None self.hide() return - prompt.question.aborted.connect( + classes = { + usertypes.PromptMode.yesno: YesNoPrompt, + usertypes.PromptMode.text: LineEditPrompt, + usertypes.PromptMode.user_pwd: AuthenticationPrompt, + usertypes.PromptMode.download: DownloadFilenamePrompt, + usertypes.PromptMode.alert: AlertPrompt, + } + klass = classes[question.mode] + prompt = klass(question) + + log.prompt.debug("Displaying prompt {}".format(prompt)) + self._prompt = prompt + + question.aborted.connect( lambda: modeman.maybe_leave(self._win_id, prompt.KEY_MODE, 'aborted')) modeman.enter(self._win_id, prompt.KEY_MODE, 'question asked') @@ -368,6 +297,78 @@ class PromptContainer(QWidget): """Leave the prompt mode in this window if a question was answered.""" modeman.maybe_leave(self._win_id, key_mode, ':prompt-accept') + @cmdutils.register(instance='prompt-container', hide=True, scope='window', + modes=[usertypes.KeyMode.prompt, + usertypes.KeyMode.yesno]) + def prompt_accept(self, value=None): + """Accept the current prompt. + + // + + This executes the next action depending on the question mode, e.g. asks + for the password or leaves the mode. + + Args: + value: If given, uses this value instead of the entered one. + For boolean prompts, "yes"/"no" are accepted as value. + """ + question = self._prompt.question + try: + done = self._prompt.accept(value) + except Error as e: + raise cmdexc.CommandError(str(e)) + if done: + message.global_bridge.prompt_done.emit(self._prompt.KEY_MODE) + question.done() + + @cmdutils.register(instance='prompt-container', hide=True, scope='window', + modes=[usertypes.KeyMode.yesno], + deprecated='Use :prompt-accept yes instead!') + def prompt_yes(self): + """Answer yes to a yes/no prompt.""" + self.prompt_accept('yes') + + @cmdutils.register(instance='prompt-container', hide=True, scope='window', + modes=[usertypes.KeyMode.yesno], + deprecated='Use :prompt-accept no instead!') + def prompt_no(self): + """Answer no to a yes/no prompt.""" + self.prompt_accept('no') + + @cmdutils.register(instance='prompt-container', hide=True, scope='window', + modes=[usertypes.KeyMode.prompt], maxsplit=0) + def prompt_open_download(self, cmdline: str=None): + """Immediately open a download. + + If no specific command is given, this will use the system's default + application to open the file. + + Args: + cmdline: The command which should be used to open the file. A `{}` + is expanded to the temporary file name. If no `{}` is + present, the filename is automatically appended to the + cmdline. + """ + try: + self._prompt.download_open(cmdline) + except UnsupportedOperationError: + pass + + @cmdutils.register(instance='prompt-container', hide=True, scope='window', + modes=[usertypes.KeyMode.prompt]) + @cmdutils.argument('which', choices=['next', 'prev']) + def prompt_item_focus(self, which): + """Shift the focus of the prompt file completion menu to another item. + + Args: + which: 'next', 'prev' + """ + try: + self._prompt.item_focus(which) + except UnsupportedOperationError: + pass + + class LineEdit(QLineEdit): From 6f83590553d0af3d3951ab73108eb1093e2bd002 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 28 Oct 2016 06:37:25 +0200 Subject: [PATCH 45/87] Add some more logging for prompts --- qutebrowser/mainwindow/prompt.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 5e7242a0d..52ec5313c 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -122,6 +122,7 @@ class PromptQueue(QObject): True if loops needed to be aborted, False otherwise. """ + log.prompt.debug("Shutting down with loops {}".format(self._loops)) self._shutting_down = True if self._loops: for loop in self._loops: @@ -165,6 +166,8 @@ class PromptQueue(QObject): if blocking: # If we're blocking we save the old question on the stack, so we # can restore it after exec, if exec gets called multiple times. + log.prompt.debug("New question is blocking, saving {}".format( + self._question)) old_question = self._question self._question = question @@ -176,8 +179,11 @@ class PromptQueue(QObject): loop.destroyed.connect(lambda: self._loops.remove(loop)) question.completed.connect(loop.quit) question.completed.connect(loop.deleteLater) + log.prompt.debug("Starting loop.exec_() for {}".format(question)) loop.exec_() + log.prompt.debug("Ending loop.exec_() for {}".format(question)) # FIXME don't we end up connecting modeman signals twice here now? + log.prompt.debug("Restoring old question {}".format(old_question)) self.show_prompts.emit(old_question) if old_question is None: # Nothing left to restore, so we can go back to popping async @@ -192,6 +198,8 @@ class PromptQueue(QObject): def on_mode_left(self, mode): """Clear and reset input when the mode was left.""" if self._question is not None: + log.prompt.debug("Left mode {}, hiding {}".format( + mode, self._question)) self.show_prompts.emit(None) # FIXME move this somewhere else? if self._question.answer is None and not self._question.is_aborted: From 5eb66f269366c10dae993696280bd3528c5d3b83 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 28 Oct 2016 06:39:33 +0200 Subject: [PATCH 46/87] Restore self._question when restoring prompt --- qutebrowser/mainwindow/prompt.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 52ec5313c..dcef2023d 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -182,14 +182,17 @@ class PromptQueue(QObject): log.prompt.debug("Starting loop.exec_() for {}".format(question)) loop.exec_() log.prompt.debug("Ending loop.exec_() for {}".format(question)) + # FIXME don't we end up connecting modeman signals twice here now? log.prompt.debug("Restoring old question {}".format(old_question)) + self._question = old_question self.show_prompts.emit(old_question) if old_question is None: # 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) From 54a694e35e84066f604a5cebaea331e72fa17f73 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 28 Oct 2016 07:02:04 +0200 Subject: [PATCH 47/87] Hide prompts correctly after :prompt-download-open --- qutebrowser/mainwindow/prompt.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index dcef2023d..5a28bb03a 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -633,10 +633,8 @@ class DownloadFilenamePrompt(FilenamePrompt): def download_open(self, cmdline): self.question.answer = usertypes.OpenFileDownloadTarget(cmdline) - # FIXME now we don't have a window ID here... - # modeman.maybe_leave(self._win_id, usertypes.KeyMode.prompt, - # 'download open') self.question.done() + message.global_bridge.prompt_done.emit(self.KEY_MODE) def _allowed_commands(self): cmds = [ From b0a04bff8e48f5a7233e967b67b60b6e3e5e0792 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 28 Oct 2016 07:15:46 +0200 Subject: [PATCH 48/87] Use OS-independent path separators --- qutebrowser/mainwindow/prompt.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 5a28bb03a..d0d807d26 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -529,9 +529,13 @@ class FilenamePrompt(_BasePrompt): @pyqtSlot(str) def _set_fileview_root(self, path): """Set the root path for the file display.""" - if not path.endswith('/') or path == '/': + separators = os.sep + if os.altsep is not None: + separators += os.altsep + + if path == '/' or (path and path[-1] not in separators): return - path.rstrip('/') + path.rstrip(separators) try: if os.path.isdir(path): From 4a360ba185341f6404e7f2751f01ba2a3102b3be Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 28 Oct 2016 07:17:57 +0200 Subject: [PATCH 49/87] Simplify _set_fileview_root --- qutebrowser/mainwindow/prompt.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index d0d807d26..ccf2c3d4d 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -538,16 +538,9 @@ class FilenamePrompt(_BasePrompt): path.rstrip(separators) try: - if os.path.isdir(path): - path = path - elif os.path.isdir(os.path.basename(path)): - path = os.path.basename(path) - else: - path = None + if not os.path.isdir(path): + return except OSError: - path = None - - if path is None: return root = self._file_model.setRootPath(path) From 1b3e9613eaa6fc511bc23fed29e4944c8d484620 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 28 Oct 2016 07:34:45 +0200 Subject: [PATCH 50/87] Fix mode handling with multi-window questions When the prompt mode is left in any window, we need to take care of: - Cancelling the question if needed (exactly once) - Leaving the prompt mode in all other windows too --- qutebrowser/mainwindow/mainwindow.py | 8 ++++---- qutebrowser/mainwindow/prompt.py | 21 ++++++++++++++++++--- qutebrowser/utils/message.py | 2 ++ 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index 490529180..48763a2f7 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -175,6 +175,9 @@ class MainWindow(QWidget): self._init_completion() + log.init.debug("Initializing modes...") + modeman.init(self.win_id, self) + self._commandrunner = runners.CommandRunner(self.win_id, partial_match=True) @@ -191,9 +194,6 @@ class MainWindow(QWidget): scope='window', window=self.win_id) self._prompt_container.hide() - log.init.debug("Initializing modes...") - modeman.init(self.win_id, self) - if geometry is not None: self._load_geometry(geometry) elif self.win_id == 0: @@ -397,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(prompt.prompt_queue.on_mode_left) + mode_manager.left.connect(message.global_bridge.mode_left) # commands keyparsers[usertypes.KeyMode.normal].keystring_updated.connect( diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index ccf2c3d4d..b4b1c5813 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -93,6 +93,7 @@ class PromptQueue(QObject): self._shutting_down = False self._loops = [] self._queue = collections.deque() + message.global_bridge.mode_left.connect(self._on_mode_left) def __repr__(self): return utils.get_repr(self, loops=len(self._loops), @@ -198,14 +199,15 @@ class PromptQueue(QObject): question.completed.connect(self._pop_later) @pyqtSlot(usertypes.KeyMode) - def on_mode_left(self, mode): - """Clear and reset input when the mode was left.""" + def _on_mode_left(self, mode): + """Abort question when a mode was left.""" if self._question is not None: log.prompt.debug("Left mode {}, hiding {}".format( mode, self._question)) self.show_prompts.emit(None) - # FIXME move this somewhere else? if self._question.answer is None and not self._question.is_aborted: + log.prompt.debug("Cancelling {} because {} was left".format( + self._question, mode)) self._question.cancel() self._question = None @@ -257,6 +259,7 @@ class PromptContainer(QWidget): message.global_bridge.prompt_done.connect(self._on_prompt_done) prompt_queue.show_prompts.connect(self._on_show_prompts) + message.global_bridge.mode_left.connect(self._on_global_mode_left) def __repr__(self): return utils.get_repr(self, win_id=self._win_id) @@ -308,6 +311,18 @@ class PromptContainer(QWidget): """Leave the prompt mode in this window if a question was answered.""" modeman.maybe_leave(self._win_id, key_mode, ':prompt-accept') + @pyqtSlot(usertypes.KeyMode) + def _on_global_mode_left(self, mode): + """Leave prompt/yesno mode in this window if it was left elsewhere. + + PromptQueue takes care of getting rid of the question if a mode was + left, but if that happens in a different window, this window will still + be stuck in prompt mode. Here we make sure to leave that if it was left + anywhere else. + """ + if mode in [usertypes.KeyMode.prompt, usertypes.KeyMode.yesno]: + modeman.maybe_leave(self._win_id, mode, 'left in other window') + @cmdutils.register(instance='prompt-container', hide=True, scope='window', modes=[usertypes.KeyMode.prompt, usertypes.KeyMode.yesno]) diff --git a/qutebrowser/utils/message.py b/qutebrowser/utils/message.py index 642ec55b0..4e1250e77 100644 --- a/qutebrowser/utils/message.py +++ b/qutebrowser/utils/message.py @@ -170,11 +170,13 @@ class GlobalMessageBridge(QObject): IMPORTANT: Slots need to be connected to this signal via a Qt.DirectConnection! + mode_left: Emitted when a keymode was left in any window. """ show_message = pyqtSignal(usertypes.MessageLevel, str) prompt_done = pyqtSignal(usertypes.KeyMode) ask_question = pyqtSignal(usertypes.Question, bool) + mode_left = pyqtSignal(usertypes.KeyMode) def ask(self, question, blocking, *, log_stack=False): """Ask a question to the user. From 5ebaddfaf2c0fdeb780e4706ed26619d54c61bf1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 28 Oct 2016 07:43:40 +0200 Subject: [PATCH 51/87] Ignore "Unexpected null parameter" warning --- qutebrowser/utils/log.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index a40ed9046..1a3db19be 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -409,6 +409,8 @@ def qt_message_handler(msg_type, context, msg): "QXcbClipboard: SelectionRequest too old", # https://github.com/The-Compiler/qutebrowser/issues/2071 'QXcbWindow: Unhandled client message: ""', + # No idea where this comes from... + "QObject::disconnect: Unexpected null parameter", ] if sys.platform == 'darwin': suppressed_msgs += [ From a87e46101c23e66974afa7a5ea1b1f2f80055c33 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 28 Oct 2016 11:31:39 +0200 Subject: [PATCH 52/87] Fix lint --- qutebrowser/browser/webkit/downloads.py | 5 ++--- qutebrowser/mainwindow/prompt.py | 8 ++++---- qutebrowser/utils/message.py | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/qutebrowser/browser/webkit/downloads.py b/qutebrowser/browser/webkit/downloads.py index 610dae610..a9c6153f8 100644 --- a/qutebrowser/browser/webkit/downloads.py +++ b/qutebrowser/browser/webkit/downloads.py @@ -119,7 +119,7 @@ def create_full_filename(basename, filename): return None -def ask_for_filename(suggested_filename, win_id, *, parent=None, +def ask_for_filename(suggested_filename, *, parent=None, prompt_download_directory=None): """Prepare a question for a download-path. @@ -133,7 +133,6 @@ def ask_for_filename(suggested_filename, win_id, *, parent=None, Args: suggested_filename: The "default"-name that is pre-entered as path. - win_id: The window where the question will be asked. parent: The parent of the question (a QObject). prompt_download_directory: If this is something else than None, it will overwrite the @@ -954,7 +953,7 @@ class DownloadManager(QObject): # Neither filename nor fileobj were given, prepare a question filename, q = ask_for_filename( - suggested_filename, self._win_id, parent=self, + suggested_filename, parent=self, prompt_download_directory=prompt_download_directory, ) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index b4b1c5813..c627098ed 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -27,8 +27,7 @@ import sip from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QTimer, QDir, QModelIndex, QItemSelectionModel, QObject) from PyQt5.QtWidgets import (QWidget, QGridLayout, QVBoxLayout, QLineEdit, - QLabel, QWidgetItem, QFileSystemModel, QTreeView, - QSizePolicy) + QLabel, QFileSystemModel, QTreeView, QSizePolicy) from qutebrowser.config import style from qutebrowser.utils import usertypes, log, utils, qtutils, objreg, message @@ -82,7 +81,8 @@ class PromptQueue(QObject): _question: The current Question object if we're handling a question. Signals: - show_prompts: Emitted with a Question object when prompts should be shown. + show_prompts: Emitted with a Question object when prompts should be + shown. """ show_prompts = pyqtSignal(usertypes.Question) @@ -395,7 +395,6 @@ class PromptContainer(QWidget): pass - class LineEdit(QLineEdit): """A line edit used in prompts.""" @@ -774,6 +773,7 @@ class AlertPrompt(_BasePrompt): def init(): + """Initialize global prompt objects.""" global prompt_queue prompt_queue = PromptQueue() objreg.register('prompt-queue', prompt_queue) # for commands diff --git a/qutebrowser/utils/message.py b/qutebrowser/utils/message.py index 4e1250e77..62261f498 100644 --- a/qutebrowser/utils/message.py +++ b/qutebrowser/utils/message.py @@ -26,7 +26,7 @@ import traceback from PyQt5.QtCore import pyqtSignal, QObject -from qutebrowser.utils import usertypes, log, objreg, utils +from qutebrowser.utils import usertypes, log, utils def _log_stack(typ, stack): From 4552e067977b7bc54a1947c6e7fa761effe7e6bd Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 28 Oct 2016 12:52:12 +0200 Subject: [PATCH 53/87] Only connect interrupted signal for questions once --- qutebrowser/mainwindow/prompt.py | 11 +++++++---- qutebrowser/utils/usertypes.py | 2 ++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index c627098ed..63b95e2fa 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -170,6 +170,8 @@ class PromptQueue(QObject): log.prompt.debug("New question is blocking, saving {}".format( self._question)) old_question = self._question + if old_question is not None: + old_question.interrupted = True self._question = question self.show_prompts.emit(question) @@ -184,7 +186,6 @@ class PromptQueue(QObject): loop.exec_() log.prompt.debug("Ending loop.exec_() for {}".format(question)) - # FIXME don't we end up connecting modeman signals twice here now? log.prompt.debug("Restoring old question {}".format(old_question)) self._question = old_question self.show_prompts.emit(old_question) @@ -294,9 +295,11 @@ class PromptContainer(QWidget): log.prompt.debug("Displaying prompt {}".format(prompt)) self._prompt = prompt - question.aborted.connect( - lambda: modeman.maybe_leave(self._win_id, prompt.KEY_MODE, - 'aborted')) + if not question.interrupted: + # If this question was interrupted, we already connected the signal + 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.setSizePolicy(prompt.sizePolicy()) diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index 71517136c..40ec1c4a6 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -339,6 +339,7 @@ class Question(QObject): text: The prompt text to display to the user. answer: The value the user entered (as password for user_pwd). is_aborted: Whether the question was aborted. + interrupted: Whether the question was interrupted by another one. Signals: answered: Emitted when the question has been answered by the user. @@ -368,6 +369,7 @@ class Question(QObject): self.text = None self.answer = None self.is_aborted = False + self.interrupted = False def __repr__(self): return utils.get_repr(self, title=self.title, text=self.text, From d85cfcbc07857339aa62e528870f880844dc25de Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 28 Oct 2016 12:56:55 +0200 Subject: [PATCH 54/87] Fix ask_for_filename call --- qutebrowser/browser/commands.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 55051e003..960915123 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1355,9 +1355,7 @@ class CommandDispatcher: if dest is None: suggested_fn = self._current_title() + ".mht" suggested_fn = utils.sanitize_filename(suggested_fn) - filename, q = downloads.ask_for_filename( - suggested_fn, self._win_id, parent=tab, - ) + filename, q = downloads.ask_for_filename(suggested_fn, parent=tab) if filename is not None: mhtml.start_download_checked(filename, tab=tab) else: From 4ad741d26d382d20b66c86c1101e682af821a19f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 28 Oct 2016 17:43:59 +0200 Subject: [PATCH 55/87] Simplify shutdown in app.py --- qutebrowser/app.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index cdce0be6d..73485a634 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -646,10 +646,7 @@ class Quitter: session_manager.save(sessions.default, last_window=last_window, load_next_time=True) - deferrer = False if prompt.prompt_queue.shutdown(): - deferrer = True - if deferrer: # If shutdown was called while we were asking a question, we're in # a still sub-eventloop (which gets quit now) and not in the main # one. From 3b7f65d9565fc9200c6936ca255be3d15347710c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 28 Oct 2016 18:40:55 +0200 Subject: [PATCH 56/87] Show URL in most questions --- qutebrowser/browser/commands.py | 3 +- qutebrowser/browser/urlmarks.py | 5 ++- qutebrowser/browser/webkit/downloads.py | 15 +++++--- .../browser/webkit/network/networkmanager.py | 22 +++++++++-- qutebrowser/browser/webkit/webpage.py | 37 +++++++++++++------ qutebrowser/utils/message.py | 4 +- tests/end2end/features/downloads.feature | 10 ++--- tests/end2end/features/misc.feature | 2 +- tests/end2end/features/test_downloads_bdd.py | 2 +- 9 files changed, 68 insertions(+), 32 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 960915123..2c0e01b5f 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1355,7 +1355,8 @@ class CommandDispatcher: if dest is None: suggested_fn = self._current_title() + ".mht" suggested_fn = utils.sanitize_filename(suggested_fn) - filename, q = downloads.ask_for_filename(suggested_fn, parent=tab) + filename, q = downloads.ask_for_filename(suggested_fn, parent=tab, + url=tab.url()) if filename is not None: mhtml.start_download_checked(filename, tab=tab) else: diff --git a/qutebrowser/browser/urlmarks.py b/qutebrowser/browser/urlmarks.py index 16d9f7bd6..fc727a284 100644 --- a/qutebrowser/browser/urlmarks.py +++ b/qutebrowser/browser/urlmarks.py @@ -26,6 +26,7 @@ to a file on shutdown, so it makes sense to keep them as strings here. """ import os +import html import os.path import functools import collections @@ -171,7 +172,9 @@ class QuickmarkManager(UrlMarkManager): urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded) message.ask_async( "Add quickmark:", usertypes.PromptMode.text, - functools.partial(self.quickmark_add, urlstr)) + functools.partial(self.quickmark_add, urlstr), + text="Please enter a quickmark name for
{}".format( + html.escape(url.toDisplayString()))) @cmdutils.register(instance='quickmark-manager') def quickmark_add(self, url, name): diff --git a/qutebrowser/browser/webkit/downloads.py b/qutebrowser/browser/webkit/downloads.py index a9c6153f8..d25c363ea 100644 --- a/qutebrowser/browser/webkit/downloads.py +++ b/qutebrowser/browser/webkit/downloads.py @@ -28,6 +28,7 @@ import shutil import functools import tempfile import collections +import html import sip from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QObject, QTimer, @@ -119,7 +120,7 @@ def create_full_filename(basename, filename): return None -def ask_for_filename(suggested_filename, *, parent=None, +def ask_for_filename(suggested_filename, *, url, parent=None, prompt_download_directory=None): """Prepare a question for a download-path. @@ -133,6 +134,7 @@ def ask_for_filename(suggested_filename, *, parent=None, Args: suggested_filename: The "default"-name that is pre-entered as path. + url: The URL the download originated from. parent: The parent of the question (a QObject). prompt_download_directory: If this is something else than None, it will overwrite the @@ -150,6 +152,8 @@ def ask_for_filename(suggested_filename, *, parent=None, q = usertypes.Question(parent) q.title = "Save file to:" + q.text = "Please enter a location for {}".format( + html.escape(url.toDisplayString())) q.mode = usertypes.PromptMode.text q.completed.connect(q.deleteLater) q.default = _path_suggestion(suggested_filename) @@ -604,13 +608,14 @@ class DownloadItem(QObject): if os.path.isfile(self._filename): # The file already exists, so ask the user if it should be # overwritten. - txt = self._filename + " already exists. Overwrite?" + txt = "{} already exists. Overwrite?".format( + html.escape(self._filename)) self._ask_confirm_question("Overwrite existing file?", txt) # FIFO, device node, etc. Make sure we want to do this elif (os.path.exists(self._filename) and not os.path.isdir(self._filename)): - txt = (self._filename + " already exists and is a special file. " - "Write to this?") + txt = ("{} already exists and is a special file. Write to " + "it anyways?".format(html.escape(self._filename))) self._ask_confirm_question("Overwrite special file?", txt) else: self._create_fileobj() @@ -955,7 +960,7 @@ class DownloadManager(QObject): filename, q = ask_for_filename( suggested_filename, parent=self, prompt_download_directory=prompt_download_directory, - ) + url=reply.url()) # User doesn't want to be asked, so just use the download_dir if filename is not None: diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py index d54eca76d..00d945982 100644 --- a/qutebrowser/browser/webkit/network/networkmanager.py +++ b/qutebrowser/browser/webkit/network/networkmanager.py @@ -22,6 +22,7 @@ import os import collections import netrc +import html from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, QCoreApplication, QUrl, QByteArray) @@ -279,7 +280,15 @@ class NetworkManager(QNetworkAccessManager): return if ssl_strict == 'ask': - err_string = '\n'.join('- ' + err.errorString() for err in errors) + err_list = [] + for err in errors: + err_list.append('
  • {}
  • '.format( + html.escape(err.errorString()))) + err_string = ("Errors while loading {}:
    " + "
      {}
    ".format( + html.escape(reply.url().toDisplayString()), + '\n'.join(err_list))) + answer = self._ask('SSL errors - continue?', err_string, mode=usertypes.PromptMode.yesno, owner=reply, default=False) @@ -340,9 +349,11 @@ class NetworkManager(QNetworkAccessManager): if user is None: # netrc check failed + msg = '{} says:
    {}'.format( + html.escape(reply.url().toDisplayString()), + html.escape(authenticator.realm())) answer = self._ask("Authentication required", - authenticator.realm(), - mode=usertypes.PromptMode.user_pwd, + text=msg, mode=usertypes.PromptMode.user_pwd, owner=reply) if answer is not None: user, password = answer.user, answer.password @@ -359,8 +370,11 @@ class NetworkManager(QNetworkAccessManager): authenticator.setUser(user) authenticator.setPassword(password) else: + msg = '{} says:
    {}'.format( + html.escape(proxy.hostName()), + html.escape(authenticator.realm())) answer = self._ask( - "Proxy authentication required", authenticator.realm(), + "Proxy authentication required", msg, mode=usertypes.PromptMode.user_pwd) if answer is not None: authenticator.setUser(answer.user) diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py index 92a9b9dfb..00b253766 100644 --- a/qutebrowser/browser/webkit/webpage.py +++ b/qutebrowser/browser/webkit/webpage.py @@ -19,6 +19,7 @@ """The main browser widgets.""" +import html import functools from PyQt5.QtCore import pyqtSlot, pyqtSignal, PYQT_VERSION, Qt, QUrl, QPoint @@ -93,11 +94,14 @@ class BrowserPage(QWebPage): # of a bug in PyQt. # See http://www.riverbankcomputing.com/pipermail/pyqt/2014-June/034385.html - def javaScriptPrompt(self, _frame, msg, default): + def javaScriptPrompt(self, _frame, js_msg, default): """Override javaScriptPrompt to use the statusbar.""" if (self._is_shutting_down or config.get('content', 'ignore-javascript-prompt')): return (False, "") + msg = '{} asks:
    {}'.format( + html.escape(self.mainFrame().url().toDisplayString()), + html.escape(js_msg)) answer = message.ask('Javascript prompt', msg, mode=usertypes.PromptMode.text, default=default, @@ -140,7 +144,8 @@ class BrowserPage(QWebPage): scheme = url.scheme() message.confirm_async( title="Open external application for {}-link?".format(scheme), - text="URL: {}".format(url.toDisplayString()), + text="URL: {}".format( + html.escape(url.toDisplayString())), yes_action=functools.partial(QDesktopServices.openUrl, url)) return True elif (info.domain, info.error) in ignored_errors: @@ -171,11 +176,11 @@ class BrowserPage(QWebPage): log.webview.debug("Error domain: {}, error code: {}".format( info.domain, info.error)) title = "Error loading page: {}".format(urlstr) - html = jinja.render( + error_html = jinja.render( 'error.html', title=title, url=urlstr, error=error_str, icon='', qutescheme=False) - errpage.content = html.encode('utf-8') + errpage.content = error_html.encode('utf-8') errpage.encoding = 'utf-8' return True @@ -320,8 +325,8 @@ class BrowserPage(QWebPage): host = frame.url().host() if host: - text = "Allow the website at {} to {}?".format( - frame.url().host(), msgs[feature]) + text = "Allow the website at {} to {}?".format( + html.escape(frame.url().toDisplayString()), msgs[feature]) else: text = "Allow the website to {}?".format(msgs[feature]) @@ -442,26 +447,34 @@ class BrowserPage(QWebPage): return super().extension(ext, opt, out) return handler(opt, out) - def javaScriptAlert(self, frame, msg): + def javaScriptAlert(self, frame, js_msg): """Override javaScriptAlert to use the statusbar.""" - log.js.debug("alert: {}".format(msg)) + log.js.debug("alert: {}".format(js_msg)) if config.get('ui', 'modal-js-dialog'): - return super().javaScriptAlert(frame, msg) + return super().javaScriptAlert(frame, js_msg) if (self._is_shutting_down or config.get('content', 'ignore-javascript-alert')): return + + msg = 'From {}:
    {}'.format( + html.escape(self.mainFrame().url().toDisplayString()), + html.escape(js_msg)) message.ask('Javascript alert', msg, mode=usertypes.PromptMode.alert, abort_on=[self.loadStarted, self.shutting_down]) - def javaScriptConfirm(self, frame, msg): + def javaScriptConfirm(self, frame, js_msg): """Override javaScriptConfirm to use the statusbar.""" - log.js.debug("confirm: {}".format(msg)) + log.js.debug("confirm: {}".format(js_msg)) if config.get('ui', 'modal-js-dialog'): - return super().javaScriptConfirm(frame, msg) + return super().javaScriptConfirm(frame, js_msg) if self._is_shutting_down: return False + + msg = 'From {}:
    {}'.format( + html.escape(self.mainFrame().url().toDisplayString()), + html.escape(js_msg)) ans = message.ask('Javascript confirm', msg, mode=usertypes.PromptMode.yesno, abort_on=[self.loadStarted, self.shutting_down]) diff --git a/qutebrowser/utils/message.py b/qutebrowser/utils/message.py index 62261f498..368bb8289 100644 --- a/qutebrowser/utils/message.py +++ b/qutebrowser/utils/message.py @@ -110,7 +110,7 @@ def ask(*args, **kwargs): return answer -def ask_async(text, mode, handler, **kwargs): +def ask_async(title, mode, handler, **kwargs): """Ask an async question in the statusbar. Args: @@ -120,7 +120,7 @@ def ask_async(text, mode, handler, **kwargs): default: The default value to display. text: Additional text to show. """ - question = _build_question(text, mode=mode, **kwargs) + question = _build_question(title, mode=mode, **kwargs) question.answered.connect(handler) question.completed.connect(question.deleteLater) global_bridge.ask(question, blocking=False) diff --git a/tests/end2end/features/downloads.feature b/tests/end2end/features/downloads.feature index dd8419671..9a868cd57 100644 --- a/tests/end2end/features/downloads.feature +++ b/tests/end2end/features/downloads.feature @@ -63,7 +63,7 @@ Feature: Downloading things from a website. And I set storage -> prompt-download-directory to true And I open data/downloads/issue1243.html And I hint with args "links download" and follow a - And I wait for "Asking question text=None title='Save file to:'>, *" in the log + And I wait for "Asking question text=* title='Save file to:'>, *" in the log Then the error "Download error: No handler found for qute://!" should be shown Scenario: Downloading a data: link (issue 1214) @@ -71,7 +71,7 @@ Feature: Downloading things from a website. And I set storage -> prompt-download-directory to true And I open data/downloads/issue1214.html And I hint with args "links download" and follow a - And I wait for "Asking question text=None title='Save file to:'>, *" in the log + And I wait for "Asking question text=* title='Save file to:'>, *" in the log And I run :leave-mode Then no crash should happen @@ -338,7 +338,7 @@ Feature: Downloading things from a website. When I set storage -> prompt-download-directory to true And I open data/downloads/issue1725.html And I run :click-element id long-link - And I wait for "Asking question text=None title='Save file to:'>, *" in the log + And I wait for "Asking question text=* title='Save file to:'>, *" in the log And I directly open the download And I wait until the download is finished Then "Opening * with [*python*]" should be logged @@ -488,9 +488,9 @@ Feature: Downloading things from a website. Scenario: Answering a question for a cancelled download (#415) When I set storage -> prompt-download-directory to true And I run :download http://localhost:(port)/data/downloads/download.bin - And I wait for "Asking question text=None title='Save file to:'>, *" in the log + And I wait for "Asking question text=* title='Save file to:'>, *" in the log And I run :download http://localhost:(port)/data/downloads/download2.bin - And I wait for "Asking question text=None title='Save file to:'>, *" in the log + And I wait for "Asking question text=* title='Save file to:'>, *" in the log And I run :download-cancel with count 2 And I run :prompt-accept And I wait until the download is finished diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index e0b60f9af..46e4dc287 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -324,7 +324,7 @@ Feature: Various utility commands. And I open data/misc/test.pdf And I wait for "[qute://pdfjs/*] PDF * (PDF.js: *)" in the log And I run :jseval document.getElementById("download").click() - And I wait for "Asking question text=None title='Save file to:'>, *" in the log + And I wait for "Asking question text=* title='Save file to:'>, *" in the log And I run :leave-mode Then no crash should happen diff --git a/tests/end2end/features/test_downloads_bdd.py b/tests/end2end/features/test_downloads_bdd.py index 76e1584ab..ac82aa04f 100644 --- a/tests/end2end/features/test_downloads_bdd.py +++ b/tests/end2end/features/test_downloads_bdd.py @@ -31,7 +31,7 @@ pytestmark = pytest.mark.qtwebengine_todo("Downloads not implemented yet", PROMPT_MSG = ("Asking question text=None " + "default={!r} mode= text=* " "title='Save file to:'>, *") From efdcbd860e95da3da0e69b1e3efd6eecff6c05cb Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 28 Oct 2016 18:47:14 +0200 Subject: [PATCH 57/87] Increase font size for prompt titles --- qutebrowser/mainwindow/prompt.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 63b95e2fa..3e2ea3999 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -447,7 +447,8 @@ class _BasePrompt(QWidget): def _init_title(self, question): assert question.title is not None, question - title_label = QLabel('{}'.format(question.title), self) + title = '{}'.format(question.title) + title_label = QLabel(title, self) self._vbox.addWidget(title_label) if question.text is not None: text_label = QLabel(question.text) From 27f96943a2a699187a9b37018bbc7847c0572c96 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 31 Oct 2016 07:50:56 +0100 Subject: [PATCH 58/87] Add missing html.escape --- qutebrowser/mainwindow/prompt.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 3e2ea3999..63317e5b9 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -447,7 +447,8 @@ class _BasePrompt(QWidget): def _init_title(self, question): assert question.title is not None, question - title = '{}'.format(question.title) + title = '{}'.format( + html.escape(question.title)) title_label = QLabel(title, self) self._vbox.addWidget(title_label) if question.text is not None: From 64eeb95c307ede4f1e5184ae349ae50661e6d2ca Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 31 Oct 2016 07:51:43 +0100 Subject: [PATCH 59/87] Rename _init_title --- qutebrowser/mainwindow/prompt.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 63317e5b9..89a303f6b 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -445,7 +445,7 @@ class _BasePrompt(QWidget): def __repr__(self): return utils.get_repr(self, question=self.question, constructor=True) - def _init_title(self, question): + def _init_texts(self, question): assert question.title is not None, question title = '{}'.format( html.escape(question.title)) @@ -508,7 +508,7 @@ class LineEditPrompt(_BasePrompt): def __init__(self, question, parent=None): super().__init__(question, parent) self._lineedit = LineEdit(self) - self._init_title(question) + self._init_texts(question) self._vbox.addWidget(self._lineedit) if question.default: self._lineedit.setText(question.default) @@ -531,7 +531,7 @@ class FilenamePrompt(_BasePrompt): def __init__(self, question, parent=None): super().__init__(question, parent) - self._init_title(question) + self._init_texts(question) self._init_fileview() self._set_fileview_root(question.default) @@ -667,7 +667,7 @@ class AuthenticationPrompt(_BasePrompt): def __init__(self, question, parent=None): super().__init__(question, parent) - self._init_title(question) + self._init_texts(question) user_label = QLabel("Username:", self) self._user_lineedit = LineEdit(self) @@ -727,7 +727,7 @@ class YesNoPrompt(_BasePrompt): def __init__(self, question, parent=None): super().__init__(question, parent) - self._init_title(question) + self._init_texts(question) self._init_key_label() def accept(self, value=None): @@ -764,7 +764,7 @@ class AlertPrompt(_BasePrompt): def __init__(self, question, parent=None): super().__init__(question, parent) - self._init_title(question) + self._init_texts(question) self._init_key_label() def accept(self, value=None): From 28a32e680db851d1145695bbce0cb59f0b05f450 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 31 Oct 2016 07:52:32 +0100 Subject: [PATCH 60/87] Add comment about HTML escaping --- qutebrowser/mainwindow/prompt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 89a303f6b..7cc476e48 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -452,6 +452,7 @@ class _BasePrompt(QWidget): title_label = QLabel(title, self) self._vbox.addWidget(title_label) if question.text is not None: + # Not doing any HTML escaping here as the text can be formatted text_label = QLabel(question.text) self._vbox.addWidget(text_label) From 14ecbc0cec29c5a27824439c67099a29b576f99b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 31 Oct 2016 09:02:32 +0100 Subject: [PATCH 61/87] Use jinja for SSL error message --- .../browser/webkit/network/networkmanager.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py index 00d945982..7024e7635 100644 --- a/qutebrowser/browser/webkit/network/networkmanager.py +++ b/qutebrowser/browser/webkit/network/networkmanager.py @@ -24,6 +24,7 @@ import collections import netrc import html +import jinja2 from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, QCoreApplication, QUrl, QByteArray) from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkReply, QSslError, @@ -280,16 +281,17 @@ class NetworkManager(QNetworkAccessManager): return if ssl_strict == 'ask': - err_list = [] - for err in errors: - err_list.append('
  • {}
  • '.format( - html.escape(err.errorString()))) - err_string = ("Errors while loading {}:
    " - "
      {}
    ".format( - html.escape(reply.url().toDisplayString()), - '\n'.join(err_list))) + err_template = jinja2.Template(""" + Errors while loading {{url.toDisplayString()}}:
    +
      + {% for err in errors %} +
    • {{err.errorString()}}
    • + {% endfor %} +
    + """.strip()) + msg = err_template.render(url=reply.url(), errors=errors) - answer = self._ask('SSL errors - continue?', err_string, + answer = self._ask('SSL errors - continue?', msg, mode=usertypes.PromptMode.yesno, owner=reply, default=False) log.webview.debug("Asked for SSL errors, answer {}".format(answer)) From 7d1b1d354d3b2e84135778c787441989a1df4b69 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 31 Oct 2016 09:10:52 +0100 Subject: [PATCH 62/87] Remove duplicated docstrings --- qutebrowser/mainwindow/prompt.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 7cc476e48..2cd61ecac 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -522,7 +522,6 @@ class LineEditPrompt(_BasePrompt): return True def _allowed_commands(self): - """Get the commands we could run as response to this message.""" return [('prompt-accept', 'Accept'), ('leave-mode', 'Abort')] @@ -631,7 +630,6 @@ class FilenamePrompt(_BasePrompt): self._insert_path(idx, clicked=False) def _allowed_commands(self): - """Get the commands we could run as response to this message.""" return [('prompt-accept', 'Accept'), ('leave-mode', 'Abort')] From fdc4b2e2f683e54caf51d694b9324a72ca373ab7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 31 Oct 2016 09:25:48 +0100 Subject: [PATCH 63/87] Move setting FilenamePrompt lineedit text up --- qutebrowser/mainwindow/prompt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 2cd61ecac..84b5fff52 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -536,11 +536,11 @@ class FilenamePrompt(_BasePrompt): self._set_fileview_root(question.default) self._lineedit = LineEdit(self) + if question.default: + self._lineedit.setText(question.default) self._lineedit.textChanged.connect(self._set_fileview_root) self._vbox.addWidget(self._lineedit) - if question.default: - self._lineedit.setText(question.default) self.setFocusProxy(self._lineedit) self._init_key_label() self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) From 63c6381e18fcb1414cb8775cd0c5cb9eaa1128ac Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 31 Oct 2016 09:29:08 +0100 Subject: [PATCH 64/87] Add blank line --- qutebrowser/mainwindow/prompt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 84b5fff52..f96bf615b 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -581,6 +581,7 @@ class FilenamePrompt(_BasePrompt): path = os.path.normpath(os.path.join(*reversed(parts))) if clicked: path += os.sep + log.prompt.debug('Clicked {!r} -> {}'.format(parts, path)) self._lineedit.setText(path) self._lineedit.setFocus() From 4e793180c1de4883948b9cb7d3e9279b19e8d653 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 31 Oct 2016 13:53:55 +0100 Subject: [PATCH 65/87] Fix completion in file prompts --- qutebrowser/mainwindow/prompt.py | 22 +++++++++--- tests/unit/mainwindow/test_prompt.py | 53 ++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 tests/unit/mainwindow/test_prompt.py diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index f96bf615b..4e4c4c550 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -599,6 +599,11 @@ class FilenamePrompt(_BasePrompt): self._file_view.setHeaderHidden(True) for col in range(1, 4): self._file_view.setColumnHidden(col, True) + # Nothing selected initially + self._file_view.setCurrentIndex(QModelIndex()) + # The model needs to be sorted so we get the correct first/last index + self._file_model.directoryLoaded.connect( + lambda: self._file_model.sort(0)) def accept(self, value=None): text = value if value is not None else self._lineedit.text() @@ -610,18 +615,27 @@ class FilenamePrompt(_BasePrompt): assert which in ['prev', 'next'], which selmodel = self._file_view.selectionModel() - first_index = self._file_model.index(0, 0) - last_index = self._file_model.index(self._file_model.rowCount() - 1, 0) + parent = self._file_view.rootIndex() + first_index = self._file_model.index(0, 0, parent) + row = self._file_model.rowCount(parent) - 1 + last_index = self._file_model.index(row, 0, parent) + + if not first_index.isValid(): + # No entries + return + + assert last_index.isValid() idx = selmodel.currentIndex() if not idx.isValid(): # No item selected yet idx = last_index if which == 'prev' else first_index - - if which == 'prev': + elif which == 'prev': idx = self._file_view.indexAbove(idx) else: + assert which == 'next', which idx = self._file_view.indexBelow(idx) + # wrap around if we arrived at beginning/end if not idx.isValid(): idx = last_index if which == 'prev' else first_index diff --git a/tests/unit/mainwindow/test_prompt.py b/tests/unit/mainwindow/test_prompt.py new file mode 100644 index 000000000..6d50a515d --- /dev/null +++ b/tests/unit/mainwindow/test_prompt.py @@ -0,0 +1,53 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 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 . + +import pytest + +from qutebrowser.mainwindow import prompt as promptmod +from qutebrowser.utils import usertypes + + +@pytest.fixture(autouse=True) +def setup(qapp, key_config_stub): + key_config_stub.set_bindings_for('prompt', {}) + + +@pytest.mark.parametrize('steps, where, subfolder', [ + (1, 'next', '..'), + (1, 'prev', 'c'), + (2, 'next', 'a'), + (2, 'prev', 'b'), +]) +def test_file_completion(tmpdir, qtbot, steps, where, subfolder): + for directory in 'abc': + (tmpdir / directory).ensure(dir=True) + question = usertypes.Question() + question.title = "test" + question.default = str(tmpdir) + '/' + + prompt = promptmod.DownloadFilenamePrompt(question) + qtbot.add_widget(prompt) + with qtbot.wait_signal(prompt._file_model.directoryLoaded): + pass + assert prompt._lineedit.text() == str(tmpdir) + '/' + + for _ in range(steps): + prompt.item_focus(where) + + assert prompt._lineedit.text() == str(tmpdir / subfolder) From 5b33978259521cada5d7ad4548b7ba1c6892925c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 31 Oct 2016 15:00:30 +0100 Subject: [PATCH 66/87] Improve comment --- qutebrowser/mainwindow/prompt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 4e4c4c550..9d75a75d1 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -784,7 +784,7 @@ class AlertPrompt(_BasePrompt): def accept(self, value=None): if value is not None: raise Error("No value is permitted with alert prompts!") - # Doing nothing otherwise + # Simply mark prompt as done without setting self.question.answer return True def _allowed_commands(self): From 60c6b7f0abcc9e602fa3912990e5aa16e14a396a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 31 Oct 2016 16:08:46 +0100 Subject: [PATCH 67/87] Simplify FilenamePrompt._insert_text --- qutebrowser/mainwindow/prompt.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 9d75a75d1..e052fcbd5 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -573,16 +573,11 @@ class FilenamePrompt(_BasePrompt): index: The QModelIndex of the selected element. clicked: Whether the element was clicked. """ - parts = [] - cur = index - while cur.isValid(): - parts.append(cur.data()) - cur = cur.parent() - path = os.path.normpath(os.path.join(*reversed(parts))) + path = os.path.normpath(self._file_model.filePath(index)) if clicked: path += os.sep - log.prompt.debug('Clicked {!r} -> {}'.format(parts, path)) + log.prompt.debug('Inserting path {}'.format(path)) self._lineedit.setText(path) self._lineedit.setFocus() if clicked: From bbd8cc56a2c0a646693546f55a7cbeb94e4714b8 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 2 Nov 2016 09:25:24 +0100 Subject: [PATCH 68/87] Restructure test_prompt --- tests/unit/mainwindow/test_prompt.py | 54 +++++++++++++++++----------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/tests/unit/mainwindow/test_prompt.py b/tests/unit/mainwindow/test_prompt.py index 6d50a515d..9018e3b88 100644 --- a/tests/unit/mainwindow/test_prompt.py +++ b/tests/unit/mainwindow/test_prompt.py @@ -17,6 +17,8 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . +import os + import pytest from qutebrowser.mainwindow import prompt as promptmod @@ -28,26 +30,38 @@ def setup(qapp, key_config_stub): key_config_stub.set_bindings_for('prompt', {}) -@pytest.mark.parametrize('steps, where, subfolder', [ - (1, 'next', '..'), - (1, 'prev', 'c'), - (2, 'next', 'a'), - (2, 'prev', 'b'), -]) -def test_file_completion(tmpdir, qtbot, steps, where, subfolder): - for directory in 'abc': - (tmpdir / directory).ensure(dir=True) - question = usertypes.Question() - question.title = "test" - question.default = str(tmpdir) + '/' +class TestFileCompletion: - prompt = promptmod.DownloadFilenamePrompt(question) - qtbot.add_widget(prompt) - with qtbot.wait_signal(prompt._file_model.directoryLoaded): - pass - assert prompt._lineedit.text() == str(tmpdir) + '/' + @pytest.fixture + def get_prompt(self, qtbot): + def _get_prompt_func(path): + question = usertypes.Question() + question.title = "test" + question.default = path - for _ in range(steps): - prompt.item_focus(where) + prompt = promptmod.DownloadFilenamePrompt(question) + qtbot.add_widget(prompt) + with qtbot.wait_signal(prompt._file_model.directoryLoaded): + pass + assert prompt._lineedit.text() == path - assert prompt._lineedit.text() == str(tmpdir / subfolder) + return prompt + return _get_prompt_func + + @pytest.mark.parametrize('steps, where, subfolder', [ + (1, 'next', '..'), + (1, 'prev', 'c'), + (2, 'next', 'a'), + (2, 'prev', 'b'), + ]) + def test_simple_completion(self, tmpdir, get_prompt, steps, where, + subfolder): + for directory in 'abc': + (tmpdir / directory).ensure(dir=True) + + prompt = get_prompt(str(tmpdir) + os.sep) + + for _ in range(steps): + prompt.item_focus(where) + + assert prompt._lineedit.text() == str(tmpdir / subfolder) From fa1846ab0ea33f3e8df272476dc0226bf04e2279 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 2 Nov 2016 12:47:08 +0100 Subject: [PATCH 69/87] Adjust prompt path when backspacing a path --- qutebrowser/mainwindow/prompt.py | 18 +++++++++++++----- tests/unit/mainwindow/test_prompt.py | 20 ++++++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index e052fcbd5..547b84b8a 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -538,7 +538,7 @@ class FilenamePrompt(_BasePrompt): self._lineedit = LineEdit(self) if question.default: self._lineedit.setText(question.default) - self._lineedit.textChanged.connect(self._set_fileview_root) + self._lineedit.textEdited.connect(self._set_fileview_root) self._vbox.addWidget(self._lineedit) self.setFocusProxy(self._lineedit) @@ -546,18 +546,25 @@ class FilenamePrompt(_BasePrompt): self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) @pyqtSlot(str) - def _set_fileview_root(self, path): + def _set_fileview_root(self, path, *, tabbed=False): """Set the root path for the file display.""" separators = os.sep if os.altsep is not None: separators += os.altsep - if path == '/' or (path and path[-1] not in separators): + if not path: return - path.rstrip(separators) + + dirname = os.path.dirname(path) try: - if not os.path.isdir(path): + if path[-1] in separators and os.path.isdir(path): + # Input like /foo/bar/ -> show /foo/bar/ contents + path = path.rstrip(separators) + elif os.path.isdir(dirname) and not tabbed: + # Input like /foo/ba -> show /foo contents + path = dirname + else: return except OSError: return @@ -580,6 +587,7 @@ class FilenamePrompt(_BasePrompt): log.prompt.debug('Inserting path {}'.format(path)) self._lineedit.setText(path) self._lineedit.setFocus() + self._set_fileview_root(path, tabbed=True) if clicked: # Avoid having a ..-subtree highlighted self._file_view.setCurrentIndex(QModelIndex()) diff --git a/tests/unit/mainwindow/test_prompt.py b/tests/unit/mainwindow/test_prompt.py index 9018e3b88..98cd8b550 100644 --- a/tests/unit/mainwindow/test_prompt.py +++ b/tests/unit/mainwindow/test_prompt.py @@ -20,6 +20,7 @@ import os import pytest +from PyQt5.QtCore import Qt from qutebrowser.mainwindow import prompt as promptmod from qutebrowser.utils import usertypes @@ -34,6 +35,7 @@ class TestFileCompletion: @pytest.fixture def get_prompt(self, qtbot): + """Get a function to display a prompt with a path.""" def _get_prompt_func(path): question = usertypes.Question() question.title = "test" @@ -56,6 +58,7 @@ class TestFileCompletion: ]) def test_simple_completion(self, tmpdir, get_prompt, steps, where, subfolder): + """Simply trying to tab through items.""" for directory in 'abc': (tmpdir / directory).ensure(dir=True) @@ -65,3 +68,20 @@ class TestFileCompletion: prompt.item_focus(where) assert prompt._lineedit.text() == str(tmpdir / subfolder) + + def test_backspacing_path(self, qtbot, tmpdir, get_prompt): + """When we start deleting a path we want to see the subdir.""" + for directory in ['bar', 'foo']: + (tmpdir / directory).ensure(dir=True) + + prompt = get_prompt(str(tmpdir / 'foo') + os.sep) + + # Deleting /f[oo/] + with qtbot.wait_signal(prompt._file_model.directoryLoaded): + for _ in range(3): + qtbot.keyPress(prompt._lineedit, Qt.Key_Backspace) + + # We should now show / again, so tabbing twice gives us .. -> bar + prompt.item_focus('next') + prompt.item_focus('next') + assert prompt._lineedit.text() == str(tmpdir / 'bar') From 293a388ac6c3e0cc2ccb7d99ed7c8818b83d7fe0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 2 Nov 2016 12:55:42 +0100 Subject: [PATCH 70/87] Use HTML list for quit confirmation message --- qutebrowser/mainwindow/mainwindow.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index 48763a2f7..248f9a68e 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -24,6 +24,7 @@ import base64 import itertools import functools +import jinja2 from PyQt5.QtCore import pyqtSlot, QRect, QPoint, QTimer, Qt from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QSizePolicy @@ -517,9 +518,17 @@ class MainWindow(QWidget): "download is" if download_count == 1 else "downloads are")) # Process all quit messages that user must confirm if quit_texts or 'always' in confirm_quit: - text = '\n'.join(['Really quit?'] + quit_texts) - confirmed = message.ask(text, mode=usertypes.PromptMode.yesno, + msg = jinja2.Template(""" +
      + {% for text in quit_texts %} +
    • {{text}}
    • + {% endfor %} +
    + """.strip()).render(quit_texts=quit_texts) + confirmed = message.ask('Really quit?', msg, + mode=usertypes.PromptMode.yesno, default=True) + # Stop asking if the user cancels if not confirmed: log.destroy.debug("Cancelling closing of window {}".format( From 9cf5d645f03f473525c673b1abae855b84212755 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 2 Nov 2016 22:16:49 +0100 Subject: [PATCH 71/87] Hide prompts correctly We now make sure to hide prompts everywhere when the prompt mode was left. We also make sure to hide the prompt widget before deleting, as it might be deleted later. --- qutebrowser/mainwindow/prompt.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 547b84b8a..f489a81f0 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -272,11 +272,6 @@ class PromptContainer(QWidget): Args: question: A Question object or None. """ - item = self._layout.takeAt(0) - assert self._layout.count() == 0 - if item is not None: - item.widget().deleteLater() - if question is None: self._prompt = None self.hide() @@ -318,13 +313,16 @@ class PromptContainer(QWidget): def _on_global_mode_left(self, mode): """Leave prompt/yesno mode in this window if it was left elsewhere. - PromptQueue takes care of getting rid of the question if a mode was - left, but if that happens in a different window, this window will still - be stuck in prompt mode. Here we make sure to leave that if it was left - anywhere else. + This ensures no matter where a prompt was answered, we leave the prompt + mode and dispose of the prompt object in every window. """ if mode in [usertypes.KeyMode.prompt, usertypes.KeyMode.yesno]: modeman.maybe_leave(self._win_id, mode, 'left in other window') + item = self._layout.takeAt(0) + if item is not None: + widget = item.widget() + widget.hide() + widget.deleteLater() @cmdutils.register(instance='prompt-container', hide=True, scope='window', modes=[usertypes.KeyMode.prompt, From 25bb720f095b8fa68656309dec8d6fc7d03fe909 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 2 Nov 2016 22:49:33 +0100 Subject: [PATCH 72/87] Bound maximum size for prompts --- qutebrowser/mainwindow/mainwindow.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index 248f9a68e..8ed6a19a2 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -240,17 +240,19 @@ class MainWindow(QWidget): width = size_hint.width() left = (self.width() - size_hint.width()) / 2 if centered else 0 + max_height_padding = 20 status_position = config.get('ui', 'status-position') if status_position == 'bottom': top = self.height() - self.status.height() - size_hint.height() top = qtutils.check_overflow(top, 'int', fatal=False) - topleft = QPoint(left, top) + topleft = QPoint(left, max(max_height_padding, top)) bottomright = QPoint(left + width, self.status.geometry().top()) elif status_position == 'top': topleft = QPoint(left, self.status.geometry().bottom()) bottom = self.status.height() + size_hint.height() bottom = qtutils.check_overflow(bottom, 'int', fatal=False) - bottomright = QPoint(width, bottom) + bottomright = QPoint(width, + min(self.height() - max_height_padding, bottom)) else: raise ValueError("Invalid position {}!".format(status_position)) From 16b3b7a2620d82e29f7baac86cd980d59cd46fce Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 2 Nov 2016 22:51:27 +0100 Subject: [PATCH 73/87] Fix prompt positioning with top statusbar --- qutebrowser/mainwindow/mainwindow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index 8ed6a19a2..ecfd46c5d 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -251,7 +251,7 @@ class MainWindow(QWidget): topleft = QPoint(left, self.status.geometry().bottom()) bottom = self.status.height() + size_hint.height() bottom = qtutils.check_overflow(bottom, 'int', fatal=False) - bottomright = QPoint(width, + bottomright = QPoint(left + width, min(self.height() - max_height_padding, bottom)) else: raise ValueError("Invalid position {}!".format(status_position)) From cbf1a44b751afdb8eaa594b1255ce2fef72bb336 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 2 Nov 2016 22:58:24 +0100 Subject: [PATCH 74/87] Fix handling of / as path --- qutebrowser/mainwindow/prompt.py | 5 ++++- tests/unit/mainwindow/test_prompt.py | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index f489a81f0..5e686a375 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -556,7 +556,10 @@ class FilenamePrompt(_BasePrompt): dirname = os.path.dirname(path) try: - if path[-1] in separators and os.path.isdir(path): + if path in separators and os.path.isdir(path): + # Input "/" -> don't strip anything + pass + elif path[-1] in separators and os.path.isdir(path): # Input like /foo/bar/ -> show /foo/bar/ contents path = path.rstrip(separators) elif os.path.isdir(dirname) and not tabbed: diff --git a/tests/unit/mainwindow/test_prompt.py b/tests/unit/mainwindow/test_prompt.py index 98cd8b550..1288ccd3f 100644 --- a/tests/unit/mainwindow/test_prompt.py +++ b/tests/unit/mainwindow/test_prompt.py @@ -85,3 +85,9 @@ class TestFileCompletion: prompt.item_focus('next') prompt.item_focus('next') assert prompt._lineedit.text() == str(tmpdir / 'bar') + + @pytest.mark.linux + def test_root_path(self, get_prompt): + """With / as path, show root contents.""" + prompt = get_prompt('/') + assert prompt._file_model.rootPath() == '/' From 732e7c260b13f7b171b530a698a0ec9e9cd35f39 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 2 Nov 2016 23:11:16 +0100 Subject: [PATCH 75/87] Handle empty path for file completion This also allows us to select drives on Windows hopefully... --- qutebrowser/mainwindow/prompt.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 5e686a375..283af269d 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -550,13 +550,12 @@ class FilenamePrompt(_BasePrompt): if os.altsep is not None: separators += os.altsep - if not path: - return - dirname = os.path.dirname(path) try: - if path in separators and os.path.isdir(path): + if not path: + pass + elif path in separators and os.path.isdir(path): # Input "/" -> don't strip anything pass elif path[-1] in separators and os.path.isdir(path): From 0f03960525190300de9ba9686f4d14a644977750 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 2 Nov 2016 23:12:19 +0100 Subject: [PATCH 76/87] Log if getting directory information failed --- qutebrowser/mainwindow/prompt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 283af269d..580faac92 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -567,6 +567,7 @@ class FilenamePrompt(_BasePrompt): else: return except OSError: + log.prompt.exception("Failed to get directory information") return root = self._file_model.setRootPath(path) From ebcae3a69cc51e33c5e8e7e444e8a02c8db50bd0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 2 Nov 2016 23:18:57 +0100 Subject: [PATCH 77/87] Fix tabbing over .. in some directories on Windows --- qutebrowser/mainwindow/prompt.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 580faac92..09fc9ab59 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -584,6 +584,9 @@ class FilenamePrompt(_BasePrompt): path = os.path.normpath(self._file_model.filePath(index)) if clicked: path += os.sep + else: + # On Windows, when we have C:\foo and tab over .., we get C:\ + path = path.rstrip(os.sep) log.prompt.debug('Inserting path {}'.format(path)) self._lineedit.setText(path) From 2b46228e3dcddb78f839c2bdbc6d7edf5742232e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 3 Nov 2016 06:38:22 +0100 Subject: [PATCH 78/87] Customize prompt selection colors Seems like if we make the foreground color configurable, the selection isn't colored at all?! --- doc/help/settings.asciidoc | 7 +++++++ qutebrowser/config/configdata.py | 4 ++++ qutebrowser/mainwindow/prompt.py | 8 ++++++++ 3 files changed, 19 insertions(+) diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 069376be4..f2dc26588 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -269,6 +269,7 @@ |<>|Border color of an info message. |<>|Foreground color for prompts. |<>|Background color for prompts. +|<>|Background color for the selected item in filename prompts. |============== .Quick reference for section ``fonts'' @@ -2192,6 +2193,12 @@ Background color for prompts. Default: +pass:[darkblue]+ +[[colors-prompts.selected.bg]] +=== prompts.selected.bg +Background color for the selected item in filename prompts. + +Default: +pass:[#308cc6]+ + == fonts Fonts used for the UI, with optional style/weight/size. diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 2056cca01..bad6fa51a 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1310,6 +1310,10 @@ def data(readonly=False): SettingValue(typ.QssColor(), 'darkblue'), "Background color for prompts."), + ('prompts.selected.bg', + SettingValue(typ.QssColor(), '#308cc6'), + "Background color for the selected item in filename prompts."), + readonly=readonly )), diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 09fc9ab59..2bcfef61f 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -244,6 +244,14 @@ class PromptContainer(QWidget): color: {{ color['prompts.fg'] }}; background-color: {{ color['prompts.bg'] }}; } + + QTreeView { + selection-background-color: {{ color['prompts.selected.bg'] }}; + } + + QTreeView::item:selected, QTreeView::item:selected:hover { + background-color: {{ color['prompts.selected.bg'] }}; + } """ update_geometry = pyqtSignal() From a006ecffec062756707d5cb58bebfb480ce4b10c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 3 Nov 2016 06:42:58 +0100 Subject: [PATCH 79/87] Add MainWindow._update_overlay_geometries() --- qutebrowser/mainwindow/mainwindow.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index ecfd46c5d..f0d7ce5c0 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -217,18 +217,13 @@ class MainWindow(QWidget): def _add_overlay(self, widget, signal, *, centered=False, padding=0): self._overlays.append((widget, signal, centered, padding)) - def _update_overlay_geometry(self, widget=None, centered=None, padding=0): - """Reposition/resize the given overlay. - - If no widget is given, reposition/resize all overlays. - """ - if widget is None: - for w, _signal, centered, padding in self._overlays: - self._update_overlay_geometry(w, centered, padding) - return - - assert centered is not None + def _update_overlay_geometries(self): + """Update the size/position of all overlays.""" + for w, _signal, centered, padding in self._overlays: + self._update_overlay_geometry(w, centered, padding) + def _update_overlay_geometry(self, widget, centered, padding): + """Reposition/resize the given overlay.""" if not widget.isVisible(): return @@ -298,12 +293,12 @@ class MainWindow(QWidget): if section != 'ui': return if option == 'statusbar-padding': - self._update_overlay_geometry() + self._update_overlay_geometries() elif option == 'downloads-position': self._add_widgets() elif option == 'status-position': self._add_widgets() - self._update_overlay_geometry() + self._update_overlay_geometries() def _add_widgets(self): """Add or readd all widgets to the VBox.""" @@ -370,7 +365,7 @@ class MainWindow(QWidget): signal.connect( functools.partial(self._update_overlay_geometry, widget, centered, padding)) - self._update_overlay_geometry(widget, centered) + self._update_overlay_geometry(widget, centered, padding) def _set_default_geometry(self): """Set some sensible default geometry.""" @@ -472,7 +467,7 @@ class MainWindow(QWidget): e: The QResizeEvent """ super().resizeEvent(e) - self._update_overlay_geometry() + self._update_overlay_geometries() self._downloadview.updateGeometry() self.tabbed_browser.tabBar().refresh() From 59b4ccc6209ed53b524007073f75c7d5fb42bf30 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 3 Nov 2016 06:43:52 +0100 Subject: [PATCH 80/87] Rename max_height_padding --- qutebrowser/mainwindow/mainwindow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index f0d7ce5c0..0e5a5c069 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -235,19 +235,19 @@ class MainWindow(QWidget): width = size_hint.width() left = (self.width() - size_hint.width()) / 2 if centered else 0 - max_height_padding = 20 + height_padding = 20 status_position = config.get('ui', 'status-position') if status_position == 'bottom': top = self.height() - self.status.height() - size_hint.height() top = qtutils.check_overflow(top, 'int', fatal=False) - topleft = QPoint(left, max(max_height_padding, top)) + topleft = QPoint(left, max(height_padding, top)) bottomright = QPoint(left + width, self.status.geometry().top()) elif status_position == 'top': topleft = QPoint(left, self.status.geometry().bottom()) bottom = self.status.height() + size_hint.height() bottom = qtutils.check_overflow(bottom, 'int', fatal=False) bottomright = QPoint(left + width, - min(self.height() - max_height_padding, bottom)) + min(self.height() - height_padding, bottom)) else: raise ValueError("Invalid position {}!".format(status_position)) From 3a79faac164020899f411dc509c4400f695ae0ff Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 3 Nov 2016 23:03:42 +0100 Subject: [PATCH 81/87] Also hide existing prompts when showing a new one Otherwise when a prompt is interrupted, we'd still see the older one. --- qutebrowser/mainwindow/prompt.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 2bcfef61f..aa4fbcfd2 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -280,6 +280,12 @@ class PromptContainer(QWidget): Args: question: A Question object or None. """ + item = self._layout.takeAt(0) + if item is not None: + widget = item.widget() + widget.hide() + widget.deleteLater() + if question is None: self._prompt = None self.hide() From dd0ce8fe3b309d78b4982a307c8dcfb5b36f69dd Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 4 Nov 2016 06:47:10 +0100 Subject: [PATCH 82/87] Add some more prompt logging --- qutebrowser/mainwindow/prompt.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index aa4fbcfd2..df8f03352 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -283,10 +283,12 @@ class PromptContainer(QWidget): item = self._layout.takeAt(0) if item is not None: widget = item.widget() + log.prompt.debug("Deleting old prompt {}".format(widget)) widget.hide() widget.deleteLater() if question is None: + log.prompt.debug("No prompts left, hiding prompt container.") self._prompt = None self.hide() return @@ -335,6 +337,7 @@ class PromptContainer(QWidget): item = self._layout.takeAt(0) if item is not None: widget = item.widget() + log.prompt.debug("Deleting prompt {}".format(widget)) widget.hide() widget.deleteLater() From a16d41333b633a867f0878215cd4e89781559c02 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 4 Nov 2016 06:47:56 +0100 Subject: [PATCH 83/87] Fix prompts while in command mode --- qutebrowser/mainwindow/prompt.py | 41 +++++++++++++++----------- tests/end2end/features/prompts.feature | 8 +++++ 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index df8f03352..7435a46a7 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -201,16 +201,20 @@ class PromptQueue(QObject): @pyqtSlot(usertypes.KeyMode) def _on_mode_left(self, mode): - """Abort question when a mode was left.""" - if self._question is not None: - log.prompt.debug("Left mode {}, hiding {}".format( - mode, self._question)) - self.show_prompts.emit(None) - if self._question.answer is None and not self._question.is_aborted: - log.prompt.debug("Cancelling {} because {} was left".format( - self._question, mode)) - self._question.cancel() - self._question = None + """Abort question when a prompt mode was left.""" + if mode not in [usertypes.KeyMode.prompt, usertypes.KeyMode.yesno]: + return + if self._question is None: + return + + log.prompt.debug("Left mode {}, hiding {}".format( + mode, self._question)) + self.show_prompts.emit(None) + if self._question.answer is None and not self._question.is_aborted: + log.prompt.debug("Cancelling {} because {} was left".format( + self._question, mode)) + self._question.cancel() + self._question = None class PromptContainer(QWidget): @@ -332,14 +336,15 @@ class PromptContainer(QWidget): This ensures no matter where a prompt was answered, we leave the prompt mode and dispose of the prompt object in every window. """ - if mode in [usertypes.KeyMode.prompt, usertypes.KeyMode.yesno]: - modeman.maybe_leave(self._win_id, mode, 'left in other window') - item = self._layout.takeAt(0) - if item is not None: - widget = item.widget() - log.prompt.debug("Deleting prompt {}".format(widget)) - widget.hide() - widget.deleteLater() + if mode not in [usertypes.KeyMode.prompt, usertypes.KeyMode.yesno]: + return + modeman.maybe_leave(self._win_id, mode, 'left in other window') + item = self._layout.takeAt(0) + if item is not None: + widget = item.widget() + log.prompt.debug("Deleting prompt {}".format(widget)) + widget.hide() + widget.deleteLater() @cmdutils.register(instance='prompt-container', hide=True, scope='window', modes=[usertypes.KeyMode.prompt, diff --git a/tests/end2end/features/prompts.feature b/tests/end2end/features/prompts.feature index 316f45b4c..85a592646 100644 --- a/tests/end2end/features/prompts.feature +++ b/tests/end2end/features/prompts.feature @@ -419,3 +419,11 @@ Feature: Prompts And I run :prompt-item-focus next And I run :prompt-accept test-prompt-item-focus Then "Added quickmark test-prompt-item-focus for *" should be logged + + Scenario: Getting question in command mode + When I open data/hello.txt + And I run :later 500 quickmark-save + And I run :set-cmd-text : + And I wait for a prompt + And I run :prompt-accept prompt-in-command-mode + Then "Added quickmark prompt-in-command-mode for *" should be logged From fb9415a80903738e29456a2d77370bb992ed3475 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 4 Nov 2016 07:20:48 +0100 Subject: [PATCH 84/87] bdd: Allow multiple "When I open" suffixes --- tests/end2end/features/conftest.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/tests/end2end/features/conftest.py b/tests/end2end/features/conftest.py index 3eb4daf0d..9b7a9e2d5 100644 --- a/tests/end2end/features/conftest.py +++ b/tests/end2end/features/conftest.py @@ -188,19 +188,21 @@ def open_path(quteproc, path): do_not_wait_suffix = ' without waiting' as_url_suffix = ' as a URL' - if path.endswith(new_tab_suffix): - path = path[:-len(new_tab_suffix)] - new_tab = True - elif path.endswith(new_window_suffix): - path = path[:-len(new_window_suffix)] - new_window = True - elif path.endswith(as_url_suffix): - path = path[:-len(as_url_suffix)] - as_url = True - - if path.endswith(do_not_wait_suffix): - path = path[:-len(do_not_wait_suffix)] - wait = False + while True: + if path.endswith(new_tab_suffix): + path = path[:-len(new_tab_suffix)] + new_tab = True + elif path.endswith(new_window_suffix): + path = path[:-len(new_window_suffix)] + new_window = True + elif path.endswith(as_url_suffix): + path = path[:-len(as_url_suffix)] + as_url = True + elif path.endswith(do_not_wait_suffix): + path = path[:-len(do_not_wait_suffix)] + wait = False + else: + break quteproc.open_path(path, new_tab=new_tab, new_window=new_window, as_url=as_url, wait=wait) From 76728f0f73702bbef319e946da27f812cb90e21c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 4 Nov 2016 07:21:04 +0100 Subject: [PATCH 85/87] tests: Allow more auth paths in end2end.fixtures.webserver --- tests/end2end/fixtures/webserver.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/end2end/fixtures/webserver.py b/tests/end2end/fixtures/webserver.py index 313884118..2c5d23e65 100644 --- a/tests/end2end/fixtures/webserver.py +++ b/tests/end2end/fixtures/webserver.py @@ -67,12 +67,12 @@ class Request(testprocess.Line): '/favicon.ico': [http.client.NOT_FOUND], '/does-not-exist': [http.client.NOT_FOUND], '/does-not-exist-2': [http.client.NOT_FOUND], - '/basic-auth/user/password': - [http.client.UNAUTHORIZED, http.client.OK], + '/status/404': [http.client.NOT_FOUND], + '/custom/redirect-later': [http.client.FOUND], '/custom/redirect-self': [http.client.FOUND], '/redirect-to': [http.client.FOUND], - '/status/404': [http.client.NOT_FOUND], + '/cookies/set': [http.client.FOUND], } for i in range(15): @@ -81,6 +81,10 @@ class Request(testprocess.Line): http.client.FOUND] path_to_statuses['/absolute-redirect/{}'.format(i)] = [ http.client.FOUND] + for suffix in ['', '1', '2']: + key = '/basic-auth/user{}/password{}'.format(suffix, suffix) + path_to_statuses[key] = [http.client.UNAUTHORIZED, http.client.OK] + default_statuses = [http.client.OK, http.client.NOT_MODIFIED] sanitized = QUrl('http://localhost' + self.path).path() # Remove ?foo From 15de465dbebe5bb7901bca38715c3d2aa7124597 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 4 Nov 2016 07:21:31 +0100 Subject: [PATCH 86/87] bdd: Add a test for keyboard focus with multiple auth prompts --- tests/end2end/features/prompts.feature | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/end2end/features/prompts.feature b/tests/end2end/features/prompts.feature index 85a592646..9853b6176 100644 --- a/tests/end2end/features/prompts.feature +++ b/tests/end2end/features/prompts.feature @@ -427,3 +427,28 @@ Feature: Prompts And I wait for a prompt And I run :prompt-accept prompt-in-command-mode Then "Added quickmark prompt-in-command-mode for *" should be logged + + # https://github.com/The-Compiler/qutebrowser/issues/1093 + Scenario: Keyboard focus with multiple auth prompts + When I open basic-auth/user1/password1 without waiting + And I open basic-auth/user2/password2 in a new tab without waiting + And I wait for a prompt + And I wait for a prompt + # Second prompt (showed first) + And I press the keys "user2" + And I press the key "" + And I press the keys "password2" + And I press the key "" + And I wait until basic-auth/user2/password2 is loaded + # First prompt + And I press the keys "user1" + And I press the key "" + And I press the keys "password1" + And I press the key "" + And I wait until basic-auth/user1/password1 is loaded + # We're on the second page + Then the json on the page should be: + { + "authenticated": true, + "user": "user2" + } From 72566412882c938662f44c37d5ccfd5383dedc57 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 4 Nov 2016 13:21:22 +0100 Subject: [PATCH 87/87] Prevent questions from being aborted twice When we abort a question (e.g. by closing a tab with SSL errors) while another prompt is queued, the right prompt gets closed by the question being aborted, but that in turn aborts the question again, affecting the next prompt. --- qutebrowser/utils/usertypes.py | 3 +++ tests/end2end/features/prompts.feature | 15 +++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index 40ec1c4a6..4e67a2eac 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -407,6 +407,9 @@ class Question(QObject): @pyqtSlot() def abort(self): """Abort the question.""" + if self.is_aborted: + log.misc.debug("Question was already aborted") + return self.is_aborted = True try: self.aborted.emit() diff --git a/tests/end2end/features/prompts.feature b/tests/end2end/features/prompts.feature index 9853b6176..fe39a90be 100644 --- a/tests/end2end/features/prompts.feature +++ b/tests/end2end/features/prompts.feature @@ -452,3 +452,18 @@ Feature: Prompts "authenticated": true, "user": "user2" } + + # https://github.com/The-Compiler/qutebrowser/issues/1249#issuecomment-175205531 + # https://github.com/The-Compiler/qutebrowser/pull/2054#issuecomment-258285544 + Scenario: Interrupting SSL prompt during a notification prompt + When I set content -> notifications to ask + And I set network -> ssl-strict to ask + And I open data/prompt/notifications.html in a new tab + And I run :click-element id button + And I wait for a prompt + And I open about:blank in a new tab + And I load an SSL page + And I wait for a prompt + And I run :tab-close + And I run :prompt-accept yes + Then the javascript message "notification permission granted" should be logged