diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 407ee85fc..8f5b425d4 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -212,6 +212,7 @@ class QuteBrowser(QApplication): 'insert': PassthroughKeyParser('keybind.insert', self), 'passthrough': PassthroughKeyParser('keybind.passthrough', self), 'command': PassthroughKeyParser('keybind.command', self), + 'prompt': PassthroughKeyParser('keybind.prompt', self), } self.modeman = ModeManager() self.modeman.register('normal', self._keyparsers['normal'].handle) @@ -223,6 +224,8 @@ class QuteBrowser(QApplication): passthrough=True) self.modeman.register('command', self._keyparsers['command'].handle, passthrough=True) + self.modeman.register('prompt', self._keyparsers['prompt'].handle, + passthrough=True) def _init_log(self): """Initialisation of the logging output. @@ -363,6 +366,8 @@ class QuteBrowser(QApplication): self.lastWindowClosed.connect(self.shutdown) tabs.quit.connect(self.shutdown) tabs.currentChanged.connect(self.mainwindow.update_inspector) + self.networkmanager.authenticationRequired.connect( + status.on_authentication_required) # status bar self.modeman.entered.connect(status.on_mode_entered) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index b8e0226c8..a8df0d616 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -119,6 +119,11 @@ SECTION_DESC = { " completion-item-next: Select next item in completion.\n" " command-accept: Execute the command currently in the commandline.\n" " leave-mode: Leave the command mode."), + 'keybind.prompt': ( + "Keybindings for prompts in the status line.\n" + "Useful hidden commands to map in this section:\n" + " prompt-accept: Confirm the entered value\n" + " leave-mode: Leave the prompt mode."), 'aliases': ( "Aliases for commands.\n" "By default, no aliases are defined. Example which adds a new command " @@ -691,6 +696,13 @@ DATA = OrderedDict([ ('', 'command-accept'), )), + ('keybind.prompt', sect.ValueList( + types.KeyBindingName(), types.KeyBinding(), + #('', 'leave-mode'), + #('', 'leave-mode'), + ('', 'prompt-accept'), + )), + ('aliases', sect.ValueList( types.String(forbidden=' '), types.Command(), )), diff --git a/qutebrowser/widgets/statusbar/_prompt.py b/qutebrowser/widgets/statusbar/_prompt.py new file mode 100644 index 000000000..6f68b584f --- /dev/null +++ b/qutebrowser/widgets/statusbar/_prompt.py @@ -0,0 +1,147 @@ +# Copyright 2014 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.""" + +from PyQt5.QtCore import pyqtSignal, QEventLoop +from PyQt5.QtWidgets import QLineEdit, QHBoxLayout + +import qutebrowser.keyinput.modeman as modeman +import qutebrowser.commands.utils as cmdutils +from qutebrowser.widgets.statusbar._textbase import TextBase +from qutebrowser.utils.usertypes import enum + +PromptMode = enum('yesno', 'text', 'user_pwd') + + +class Prompt(TextBase): + + answered = pyqtSignal([str], [bool], [str, str]) + accepted = pyqtSignal() + hide_prompt = pyqtSignal() + + def __init__(self, parent=None): + super().__init__(parent) + + self.mode = None + self.default = None + self.text = None + self.answer = None + + self.loop = QEventLoop() + + self._hbox = QHBoxLayout(self) + self._hbox.setContentsMargins(0, 0, 0, 0) + self._hbox.setSpacing(5) + + self._txt = TextBase() + self._hbox.addWidget(self._txt) + + self._input = _QueryInput() + self._hbox.addWidget(self._input) + + def _user_entered(self): + self._user = self._input.text() + self._txt.setText("Password:") + self._input.clear() + self._input.setEchoMode(QLineEdit.Password) + self.accepted.disconnect(self._user_entered) + self.accepted.connect(self._password_entered) + + def _password_entered(self): + self.accepted.disconnect(self._password_entered) + password = self._input.text() + self.answer = (self._user, password) + self._txt.setText('') + self._input.clear() + self._input.setEchoMode(QLineEdit.Normal) + self.default = None + self.mode = None + self.text = None + self.answered[str, str].emit(*self.answer) + modeman.leave('prompt', 'prompt accept') + self.hide_prompt.emit() + + def on_return_pressed(self): + self.accepted.disconnect(self.on_return_pressed) + self.answer = self._input.text() + self._txt.setText('') + self.default = None + self.mode = None + self.text = None + # FIXME handle bool correctly + self.answered[str].emit(self.answer) + modeman.leave('prompt', 'prompt accept') + self.hide_prompt.emit() + + @cmdutils.register(instance='mainwindow.status.prompt', hide=True, + modes=['prompt']) + def prompt_accept(self): + """Accept the prompt. """ + self.accepted.emit() + + def display(self): + if self.mode == PromptMode.yesno: + if self.default is None: + suffix = " [y/n]" + elif self.default: + suffix = " [Y/n]" + else: + suffix = " [y/N]" + self._txt.setText(self.text + suffix) + self._input.hide() + elif self.mode == PromptMode.text: + self._txt.setText(self.text) + if self.default: + self._input.setText(self.default) + self._input.show() + self.accepted.connect(self.on_return_pressed) + elif self.mode == PromptMode.user_pwd: + self._txt.setText(self.text) + if self.default: + self._input.setText(self.default) + self._input.show() + self.accepted.connect(self._user_entered) + else: + raise ValueError("Invalid prompt mode!") + self._input.setFocus() + + def exec_(self): + self.display() + self.answered[str, str].connect(self.loop.quit) + self.loop.exec_() + return self.answer + + +class _QueryInput(QLineEdit): + + """Minimal QLineEdit used for input.""" + + def __init__(self, parent=None): + super().__init__(parent) + self.setStyleSheet(""" + QLineEdit { + border: 0px; + padding-left: 1px; + background-color: transparent; + } + """) + + def focusInEvent(self, e): + """Extend focusInEvent to enter command mode.""" + modeman.enter('prompt', 'auth focus') + super().focusInEvent(e) diff --git a/qutebrowser/widgets/statusbar/_textbase.py b/qutebrowser/widgets/statusbar/_textbase.py index 6e22f6a6b..6baef2588 100644 --- a/qutebrowser/widgets/statusbar/_textbase.py +++ b/qutebrowser/widgets/statusbar/_textbase.py @@ -36,8 +36,8 @@ class TextBase(QLabel): _elided_text: The current elided text. """ - def __init__(self, bar, elidemode=Qt.ElideRight): - super().__init__(bar) + def __init__(self, parent=None, elidemode=Qt.ElideRight): + super().__init__(parent) self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum) self._elidemode = elidemode self._elided_text = '' @@ -48,8 +48,9 @@ class TextBase(QLabel): Args: width: The maximal width the text should take. """ - self._elided_text = self.fontMetrics().elidedText( - self.text(), self._elidemode, width, Qt.TextShowMnemonic) + if self.text is not None: + self._elided_text = self.fontMetrics().elidedText( + self.text(), self._elidemode, width, Qt.TextShowMnemonic) def setText(self, txt): """Extend QLabel::setText. diff --git a/qutebrowser/widgets/statusbar/bar.py b/qutebrowser/widgets/statusbar/bar.py index 7c3df5afb..f1c5588ac 100644 --- a/qutebrowser/widgets/statusbar/bar.py +++ b/qutebrowser/widgets/statusbar/bar.py @@ -32,6 +32,7 @@ from qutebrowser.widgets.statusbar._text import Text from qutebrowser.widgets.statusbar._keystring import KeyString from qutebrowser.widgets.statusbar._percentage import Percentage from qutebrowser.widgets.statusbar._url import Url +from qutebrowser.widgets.statusbar._prompt import Prompt, PromptMode from qutebrowser.config.style import set_register_stylesheet, get_stylesheet @@ -122,9 +123,14 @@ class StatusBar(QWidget): self._text_pop_timer.setInterval(config.get('ui', 'message-timeout')) self._text_pop_timer.timeout.connect(self._pop_text) + self.prompt = Prompt(self) + 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() + self.prompt.hide_prompt.connect(self._hide_prompt_widget) + self._hide_prompt_widget() self._hbox.addLayout(self._stack) @@ -189,6 +195,24 @@ class StatusBar(QWidget): self._timer_was_active = False self._stack.setCurrentWidget(self.txt) + def _show_prompt_widget(self): + """Show prompt widget instead of temporary text.""" + self.error = False + if self._text_pop_timer.isActive(): + self._timer_was_active = True + self._text_pop_timer.stop() + self._stack.setCurrentWidget(self.prompt) + + def _hide_prompt_widget(self): + """Show temporary text instead of prompt widget.""" + logging.debug("Hiding prompt widget, queue: {}".format(self._text_queue)) + if self._timer_was_active: + # Restart the text pop timer if it was active before hiding. + self._pop_text() + self._text_pop_timer.start() + self._timer_was_active = False + self._stack.setCurrentWidget(self.txt) + def _disp_text(self, text, error, queue=False): """Inner logic for disp_error and disp_temp_text. @@ -292,6 +316,17 @@ class StatusBar(QWidget): self._text_pop_timer.setInterval(config.get('ui', 'message-timeout')) + @pyqtSlot('QNetworkReply', 'QAuthenticator') + def on_authentication_required(self, reply, authenticator): + self._show_prompt_widget() + self.prompt.mode = PromptMode.user_pwd + self.prompt.text = "Username ({}):".format(authenticator.realm()) + user, password = self.prompt.exec_() + self._hide_prompt_widget() + authenticator.setUser(user) + authenticator.setPassword(password) + logging.debug("user: {} / password: {}".format(user, password)) + def resizeEvent(self, e): """Extend resizeEvent of QWidget to emit a resized signal afterwards.