Merge branch 'prompts'
This commit is contained in:
commit
aa9c23d1c1
@ -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']+
|
||||
|
@ -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]+
|
||||
|
@ -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.
|
||||
|
@ -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:
|
||||
|
@ -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.
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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'),
|
||||
|
@ -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([
|
||||
|
@ -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):
|
||||
|
@ -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(
|
||||
|
829
qutebrowser/mainwindow/prompt.py
Normal file
829
qutebrowser/mainwindow/prompt.py
Normal 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)
|
@ -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):
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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)
|
@ -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)
|
@ -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).
|
||||
|
@ -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 += [
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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',
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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', [
|
||||
|
@ -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
|
93
tests/unit/mainwindow/test_prompt.py
Normal file
93
tests/unit/mainwindow/test_prompt.py
Normal 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() == '/'
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user