Merge branch 'prompts'

This commit is contained in:
Florian Bruhin 2016-11-04 18:43:08 +01:00
commit aa9c23d1c1
36 changed files with 1552 additions and 940 deletions

View File

@ -977,6 +977,7 @@ How many steps to zoom out.
|<<move-to-start-of-prev-block,move-to-start-of-prev-block>>|Move the cursor or selection to the start of previous block.
|<<open-editor,open-editor>>|Open an external editor with the currently selected form field.
|<<prompt-accept,prompt-accept>>|Accept the current prompt.
|<<prompt-item-focus,prompt-item-focus>>|Shift the focus of the prompt file completion menu to another item.
|<<prompt-open-download,prompt-open-download>>|Immediately open a download.
|<<repeat-command,repeat-command>>|Repeat the last executed command.
|<<rl-backward-char,rl-backward-char>>|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']+

View File

@ -54,6 +54,7 @@
|<<ui-modal-js-dialog,modal-js-dialog>>|Use standard JavaScript modal dialog for alert() and confirm()
|<<ui-hide-wayland-decoration,hide-wayland-decoration>>|Hide the window decoration when using wayland (requires restart)
|<<ui-keyhint-blacklist,keyhint-blacklist>>|Keychains that shouldn't be shown in the keyhint dialog
|<<ui-prompt-radius,prompt-radius>>|The rounding radius for the edges of prompts.
|==============
.Quick reference for section ``network''
@ -213,8 +214,6 @@
|<<colors-completion.scrollbar.bg,completion.scrollbar.bg>>|Color of the scrollbar in completion view
|<<colors-statusbar.fg,statusbar.fg>>|Foreground color of the statusbar.
|<<colors-statusbar.bg,statusbar.bg>>|Background color of the statusbar.
|<<colors-statusbar.fg.prompt,statusbar.fg.prompt>>|Foreground color of the statusbar if there is a prompt.
|<<colors-statusbar.bg.prompt,statusbar.bg.prompt>>|Background color of the statusbar if there is a prompt.
|<<colors-statusbar.fg.insert,statusbar.fg.insert>>|Foreground color of the statusbar in insert mode.
|<<colors-statusbar.bg.insert,statusbar.bg.insert>>|Background color of the statusbar in insert mode.
|<<colors-statusbar.fg.command,statusbar.fg.command>>|Foreground color of the statusbar in command mode.
@ -268,6 +267,9 @@
|<<colors-messages.fg.info,messages.fg.info>>|Foreground color an info message.
|<<colors-messages.bg.info,messages.bg.info>>|Background color of an info message.
|<<colors-messages.border.info,messages.border.info>>|Border color of an info message.
|<<colors-prompts.fg,prompts.fg>>|Foreground color for prompts.
|<<colors-prompts.bg,prompts.bg>>|Background color for prompts.
|<<colors-prompts.selected.bg,prompts.selected.bg>>|Background color for the selected item in filename prompts.
|==============
.Quick reference for section ``fonts''
@ -296,6 +298,7 @@
|<<fonts-messages.error,messages.error>>|Font used for error messages.
|<<fonts-messages.warning,messages.warning>>|Font used for warning messages.
|<<fonts-messages.info,messages.info>>|Font used for info messages.
|<<fonts-prompts,prompts>>|Font used for prompts.
|==============
== general
@ -706,6 +709,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 +1840,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 +2181,24 @@ 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]+
[[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.
@ -2322,3 +2337,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]+

View File

@ -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)
@ -643,13 +646,7 @@ class Quitter:
session_manager.save(sessions.default, last_window=last_window,
load_next_time=True)
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():
deferrer = True
if deferrer:
if prompt.prompt_queue.shutdown():
# 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.

View File

@ -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)
@ -1355,9 +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, self._win_id, 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:

View File

@ -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.

View File

@ -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
@ -33,7 +34,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
@ -159,11 +160,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 +171,19 @@ 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),
text="Please enter a quickmark name for<br/><b>{}</b>".format(
html.escape(url.toDisplayString())))
@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.
"""
@ -201,10 +201,12 @@ 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(
win_id, "Override existing quickmark?", set_mark, default=True)
title="Override existing quickmark?",
yes_action=set_mark, default=True)
else:
set_mark()

View File

@ -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, win_id, *, parent=None,
def ask_for_filename(suggested_filename, *, url, parent=None,
prompt_download_directory=None):
"""Prepare a question for a download-path.
@ -133,7 +134,7 @@ 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.
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,14 +151,14 @@ 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.text = "Please enter a location for <b>{}</b>".format(
html.escape(url.toDisplayString()))
q.mode = usertypes.PromptMode.text
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)
@ -382,20 +383,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(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."""
@ -614,14 +608,15 @@ 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?"
self._ask_confirm_question(txt)
txt = "<b>{}</b> 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?")
self._ask_confirm_question(txt)
txt = ("<b>{}</b> 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()
@ -963,9 +958,9 @@ 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,
)
url=reply.url())
# User doesn't want to be asked, so just use the download_dir
if filename is not None:

View File

@ -22,7 +22,9 @@
import os
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,
@ -207,10 +209,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, default=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
@ -219,24 +222,19 @@ class NetworkManager(QNetworkAccessManager):
Return:
The answer the user gave or None if the prompt was cancelled.
"""
q = usertypes.Question()
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(title=title, text=text, mode=mode,
abort_on=abort_on, default=default)
def shutdown(self):
"""Abort all running requests."""
@ -283,9 +281,19 @@ class NetworkManager(QNetworkAccessManager):
return
if ssl_strict == 'ask':
err_string = '\n'.join('- ' + err.errorString() for err in errors)
answer = self._ask('SSL errors - continue?\n{}'.format(err_string),
mode=usertypes.PromptMode.yesno, owner=reply)
err_template = jinja2.Template("""
Errors while loading <b>{{url.toDisplayString()}}</b>:<br/>
<ul>
{% for err in errors %}
<li>{{err.errorString()}}</li>
{% endfor %}
</ul>
""".strip())
msg = err_template.render(url=reply.url(), errors=errors)
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))
if answer:
reply.ignoreSslErrors()
@ -343,8 +351,11 @@ class NetworkManager(QNetworkAccessManager):
if user is None:
# netrc check failed
answer = self._ask("Username ({}):".format(authenticator.realm()),
mode=usertypes.PromptMode.user_pwd,
msg = '<b>{}</b> says:<br/>{}'.format(
html.escape(reply.url().toDisplayString()),
html.escape(authenticator.realm()))
answer = self._ask("Authentication required",
text=msg, mode=usertypes.PromptMode.user_pwd,
owner=reply)
if answer is not None:
user, password = answer.user, answer.password
@ -361,8 +372,11 @@ class NetworkManager(QNetworkAccessManager):
authenticator.setUser(user)
authenticator.setPassword(password)
else:
msg = '<b>{}</b> says:<br/>{}'.format(
html.escape(proxy.hostName()),
html.escape(authenticator.realm()))
answer = self._ask(
"Proxy username ({}):".format(authenticator.realm()),
"Proxy authentication required", msg,
mode=usertypes.PromptMode.user_pwd)
if answer is not None:
authenticator.setUser(answer.user)

View File

@ -19,6 +19,7 @@
"""The main browser widgets."""
import html
import functools
from PyQt5.QtCore import pyqtSlot, pyqtSignal, PYQT_VERSION, Qt, QUrl, QPoint
@ -93,13 +94,19 @@ 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, "")
answer = self._ask("js: {}".format(msg), usertypes.PromptMode.text,
default)
msg = '<b>{}</b> asks:<br/>{}'.format(
html.escape(self.mainFrame().url().toDisplayString()),
html.escape(js_msg))
answer = message.ask('Javascript prompt', msg,
mode=usertypes.PromptMode.text,
default=default,
abort_on=[self.loadStarted,
self.shutting_down])
if answer is None:
return (False, "")
else:
@ -134,11 +141,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))
title="Open external application for {}-link?".format(scheme),
text="URL: <b>{}</b>".format(
html.escape(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: {}, "
@ -168,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
@ -196,29 +204,6 @@ class BrowserPage(QWebPage):
suggested_file)
return True
def _ask(self, text, mode, default=None):
"""Ask a blocking question in the statusbar.
Args:
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.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:
@ -333,11 +318,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',
@ -345,30 +325,28 @@ class BrowserPage(QWebPage):
host = frame.url().host()
if host:
q.text = "Allow the website at {} to {}?".format(
frame.url().host(), msgs[feature])
text = "Allow the website at <b>{}</b> to {}?".format(
html.escape(frame.url().toDisplayString()), 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)
self.featurePermissionRequestCanceled.connect(functools.partial(
self.on_feature_permission_cancelled, q, frame, feature))
self.loadStarted.connect(q.abort)
bridge.ask(q, blocking=False)
question = message.confirm_async(yes_action=yes_action,
no_action=no_action,
cancel_action=no_action,
abort_on=[self.shutting_down,
self.loadStarted],
title='Permission request',
text=text)
self.featurePermissionRequestCanceled.connect(
functools.partial(self.on_feature_permission_cancelled,
question, frame, feature))
elif config_val:
self.setFeaturePermission(frame, feature,
QWebPage.PermissionGrantedByUser)
@ -469,27 +447,37 @@ 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
self._ask("[js alert] {}".format(msg), usertypes.PromptMode.alert)
def javaScriptConfirm(self, frame, msg):
msg = 'From <b>{}</b>:<br/>{}'.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, 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
ans = self._ask("[js confirm] {}".format(msg),
usertypes.PromptMode.yesno)
msg = 'From <b>{}</b>:<br/>{}'.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])
return bool(ans)
def javaScriptConsoleMessage(self, msg, line, source):

View File

@ -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'),

View File

@ -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
)),
@ -1075,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."),
@ -1306,6 +1302,18 @@ 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."),
('prompts.selected.bg',
SettingValue(typ.QssColor(), '#308cc6'),
"Background color for the selected item in filename prompts."),
readonly=readonly
)),
@ -1407,6 +1415,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
)),
])
@ -1672,6 +1684,8 @@ KEY_DATA = collections.OrderedDict([
('prompt-accept yes', ['y']),
('prompt-accept no', ['n']),
('prompt-open-download', ['<Ctrl-X>']),
('prompt-item-focus prev', ['<Shift-Tab>', '<Up>']),
('prompt-item-focus next', ['<Tab>', '<Down>']),
])),
('command,prompt', collections.OrderedDict([

View File

@ -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):

View File

@ -24,13 +24,14 @@ 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
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
@ -175,17 +176,24 @@ 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)
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)
log.init.debug("Initializing modes...")
modeman.init(self.win_id, self)
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()
if geometry is not None:
self._load_geometry(geometry)
@ -206,36 +214,40 @@ class MainWindow(QWidget):
objreg.get("app").new_window.emit(self)
def _update_overlay_geometry(self, widget=None):
"""Reposition/resize the given overlay.
def _add_overlay(self, widget, signal, *, centered=False, padding=0):
self._overlays.append((widget, signal, centered, padding))
If no widget is given, reposition/resize all overlays.
"""
if widget is None:
for w, _signal in self._overlays:
self._update_overlay_geometry(w)
return
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
size_hint = widget.sizeHint()
if widget.sizePolicy().horizontalPolicy() == QSizePolicy.Expanding:
width = self.width()
width = self.width() - 2 * padding
left = padding
else:
width = size_hint.width()
left = (self.width() - size_hint.width()) / 2 if centered else 0
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(0, top)
bottomright = QPoint(width, self.status.geometry().top())
topleft = QPoint(left, max(height_padding, 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)
bottomright = QPoint(left + width,
min(self.height() - height_padding, bottom))
else:
raise ValueError("Invalid position {}!".format(status_position))
@ -261,8 +273,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,
@ -282,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."""
@ -350,10 +361,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, padding 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, padding))
self._update_overlay_geometry(widget, centered, padding)
def _set_default_geometry(self):
"""Set some sensible default geometry."""
@ -374,7 +386,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)
@ -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(message.global_bridge.mode_left)
# commands
keyparsers[usertypes.KeyMode.normal].keystring_updated.connect(
@ -407,9 +418,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(prompter.ask_question,
Qt.DirectConnection)
# statusbar
tabs.current_tab_changed.connect(status.prog.on_tab_changed)
@ -459,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()
@ -507,10 +515,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(self.win_id, text,
usertypes.PromptMode.yesno,
msg = jinja2.Template("""
<ul>
{% for text in quit_texts %}
<li>{{text}}</li>
{% endfor %}
</ul>
""".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(

View File

@ -0,0 +1,829 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Showing prompts above the statusbar."""
import os.path
import html
import collections
import sip
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QTimer, QDir, QModelIndex,
QItemSelectionModel, QObject)
from PyQt5.QtWidgets import (QWidget, QGridLayout, QVBoxLayout, QLineEdit,
QLabel, QFileSystemModel, QTreeView, QSizePolicy)
from qutebrowser.config import style
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'])
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 PromptQueue(QObject):
"""Global manager and queue for upcoming prompts.
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 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
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.
_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 = pyqtSignal(usertypes.Question)
def __init__(self, parent=None):
super().__init__(parent)
self._question = None
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),
queue=len(self._queue), question=self._question)
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.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 shutdown(self):
"""Cancel all blocking questions.
Quits and removes all running event loops.
Return:
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:
loop.quit()
loop.deleteLater()
return True
else:
return False
@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._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))
self._queue.append(question)
return
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
if old_question is not None:
old_question.interrupted = True
self._question = question
self.show_prompts.emit(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)
log.prompt.debug("Starting loop.exec_() for {}".format(question))
loop.exec_()
log.prompt.debug("Ending loop.exec_() for {}".format(question))
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)
@pyqtSlot(usertypes.KeyMode)
def _on_mode_left(self, mode):
"""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):
"""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'] }};
}
QTreeView {
selection-background-color: {{ color['prompts.selected.bg'] }};
}
QTreeView::item:selected, QTreeView::item:selected:hover {
background-color: {{ color['prompts.selected.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._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_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)
@pyqtSlot(usertypes.Question)
def _on_show_prompts(self, question):
"""Show a prompt for the given question.
Args:
question: A Question object or None.
"""
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
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
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())
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')
@pyqtSlot(usertypes.KeyMode)
def _on_global_mode_left(self, mode):
"""Leave prompt/yesno mode in this window if it was left elsewhere.
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 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,
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):
"""A line edit used in prompts."""
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: # pragma: no cover
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."""
KEY_MODE = usertypes.KeyMode.prompt
def __init__(self, question, parent=None):
super().__init__(parent)
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)
def _init_texts(self, question):
assert question.title is not None, question
title = '<font size="4"><b>{}</b></font>'.format(
html.escape(question.title))
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)
def _init_key_label(self):
assert self._key_grid is None, self._key_grid
self._key_grid = QGridLayout()
self._key_grid.setVerticalSpacing(0)
key_config = objreg.get('key-config')
# 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():
bindings = all_bindings.get(cmd, [])
if bindings:
binding = None
preferred = ['<enter>', '<escape>']
for pref in preferred:
if pref in bindings:
binding = pref
if binding is None:
binding = bindings[0]
key_label = QLabel('<b>{}</b>'.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 download_open(self, _cmdline):
"""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
class LineEditPrompt(_BasePrompt):
"""A prompt for a single text value."""
def __init__(self, question, parent=None):
super().__init__(question, parent)
self._lineedit = LineEdit(self)
self._init_texts(question)
self._vbox.addWidget(self._lineedit)
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):
return [('prompt-accept', 'Accept'), ('leave-mode', 'Abort')]
class FilenamePrompt(_BasePrompt):
"""A prompt for a filename."""
def __init__(self, question, parent=None):
super().__init__(question, parent)
self._init_texts(question)
self._init_fileview()
self._set_fileview_root(question.default)
self._lineedit = LineEdit(self)
if question.default:
self._lineedit.setText(question.default)
self._lineedit.textEdited.connect(self._set_fileview_root)
self._vbox.addWidget(self._lineedit)
self.setFocusProxy(self._lineedit)
self._init_key_label()
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
@pyqtSlot(str)
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
dirname = os.path.dirname(path)
try:
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):
# 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:
log.prompt.exception("Failed to get directory information")
return
root = self._file_model.setRootPath(path)
self._file_view.setRootIndex(root)
@pyqtSlot(QModelIndex)
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.
"""
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)
self._lineedit.setFocus()
self._set_fileview_root(path, tabbed=True)
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._insert_path)
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)
# 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()
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()
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
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
selmodel.setCurrentIndex(
idx, QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
self._insert_path(idx, clicked=False)
def _allowed_commands(self):
return [('prompt-accept', 'Accept'), ('leave-mode', 'Abort')]
class DownloadFilenamePrompt(FilenamePrompt):
"""A prompt for a filename for downloads."""
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):
text = value if value is not None else self._lineedit.text()
self.question.answer = usertypes.FileDownloadTarget(text)
return True
def download_open(self, cmdline):
self.question.answer = usertypes.OpenFileDownloadTarget(cmdline)
self.question.done()
message.global_bridge.prompt_done.emit(self.KEY_MODE)
def _allowed_commands(self):
cmds = [
('prompt-accept', 'Accept'),
('leave-mode', 'Abort'),
('prompt-open-download', "Open download"),
]
return cmds
class AuthenticationPrompt(_BasePrompt):
"""A prompt for username/password."""
def __init__(self, question, parent=None):
super().__init__(question, parent)
self._init_texts(question)
user_label = QLabel("Username:", self)
self._user_lineedit = LineEdit(self)
password_label = QLabel("Password:", self)
self._password_lineedit = LineEdit(self)
self._password_lineedit.setEchoMode(QLineEdit.Password)
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)
self._init_key_label()
assert not question.default, question.default
self.setFocusProxy(self._user_lineedit)
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)
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())
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")]
class YesNoPrompt(_BasePrompt):
"""A prompt with yes/no answers."""
KEY_MODE = usertypes.KeyMode.yesno
def __init__(self, question, parent=None):
super().__init__(question, parent)
self._init_texts(question)
self._init_key_label()
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
elif value == 'no':
self.question.answer = False
else:
raise Error("Invalid value {} - expected yes/no!".format(value))
return True
def _allowed_commands(self):
cmds = [
('prompt-accept yes', "Yes"),
('prompt-accept no', "No"),
]
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
class AlertPrompt(_BasePrompt):
"""A prompt without any answer possibility."""
def __init__(self, question, parent=None):
super().__init__(question, parent)
self._init_texts(question)
self._init_key_label()
def accept(self, value=None):
if value is not None:
raise Error("No value is permitted with alert prompts!")
# Simply mark prompt as done without setting self.question.answer
return True
def _allowed_commands(self):
return [('prompt-accept', "Hide")]
def init():
"""Initialize global prompt objects."""
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)

View File

@ -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
@ -113,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"],
@ -162,16 +161,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)
@ -216,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."""
@ -253,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()
@ -285,21 +270,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."""
@ -314,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)
@ -329,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):

View File

@ -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.

View File

@ -1,84 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Prompt shown in the statusbar."""
import functools
from PyQt5.QtCore import QSize
from PyQt5.QtWidgets import QHBoxLayout, QWidget, QLineEdit, QSizePolicy
from qutebrowser.mainwindow.statusbar import textbase, prompter
from qutebrowser.utils import objreg, utils
from qutebrowser.misc import miscwidgets as misc
class PromptLineEdit(misc.MinimalLineEditMixin, QLineEdit):
"""QLineEdit with a minimal stylesheet."""
def __init__(self, parent=None):
QLineEdit.__init__(self, parent)
misc.MinimalLineEditMixin.__init__(self)
self.textChanged.connect(self.updateGeometry)
def sizeHint(self):
"""Dynamically calculate the needed size."""
height = super().sizeHint().height()
text = self.text()
if not text:
text = 'x'
width = self.fontMetrics().width(text)
return QSize(width, height)
class Prompt(QWidget):
"""The prompt widget shown in the statusbar.
Attributes:
txt: The TextBase instance (QLabel) used to display the prompt text.
lineedit: The MinimalLineEdit instance (QLineEdit) used for the input.
_hbox: The QHBoxLayout used to display the text and prompt.
"""
def __init__(self, win_id, parent=None):
super().__init__(parent)
objreg.register('prompt', self, scope='window', window=win_id)
self._hbox = QHBoxLayout(self)
self._hbox.setContentsMargins(0, 0, 0, 0)
self._hbox.setSpacing(5)
self.txt = textbase.TextBase()
self._hbox.addWidget(self.txt)
self.lineedit = PromptLineEdit()
self.lineedit.setSizePolicy(QSizePolicy.MinimumExpanding,
QSizePolicy.Fixed)
self._hbox.addWidget(self.lineedit)
prompter_obj = prompter.Prompter(win_id)
objreg.register('prompter', prompter_obj, scope='window',
window=win_id)
self.destroyed.connect(
functools.partial(objreg.delete, 'prompter', scope='window',
window=win_id))
def __repr__(self):
return utils.get_repr(self)

View File

@ -1,411 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Manager for questions to be shown in the statusbar."""
import sip
import collections
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer, QObject
from PyQt5.QtWidgets import QLineEdit
from qutebrowser.keyinput import modeman
from qutebrowser.commands import cmdutils, cmdexc
from qutebrowser.utils import usertypes, log, qtutils, objreg, utils
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.
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.
_busy: If we're currently busy with asking 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 = {
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,
}
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._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)
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 _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.
Args:
ctx: A PromptContext previously saved by _get_ctx, or None.
Return: True if a context was restored, False otherwise.
"""
log.statusbar.debug("Restoring context {}".format(ctx))
if ctx is None:
self.hide_prompt.emit()
self._busy = False
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()
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,
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()
self.show_prompt.emit()
self._busy = 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
@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.hide_prompt.emit()
self._busy = False
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()
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!")
@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()
@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()
@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._busy 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.
context = self._get_ctx()
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_ctx(context):
# 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)

View File

@ -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).

View File

@ -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
@ -408,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 += [

View File

@ -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):
@ -76,70 +76,83 @@ def info(message):
global_bridge.show_message.emit(usertypes.MessageLevel.info, message)
def ask(win_id, message, mode, default=None):
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))
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(*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.
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
bridge = objreg.get('message-bridge', scope='window', window=win_id)
bridge.ask(q, blocking=True)
q.deleteLater()
return q.answer
question = _build_question(*args, **kwargs) # pylint: disable=missing-kwoa
global_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(title, 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.
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)
bridge = objreg.get('message-bridge', scope='window', window=win_id)
bridge.ask(q, blocking=False)
question = _build_question(title, mode=mode, **kwargs)
question.answered.connect(handler)
question.completed.connect(question.deleteLater)
global_bridge.ask(question, blocking=False)
def confirm_async(win_id, message, yes_action, no_action=None, default=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.
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)
kwargs['mode'] = usertypes.PromptMode.yesno
question = _build_question(*args, **kwargs) # pylint: disable=missing-kwoa
question.answered_yes.connect(yes_action)
if no_action is not None:
q.answered_no.connect(no_action)
q.completed.connect(q.deleteLater)
bridge = objreg.get('message-bridge', scope='window', window=win_id)
bridge.ask(q, blocking=False)
question.answered_no.connect(no_action)
if cancel_action is not None:
question.cancelled.connect(cancel_action)
question.completed.connect(question.deleteLater)
global_bridge.ask(question, blocking=False)
return question
class GlobalMessageBridge(QObject):
@ -150,9 +163,34 @@ 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!
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.
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):
@ -164,36 +202,14 @@ 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.
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_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.
@ -214,19 +230,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()

View File

@ -335,10 +335,11 @@ 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.
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.
interrupted: Whether the question was interrupted by another one.
Signals:
answered: Emitted when the question has been answered by the user.
@ -364,14 +365,15 @@ class Question(QObject):
super().__init__(parent)
self._mode = None
self.default = None
self.title = None
self.text = None
self.user = None
self.answer = None
self.is_aborted = False
self.interrupted = 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):
@ -405,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()

View File

@ -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',

View File

@ -3,13 +3,14 @@
<head>
<meta charset="utf-8">
<script type="text/javascript">
function prompter() {
var reply = prompt("js prompt", "")
console.log("Prompt reply: " + reply)
function prompter(defaultval) {
var reply = prompt("js prompt", defaultval);
console.log("Prompt reply: " + reply);
}
</script>
</head>
<body>
<input type="button" onclick="prompter()" value="Show prompt" id="button">
<input type="button" onclick="prompter('')" value="Show prompt" id="button">
<input type="button" onclick="prompter('default')" value="Show prompt with default value" id="button-default">
</body>
</html>

View File

@ -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)

View File

@ -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 <qutebrowser.utils.usertypes.Question default='qutebrowser-download' mode=<PromptMode.download: 5> text='Save file to:'>, *" in the log
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='qutebrowser-download' mode=<PromptMode.download: 5> 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 <qutebrowser.utils.usertypes.Question default='binary blob' mode=<PromptMode.download: 5> text='Save file to:'>, *" in the log
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='binary blob' mode=<PromptMode.download: 5> 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 <qutebrowser.utils.usertypes.Question default=* mode=<PromptMode.download: 5> text='Save file to:'>, *" in the log
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default=* mode=<PromptMode.download: 5> 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
@ -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 <qutebrowser.utils.usertypes.Question default='*' mode=<PromptMode.download: 5> 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 <qutebrowser.utils.usertypes.Question default='*' mode=<PromptMode.download: 5> 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
Then the downloaded file download.bin should exist
And the downloaded file download2.bin should not exist

View File

@ -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 <qutebrowser.utils.usertypes.Question default='test.pdf' mode=<PromptMode.download: 5> text='Save file to:'>, *" in the log
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='test.pdf' mode=<PromptMode.download: 5> text=* title='Save file to:'>, *" in the log
And I run :leave-mode
Then no crash should happen

View File

@ -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 "<Shift-Insert>"
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
@ -249,3 +368,102 @@ 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
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
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
# 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 "<Enter>"
And I press the keys "password2"
And I press the key "<Enter>"
And I wait until basic-auth/user2/password2 is loaded
# First prompt
And I press the keys "user1"
And I press the key "<Enter>"
And I press the keys "password1"
And I press the key "<Enter>"
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"
}
# 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

View File

@ -31,8 +31,8 @@ pytestmark = pytest.mark.qtwebengine_todo("Downloads not implemented yet",
PROMPT_MSG = ("Asking question <qutebrowser.utils.usertypes.Question "
"default={!r} mode=<PromptMode.download: 5> "
"text='Save file to:'>, *")
"default={!r} mode=<PromptMode.download: 5> text=* "
"title='Save file to:'>, *")
@bdd.given("I set up a temporary download dir")

View File

@ -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")

View File

@ -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

View File

@ -1,8 +1,8 @@
<head>
<script type="text/javascript">
function prompter() {
var reply = prompt("js prompt", "")
alert("JS alert: " + reply + "!")
var reply = prompt("js prompt", "");
alert("JS alert: " + reply + "!");
}
</script>
</head>

View File

@ -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', [

View File

@ -1,57 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""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

View File

@ -0,0 +1,93 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
import os
import pytest
from PyQt5.QtCore import Qt
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', {})
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"
question.default = path
prompt = promptmod.DownloadFilenamePrompt(question)
qtbot.add_widget(prompt)
with qtbot.wait_signal(prompt._file_model.directoryLoaded):
pass
assert prompt._lineedit.text() == path
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):
"""Simply trying to tab through items."""
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)
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')
@pytest.mark.linux
def test_root_path(self, get_prompt):
"""With / as path, show root contents."""
prompt = get_prompt('/')
assert prompt._file_model.rootPath() == '/'

View File

@ -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)