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.
|
|<<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.
|
|<<open-editor,open-editor>>|Open an external editor with the currently selected form field.
|
||||||
|<<prompt-accept,prompt-accept>>|Accept the current prompt.
|
|<<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.
|
|<<prompt-open-download,prompt-open-download>>|Immediately open a download.
|
||||||
|<<repeat-command,repeat-command>>|Repeat the last executed command.
|
|<<repeat-command,repeat-command>>|Repeat the last executed command.
|
||||||
|<<rl-backward-char,rl-backward-char>>|Move back a character.
|
|<<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.
|
* +'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]]
|
||||||
=== prompt-open-download
|
=== prompt-open-download
|
||||||
Syntax: +:prompt-open-download ['cmdline']+
|
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-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-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-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''
|
.Quick reference for section ``network''
|
||||||
@ -213,8 +214,6 @@
|
|||||||
|<<colors-completion.scrollbar.bg,completion.scrollbar.bg>>|Color of the scrollbar in completion view
|
|<<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.fg,statusbar.fg>>|Foreground color of the statusbar.
|
||||||
|<<colors-statusbar.bg,statusbar.bg>>|Background 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.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.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.
|
|<<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.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.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-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''
|
.Quick reference for section ``fonts''
|
||||||
@ -296,6 +298,7 @@
|
|||||||
|<<fonts-messages.error,messages.error>>|Font used for error messages.
|
|<<fonts-messages.error,messages.error>>|Font used for error messages.
|
||||||
|<<fonts-messages.warning,messages.warning>>|Font used for warning messages.
|
|<<fonts-messages.warning,messages.warning>>|Font used for warning messages.
|
||||||
|<<fonts-messages.info,messages.info>>|Font used for info messages.
|
|<<fonts-messages.info,messages.info>>|Font used for info messages.
|
||||||
|
|<<fonts-prompts,prompts>>|Font used for prompts.
|
||||||
|==============
|
|==============
|
||||||
|
|
||||||
== general
|
== general
|
||||||
@ -706,6 +709,12 @@ Globs are supported, so ';*' will blacklist all keychainsstarting with ';'. Use
|
|||||||
|
|
||||||
Default: empty
|
Default: empty
|
||||||
|
|
||||||
|
[[ui-prompt-radius]]
|
||||||
|
=== prompt-radius
|
||||||
|
The rounding radius for the edges of prompts.
|
||||||
|
|
||||||
|
Default: +pass:[8]+
|
||||||
|
|
||||||
== network
|
== network
|
||||||
Settings related to the network.
|
Settings related to the network.
|
||||||
|
|
||||||
@ -1831,18 +1840,6 @@ Background color of the statusbar.
|
|||||||
|
|
||||||
Default: +pass:[black]+
|
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]]
|
[[colors-statusbar.fg.insert]]
|
||||||
=== statusbar.fg.insert
|
=== statusbar.fg.insert
|
||||||
Foreground color of the statusbar in insert mode.
|
Foreground color of the statusbar in insert mode.
|
||||||
@ -2184,6 +2181,24 @@ Border color of an info message.
|
|||||||
|
|
||||||
Default: +pass:[#333333]+
|
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
|
||||||
Fonts used for the UI, with optional style/weight/size.
|
Fonts used for the UI, with optional style/weight/size.
|
||||||
|
|
||||||
@ -2322,3 +2337,9 @@ Default: +pass:[8pt ${_monospace}]+
|
|||||||
Font used for info messages.
|
Font used for info messages.
|
||||||
|
|
||||||
Default: +pass:[8pt ${_monospace}]+
|
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 import urlmarks, adblock, history, browsertab
|
||||||
from qutebrowser.browser.webkit import cookies, cache, downloads
|
from qutebrowser.browser.webkit import cookies, cache, downloads
|
||||||
from qutebrowser.browser.webkit.network import networkmanager
|
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,
|
from qutebrowser.misc import (readline, ipc, savemanager, sessions,
|
||||||
crashsignal, earlyinit)
|
crashsignal, earlyinit)
|
||||||
from qutebrowser.misc import utilcmds # pylint: disable=unused-import
|
from qutebrowser.misc import utilcmds # pylint: disable=unused-import
|
||||||
@ -372,6 +372,9 @@ def _init_modules(args, crash_handler):
|
|||||||
crash_handler: The CrashHandler instance.
|
crash_handler: The CrashHandler instance.
|
||||||
"""
|
"""
|
||||||
# pylint: disable=too-many-statements
|
# pylint: disable=too-many-statements
|
||||||
|
log.init.debug("Initializing prompts...")
|
||||||
|
prompt.init()
|
||||||
|
|
||||||
log.init.debug("Initializing save manager...")
|
log.init.debug("Initializing save manager...")
|
||||||
save_manager = savemanager.SaveManager(qApp)
|
save_manager = savemanager.SaveManager(qApp)
|
||||||
objreg.register('save-manager', save_manager)
|
objreg.register('save-manager', save_manager)
|
||||||
@ -643,13 +646,7 @@ class Quitter:
|
|||||||
session_manager.save(sessions.default, last_window=last_window,
|
session_manager.save(sessions.default, last_window=last_window,
|
||||||
load_next_time=True)
|
load_next_time=True)
|
||||||
|
|
||||||
deferrer = False
|
if prompt.prompt_queue.shutdown():
|
||||||
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 shutdown was called while we were asking a question, we're in
|
# 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
|
# a still sub-eventloop (which gets quit now) and not in the main
|
||||||
# one.
|
# one.
|
||||||
|
@ -1137,7 +1137,7 @@ class CommandDispatcher:
|
|||||||
def quickmark_save(self):
|
def quickmark_save(self):
|
||||||
"""Save the current page as a quickmark."""
|
"""Save the current page as a quickmark."""
|
||||||
quickmark_manager = objreg.get('quickmark-manager')
|
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',
|
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||||
maxsplit=0)
|
maxsplit=0)
|
||||||
@ -1355,9 +1355,8 @@ class CommandDispatcher:
|
|||||||
if dest is None:
|
if dest is None:
|
||||||
suggested_fn = self._current_title() + ".mht"
|
suggested_fn = self._current_title() + ".mht"
|
||||||
suggested_fn = utils.sanitize_filename(suggested_fn)
|
suggested_fn = utils.sanitize_filename(suggested_fn)
|
||||||
filename, q = downloads.ask_for_filename(
|
filename, q = downloads.ask_for_filename(suggested_fn, parent=tab,
|
||||||
suggested_fn, self._win_id, parent=tab,
|
url=tab.url())
|
||||||
)
|
|
||||||
if filename is not None:
|
if filename is not None:
|
||||||
mhtml.start_download_checked(filename, tab=tab)
|
mhtml.start_download_checked(filename, tab=tab)
|
||||||
else:
|
else:
|
||||||
|
@ -265,9 +265,8 @@ class HintActions:
|
|||||||
if text[0] not in modeparsers.STARTCHARS:
|
if text[0] not in modeparsers.STARTCHARS:
|
||||||
raise HintingError("Invalid command text '{}'.".format(text))
|
raise HintingError("Invalid command text '{}'.".format(text))
|
||||||
|
|
||||||
bridge = objreg.get('message-bridge', scope='window',
|
cmd = objreg.get('status-command', scope='window', window=self._win_id)
|
||||||
window=self._win_id)
|
cmd.set_cmd_text(text)
|
||||||
bridge.set_cmd_text(text)
|
|
||||||
|
|
||||||
def download(self, elem, context):
|
def download(self, elem, context):
|
||||||
"""Download a hint URL.
|
"""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 os
|
||||||
|
import html
|
||||||
import os.path
|
import os.path
|
||||||
import functools
|
import functools
|
||||||
import collections
|
import collections
|
||||||
@ -33,7 +34,7 @@ import collections
|
|||||||
from PyQt5.QtCore import pyqtSignal, QUrl, QObject
|
from PyQt5.QtCore import pyqtSignal, QUrl, QObject
|
||||||
|
|
||||||
from qutebrowser.utils import (message, usertypes, qtutils, urlutils,
|
from qutebrowser.utils import (message, usertypes, qtutils, urlutils,
|
||||||
standarddir, objreg)
|
standarddir, objreg, log)
|
||||||
from qutebrowser.commands import cmdutils
|
from qutebrowser.commands import cmdutils
|
||||||
from qutebrowser.misc import lineparser
|
from qutebrowser.misc import lineparser
|
||||||
|
|
||||||
@ -159,11 +160,10 @@ class QuickmarkManager(UrlMarkManager):
|
|||||||
else:
|
else:
|
||||||
self.marks[key] = url
|
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.
|
"""Prompt for a new quickmark name to be added and add it.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
win_id: The current window ID.
|
|
||||||
url: The quickmark url as a QUrl.
|
url: The quickmark url as a QUrl.
|
||||||
"""
|
"""
|
||||||
if not url.isValid():
|
if not url.isValid():
|
||||||
@ -171,19 +171,19 @@ class QuickmarkManager(UrlMarkManager):
|
|||||||
return
|
return
|
||||||
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
|
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
|
||||||
message.ask_async(
|
message.ask_async(
|
||||||
win_id, "Add quickmark:", usertypes.PromptMode.text,
|
"Add quickmark:", usertypes.PromptMode.text,
|
||||||
functools.partial(self.quickmark_add, win_id, urlstr))
|
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.register(instance='quickmark-manager')
|
||||||
@cmdutils.argument('win_id', win_id=True)
|
def quickmark_add(self, url, name):
|
||||||
def quickmark_add(self, win_id, url, name):
|
|
||||||
"""Add a new quickmark.
|
"""Add a new quickmark.
|
||||||
|
|
||||||
You can view all saved quickmarks on the
|
You can view all saved quickmarks on the
|
||||||
link:qute://bookmarks[bookmarks page].
|
link:qute://bookmarks[bookmarks page].
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
win_id: The window ID to display the errors in.
|
|
||||||
url: The url to add as quickmark.
|
url: The url to add as quickmark.
|
||||||
name: The name for the new quickmark.
|
name: The name for the new quickmark.
|
||||||
"""
|
"""
|
||||||
@ -201,10 +201,12 @@ class QuickmarkManager(UrlMarkManager):
|
|||||||
self.marks[name] = url
|
self.marks[name] = url
|
||||||
self.changed.emit()
|
self.changed.emit()
|
||||||
self.added.emit(name, url)
|
self.added.emit(name, url)
|
||||||
|
log.misc.debug("Added quickmark {} for {}".format(name, url))
|
||||||
|
|
||||||
if name in self.marks:
|
if name in self.marks:
|
||||||
message.confirm_async(
|
message.confirm_async(
|
||||||
win_id, "Override existing quickmark?", set_mark, default=True)
|
title="Override existing quickmark?",
|
||||||
|
yes_action=set_mark, default=True)
|
||||||
else:
|
else:
|
||||||
set_mark()
|
set_mark()
|
||||||
|
|
||||||
|
@ -28,6 +28,7 @@ import shutil
|
|||||||
import functools
|
import functools
|
||||||
import tempfile
|
import tempfile
|
||||||
import collections
|
import collections
|
||||||
|
import html
|
||||||
|
|
||||||
import sip
|
import sip
|
||||||
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QObject, QTimer,
|
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QObject, QTimer,
|
||||||
@ -119,7 +120,7 @@ def create_full_filename(basename, filename):
|
|||||||
return None
|
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):
|
prompt_download_directory=None):
|
||||||
"""Prepare a question for a download-path.
|
"""Prepare a question for a download-path.
|
||||||
|
|
||||||
@ -133,7 +134,7 @@ def ask_for_filename(suggested_filename, win_id, *, parent=None,
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
suggested_filename: The "default"-name that is pre-entered as path.
|
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).
|
parent: The parent of the question (a QObject).
|
||||||
prompt_download_directory: If this is something else than None, it
|
prompt_download_directory: If this is something else than None, it
|
||||||
will overwrite the
|
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)
|
suggested_filename = utils.force_encoding(suggested_filename, encoding)
|
||||||
|
|
||||||
q = usertypes.Question(parent)
|
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.mode = usertypes.PromptMode.text
|
||||||
q.completed.connect(q.deleteLater)
|
q.completed.connect(q.deleteLater)
|
||||||
q.default = _path_suggestion(suggested_filename)
|
q.default = _path_suggestion(suggested_filename)
|
||||||
|
|
||||||
message_bridge = objreg.get('message-bridge', scope='window',
|
q.ask = lambda: message.global_bridge.ask(q, blocking=False)
|
||||||
window=win_id)
|
|
||||||
q.ask = lambda: message_bridge.ask(q, blocking=False)
|
|
||||||
return _DownloadPath(filename=None, question=q)
|
return _DownloadPath(filename=None, question=q)
|
||||||
|
|
||||||
|
|
||||||
@ -382,20 +383,13 @@ class DownloadItem(QObject):
|
|||||||
else:
|
else:
|
||||||
self.set_fileobj(fileobj)
|
self.set_fileobj(fileobj)
|
||||||
|
|
||||||
def _ask_confirm_question(self, msg):
|
def _ask_confirm_question(self, title, msg):
|
||||||
"""Create a Question object to be asked."""
|
"""Create a Question object to be asked."""
|
||||||
q = usertypes.Question(self)
|
no_action = functools.partial(self.cancel, remove_data=False)
|
||||||
q.text = msg
|
message.confirm_async(title=title, text=msg,
|
||||||
q.mode = usertypes.PromptMode.yesno
|
yes_action=self._create_fileobj,
|
||||||
q.answered_yes.connect(self._create_fileobj)
|
no_action=no_action, cancel_action=no_action,
|
||||||
q.answered_no.connect(functools.partial(self.cancel,
|
abort_on=[self.cancelled, self.error])
|
||||||
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)
|
|
||||||
|
|
||||||
def _die(self, msg):
|
def _die(self, msg):
|
||||||
"""Abort the download and emit an error."""
|
"""Abort the download and emit an error."""
|
||||||
@ -614,14 +608,15 @@ class DownloadItem(QObject):
|
|||||||
if os.path.isfile(self._filename):
|
if os.path.isfile(self._filename):
|
||||||
# The file already exists, so ask the user if it should be
|
# The file already exists, so ask the user if it should be
|
||||||
# overwritten.
|
# overwritten.
|
||||||
txt = self._filename + " already exists. Overwrite?"
|
txt = "<b>{}</b> already exists. Overwrite?".format(
|
||||||
self._ask_confirm_question(txt)
|
html.escape(self._filename))
|
||||||
|
self._ask_confirm_question("Overwrite existing file?", txt)
|
||||||
# FIFO, device node, etc. Make sure we want to do this
|
# FIFO, device node, etc. Make sure we want to do this
|
||||||
elif (os.path.exists(self._filename) and
|
elif (os.path.exists(self._filename) and
|
||||||
not os.path.isdir(self._filename)):
|
not os.path.isdir(self._filename)):
|
||||||
txt = (self._filename + " already exists and is a special file. "
|
txt = ("<b>{}</b> already exists and is a special file. Write to "
|
||||||
"Write to this?")
|
"it anyways?".format(html.escape(self._filename)))
|
||||||
self._ask_confirm_question(txt)
|
self._ask_confirm_question("Overwrite special file?", txt)
|
||||||
else:
|
else:
|
||||||
self._create_fileobj()
|
self._create_fileobj()
|
||||||
|
|
||||||
@ -963,9 +958,9 @@ class DownloadManager(QObject):
|
|||||||
|
|
||||||
# Neither filename nor fileobj were given, prepare a question
|
# Neither filename nor fileobj were given, prepare a question
|
||||||
filename, q = ask_for_filename(
|
filename, q = ask_for_filename(
|
||||||
suggested_filename, self._win_id, parent=self,
|
suggested_filename, parent=self,
|
||||||
prompt_download_directory=prompt_download_directory,
|
prompt_download_directory=prompt_download_directory,
|
||||||
)
|
url=reply.url())
|
||||||
|
|
||||||
# User doesn't want to be asked, so just use the download_dir
|
# User doesn't want to be asked, so just use the download_dir
|
||||||
if filename is not None:
|
if filename is not None:
|
||||||
|
@ -22,7 +22,9 @@
|
|||||||
import os
|
import os
|
||||||
import collections
|
import collections
|
||||||
import netrc
|
import netrc
|
||||||
|
import html
|
||||||
|
|
||||||
|
import jinja2
|
||||||
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, QCoreApplication,
|
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, QCoreApplication,
|
||||||
QUrl, QByteArray)
|
QUrl, QByteArray)
|
||||||
from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkReply, QSslError,
|
from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkReply, QSslError,
|
||||||
@ -207,10 +209,11 @@ class NetworkManager(QNetworkAccessManager):
|
|||||||
self.setCache(cache)
|
self.setCache(cache)
|
||||||
cache.setParent(app)
|
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.
|
"""Ask a blocking question in the statusbar.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
title: The title to display to the user.
|
||||||
text: The text to display to the user.
|
text: The text to display to the user.
|
||||||
mode: A PromptMode.
|
mode: A PromptMode.
|
||||||
owner: An object which will abort the question if destroyed, or
|
owner: An object which will abort the question if destroyed, or
|
||||||
@ -219,24 +222,19 @@ class NetworkManager(QNetworkAccessManager):
|
|||||||
Return:
|
Return:
|
||||||
The answer the user gave or None if the prompt was cancelled.
|
The answer the user gave or None if the prompt was cancelled.
|
||||||
"""
|
"""
|
||||||
q = usertypes.Question()
|
abort_on = [self.shutting_down]
|
||||||
q.text = text
|
|
||||||
q.mode = mode
|
|
||||||
self.shutting_down.connect(q.abort)
|
|
||||||
if owner is not None:
|
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
|
# This might be a generic network manager, e.g. one belonging to a
|
||||||
# DownloadManager. In this case, just skip the webview thing.
|
# DownloadManager. In this case, just skip the webview thing.
|
||||||
if self._tab_id is not None:
|
if self._tab_id is not None:
|
||||||
tab = objreg.get('tab', scope='tab', window=self._win_id,
|
tab = objreg.get('tab', scope='tab', window=self._win_id,
|
||||||
tab=self._tab_id)
|
tab=self._tab_id)
|
||||||
tab.load_started.connect(q.abort)
|
abort_on.append(tab.load_started)
|
||||||
bridge = objreg.get('message-bridge', scope='window',
|
|
||||||
window=self._win_id)
|
return message.ask(title=title, text=text, mode=mode,
|
||||||
bridge.ask(q, blocking=True)
|
abort_on=abort_on, default=default)
|
||||||
q.deleteLater()
|
|
||||||
return q.answer
|
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
"""Abort all running requests."""
|
"""Abort all running requests."""
|
||||||
@ -283,9 +281,19 @@ class NetworkManager(QNetworkAccessManager):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if ssl_strict == 'ask':
|
if ssl_strict == 'ask':
|
||||||
err_string = '\n'.join('- ' + err.errorString() for err in errors)
|
err_template = jinja2.Template("""
|
||||||
answer = self._ask('SSL errors - continue?\n{}'.format(err_string),
|
Errors while loading <b>{{url.toDisplayString()}}</b>:<br/>
|
||||||
mode=usertypes.PromptMode.yesno, owner=reply)
|
<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))
|
log.webview.debug("Asked for SSL errors, answer {}".format(answer))
|
||||||
if answer:
|
if answer:
|
||||||
reply.ignoreSslErrors()
|
reply.ignoreSslErrors()
|
||||||
@ -343,8 +351,11 @@ class NetworkManager(QNetworkAccessManager):
|
|||||||
|
|
||||||
if user is None:
|
if user is None:
|
||||||
# netrc check failed
|
# netrc check failed
|
||||||
answer = self._ask("Username ({}):".format(authenticator.realm()),
|
msg = '<b>{}</b> says:<br/>{}'.format(
|
||||||
mode=usertypes.PromptMode.user_pwd,
|
html.escape(reply.url().toDisplayString()),
|
||||||
|
html.escape(authenticator.realm()))
|
||||||
|
answer = self._ask("Authentication required",
|
||||||
|
text=msg, mode=usertypes.PromptMode.user_pwd,
|
||||||
owner=reply)
|
owner=reply)
|
||||||
if answer is not None:
|
if answer is not None:
|
||||||
user, password = answer.user, answer.password
|
user, password = answer.user, answer.password
|
||||||
@ -361,8 +372,11 @@ class NetworkManager(QNetworkAccessManager):
|
|||||||
authenticator.setUser(user)
|
authenticator.setUser(user)
|
||||||
authenticator.setPassword(password)
|
authenticator.setPassword(password)
|
||||||
else:
|
else:
|
||||||
|
msg = '<b>{}</b> says:<br/>{}'.format(
|
||||||
|
html.escape(proxy.hostName()),
|
||||||
|
html.escape(authenticator.realm()))
|
||||||
answer = self._ask(
|
answer = self._ask(
|
||||||
"Proxy username ({}):".format(authenticator.realm()),
|
"Proxy authentication required", msg,
|
||||||
mode=usertypes.PromptMode.user_pwd)
|
mode=usertypes.PromptMode.user_pwd)
|
||||||
if answer is not None:
|
if answer is not None:
|
||||||
authenticator.setUser(answer.user)
|
authenticator.setUser(answer.user)
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
"""The main browser widgets."""
|
"""The main browser widgets."""
|
||||||
|
|
||||||
|
import html
|
||||||
import functools
|
import functools
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtSlot, pyqtSignal, PYQT_VERSION, Qt, QUrl, QPoint
|
from PyQt5.QtCore import pyqtSlot, pyqtSignal, PYQT_VERSION, Qt, QUrl, QPoint
|
||||||
@ -93,13 +94,19 @@ class BrowserPage(QWebPage):
|
|||||||
# of a bug in PyQt.
|
# of a bug in PyQt.
|
||||||
# See http://www.riverbankcomputing.com/pipermail/pyqt/2014-June/034385.html
|
# 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."""
|
"""Override javaScriptPrompt to use the statusbar."""
|
||||||
if (self._is_shutting_down or
|
if (self._is_shutting_down or
|
||||||
config.get('content', 'ignore-javascript-prompt')):
|
config.get('content', 'ignore-javascript-prompt')):
|
||||||
return (False, "")
|
return (False, "")
|
||||||
answer = self._ask("js: {}".format(msg), usertypes.PromptMode.text,
|
msg = '<b>{}</b> asks:<br/>{}'.format(
|
||||||
default)
|
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:
|
if answer is None:
|
||||||
return (False, "")
|
return (False, "")
|
||||||
else:
|
else:
|
||||||
@ -134,11 +141,12 @@ class BrowserPage(QWebPage):
|
|||||||
# QDesktopServices::openUrl with info.url directly - however it
|
# QDesktopServices::openUrl with info.url directly - however it
|
||||||
# works when we construct a copy of it.
|
# works when we construct a copy of it.
|
||||||
url = QUrl(info.url)
|
url = QUrl(info.url)
|
||||||
msg = "Open external application for {}-link?\nURL: {}".format(
|
scheme = url.scheme()
|
||||||
url.scheme(), url.toDisplayString())
|
|
||||||
message.confirm_async(
|
message.confirm_async(
|
||||||
self._win_id, msg,
|
title="Open external application for {}-link?".format(scheme),
|
||||||
functools.partial(QDesktopServices.openUrl, url))
|
text="URL: <b>{}</b>".format(
|
||||||
|
html.escape(url.toDisplayString())),
|
||||||
|
yes_action=functools.partial(QDesktopServices.openUrl, url))
|
||||||
return True
|
return True
|
||||||
elif (info.domain, info.error) in ignored_errors:
|
elif (info.domain, info.error) in ignored_errors:
|
||||||
log.webview.debug("Ignored error on {}: {} (error domain: {}, "
|
log.webview.debug("Ignored error on {}: {} (error domain: {}, "
|
||||||
@ -168,11 +176,11 @@ class BrowserPage(QWebPage):
|
|||||||
log.webview.debug("Error domain: {}, error code: {}".format(
|
log.webview.debug("Error domain: {}, error code: {}".format(
|
||||||
info.domain, info.error))
|
info.domain, info.error))
|
||||||
title = "Error loading page: {}".format(urlstr)
|
title = "Error loading page: {}".format(urlstr)
|
||||||
html = jinja.render(
|
error_html = jinja.render(
|
||||||
'error.html',
|
'error.html',
|
||||||
title=title, url=urlstr, error=error_str, icon='',
|
title=title, url=urlstr, error=error_str, icon='',
|
||||||
qutescheme=False)
|
qutescheme=False)
|
||||||
errpage.content = html.encode('utf-8')
|
errpage.content = error_html.encode('utf-8')
|
||||||
errpage.encoding = 'utf-8'
|
errpage.encoding = 'utf-8'
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -196,29 +204,6 @@ class BrowserPage(QWebPage):
|
|||||||
suggested_file)
|
suggested_file)
|
||||||
return True
|
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):
|
def _show_pdfjs(self, reply):
|
||||||
"""Show the reply with pdfjs."""
|
"""Show the reply with pdfjs."""
|
||||||
try:
|
try:
|
||||||
@ -333,11 +318,6 @@ class BrowserPage(QWebPage):
|
|||||||
}
|
}
|
||||||
config_val = config.get(*options[feature])
|
config_val = config.get(*options[feature])
|
||||||
if config_val == 'ask':
|
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 = {
|
msgs = {
|
||||||
QWebPage.Notifications: 'show notifications',
|
QWebPage.Notifications: 'show notifications',
|
||||||
QWebPage.Geolocation: 'access your location',
|
QWebPage.Geolocation: 'access your location',
|
||||||
@ -345,30 +325,28 @@ class BrowserPage(QWebPage):
|
|||||||
|
|
||||||
host = frame.url().host()
|
host = frame.url().host()
|
||||||
if host:
|
if host:
|
||||||
q.text = "Allow the website at {} to {}?".format(
|
text = "Allow the website at <b>{}</b> to {}?".format(
|
||||||
frame.url().host(), msgs[feature])
|
html.escape(frame.url().toDisplayString()), msgs[feature])
|
||||||
else:
|
else:
|
||||||
q.text = "Allow the website to {}?".format(msgs[feature])
|
text = "Allow the website to {}?".format(msgs[feature])
|
||||||
|
|
||||||
yes_action = functools.partial(
|
yes_action = functools.partial(
|
||||||
self.setFeaturePermission, frame, feature,
|
self.setFeaturePermission, frame, feature,
|
||||||
QWebPage.PermissionGrantedByUser)
|
QWebPage.PermissionGrantedByUser)
|
||||||
q.answered_yes.connect(yes_action)
|
|
||||||
|
|
||||||
no_action = functools.partial(
|
no_action = functools.partial(
|
||||||
self.setFeaturePermission, frame, feature,
|
self.setFeaturePermission, frame, feature,
|
||||||
QWebPage.PermissionDeniedByUser)
|
QWebPage.PermissionDeniedByUser)
|
||||||
q.answered_no.connect(no_action)
|
|
||||||
q.cancelled.connect(no_action)
|
|
||||||
|
|
||||||
self.shutting_down.connect(q.abort)
|
question = message.confirm_async(yes_action=yes_action,
|
||||||
q.completed.connect(q.deleteLater)
|
no_action=no_action,
|
||||||
|
cancel_action=no_action,
|
||||||
self.featurePermissionRequestCanceled.connect(functools.partial(
|
abort_on=[self.shutting_down,
|
||||||
self.on_feature_permission_cancelled, q, frame, feature))
|
self.loadStarted],
|
||||||
self.loadStarted.connect(q.abort)
|
title='Permission request',
|
||||||
|
text=text)
|
||||||
bridge.ask(q, blocking=False)
|
self.featurePermissionRequestCanceled.connect(
|
||||||
|
functools.partial(self.on_feature_permission_cancelled,
|
||||||
|
question, frame, feature))
|
||||||
elif config_val:
|
elif config_val:
|
||||||
self.setFeaturePermission(frame, feature,
|
self.setFeaturePermission(frame, feature,
|
||||||
QWebPage.PermissionGrantedByUser)
|
QWebPage.PermissionGrantedByUser)
|
||||||
@ -469,27 +447,37 @@ class BrowserPage(QWebPage):
|
|||||||
return super().extension(ext, opt, out)
|
return super().extension(ext, opt, out)
|
||||||
return handler(opt, out)
|
return handler(opt, out)
|
||||||
|
|
||||||
def javaScriptAlert(self, frame, msg):
|
def javaScriptAlert(self, frame, js_msg):
|
||||||
"""Override javaScriptAlert to use the statusbar."""
|
"""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'):
|
if config.get('ui', 'modal-js-dialog'):
|
||||||
return super().javaScriptAlert(frame, msg)
|
return super().javaScriptAlert(frame, js_msg)
|
||||||
|
|
||||||
if (self._is_shutting_down or
|
if (self._is_shutting_down or
|
||||||
config.get('content', 'ignore-javascript-alert')):
|
config.get('content', 'ignore-javascript-alert')):
|
||||||
return
|
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."""
|
"""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'):
|
if config.get('ui', 'modal-js-dialog'):
|
||||||
return super().javaScriptConfirm(frame, msg)
|
return super().javaScriptConfirm(frame, js_msg)
|
||||||
|
|
||||||
if self._is_shutting_down:
|
if self._is_shutting_down:
|
||||||
return False
|
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)
|
return bool(ans)
|
||||||
|
|
||||||
def javaScriptConsoleMessage(self, msg, line, source):
|
def javaScriptConsoleMessage(self, msg, line, source):
|
||||||
|
@ -391,6 +391,8 @@ class ConfigManager(QObject):
|
|||||||
('colors', 'statusbar.bg.error'): 'messages.bg.error',
|
('colors', 'statusbar.bg.error'): 'messages.bg.error',
|
||||||
('colors', 'statusbar.fg.warning'): 'messages.fg.warning',
|
('colors', 'statusbar.fg.warning'): 'messages.fg.warning',
|
||||||
('colors', 'statusbar.bg.warning'): 'messages.bg.warning',
|
('colors', 'statusbar.bg.warning'): 'messages.bg.warning',
|
||||||
|
('colors', 'statusbar.fg.prompt'): 'prompts.fg',
|
||||||
|
('colors', 'statusbar.bg.prompt'): 'prompts.bg',
|
||||||
}
|
}
|
||||||
DELETED_OPTIONS = [
|
DELETED_OPTIONS = [
|
||||||
('colors', 'tab.separator'),
|
('colors', 'tab.separator'),
|
||||||
|
@ -384,6 +384,10 @@ def data(readonly=False):
|
|||||||
"Globs are supported, so ';*' will blacklist all keychains"
|
"Globs are supported, so ';*' will blacklist all keychains"
|
||||||
"starting with ';'. Use '*' to disable keyhints"),
|
"starting with ';'. Use '*' to disable keyhints"),
|
||||||
|
|
||||||
|
('prompt-radius',
|
||||||
|
SettingValue(typ.Int(minval=0), '8'),
|
||||||
|
"The rounding radius for the edges of prompts."),
|
||||||
|
|
||||||
readonly=readonly
|
readonly=readonly
|
||||||
)),
|
)),
|
||||||
|
|
||||||
@ -1075,14 +1079,6 @@ def data(readonly=False):
|
|||||||
SettingValue(typ.QssColor(), 'black'),
|
SettingValue(typ.QssColor(), 'black'),
|
||||||
"Background color of the statusbar."),
|
"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',
|
('statusbar.fg.insert',
|
||||||
SettingValue(typ.QssColor(), '${statusbar.fg}'),
|
SettingValue(typ.QssColor(), '${statusbar.fg}'),
|
||||||
"Foreground color of the statusbar in insert mode."),
|
"Foreground color of the statusbar in insert mode."),
|
||||||
@ -1306,6 +1302,18 @@ def data(readonly=False):
|
|||||||
SettingValue(typ.QssColor(), '#333333'),
|
SettingValue(typ.QssColor(), '#333333'),
|
||||||
"Border color of an info message."),
|
"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
|
readonly=readonly
|
||||||
)),
|
)),
|
||||||
|
|
||||||
@ -1407,6 +1415,10 @@ def data(readonly=False):
|
|||||||
SettingValue(typ.Font(), DEFAULT_FONT_SIZE + ' ${_monospace}'),
|
SettingValue(typ.Font(), DEFAULT_FONT_SIZE + ' ${_monospace}'),
|
||||||
"Font used for info messages."),
|
"Font used for info messages."),
|
||||||
|
|
||||||
|
('prompts',
|
||||||
|
SettingValue(typ.Font(), DEFAULT_FONT_SIZE + ' sans-serif'),
|
||||||
|
"Font used for prompts."),
|
||||||
|
|
||||||
readonly=readonly
|
readonly=readonly
|
||||||
)),
|
)),
|
||||||
])
|
])
|
||||||
@ -1672,6 +1684,8 @@ KEY_DATA = collections.OrderedDict([
|
|||||||
('prompt-accept yes', ['y']),
|
('prompt-accept yes', ['y']),
|
||||||
('prompt-accept no', ['n']),
|
('prompt-accept no', ['n']),
|
||||||
('prompt-open-download', ['<Ctrl-X>']),
|
('prompt-open-download', ['<Ctrl-X>']),
|
||||||
|
('prompt-item-focus prev', ['<Shift-Tab>', '<Up>']),
|
||||||
|
('prompt-item-focus next', ['<Tab>', '<Down>']),
|
||||||
])),
|
])),
|
||||||
|
|
||||||
('command,prompt', collections.OrderedDict([
|
('command,prompt', collections.OrderedDict([
|
||||||
|
@ -46,7 +46,7 @@ def get_stylesheet(template_str):
|
|||||||
config=objreg.get('config'))
|
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.
|
"""Set the stylesheet for an object based on it's STYLESHEET attribute.
|
||||||
|
|
||||||
Also, register an update when the config is changed.
|
Also, register an update when the config is changed.
|
||||||
@ -54,23 +54,20 @@ def set_register_stylesheet(obj, *, generator=None):
|
|||||||
Args:
|
Args:
|
||||||
obj: The object to set the stylesheet for and register.
|
obj: The object to set the stylesheet for and register.
|
||||||
Must have a STYLESHEET attribute.
|
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(obj.STYLESHEET)
|
||||||
qss = get_stylesheet(stylesheet)
|
|
||||||
log.config.vdebug("stylesheet for {}: {}".format(
|
log.config.vdebug("stylesheet for {}: {}".format(
|
||||||
obj.__class__.__name__, qss))
|
obj.__class__.__name__, qss))
|
||||||
obj.setStyleSheet(qss)
|
obj.setStyleSheet(qss)
|
||||||
objreg.get('config').changed.connect(
|
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."""
|
"""Update the stylesheet for obj."""
|
||||||
|
get_stylesheet.cache_clear()
|
||||||
if not sip.isdeleted(obj):
|
if not sip.isdeleted(obj):
|
||||||
stylesheet = generator() if generator is not None else obj.STYLESHEET
|
obj.setStyleSheet(get_stylesheet(obj.STYLESHEET))
|
||||||
obj.setStyleSheet(get_stylesheet(stylesheet))
|
|
||||||
|
|
||||||
|
|
||||||
class ColorDict(collections.UserDict):
|
class ColorDict(collections.UserDict):
|
||||||
|
@ -24,13 +24,14 @@ import base64
|
|||||||
import itertools
|
import itertools
|
||||||
import functools
|
import functools
|
||||||
|
|
||||||
|
import jinja2
|
||||||
from PyQt5.QtCore import pyqtSlot, QRect, QPoint, QTimer, Qt
|
from PyQt5.QtCore import pyqtSlot, QRect, QPoint, QTimer, Qt
|
||||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QSizePolicy
|
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QSizePolicy
|
||||||
|
|
||||||
from qutebrowser.commands import runners, cmdutils
|
from qutebrowser.commands import runners, cmdutils
|
||||||
from qutebrowser.config import config
|
from qutebrowser.config import config
|
||||||
from qutebrowser.utils import message, log, usertypes, qtutils, objreg, utils
|
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.mainwindow.statusbar import bar
|
||||||
from qutebrowser.completion import completionwidget, completer
|
from qutebrowser.completion import completionwidget, completer
|
||||||
from qutebrowser.keyinput import modeman
|
from qutebrowser.keyinput import modeman
|
||||||
@ -175,17 +176,24 @@ class MainWindow(QWidget):
|
|||||||
|
|
||||||
self._init_completion()
|
self._init_completion()
|
||||||
|
|
||||||
|
log.init.debug("Initializing modes...")
|
||||||
|
modeman.init(self.win_id, self)
|
||||||
|
|
||||||
self._commandrunner = runners.CommandRunner(self.win_id,
|
self._commandrunner = runners.CommandRunner(self.win_id,
|
||||||
partial_match=True)
|
partial_match=True)
|
||||||
|
|
||||||
self._keyhint = keyhintwidget.KeyHintView(self.win_id, self)
|
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._messageview = messageview.MessageView(parent=self)
|
||||||
self._overlays.append((self._messageview,
|
self._add_overlay(self._messageview, self._messageview.update_geometry)
|
||||||
self._messageview.update_geometry))
|
|
||||||
|
|
||||||
log.init.debug("Initializing modes...")
|
self._prompt_container = prompt.PromptContainer(self.win_id, self)
|
||||||
modeman.init(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:
|
if geometry is not None:
|
||||||
self._load_geometry(geometry)
|
self._load_geometry(geometry)
|
||||||
@ -206,36 +214,40 @@ class MainWindow(QWidget):
|
|||||||
|
|
||||||
objreg.get("app").new_window.emit(self)
|
objreg.get("app").new_window.emit(self)
|
||||||
|
|
||||||
def _update_overlay_geometry(self, widget=None):
|
def _add_overlay(self, widget, signal, *, centered=False, padding=0):
|
||||||
"""Reposition/resize the given overlay.
|
self._overlays.append((widget, signal, centered, padding))
|
||||||
|
|
||||||
If no widget is given, reposition/resize all overlays.
|
def _update_overlay_geometries(self):
|
||||||
"""
|
"""Update the size/position of all overlays."""
|
||||||
if widget is None:
|
for w, _signal, centered, padding in self._overlays:
|
||||||
for w, _signal in self._overlays:
|
self._update_overlay_geometry(w, centered, padding)
|
||||||
self._update_overlay_geometry(w)
|
|
||||||
return
|
|
||||||
|
|
||||||
|
def _update_overlay_geometry(self, widget, centered, padding):
|
||||||
|
"""Reposition/resize the given overlay."""
|
||||||
if not widget.isVisible():
|
if not widget.isVisible():
|
||||||
return
|
return
|
||||||
|
|
||||||
size_hint = widget.sizeHint()
|
size_hint = widget.sizeHint()
|
||||||
if widget.sizePolicy().horizontalPolicy() == QSizePolicy.Expanding:
|
if widget.sizePolicy().horizontalPolicy() == QSizePolicy.Expanding:
|
||||||
width = self.width()
|
width = self.width() - 2 * padding
|
||||||
|
left = padding
|
||||||
else:
|
else:
|
||||||
width = size_hint.width()
|
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')
|
status_position = config.get('ui', 'status-position')
|
||||||
if status_position == 'bottom':
|
if status_position == 'bottom':
|
||||||
top = self.height() - self.status.height() - size_hint.height()
|
top = self.height() - self.status.height() - size_hint.height()
|
||||||
top = qtutils.check_overflow(top, 'int', fatal=False)
|
top = qtutils.check_overflow(top, 'int', fatal=False)
|
||||||
topleft = QPoint(0, top)
|
topleft = QPoint(left, max(height_padding, top))
|
||||||
bottomright = QPoint(width, self.status.geometry().top())
|
bottomright = QPoint(left + width, self.status.geometry().top())
|
||||||
elif status_position == '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 = self.status.height() + size_hint.height()
|
||||||
bottom = qtutils.check_overflow(bottom, 'int', fatal=False)
|
bottom = qtutils.check_overflow(bottom, 'int', fatal=False)
|
||||||
bottomright = QPoint(width, bottom)
|
bottomright = QPoint(left + width,
|
||||||
|
min(self.height() - height_padding, bottom))
|
||||||
else:
|
else:
|
||||||
raise ValueError("Invalid position {}!".format(status_position))
|
raise ValueError("Invalid position {}!".format(status_position))
|
||||||
|
|
||||||
@ -261,8 +273,7 @@ class MainWindow(QWidget):
|
|||||||
completer_obj.on_selection_changed)
|
completer_obj.on_selection_changed)
|
||||||
objreg.register('completion', self._completion, scope='window',
|
objreg.register('completion', self._completion, scope='window',
|
||||||
window=self.win_id)
|
window=self.win_id)
|
||||||
self._overlays.append((self._completion,
|
self._add_overlay(self._completion, self._completion.update_geometry)
|
||||||
self._completion.update_geometry))
|
|
||||||
|
|
||||||
def _init_command_dispatcher(self):
|
def _init_command_dispatcher(self):
|
||||||
dispatcher = commands.CommandDispatcher(self.win_id,
|
dispatcher = commands.CommandDispatcher(self.win_id,
|
||||||
@ -282,12 +293,12 @@ class MainWindow(QWidget):
|
|||||||
if section != 'ui':
|
if section != 'ui':
|
||||||
return
|
return
|
||||||
if option == 'statusbar-padding':
|
if option == 'statusbar-padding':
|
||||||
self._update_overlay_geometry()
|
self._update_overlay_geometries()
|
||||||
elif option == 'downloads-position':
|
elif option == 'downloads-position':
|
||||||
self._add_widgets()
|
self._add_widgets()
|
||||||
elif option == 'status-position':
|
elif option == 'status-position':
|
||||||
self._add_widgets()
|
self._add_widgets()
|
||||||
self._update_overlay_geometry()
|
self._update_overlay_geometries()
|
||||||
|
|
||||||
def _add_widgets(self):
|
def _add_widgets(self):
|
||||||
"""Add or readd all widgets to the VBox."""
|
"""Add or readd all widgets to the VBox."""
|
||||||
@ -350,10 +361,11 @@ class MainWindow(QWidget):
|
|||||||
|
|
||||||
def _connect_overlay_signals(self):
|
def _connect_overlay_signals(self):
|
||||||
"""Connect the resize signal and resize everything once."""
|
"""Connect the resize signal and resize everything once."""
|
||||||
for widget, signal in self._overlays:
|
for widget, signal, centered, padding in self._overlays:
|
||||||
signal.connect(
|
signal.connect(
|
||||||
functools.partial(self._update_overlay_geometry, widget))
|
functools.partial(self._update_overlay_geometry, widget,
|
||||||
self._update_overlay_geometry(widget)
|
centered, padding))
|
||||||
|
self._update_overlay_geometry(widget, centered, padding)
|
||||||
|
|
||||||
def _set_default_geometry(self):
|
def _set_default_geometry(self):
|
||||||
"""Set some sensible default geometry."""
|
"""Set some sensible default geometry."""
|
||||||
@ -374,7 +386,6 @@ class MainWindow(QWidget):
|
|||||||
cmd = self._get_object('status-command')
|
cmd = self._get_object('status-command')
|
||||||
message_bridge = self._get_object('message-bridge')
|
message_bridge = self._get_object('message-bridge')
|
||||||
mode_manager = self._get_object('mode-manager')
|
mode_manager = self._get_object('mode-manager')
|
||||||
prompter = self._get_object('prompter')
|
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
self.tabbed_browser.close_window.connect(self.close)
|
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.entered.connect(status.on_mode_entered)
|
||||||
mode_manager.left.connect(status.on_mode_left)
|
mode_manager.left.connect(status.on_mode_left)
|
||||||
mode_manager.left.connect(cmd.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
|
# commands
|
||||||
keyparsers[usertypes.KeyMode.normal].keystring_updated.connect(
|
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_set_text.connect(status.set_text)
|
||||||
message_bridge.s_maybe_reset_text.connect(status.txt.maybe_reset_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
|
# statusbar
|
||||||
tabs.current_tab_changed.connect(status.prog.on_tab_changed)
|
tabs.current_tab_changed.connect(status.prog.on_tab_changed)
|
||||||
@ -459,7 +467,7 @@ class MainWindow(QWidget):
|
|||||||
e: The QResizeEvent
|
e: The QResizeEvent
|
||||||
"""
|
"""
|
||||||
super().resizeEvent(e)
|
super().resizeEvent(e)
|
||||||
self._update_overlay_geometry()
|
self._update_overlay_geometries()
|
||||||
self._downloadview.updateGeometry()
|
self._downloadview.updateGeometry()
|
||||||
self.tabbed_browser.tabBar().refresh()
|
self.tabbed_browser.tabBar().refresh()
|
||||||
|
|
||||||
@ -507,10 +515,17 @@ class MainWindow(QWidget):
|
|||||||
"download is" if download_count == 1 else "downloads are"))
|
"download is" if download_count == 1 else "downloads are"))
|
||||||
# Process all quit messages that user must confirm
|
# Process all quit messages that user must confirm
|
||||||
if quit_texts or 'always' in confirm_quit:
|
if quit_texts or 'always' in confirm_quit:
|
||||||
text = '\n'.join(['Really quit?'] + quit_texts)
|
msg = jinja2.Template("""
|
||||||
confirmed = message.ask(self.win_id, text,
|
<ul>
|
||||||
usertypes.PromptMode.yesno,
|
{% 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)
|
default=True)
|
||||||
|
|
||||||
# Stop asking if the user cancels
|
# Stop asking if the user cancels
|
||||||
if not confirmed:
|
if not confirmed:
|
||||||
log.destroy.debug("Cancelling closing of window {}".format(
|
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.config import config, style
|
||||||
from qutebrowser.utils import usertypes, log, objreg, utils
|
from qutebrowser.utils import usertypes, log, objreg, utils
|
||||||
from qutebrowser.mainwindow.statusbar import (command, progress, keystring,
|
from qutebrowser.mainwindow.statusbar import (command, progress, keystring,
|
||||||
percentage, url, prompt,
|
percentage, url, tabindex)
|
||||||
tabindex)
|
|
||||||
from qutebrowser.mainwindow.statusbar import text as textwidget
|
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"],
|
||||||
QWidget#StatusBar[prompt_active="true"] QLabel,
|
QWidget#StatusBar[prompt_active="true"] QLabel,
|
||||||
QWidget#StatusBar[prompt_active="true"] QLineEdit {
|
QWidget#StatusBar[prompt_active="true"] QLineEdit {
|
||||||
color: {{ color['statusbar.fg.prompt'] }};
|
color: {{ color['prompts.fg'] }};
|
||||||
background-color: {{ color['statusbar.bg.prompt'] }};
|
background-color: {{ color['prompts.bg'] }};
|
||||||
}
|
}
|
||||||
|
|
||||||
QWidget#StatusBar[insert_active="true"],
|
QWidget#StatusBar[insert_active="true"],
|
||||||
@ -162,16 +161,9 @@ class StatusBar(QWidget):
|
|||||||
self.txt = textwidget.Text()
|
self.txt = textwidget.Text()
|
||||||
self._stack.addWidget(self.txt)
|
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.show_cmd.connect(self._show_cmd_widget)
|
||||||
self.cmd.hide_cmd.connect(self._hide_cmd_widget)
|
self.cmd.hide_cmd.connect(self._hide_cmd_widget)
|
||||||
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.keystring = keystring.KeyString()
|
||||||
self._hbox.addWidget(self.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."""
|
"""Getter for self.prompt_active, so it can be used as Qt property."""
|
||||||
return self._prompt_active
|
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)
|
@pyqtProperty(bool)
|
||||||
def command_active(self):
|
def command_active(self):
|
||||||
"""Getter for self.command_active, so it can be used as Qt property."""
|
"""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:
|
if mode == usertypes.KeyMode.command:
|
||||||
log.statusbar.debug("Setting command_active to {}".format(val))
|
log.statusbar.debug("Setting command_active to {}".format(val))
|
||||||
self._command_active = 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:
|
elif mode == usertypes.KeyMode.caret:
|
||||||
tab = objreg.get('tabbed-browser', scope='window',
|
tab = objreg.get('tabbed-browser', scope='window',
|
||||||
window=self._win_id).currentWidget()
|
window=self._win_id).currentWidget()
|
||||||
@ -285,21 +270,6 @@ class StatusBar(QWidget):
|
|||||||
self._stack.setCurrentWidget(self.txt)
|
self._stack.setCurrentWidget(self.txt)
|
||||||
self.maybe_hide()
|
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)
|
@pyqtSlot(str)
|
||||||
def set_text(self, val):
|
def set_text(self, val):
|
||||||
"""Set a normal (persistent) text in the status bar."""
|
"""Set a normal (persistent) text in the status bar."""
|
||||||
@ -314,7 +284,9 @@ class StatusBar(QWidget):
|
|||||||
self._set_mode_text(mode.name)
|
self._set_mode_text(mode.name)
|
||||||
if mode in [usertypes.KeyMode.insert,
|
if mode in [usertypes.KeyMode.insert,
|
||||||
usertypes.KeyMode.command,
|
usertypes.KeyMode.command,
|
||||||
usertypes.KeyMode.caret]:
|
usertypes.KeyMode.caret,
|
||||||
|
usertypes.KeyMode.prompt,
|
||||||
|
usertypes.KeyMode.yesno]:
|
||||||
self.set_mode_active(mode, True)
|
self.set_mode_active(mode, True)
|
||||||
|
|
||||||
@pyqtSlot(usertypes.KeyMode, usertypes.KeyMode)
|
@pyqtSlot(usertypes.KeyMode, usertypes.KeyMode)
|
||||||
@ -329,7 +301,9 @@ class StatusBar(QWidget):
|
|||||||
self.txt.set_text(self.txt.Text.normal, '')
|
self.txt.set_text(self.txt.Text.normal, '')
|
||||||
if old_mode in [usertypes.KeyMode.insert,
|
if old_mode in [usertypes.KeyMode.insert,
|
||||||
usertypes.KeyMode.command,
|
usertypes.KeyMode.command,
|
||||||
usertypes.KeyMode.caret]:
|
usertypes.KeyMode.caret,
|
||||||
|
usertypes.KeyMode.prompt,
|
||||||
|
usertypes.KeyMode.yesno]:
|
||||||
self.set_mode_active(old_mode, False)
|
self.set_mode_active(old_mode, False)
|
||||||
|
|
||||||
def resizeEvent(self, e):
|
def resizeEvent(self, e):
|
||||||
|
@ -77,7 +77,6 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
|||||||
else:
|
else:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
@pyqtSlot(str)
|
|
||||||
def set_cmd_text(self, text):
|
def set_cmd_text(self, text):
|
||||||
"""Preset the statusbar to some 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.
|
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()
|
update_geometry = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, win_id, parent=None):
|
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 = usertypes.Timer(self, 'keyhint_show')
|
||||||
self._show_timer.setInterval(500)
|
self._show_timer.setInterval(500)
|
||||||
self._show_timer.timeout.connect(self.show)
|
self._show_timer.timeout.connect(self.show)
|
||||||
style.set_register_stylesheet(self,
|
style.set_register_stylesheet(self)
|
||||||
generator=self._generate_stylesheet)
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return utils.get_repr(self, win_id=self._win_id)
|
return utils.get_repr(self, win_id=self._win_id)
|
||||||
@ -67,22 +79,6 @@ class KeyHintView(QLabel):
|
|||||||
self.update_geometry.emit()
|
self.update_geometry.emit()
|
||||||
super().showEvent(e)
|
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)
|
@pyqtSlot(str)
|
||||||
def update_keyhint(self, modename, prefix):
|
def update_keyhint(self, modename, prefix):
|
||||||
"""Show hints for the given prefix (or hide if prefix is empty).
|
"""Show hints for the given prefix (or hide if prefix is empty).
|
||||||
|
@ -94,7 +94,7 @@ LOGGER_NAMES = [
|
|||||||
'commands', 'signals', 'downloads',
|
'commands', 'signals', 'downloads',
|
||||||
'js', 'qt', 'rfc6266', 'ipc', 'shlexer',
|
'js', 'qt', 'rfc6266', 'ipc', 'shlexer',
|
||||||
'save', 'message', 'config', 'sessions',
|
'save', 'message', 'config', 'sessions',
|
||||||
'webelem'
|
'webelem', 'prompt'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -139,6 +139,7 @@ message = logging.getLogger('message')
|
|||||||
config = logging.getLogger('config')
|
config = logging.getLogger('config')
|
||||||
sessions = logging.getLogger('sessions')
|
sessions = logging.getLogger('sessions')
|
||||||
webelem = logging.getLogger('webelem')
|
webelem = logging.getLogger('webelem')
|
||||||
|
prompt = logging.getLogger('prompt')
|
||||||
|
|
||||||
|
|
||||||
ram_handler = None
|
ram_handler = None
|
||||||
@ -408,6 +409,8 @@ def qt_message_handler(msg_type, context, msg):
|
|||||||
"QXcbClipboard: SelectionRequest too old",
|
"QXcbClipboard: SelectionRequest too old",
|
||||||
# https://github.com/The-Compiler/qutebrowser/issues/2071
|
# https://github.com/The-Compiler/qutebrowser/issues/2071
|
||||||
'QXcbWindow: Unhandled client message: ""',
|
'QXcbWindow: Unhandled client message: ""',
|
||||||
|
# No idea where this comes from...
|
||||||
|
"QObject::disconnect: Unexpected null parameter",
|
||||||
]
|
]
|
||||||
if sys.platform == 'darwin':
|
if sys.platform == 'darwin':
|
||||||
suppressed_msgs += [
|
suppressed_msgs += [
|
||||||
|
@ -26,7 +26,7 @@ import traceback
|
|||||||
|
|
||||||
from PyQt5.QtCore import pyqtSignal, QObject
|
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):
|
def _log_stack(typ, stack):
|
||||||
@ -76,70 +76,83 @@ def info(message):
|
|||||||
global_bridge.show_message.emit(usertypes.MessageLevel.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).
|
"""Ask a modular question in the statusbar (blocking).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
win_id: The ID of the window which is calling this function.
|
|
||||||
message: The message to display to the user.
|
message: The message to display to the user.
|
||||||
mode: A PromptMode.
|
mode: A PromptMode.
|
||||||
default: The default value to display.
|
default: The default value to display.
|
||||||
|
text: Additional text to show
|
||||||
|
abort_on: A list of signals which abort the question if emitted.
|
||||||
|
|
||||||
Return:
|
Return:
|
||||||
The answer the user gave or None if the prompt was cancelled.
|
The answer the user gave or None if the prompt was cancelled.
|
||||||
"""
|
"""
|
||||||
q = usertypes.Question()
|
question = _build_question(*args, **kwargs) # pylint: disable=missing-kwoa
|
||||||
q.text = message
|
global_bridge.ask(question, blocking=True)
|
||||||
q.mode = mode
|
answer = question.answer
|
||||||
q.default = default
|
question.deleteLater()
|
||||||
bridge = objreg.get('message-bridge', scope='window', window=win_id)
|
return answer
|
||||||
bridge.ask(q, blocking=True)
|
|
||||||
q.deleteLater()
|
|
||||||
return q.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.
|
"""Ask an async question in the statusbar.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
win_id: The ID of the window which is calling this function.
|
|
||||||
message: The message to display to the user.
|
message: The message to display to the user.
|
||||||
mode: A PromptMode.
|
mode: A PromptMode.
|
||||||
handler: The function to get called with the answer as argument.
|
handler: The function to get called with the answer as argument.
|
||||||
default: The default value to display.
|
default: The default value to display.
|
||||||
|
text: Additional text to show.
|
||||||
"""
|
"""
|
||||||
if not isinstance(mode, usertypes.PromptMode):
|
question = _build_question(title, mode=mode, **kwargs)
|
||||||
raise TypeError("Mode {} is no PromptMode member!".format(mode))
|
question.answered.connect(handler)
|
||||||
q = usertypes.Question()
|
question.completed.connect(question.deleteLater)
|
||||||
q.text = message
|
global_bridge.ask(question, blocking=False)
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
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.
|
"""Ask a yes/no question to the user and execute the given actions.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
win_id: The ID of the window which is calling this function.
|
|
||||||
message: The message to display to the user.
|
message: The message to display to the user.
|
||||||
yes_action: Callable to be called when the user answered yes.
|
yes_action: Callable to be called when the user answered yes.
|
||||||
no_action: Callable to be called when the user answered no.
|
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.
|
default: True/False to set a default value, or None.
|
||||||
|
text: Additional text to show.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
The question object.
|
||||||
"""
|
"""
|
||||||
q = usertypes.Question()
|
kwargs['mode'] = usertypes.PromptMode.yesno
|
||||||
q.text = message
|
question = _build_question(*args, **kwargs) # pylint: disable=missing-kwoa
|
||||||
q.mode = usertypes.PromptMode.yesno
|
question.answered_yes.connect(yes_action)
|
||||||
q.default = default
|
|
||||||
q.answered_yes.connect(yes_action)
|
|
||||||
if no_action is not None:
|
if no_action is not None:
|
||||||
q.answered_no.connect(no_action)
|
question.answered_no.connect(no_action)
|
||||||
q.completed.connect(q.deleteLater)
|
if cancel_action is not None:
|
||||||
bridge = objreg.get('message-bridge', scope='window', window=win_id)
|
question.cancelled.connect(cancel_action)
|
||||||
bridge.ask(q, blocking=False)
|
|
||||||
|
question.completed.connect(question.deleteLater)
|
||||||
|
global_bridge.ask(question, blocking=False)
|
||||||
|
return question
|
||||||
|
|
||||||
|
|
||||||
class GlobalMessageBridge(QObject):
|
class GlobalMessageBridge(QObject):
|
||||||
@ -150,9 +163,34 @@ class GlobalMessageBridge(QObject):
|
|||||||
show_message: Show a message
|
show_message: Show a message
|
||||||
arg 0: A MessageLevel member
|
arg 0: A MessageLevel member
|
||||||
arg 1: The text to show
|
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)
|
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):
|
class MessageBridge(QObject):
|
||||||
@ -164,36 +202,14 @@ class MessageBridge(QObject):
|
|||||||
arg: The text to set.
|
arg: The text to set.
|
||||||
s_maybe_reset_text: Reset the text if it hasn't been changed yet.
|
s_maybe_reset_text: Reset the text if it hasn't been changed yet.
|
||||||
arg: The expected text.
|
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_set_text = pyqtSignal(str)
|
||||||
s_maybe_reset_text = pyqtSignal(str)
|
s_maybe_reset_text = pyqtSignal(str)
|
||||||
s_set_cmd_text = pyqtSignal(str)
|
|
||||||
s_question = pyqtSignal(usertypes.Question, bool)
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return utils.get_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):
|
def set_text(self, text, *, log_stack=False):
|
||||||
"""Set the normal text of the statusbar.
|
"""Set the normal text of the statusbar.
|
||||||
|
|
||||||
@ -214,19 +230,5 @@ class MessageBridge(QObject):
|
|||||||
"""
|
"""
|
||||||
self.s_maybe_reset_text.emit(str(text))
|
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()
|
global_bridge = GlobalMessageBridge()
|
||||||
|
@ -335,10 +335,11 @@ class Question(QObject):
|
|||||||
For yesno, None (no default), True or False.
|
For yesno, None (no default), True or False.
|
||||||
For text, a default text as string.
|
For text, a default text as string.
|
||||||
For user_pwd, a default username 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.
|
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).
|
answer: The value the user entered (as password for user_pwd).
|
||||||
is_aborted: Whether the question was aborted.
|
is_aborted: Whether the question was aborted.
|
||||||
|
interrupted: Whether the question was interrupted by another one.
|
||||||
|
|
||||||
Signals:
|
Signals:
|
||||||
answered: Emitted when the question has been answered by the user.
|
answered: Emitted when the question has been answered by the user.
|
||||||
@ -364,14 +365,15 @@ class Question(QObject):
|
|||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._mode = None
|
self._mode = None
|
||||||
self.default = None
|
self.default = None
|
||||||
|
self.title = None
|
||||||
self.text = None
|
self.text = None
|
||||||
self.user = None
|
|
||||||
self.answer = None
|
self.answer = None
|
||||||
self.is_aborted = False
|
self.is_aborted = False
|
||||||
|
self.interrupted = False
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return utils.get_repr(self, text=self.text, mode=self._mode,
|
return utils.get_repr(self, title=self.title, text=self.text,
|
||||||
default=self.default)
|
mode=self._mode, default=self.default)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mode(self):
|
def mode(self):
|
||||||
@ -405,6 +407,9 @@ class Question(QObject):
|
|||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def abort(self):
|
def abort(self):
|
||||||
"""Abort the question."""
|
"""Abort the question."""
|
||||||
|
if self.is_aborted:
|
||||||
|
log.misc.debug("Question was already aborted")
|
||||||
|
return
|
||||||
self.is_aborted = True
|
self.is_aborted = True
|
||||||
try:
|
try:
|
||||||
self.aborted.emit()
|
self.aborted.emit()
|
||||||
|
@ -115,8 +115,6 @@ PERFECT_FILES = [
|
|||||||
'qutebrowser/mainwindow/statusbar/tabindex.py'),
|
'qutebrowser/mainwindow/statusbar/tabindex.py'),
|
||||||
('tests/unit/mainwindow/statusbar/test_textbase.py',
|
('tests/unit/mainwindow/statusbar/test_textbase.py',
|
||||||
'qutebrowser/mainwindow/statusbar/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',
|
('tests/unit/mainwindow/statusbar/test_url.py',
|
||||||
'qutebrowser/mainwindow/statusbar/url.py'),
|
'qutebrowser/mainwindow/statusbar/url.py'),
|
||||||
('tests/unit/mainwindow/test_messageview.py',
|
('tests/unit/mainwindow/test_messageview.py',
|
||||||
|
@ -3,13 +3,14 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
function prompter() {
|
function prompter(defaultval) {
|
||||||
var reply = prompt("js prompt", "")
|
var reply = prompt("js prompt", defaultval);
|
||||||
console.log("Prompt reply: " + reply)
|
console.log("Prompt reply: " + reply);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -188,19 +188,21 @@ def open_path(quteproc, path):
|
|||||||
do_not_wait_suffix = ' without waiting'
|
do_not_wait_suffix = ' without waiting'
|
||||||
as_url_suffix = ' as a URL'
|
as_url_suffix = ' as a URL'
|
||||||
|
|
||||||
if path.endswith(new_tab_suffix):
|
while True:
|
||||||
path = path[:-len(new_tab_suffix)]
|
if path.endswith(new_tab_suffix):
|
||||||
new_tab = True
|
path = path[:-len(new_tab_suffix)]
|
||||||
elif path.endswith(new_window_suffix):
|
new_tab = True
|
||||||
path = path[:-len(new_window_suffix)]
|
elif path.endswith(new_window_suffix):
|
||||||
new_window = True
|
path = path[:-len(new_window_suffix)]
|
||||||
elif path.endswith(as_url_suffix):
|
new_window = True
|
||||||
path = path[:-len(as_url_suffix)]
|
elif path.endswith(as_url_suffix):
|
||||||
as_url = True
|
path = path[:-len(as_url_suffix)]
|
||||||
|
as_url = True
|
||||||
if path.endswith(do_not_wait_suffix):
|
elif path.endswith(do_not_wait_suffix):
|
||||||
path = path[:-len(do_not_wait_suffix)]
|
path = path[:-len(do_not_wait_suffix)]
|
||||||
wait = False
|
wait = False
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
quteproc.open_path(path, new_tab=new_tab, new_window=new_window,
|
quteproc.open_path(path, new_tab=new_tab, new_window=new_window,
|
||||||
as_url=as_url, wait=wait)
|
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 set storage -> prompt-download-directory to true
|
||||||
And I open data/downloads/issue1243.html
|
And I open data/downloads/issue1243.html
|
||||||
And I hint with args "links download" and follow a
|
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
|
Then the error "Download error: No handler found for qute://!" should be shown
|
||||||
|
|
||||||
Scenario: Downloading a data: link (issue 1214)
|
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 set storage -> prompt-download-directory to true
|
||||||
And I open data/downloads/issue1214.html
|
And I open data/downloads/issue1214.html
|
||||||
And I hint with args "links download" and follow a
|
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
|
And I run :leave-mode
|
||||||
Then no crash should happen
|
Then no crash should happen
|
||||||
|
|
||||||
@ -338,7 +338,7 @@ Feature: Downloading things from a website.
|
|||||||
When I set storage -> prompt-download-directory to true
|
When I set storage -> prompt-download-directory to true
|
||||||
And I open data/downloads/issue1725.html
|
And I open data/downloads/issue1725.html
|
||||||
And I run :click-element id long-link
|
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 directly open the download
|
||||||
And I wait until the download is finished
|
And I wait until the download is finished
|
||||||
Then "Opening * with [*python*]" should be logged
|
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 run :click-element id download
|
||||||
And I wait until the download is finished
|
And I wait until the download is finished
|
||||||
Then the downloaded file test.pdf should exist
|
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 open data/misc/test.pdf
|
||||||
And I wait for "[qute://pdfjs/*] PDF * (PDF.js: *)" in the log
|
And I wait for "[qute://pdfjs/*] PDF * (PDF.js: *)" in the log
|
||||||
And I run :jseval document.getElementById("download").click()
|
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
|
And I run :leave-mode
|
||||||
Then no crash should happen
|
Then no crash should happen
|
||||||
|
|
||||||
|
@ -49,6 +49,14 @@ Feature: Prompts
|
|||||||
And I run :prompt-accept
|
And I run :prompt-accept
|
||||||
Then the javascript message "Prompt reply: prompt test" should be logged
|
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
|
@pyqt>=5.3.1
|
||||||
Scenario: Rejected javascript prompt
|
Scenario: Rejected javascript prompt
|
||||||
When I open data/prompt/jsprompt.html
|
When I open data/prompt/jsprompt.html
|
||||||
@ -58,6 +66,68 @@ Feature: Prompts
|
|||||||
And I run :leave-mode
|
And I run :leave-mode
|
||||||
Then the javascript message "Prompt reply: null" should be logged
|
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)
|
# Shift-Insert with prompt (issue 1299)
|
||||||
|
|
||||||
@ -72,6 +142,17 @@ Feature: Prompts
|
|||||||
And I run :prompt-accept
|
And I run :prompt-accept
|
||||||
Then the javascript message "Prompt reply: insert test" should be logged
|
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
|
@pyqt>=5.3.1
|
||||||
Scenario: Using content -> ignore-javascript-prompt
|
Scenario: Using content -> ignore-javascript-prompt
|
||||||
When I set content -> ignore-javascript-prompt to true
|
When I set content -> ignore-javascript-prompt to true
|
||||||
@ -219,6 +300,44 @@ Feature: Prompts
|
|||||||
"user": "user"
|
"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
|
# :prompt-accept with value argument
|
||||||
|
|
||||||
Scenario: Javascript alert with value
|
Scenario: Javascript alert with value
|
||||||
@ -249,3 +368,102 @@ Feature: Prompts
|
|||||||
And I run :prompt-accept yes
|
And I run :prompt-accept yes
|
||||||
Then the javascript message "confirm reply: true" should be logged
|
Then the javascript message "confirm reply: true" should be logged
|
||||||
And the error "Invalid value nope - expected yes/no!" should be shown
|
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 "
|
PROMPT_MSG = ("Asking question <qutebrowser.utils.usertypes.Question "
|
||||||
"default={!r} mode=<PromptMode.download: 5> "
|
"default={!r} mode=<PromptMode.download: 5> text=* "
|
||||||
"text='Save file to:'>, *")
|
"title='Save file to:'>, *")
|
||||||
|
|
||||||
|
|
||||||
@bdd.given("I set up a temporary download dir")
|
@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")
|
@bdd.when("I wait for a prompt")
|
||||||
def wait_for_prompt(quteproc):
|
def wait_for_prompt(quteproc):
|
||||||
quteproc.wait_for(message='Entering mode KeyMode.* (reason: question '
|
quteproc.wait_for(message='Asking question *')
|
||||||
'asked)')
|
|
||||||
|
|
||||||
|
|
||||||
@bdd.then("no prompt should be shown")
|
@bdd.then("no prompt should be shown")
|
||||||
|
@ -67,12 +67,12 @@ class Request(testprocess.Line):
|
|||||||
'/favicon.ico': [http.client.NOT_FOUND],
|
'/favicon.ico': [http.client.NOT_FOUND],
|
||||||
'/does-not-exist': [http.client.NOT_FOUND],
|
'/does-not-exist': [http.client.NOT_FOUND],
|
||||||
'/does-not-exist-2': [http.client.NOT_FOUND],
|
'/does-not-exist-2': [http.client.NOT_FOUND],
|
||||||
'/basic-auth/user/password':
|
'/status/404': [http.client.NOT_FOUND],
|
||||||
[http.client.UNAUTHORIZED, http.client.OK],
|
|
||||||
'/custom/redirect-later': [http.client.FOUND],
|
'/custom/redirect-later': [http.client.FOUND],
|
||||||
'/custom/redirect-self': [http.client.FOUND],
|
'/custom/redirect-self': [http.client.FOUND],
|
||||||
'/redirect-to': [http.client.FOUND],
|
'/redirect-to': [http.client.FOUND],
|
||||||
'/status/404': [http.client.NOT_FOUND],
|
|
||||||
'/cookies/set': [http.client.FOUND],
|
'/cookies/set': [http.client.FOUND],
|
||||||
}
|
}
|
||||||
for i in range(15):
|
for i in range(15):
|
||||||
@ -81,6 +81,10 @@ class Request(testprocess.Line):
|
|||||||
http.client.FOUND]
|
http.client.FOUND]
|
||||||
path_to_statuses['/absolute-redirect/{}'.format(i)] = [
|
path_to_statuses['/absolute-redirect/{}'.format(i)] = [
|
||||||
http.client.FOUND]
|
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]
|
default_statuses = [http.client.OK, http.client.NOT_MODIFIED]
|
||||||
|
|
||||||
sanitized = QUrl('http://localhost' + self.path).path() # Remove ?foo
|
sanitized = QUrl('http://localhost' + self.path).path() # Remove ?foo
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
function prompter() {
|
function prompter() {
|
||||||
var reply = prompt("js prompt", "")
|
var reply = prompt("js prompt", "");
|
||||||
alert("JS alert: " + reply + "!")
|
alert("JS alert: " + reply + "!");
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
@ -59,24 +59,6 @@ class Obj(QObject):
|
|||||||
self.rendered_stylesheet = stylesheet
|
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])
|
@pytest.mark.parametrize('delete', [True, False])
|
||||||
def test_set_register_stylesheet(delete, qtbot, config_stub, caplog):
|
def test_set_register_stylesheet(delete, qtbot, config_stub, caplog):
|
||||||
config_stub.data = {'fonts': {'foo': 'bar'}, 'colors': {}}
|
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
|
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:
|
class TestColorDict:
|
||||||
|
|
||||||
@pytest.mark.parametrize('key, expected', [
|
@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': {
|
'colors': {
|
||||||
'keyhint.fg': 'white',
|
'keyhint.fg': 'white',
|
||||||
'keyhint.fg.suffix': 'yellow',
|
'keyhint.fg.suffix': 'yellow',
|
||||||
'keyhint.bg': 'black'
|
'keyhint.bg': 'black',
|
||||||
},
|
},
|
||||||
'fonts': {'keyhint': 'Comic Sans'},
|
'fonts': {'keyhint': 'Comic Sans'},
|
||||||
'ui': {'keyhint-blacklist': '', 'status-position': 'bottom'},
|
'ui': {
|
||||||
|
'keyhint-blacklist': '',
|
||||||
|
'status-position': 'bottom',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
keyhint = KeyHintView(0, None)
|
keyhint = KeyHintView(0, None)
|
||||||
qtbot.add_widget(keyhint)
|
qtbot.add_widget(keyhint)
|
||||||
|
Loading…
Reference in New Issue
Block a user