Merge branch 'multiwin'

Conflicts:
	qutebrowser/app.py
	qutebrowser/browser/commands.py
	qutebrowser/browser/hints.py
	qutebrowser/keyinput/modeman.py
	qutebrowser/network/networkmanager.py
	qutebrowser/widgets/mainwindow.py
	qutebrowser/widgets/statusbar/command.py
	qutebrowser/widgets/statusbar/prompt.py
	qutebrowser/widgets/statusbar/prompter.py
	qutebrowser/widgets/tabbedbrowser.py
This commit is contained in:
Florian Bruhin 2014-10-06 22:03:58 +02:00
commit 105c25bc5f
43 changed files with 1171 additions and 710 deletions

View File

@ -301,17 +301,17 @@ There are currently these object registries, also called 'scopes':
* The `global` scope, with objects which are used globally (`config`, * The `global` scope, with objects which are used globally (`config`,
`cookie-jar`, etc.) `cookie-jar`, etc.)
* The `tab` scope with objects which are per-tab (`hintmanager`, `webview`, * The `tab` scope with objects which are per-tab (`hintmanager`, `webview`,
etc.). Passing this scope to `objreg.get()` always selects the object in the etc.). Passing this scope to `objreg.get()` selects the object in the currently
currently focused tab. focused tab by default. A tab can be explicitely selected by passing
* The `meta` scope which is an object registry of all other object registries, +tab=_tab-id_, window=_win-id_+ to it.
mainly intended for debugging.
A new object can be registered by using A new object can be registered by using
+objreg.register(_name_, _object_[, scope=_scope_])+. An object should not be +objreg.register(_name_, _object_[, scope=_scope_, window=_win-id_,
registered twice. To update it, `update=True` has to be given. tab=_tab-id_])+. An object should not be registered twice. To update it,
`update=True` has to be given.
An object can be retrieved by using +objreg.get(_name_[, scope=_scope_])+. The An object can be retrieved by using +objreg.get(_name_[, scope=_scope_,
default scope is `global`. window=_win-id_, tab=_tab-id_])+. The default scope is `global`.
All objects can be printed by starting with the `--debug` flag and using the All objects can be printed by starting with the `--debug` flag and using the
`:debug-all-objects` command. `:debug-all-objects` command.

View File

@ -8,6 +8,7 @@
|<<back,back>>|Go back in the history of the current tab. |<<back,back>>|Go back in the history of the current tab.
|<<bind,bind>>|Bind a key to a command. |<<bind,bind>>|Bind a key to a command.
|<<cancel-download,cancel-download>>|Cancel the first/[count]th download. |<<cancel-download,cancel-download>>|Cancel the first/[count]th download.
|<<close,close>>|Close the current window.
|<<download-page,download-page>>|Download the current page. |<<download-page,download-page>>|Download the current page.
|<<forward,forward>>|Go forward in the history of the current tab. |<<forward,forward>>|Go forward in the history of the current tab.
|<<help,help>>|Show help about a command or setting. |<<help,help>>|Show help about a command or setting.
@ -49,13 +50,14 @@
|============== |==============
[[back]] [[back]]
=== back === back
Syntax: +:back [*--tab*] [*--bg*]+ Syntax: +:back [*--tab*] [*--bg*] [*--window*]+
Go back in the history of the current tab. Go back in the history of the current tab.
==== optional arguments ==== optional arguments
* +*-t*+, +*--tab*+: Go back in a new tab. * +*-t*+, +*--tab*+: Go back in a new tab.
* +*-b*+, +*--bg*+: Go back in a background tab. * +*-b*+, +*--bg*+: Go back in a background tab.
* +*-w*+, +*--window*+: Go back in a new window.
==== count ==== count
How many pages to go back. How many pages to go back.
@ -81,26 +83,31 @@ Cancel the first/[count]th download.
==== count ==== count
The index of the download to cancel. The index of the download to cancel.
[[close]]
=== close
Close the current window.
[[download-page]] [[download-page]]
=== download-page === download-page
Download the current page. Download the current page.
[[forward]] [[forward]]
=== forward === forward
Syntax: +:forward [*--tab*] [*--bg*]+ Syntax: +:forward [*--tab*] [*--bg*] [*--window*]+
Go forward in the history of the current tab. Go forward in the history of the current tab.
==== optional arguments ==== optional arguments
* +*-t*+, +*--tab*+: Go forward in a new tab. * +*-t*+, +*--tab*+: Go forward in a new tab.
* +*-b*+, +*--bg*+: Go back in a background tab. * +*-b*+, +*--bg*+: Go forward in a background tab.
* +*-w*+, +*--window*+: Go forward in a new window.
==== count ==== count
How many pages to go forward. How many pages to go forward.
[[help]] [[help]]
=== help === help
Syntax: +:help ['topic']+ Syntax: +:help [*--tab*] [*--bg*] [*--window*] ['topic']+
Show help about a command or setting. Show help about a command or setting.
@ -111,6 +118,11 @@ Show help about a command or setting.
- __section__\->__option__ for settings. - __section__\->__option__ for settings.
==== optional arguments
* +*-t*+, +*--tab*+: Open in a new tab.
* +*-b*+, +*--bg*+: Open in a background tab.
* +*-w*+, +*--window*+: Open in a new window.
[[hint]] [[hint]]
=== hint === hint
Syntax: +:hint ['group'] ['target'] ['args' ['args' ...]]+ Syntax: +:hint ['group'] ['target'] ['args' ['args' ...]]+
@ -131,11 +143,14 @@ Start hinting.
- `normal`: Open the link in the current tab. - `normal`: Open the link in the current tab.
- `tab`: Open the link in a new tab. - `tab`: Open the link in a new tab.
- `tab-bg`: Open the link in a new background tab. - `tab-bg`: Open the link in a new background tab.
- `window`: Open the link in a new window.
- `yank`: Yank the link to the clipboard. - `yank`: Yank the link to the clipboard.
- `yank-primary`: Yank the link to the primary selection. - `yank-primary`: Yank the link to the primary selection.
- `fill`: Fill the commandline with the command given as - `fill`: Fill the commandline with the command given as
argument. argument.
- `rapid`: Open the link in a new tab and stay in hinting mode. - `rapid`: Open the link in a new tab and stay in hinting mode.
- `rapid-win`: Open the link in a new window and stay in
hinting mode.
- `download`: Download the link. - `download`: Download the link.
- `userscript`: Call an userscript with `$QUTE_URL` set to the - `userscript`: Call an userscript with `$QUTE_URL` set to the
link. link.
@ -174,7 +189,7 @@ Execute a command after some time.
[[navigate]] [[navigate]]
=== navigate === navigate
Syntax: +:navigate [*--tab*] 'where'+ Syntax: +:navigate [*--tab*] [*--bg*] [*--window*] 'where'+
Open typical prev/next links or navigate using the URL path. Open typical prev/next links or navigate using the URL path.
@ -194,10 +209,12 @@ This tries to automatically click on typical _Previous Page_ or _Next Page_ link
==== optional arguments ==== optional arguments
* +*-t*+, +*--tab*+: Open in a new tab. * +*-t*+, +*--tab*+: Open in a new tab.
* +*-b*+, +*--bg*+: Open in a background tab.
* +*-w*+, +*--window*+: Open in a new window.
[[open]] [[open]]
=== open === open
Syntax: +:open [*--bg*] [*--tab*] 'url'+ Syntax: +:open [*--bg*] [*--tab*] [*--window*] 'url'+
Open a URL in the current/[count]th tab. Open a URL in the current/[count]th tab.
@ -207,13 +224,14 @@ Open a URL in the current/[count]th tab.
==== optional arguments ==== optional arguments
* +*-b*+, +*--bg*+: Open in a new background tab. * +*-b*+, +*--bg*+: Open in a new background tab.
* +*-t*+, +*--tab*+: Open in a new tab. * +*-t*+, +*--tab*+: Open in a new tab.
* +*-w*+, +*--window*+: Open in a new window.
==== count ==== count
The tab index to open the URL in. The tab index to open the URL in.
[[paste]] [[paste]]
=== paste === paste
Syntax: +:paste [*--sel*] [*--tab*] [*--bg*]+ Syntax: +:paste [*--sel*] [*--tab*] [*--bg*] [*--window*]+
Open a page from the clipboard. Open a page from the clipboard.
@ -221,6 +239,7 @@ Open a page from the clipboard.
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard. * +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard.
* +*-t*+, +*--tab*+: Open in a new tab. * +*-t*+, +*--tab*+: Open in a new tab.
* +*-b*+, +*--bg*+: Open in a background tab. * +*-b*+, +*--bg*+: Open in a background tab.
* +*-w*+, +*--window*+: Open in new window.
[[print]] [[print]]
=== print === print
@ -246,7 +265,7 @@ Add a new quickmark.
[[quickmark-load]] [[quickmark-load]]
=== quickmark-load === quickmark-load
Syntax: +:quickmark-load [*--tab*] [*--bg*] 'name'+ Syntax: +:quickmark-load [*--tab*] [*--bg*] [*--window*] 'name'+
Load a quickmark. Load a quickmark.
@ -256,6 +275,7 @@ Load a quickmark.
==== optional arguments ==== optional arguments
* +*-t*+, +*--tab*+: Load the quickmark in a new tab. * +*-t*+, +*--tab*+: Load the quickmark in a new tab.
* +*-b*+, +*--bg*+: Load the quickmark in a new background tab. * +*-b*+, +*--bg*+: Load the quickmark in a new background tab.
* +*-w*+, +*--window*+: Load the quickmark in a new window.
[[quickmark-save]] [[quickmark-save]]
=== quickmark-save === quickmark-save
@ -339,12 +359,13 @@ The tab index to stop.
[[tab-clone]] [[tab-clone]]
=== tab-clone === tab-clone
Syntax: +:tab-clone [*--bg*]+ Syntax: +:tab-clone [*--bg*] [*--window*]+
Duplicate the current tab. Duplicate the current tab.
==== optional arguments ==== optional arguments
* +*-b*+, +*--bg*+: Open in a background tab. * +*-b*+, +*--bg*+: Open in a background tab.
* +*-w*+, +*--window*+: Open in a new window.
[[tab-close]] [[tab-close]]
=== tab-close === tab-close

View File

@ -28,7 +28,7 @@ It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl.
Commands to execute on startup. Commands to execute on startup.
*'URL'*:: *'URL'*::
URLs to open on startup. URLs to open on startup (empty as a window separator).
=== optional arguments === optional arguments
*-h*, *--help*:: *-h*, *--help*::

View File

@ -36,15 +36,16 @@ from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QTimer, Qt, QUrl,
QStandardPaths, QObject) QStandardPaths, QObject)
import qutebrowser import qutebrowser
from qutebrowser.commands import userscripts, runners, cmdutils from qutebrowser.commands import cmdutils, runners
from qutebrowser.config import style, config, websettings from qutebrowser.config import style, config, websettings
from qutebrowser.network import qutescheme, proxy from qutebrowser.network import qutescheme, proxy
from qutebrowser.browser import quickmarks, cookies, downloads, cache, hints from qutebrowser.browser import quickmarks, cookies, downloads, cache
from qutebrowser.widgets import mainwindow, console, crash from qutebrowser.widgets import mainwindow, console, crash
from qutebrowser.keyinput import modeman from qutebrowser.keyinput import modeman
from qutebrowser.utils import (log, version, message, utilcmds, readline, from qutebrowser.utils import (log, version, message, readline, utils, qtutils,
utils, qtutils, urlutils, debug, objreg, urlutils, debug, objreg, usertypes)
usertypes) # We import utilcmds to run the cmdutils.register decorators.
from qutebrowser.utils import utilcmds # pylint: disable=unused-import
class Application(QApplication): class Application(QApplication):
@ -53,11 +54,11 @@ class Application(QApplication):
Attributes: Attributes:
_args: ArgumentParser instance. _args: ArgumentParser instance.
_commandrunner: The main CommandRunner instance.
_shutting_down: True if we're currently shutting down. _shutting_down: True if we're currently shutting down.
_quit_status: The current quitting status. _quit_status: The current quitting status.
_crashdlg: The crash dialog currently open. _crashdlg: The crash dialog currently open.
_crashlogfile: A file handler to the fatal crash logfile. _crashlogfile: A file handler to the fatal crash logfile.
_event_filter: The EventFilter for the application.
""" """
def __init__(self, args): def __init__(self, args):
@ -74,7 +75,6 @@ class Application(QApplication):
self._shutting_down = False self._shutting_down = False
self._crashdlg = None self._crashdlg = None
self._crashlogfile = None self._crashlogfile = None
self._commandrunner = None
if args.debug: if args.debug:
# We don't enable this earlier because some imports trigger # We don't enable this earlier because some imports trigger
@ -110,16 +110,11 @@ class Application(QApplication):
self._init_modules() self._init_modules()
log.init.debug("Initializing eventfilter...") log.init.debug("Initializing eventfilter...")
mode_manager = objreg.get('mode-manager') self._event_filter = modeman.EventFilter(self)
self.installEventFilter(mode_manager) self.installEventFilter(self._event_filter)
log.init.debug("Connecting signals...") log.init.debug("Connecting signals...")
self._connect_signals() self._connect_signals()
modeman.enter(usertypes.KeyMode.normal, 'init')
log.init.debug("Showing mainwindow...")
if not args.nowindow:
objreg.get('main-window').show()
log.init.debug("Applying python hacks...") log.init.debug("Applying python hacks...")
self._python_hacks() self._python_hacks()
@ -134,10 +129,6 @@ class Application(QApplication):
def _init_modules(self): def _init_modules(self):
"""Initialize all 'modules' which need to be initialized.""" """Initialize all 'modules' which need to be initialized."""
log.init.debug("Initializing message-bridge...")
message_bridge = message.MessageBridge(self)
objreg.register('message-bridge', message_bridge)
log.init.debug("Initializing readline-bridge...") log.init.debug("Initializing readline-bridge...")
readline_bridge = readline.ReadlineBridge() readline_bridge = readline.ReadlineBridge()
objreg.register('readline-bridge', readline_bridge) objreg.register('readline-bridge', readline_bridge)
@ -146,35 +137,23 @@ class Application(QApplication):
config.init(self._args) config.init(self._args)
log.init.debug("Initializing crashlog...") log.init.debug("Initializing crashlog...")
self._handle_segfault() self._handle_segfault()
log.init.debug("Initializing modes...")
modeman.init()
log.init.debug("Initializing websettings...") log.init.debug("Initializing websettings...")
websettings.init() websettings.init()
log.init.debug("Initializing quickmarks...") log.init.debug("Initializing quickmarks...")
quickmarks.init() quickmarks.init()
log.init.debug("Initializing proxy...") log.init.debug("Initializing proxy...")
proxy.init() proxy.init()
log.init.debug("Initializing userscripts...")
userscripts.init()
log.init.debug("Initializing utility commands...")
utilcmds.init()
log.init.debug("Initializing cookies...") log.init.debug("Initializing cookies...")
cookie_jar = cookies.CookieJar(self) cookie_jar = cookies.CookieJar(self)
objreg.register('cookie-jar', cookie_jar) objreg.register('cookie-jar', cookie_jar)
log.init.debug("Initializing cache...") log.init.debug("Initializing cache...")
diskcache = cache.DiskCache(self) diskcache = cache.DiskCache(self)
objreg.register('cache', diskcache) objreg.register('cache', diskcache)
log.init.debug("Initializing commands...")
self._commandrunner = runners.CommandRunner()
log.init.debug("Initializing search...")
search_runner = runners.SearchRunner(self)
objreg.register('search-runner', search_runner)
log.init.debug("Initializing downloads...") log.init.debug("Initializing downloads...")
download_manager = downloads.DownloadManager(self) download_manager = downloads.DownloadManager(self)
objreg.register('download-manager', download_manager) objreg.register('download-manager', download_manager)
log.init.debug("Initializing main window...") log.init.debug("Initializing main window...")
main_window = mainwindow.MainWindow() mainwindow.MainWindow.spawn(False if self._args.nowindow else True)
objreg.register('main-window', main_window)
log.init.debug("Initializing debug console...") log.init.debug("Initializing debug console...")
debug_console = console.ConsoleWidget() debug_console = console.ConsoleWidget()
objreg.register('debug-console', debug_console) objreg.register('debug-console', debug_console)
@ -238,30 +217,41 @@ class Application(QApplication):
URLs to open have no prefix, commands to execute begin with a colon. URLs to open have no prefix, commands to execute begin with a colon.
""" """
tabbed_browser = objreg.get('tabbed-browser') win_id = 0
for cmd in self._args.command: for cmd in self._args.command:
if cmd.startswith(':'): if cmd.startswith(':'):
log.init.debug("Startup cmd {}".format(cmd)) log.init.debug("Startup cmd {}".format(cmd))
self._commandrunner.run_safely_init(cmd.lstrip(':')) commandrunner = runners.CommandRunner(win_id)
commandrunner.run_safely_init(cmd.lstrip(':'))
elif not cmd:
log.init.debug("Empty argument")
win_id = mainwindow.MainWindow.spawn()
else: else:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
log.init.debug("Startup URL {}".format(cmd)) log.init.debug("Startup URL {}".format(cmd))
try: try:
url = urlutils.fuzzy_url(cmd) url = urlutils.fuzzy_url(cmd)
except urlutils.FuzzyUrlError as e: except urlutils.FuzzyUrlError as e:
message.error("Error in startup argument '{}': {}".format( message.error(0, "Error in startup argument '{}': "
cmd, e)) "{}".format(cmd, e))
else: else:
tabbed_browser.tabopen(url) tabbed_browser.tabopen(url)
if tabbed_browser.count() == 0: for win_id in objreg.window_registry:
log.init.debug("Opening startpage") tabbed_browser = objreg.get('tabbed-browser', scope='window',
for urlstr in config.get('general', 'startpage'): window=win_id)
try: if tabbed_browser.count() == 0:
url = urlutils.fuzzy_url(urlstr) log.init.debug("Opening startpage")
except urlutils.FuzzyUrlError as e: for urlstr in config.get('general', 'startpage'):
message.error("Error when opening startpage: {}".format(e)) try:
else: url = urlutils.fuzzy_url(urlstr)
tabbed_browser.tabopen(url) except urlutils.FuzzyUrlError as e:
message.error(0, "Error when opening startpage: "
"{}".format(e))
tabbed_browser.tabopen(QUrl('about:blank'))
else:
tabbed_browser.tabopen(url)
# Open quickstart if it's the first start # Open quickstart if it's the first start
state_config = objreg.get('state-config') state_config = objreg.get('state-config')
@ -294,89 +284,9 @@ class Application(QApplication):
def _connect_signals(self): def _connect_signals(self):
"""Connect all signals to their slots.""" """Connect all signals to their slots."""
# pylint: disable=too-many-statements, too-many-locals
# syntactic sugar
kp = objreg.get('keyparsers')
main_window = objreg.get('main-window')
status = main_window.status
completion = objreg.get('completion')
tabs = objreg.get('tabbed-browser')
cmd = objreg.get('status-command')
completer = objreg.get('completer')
search_runner = objreg.get('search-runner')
message_bridge = objreg.get('message-bridge')
mode_manager = objreg.get('mode-manager')
prompter = objreg.get('prompter')
download_manager = objreg.get('download-manager')
config_obj = objreg.get('config') config_obj = objreg.get('config')
key_config = objreg.get('key-config')
# misc
self.lastWindowClosed.connect(self.shutdown) self.lastWindowClosed.connect(self.shutdown)
tabs.quit.connect(self.shutdown)
mode_manager.entered.connect(hints.on_mode_entered)
# status bar
mode_manager.entered.connect(status.on_mode_entered)
mode_manager.left.connect(status.on_mode_left)
mode_manager.left.connect(cmd.on_mode_left)
mode_manager.left.connect(prompter.on_mode_left)
# commands
cmd.got_cmd.connect(self._commandrunner.run_safely)
cmd.got_search.connect(search_runner.search)
cmd.got_search_rev.connect(search_runner.search_rev)
cmd.returnPressed.connect(tabs.setFocus)
search_runner.do_search.connect(tabs.search)
kp[usertypes.KeyMode.normal].keystring_updated.connect(
status.keystring.setText)
tabs.got_cmd.connect(self._commandrunner.run_safely)
# messages
message_bridge.s_error.connect(status.disp_error)
message_bridge.s_info.connect(status.disp_temp_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_set_cmd_text.connect(cmd.set_cmd_text)
message_bridge.s_question.connect(prompter.ask_question,
Qt.DirectConnection)
# config
config_obj.style_changed.connect(style.get_stylesheet.cache_clear) config_obj.style_changed.connect(style.get_stylesheet.cache_clear)
for obj in kp.values():
key_config.changed.connect(obj.on_keyconfig_changed)
# statusbar
# FIXME some of these probably only should be triggered on mainframe
# loadStarted.
# https://github.com/The-Compiler/qutebrowser/issues/112
tabs.current_tab_changed.connect(status.prog.on_tab_changed)
tabs.cur_progress.connect(status.prog.setValue)
tabs.cur_load_finished.connect(status.prog.hide)
tabs.cur_load_started.connect(status.prog.on_load_started)
tabs.current_tab_changed.connect(status.percentage.on_tab_changed)
tabs.cur_scroll_perc_changed.connect(status.percentage.set_perc)
tabs.current_tab_changed.connect(status.txt.on_tab_changed)
tabs.cur_statusbar_message.connect(status.txt.on_statusbar_message)
tabs.cur_load_started.connect(status.txt.on_load_started)
tabs.current_tab_changed.connect(status.url.on_tab_changed)
tabs.cur_url_text_changed.connect(status.url.set_url)
tabs.cur_link_hovered.connect(status.url.set_hover_url)
tabs.cur_load_status_changed.connect(status.url.on_load_status_changed)
# command input / completion
mode_manager.left.connect(tabs.on_mode_left)
cmd.clear_completion_selection.connect(
completion.on_clear_completion_selection)
cmd.hide_completion.connect(completion.hide)
cmd.update_completion.connect(completer.on_update_completion)
completer.change_completed_part.connect(cmd.on_change_completed_part)
# downloads
tabs.start_download.connect(download_manager.fetch)
def _get_widgets(self): def _get_widgets(self):
"""Get a string list of all widgets.""" """Get a string list of all widgets."""
@ -390,20 +300,6 @@ class Application(QApplication):
lines.append(' ' * depth + repr(kid)) lines.append(' ' * depth + repr(kid))
self._get_pyqt_objects(lines, kid, depth + 1) self._get_pyqt_objects(lines, kid, depth + 1)
def _get_registered_objects(self):
"""Get all registered objects in all registries as a string."""
blocks = []
lines = []
for name, registry in objreg.meta_registry.items():
blocks.append((name, registry.dump_objects()))
for name, data in sorted(blocks, key=lambda e: e[0]):
lines.append("")
lines.append("{} object registry - {} objects:".format(
name, len(data)))
for line in data:
lines.append(" {}".format(line))
return lines
def get_all_objects(self): def get_all_objects(self):
"""Get all children of an object recursively as a string.""" """Get all children of an object recursively as a string."""
output = [''] output = ['']
@ -419,36 +315,47 @@ class Application(QApplication):
len(pyqt_lines))) len(pyqt_lines)))
output += pyqt_lines output += pyqt_lines
output += [''] output += ['']
output += self._get_registered_objects() output += objreg.dump_objects()
return '\n'.join(output) return '\n'.join(output)
def _recover_pages(self): def _recover_pages(self, forgiving=False):
"""Try to recover all open pages. """Try to recover all open pages.
Called from _exception_hook, so as forgiving as possible. Called from _exception_hook, so as forgiving as possible.
Args:
forgiving: Whether to ignore exceptions.
Return: Return:
A list of open pages, or an empty list. A list containing a list for each window, which in turn contain the
opened URLs.
""" """
try:
tabbed_browser = objreg.get('tabbed-browser')
except KeyError:
return []
pages = [] pages = []
for tab in tabbed_browser.widgets(): for win_id in objreg.window_registry:
try: win_pages = []
url = tab.cur_url.toString( tabbed_browser = objreg.get('tabbed-browser', scope='window',
QUrl.RemovePassword | QUrl.FullyEncoded) window=win_id)
if url: for tab in tabbed_browser.widgets():
pages.append(url) try:
except Exception: # pylint: disable=broad-except urlstr = tab.cur_url.toString(
log.destroy.exception("Error while recovering tab") QUrl.RemovePassword | QUrl.FullyEncoded)
if urlstr:
win_pages.append(urlstr)
except Exception: # pylint: disable=broad-except
if forgiving:
log.destroy.exception("Error while recovering tab")
else:
raise
pages.append(win_pages)
return pages return pages
def _save_geometry(self): def _save_geometry(self):
"""Save the window geometry to the state config.""" """Save the window geometry to the state config."""
last_win_id = sorted(objreg.window_registry)[-1]
main_window = objreg.get('main-window', scope='window',
window=last_win_id)
state_config = objreg.get('state-config') state_config = objreg.get('state-config')
data = bytes(objreg.get('main-window').saveGeometry()) data = bytes(main_window.saveGeometry())
geom = base64.b64encode(data).decode('ASCII') geom = base64.b64encode(data).decode('ASCII')
try: try:
state_config.add_section('geometry') state_config.add_section('geometry')
@ -497,13 +404,13 @@ class Application(QApplication):
self._quit_status['crash'] = False self._quit_status['crash'] = False
try: try:
pages = self._recover_pages() pages = self._recover_pages(forgiving=True)
except Exception: except Exception:
log.destroy.exception("Error while recovering pages") log.destroy.exception("Error while recovering pages")
pages = [] pages = []
try: try:
history = objreg.get('status-command').history[-5:] history = objreg.get('command-history')[-5:]
except Exception: except Exception:
log.destroy.exception("Error while getting history: {}") log.destroy.exception("Error while getting history: {}")
history = [] history = []
@ -531,18 +438,16 @@ class Application(QApplication):
self._destroy_crashlogfile() self._destroy_crashlogfile()
sys.exit(1) sys.exit(1)
@cmdutils.register(instance='app', name=['quit', 'q'])
def quit(self):
"""Quit qutebrowser."""
QApplication.closeAllWindows()
@cmdutils.register(instance='app', ignore_args=True) @cmdutils.register(instance='app', ignore_args=True)
def restart(self, shutdown=True, pages=None): def restart(self, shutdown=True, pages=None):
"""Restart qutebrowser while keeping existing tabs open.""" """Restart qutebrowser while keeping existing tabs open."""
# We don't use _recover_pages here as it's too forgiving when
# exceptions occur.
if pages is None: if pages is None:
pages = [] pages = self._recover_pages()
for tab in objreg.get('tabbed-browser').widgets():
urlstr = tab.cur_url.toString(
QUrl.RemovePassword | QUrl.FullyEncoded)
if urlstr:
pages.append(urlstr)
log.destroy.debug("sys.executable: {}".format(sys.executable)) log.destroy.debug("sys.executable: {}".format(sys.executable))
log.destroy.debug("sys.path: {}".format(sys.path)) log.destroy.debug("sys.path: {}".format(sys.path))
log.destroy.debug("sys.argv: {}".format(sys.argv)) log.destroy.debug("sys.argv: {}".format(sys.argv))
@ -563,7 +468,12 @@ class Application(QApplication):
# We only want to preserve options on a restart. # We only want to preserve options on a restart.
args.append(arg) args.append(arg)
# Add all open pages so they get reopened. # Add all open pages so they get reopened.
args += pages page_args = []
for win in pages:
page_args.extend(win)
page_args.append('')
if page_args:
args.extend(page_args[:-1])
log.destroy.debug("args: {}".format(args)) log.destroy.debug("args: {}".format(args))
log.destroy.debug("cwd: {}".format(cwd)) log.destroy.debug("cwd: {}".format(cwd))
# Open a new process and immediately shutdown the existing one # Open a new process and immediately shutdown the existing one
@ -592,13 +502,15 @@ class Application(QApplication):
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
out = traceback.format_exc() out = traceback.format_exc()
qutescheme.pyeval_output = out qutescheme.pyeval_output = out
objreg.get('tabbed-browser').openurl(QUrl('qute:pyeval'), newtab=True) tabbed_browser = objreg.get('tabbed-browser', scope='window',
window='current')
tabbed_browser.openurl(QUrl('qute:pyeval'), newtab=True)
@cmdutils.register(instance='app') @cmdutils.register(instance='app')
def report(self): def report(self):
"""Report a bug in qutebrowser.""" """Report a bug in qutebrowser."""
pages = self._recover_pages() pages = self._recover_pages()
history = objreg.get('status-command').history[-5:] history = objreg.get('command-history')[-5:]
objects = self.get_all_objects() objects = self.get_all_objects()
self._crashdlg = crash.ReportDialog(pages, history, objects) self._crashdlg = crash.ReportDialog(pages, history, objects)
self._crashdlg.show() self._crashdlg.show()
@ -654,8 +566,13 @@ class Application(QApplication):
return return
self._shutting_down = True self._shutting_down = True
log.destroy.debug("Shutting down with status {}...".format(status)) log.destroy.debug("Shutting down with status {}...".format(status))
prompter = objreg.get('prompter', None) deferrer = False
if prompter is not None and prompter.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 quitted now) and not in the # a still sub-eventloop (which gets quitted now) and not in the
# main one. # main one.
@ -678,15 +595,15 @@ class Application(QApplication):
# Remove eventfilter # Remove eventfilter
try: try:
log.destroy.debug("Removing eventfilter...") log.destroy.debug("Removing eventfilter...")
self.removeEventFilter(objreg.get('mode-manager')) self.removeEventFilter(self._event_filter)
except KeyError: except KeyError:
pass pass
# Close all tabs # Close all tabs
try: for win_id in objreg.window_registry:
log.destroy.debug("Closing tabs...") log.destroy.debug("Closing tabs in window {}...".format(win_id))
objreg.get('tabbed-browser').shutdown() tabbed_browser = objreg.get('tabbed-browser', scope='window',
except KeyError: window=win_id)
pass tabbed_browser.shutdown()
# Save everything # Save everything
try: try:
config_obj = objreg.get('config') config_obj = objreg.get('config')

View File

@ -23,7 +23,7 @@ import re
import os import os
import subprocess import subprocess
import posixpath import posixpath
from functools import partial import functools
from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import Qt, QUrl from PyQt5.QtCore import Qt, QUrl
@ -53,53 +53,73 @@ class CommandDispatcher:
Attributes: Attributes:
_editor: The ExternalEditor object. _editor: The ExternalEditor object.
_win_id: The window ID the CommandDispatcher is associated with.
""" """
def __init__(self): def __init__(self, win_id):
self._editor = None self._editor = None
self._win_id = win_id
def __repr__(self): def __repr__(self):
return utils.get_repr(self) return utils.get_repr(self)
def _tabbed_browser(self, window=False):
"""Convienence method to get the right tabbed-browser.
Args:
window: If True, open a new window.
"""
if window:
main_window = objreg.get('main-window', scope='window',
window=self._win_id)
win_id = main_window.spawn()
else:
win_id = self._win_id
return objreg.get('tabbed-browser', scope='window', window=win_id)
def _count(self): def _count(self):
"""Convenience method to get the widget count.""" """Convenience method to get the widget count."""
return objreg.get('tabbed-browser').count() return self._tabbed_browser().count()
def _set_current_index(self, idx): def _set_current_index(self, idx):
"""Convenience method to set the current widget index.""" """Convenience method to set the current widget index."""
return objreg.get('tabbed-browser').setCurrentIndex(idx) return self._tabbed_browser().setCurrentIndex(idx)
def _current_index(self): def _current_index(self):
"""Convenience method to get the current widget index.""" """Convenience method to get the current widget index."""
return objreg.get('tabbed-browser').currentIndex() return self._tabbed_browser().currentIndex()
def _current_url(self): def _current_url(self):
"""Convenience method to get the current url.""" """Convenience method to get the current url."""
return objreg.get('tabbed-browser').current_url() return self._tabbed_browser().current_url()
def _current_widget(self): def _current_widget(self):
"""Get the currently active widget from a command.""" """Get the currently active widget from a command."""
widget = objreg.get('tabbed-browser').currentWidget() widget = self._tabbed_browser().currentWidget()
if widget is None: if widget is None:
raise cmdexc.CommandError("No WebView available yet!") raise cmdexc.CommandError("No WebView available yet!")
return widget return widget
def _open(self, url, tab, background): def _open(self, url, tab, background, window):
"""Helper function to open a page. """Helper function to open a page.
Args: Args:
url: The URL to open as QUrl. url: The URL to open as QUrl.
tab: Whether to open in a new tab. tab: Whether to open in a new tab.
background: Whether to open in the background. background: Whether to open in the background.
window: Whether to open in a new window
""" """
if not url.isValid(): if not url.isValid():
errstr = "Invalid URL {}" errstr = "Invalid URL {}"
if url.errorString(): if url.errorString():
errstr += " - {}".format(url.errorString()) errstr += " - {}".format(url.errorString())
raise cmdexc.CommandError(errstr) raise cmdexc.CommandError(errstr)
tabbed_browser = objreg.get('tabbed-browser') tabbed_browser = self._tabbed_browser()
if tab and background: if sum(1 for e in (tab, background, window) if e) > 1:
raise cmdexc.CommandError("Only one of -t/-b can be given!") raise cmdexc.CommandError("Only one of -t/-b/-w can be given!")
elif window:
tabbed_browser = self._tabbed_browser(window=True)
tabbed_browser.tabopen(url)
elif tab: elif tab:
tabbed_browser.tabopen(url, background=False, explicit=True) tabbed_browser.tabopen(url, background=False, explicit=True)
elif background: elif background:
@ -119,7 +139,7 @@ class CommandDispatcher:
The widget with the given tab ID if count is given. The widget with the given tab ID if count is given.
None if no widget was found. None if no widget was found.
""" """
tabbed_browser = objreg.get('tabbed-browser') tabbed_browser = self._tabbed_browser()
if count is None: if count is None:
return tabbed_browser.currentWidget() return tabbed_browser.currentWidget()
elif 1 <= count <= self._count(): elif 1 <= count <= self._count():
@ -179,10 +199,11 @@ class CommandDispatcher:
def _tab_focus_last(self): def _tab_focus_last(self):
"""Select the tab which was last focused.""" """Select the tab which was last focused."""
try: try:
tab = objreg.get('last-focused-tab') tab = objreg.get('last-focused-tab', scope='window',
window=self._win_id)
except KeyError: except KeyError:
raise cmdexc.CommandError("No last focused tab!") raise cmdexc.CommandError("No last focused tab!")
idx = objreg.get('tabbed-browser').indexOf(tab) idx = self._tabbed_browser().indexOf(tab)
if idx == -1: if idx == -1:
raise cmdexc.CommandError("Last focused tab vanished!") raise cmdexc.CommandError("Last focused tab vanished!")
self._set_current_index(idx) self._set_current_index(idx)
@ -195,7 +216,7 @@ class CommandDispatcher:
except PermissionError: except PermissionError:
raise cmdexc.CommandError("Failed to delete tempfile...") raise cmdexc.CommandError("Failed to delete tempfile...")
@cmdutils.register(instance='command-dispatcher') @cmdutils.register(instance='command-dispatcher', scope='window')
def tab_close(self, count=None): def tab_close(self, count=None):
"""Close the current/[count]th tab. """Close the current/[count]th tab.
@ -209,39 +230,41 @@ class CommandDispatcher:
tab = self._cntwidget(count) tab = self._cntwidget(count)
if tab is None: if tab is None:
return return
objreg.get('tabbed-browser').close_tab(tab) self._tabbed_browser().close_tab(tab)
@cmdutils.register(instance='command-dispatcher', name='open', @cmdutils.register(instance='command-dispatcher', name='open',
split=False) split=False, scope='window')
def openurl(self, url, bg=False, tab=False, count=None): def openurl(self, url, bg=False, tab=False, window=False, count=None):
"""Open a URL in the current/[count]th tab. """Open a URL in the current/[count]th tab.
Args: Args:
url: The URL to open. url: The URL to open.
bg: Open in a new background tab. bg: Open in a new background tab.
tab: Open in a new tab. tab: Open in a new tab.
window: Open in a new window.
count: The tab index to open the URL in, or None. count: The tab index to open the URL in, or None.
""" """
try: try:
url = urlutils.fuzzy_url(url) url = urlutils.fuzzy_url(url)
except urlutils.FuzzyUrlError as e: except urlutils.FuzzyUrlError as e:
raise cmdexc.CommandError(e) raise cmdexc.CommandError(e)
if tab or bg: if tab or bg or window:
self._open(url, tab, bg) self._open(url, tab, bg, window)
else: else:
curtab = self._cntwidget(count) curtab = self._cntwidget(count)
if curtab is None: if curtab is None:
if count is None: if count is None:
# We want to open a URL in the current tab, but none exists # We want to open a URL in the current tab, but none exists
# yet. # yet.
objreg.get('tabbed-browser').tabopen(url) self._tabbed_browser().tabopen(url)
else: else:
# Explicit count with a tab that doesn't exist. # Explicit count with a tab that doesn't exist.
return return
else: else:
curtab.openurl(url) curtab.openurl(url)
@cmdutils.register(instance='command-dispatcher', name='reload') @cmdutils.register(instance='command-dispatcher', name='reload',
scope='window')
def reloadpage(self, count=None): def reloadpage(self, count=None):
"""Reload the current/[count]th tab. """Reload the current/[count]th tab.
@ -252,7 +275,7 @@ class CommandDispatcher:
if tab is not None: if tab is not None:
tab.reload() tab.reload()
@cmdutils.register(instance='command-dispatcher') @cmdutils.register(instance='command-dispatcher', scope='window')
def stop(self, count=None): def stop(self, count=None):
"""Stop loading in the current/[count]th tab. """Stop loading in the current/[count]th tab.
@ -263,7 +286,8 @@ class CommandDispatcher:
if tab is not None: if tab is not None:
tab.stop() tab.stop()
@cmdutils.register(instance='command-dispatcher', name='print') @cmdutils.register(instance='command-dispatcher', name='print',
scope='window')
def printpage(self, preview=False, count=None): def printpage(self, preview=False, count=None):
"""Print the current/[count]th tab. """Print the current/[count]th tab.
@ -287,64 +311,77 @@ class CommandDispatcher:
diag.setAttribute(Qt.WA_DeleteOnClose) diag.setAttribute(Qt.WA_DeleteOnClose)
diag.open(lambda: tab.print(diag.printer())) diag.open(lambda: tab.print(diag.printer()))
@cmdutils.register(instance='command-dispatcher') @cmdutils.register(instance='command-dispatcher', scope='window')
def tab_clone(self, bg=False): def tab_clone(self, bg=False, window=False):
"""Duplicate the current tab. """Duplicate the current tab.
Args: Args:
bg: Open in a background tab. bg: Open in a background tab.
window: Open in a new window.
Return: Return:
The new QWebView. The new QWebView.
""" """
if bg and window:
raise cmdexc.CommandError("Only one of -b/-w can be given!")
curtab = self._current_widget() curtab = self._current_widget()
tabbed_browser = objreg.get('tabbed-browser') tabbed_browser = self._tabbed_browser(window)
newtab = tabbed_browser.tabopen(background=bg, explicit=True) newtab = tabbed_browser.tabopen(background=bg, explicit=True)
history = qtutils.serialize(curtab.history()) history = qtutils.serialize(curtab.history())
qtutils.deserialize(history, newtab.history()) qtutils.deserialize(history, newtab.history())
return newtab return newtab
def _back_forward(self, tab, bg, count, forward): def _back_forward(self, tab, bg, window, count, forward):
"""Helper function for :back/:forward.""" """Helper function for :back/:forward."""
if tab or bg: if (not forward and not
widget = self.tab_clone(bg) self._current_widget().page().history().canGoBack()):
raise cmdexc.CommandError("At beginning of history.")
if (forward and not
self._current_widget().page().history().canGoForward()):
raise cmdexc.CommandError("At end of history.")
if tab or bg or window:
widget = self.tab_clone(bg, window)
else: else:
widget = self._current_widget() widget = self._current_widget()
for _ in range(count): for _ in range(count):
if forward: if forward:
widget.go_forward() widget.forward()
else: else:
widget.go_back() widget.back()
@cmdutils.register(instance='command-dispatcher') @cmdutils.register(instance='command-dispatcher', scope='window')
def back(self, tab=False, bg=False, count=1): def back(self, tab=False, bg=False, window=False, count=1):
"""Go back in the history of the current tab. """Go back in the history of the current tab.
Args: Args:
tab: Go back in a new tab. tab: Go back in a new tab.
bg: Go back in a background tab. bg: Go back in a background tab.
window: Go back in a new window.
count: How many pages to go back. count: How many pages to go back.
""" """
self._back_forward(tab, bg, count, forward=False) self._back_forward(tab, bg, window, count, forward=False)
@cmdutils.register(instance='command-dispatcher') @cmdutils.register(instance='command-dispatcher', scope='window')
def forward(self, tab=False, bg=False, count=1): def forward(self, tab=False, bg=False, window=False, count=1):
"""Go forward in the history of the current tab. """Go forward in the history of the current tab.
Args: Args:
tab: Go forward in a new tab. tab: Go forward in a new tab.
bg: Go back in a background tab. bg: Go forward in a background tab.
window: Go forward in a new window.
count: How many pages to go forward. count: How many pages to go forward.
""" """
self._back_forward(tab, bg, count, forward=True) self._back_forward(tab, bg, window, count, forward=True)
def _navigate_incdec(self, url, tab, incdec): def _navigate_incdec(self, url, incdec, tab, background, window):
"""Helper method for :navigate when `where' is increment/decrement. """Helper method for :navigate when `where' is increment/decrement.
Args: Args:
url: The current url. url: The current url.
tab: Whether to open the link in a new tab.
incdec: Either 'increment' or 'decrement'. incdec: Either 'increment' or 'decrement'.
tab: Whether to open the link in a new tab.
background: Open the link in a new background tab.
window: Open the link in a new window.
""" """
encoded = bytes(url.toEncoded()).decode('ascii') encoded = bytes(url.toEncoded()).decode('ascii')
# Get the last number in a string # Get the last number in a string
@ -369,25 +406,27 @@ class CommandDispatcher:
raise ValueError("Invalid value {} for indec!".format(incdec)) raise ValueError("Invalid value {} for indec!".format(incdec))
urlstr = ''.join([pre, str(val), post]).encode('ascii') urlstr = ''.join([pre, str(val), post]).encode('ascii')
new_url = QUrl.fromEncoded(urlstr) new_url = QUrl.fromEncoded(urlstr)
self._open(new_url, tab, background=False) self._open(new_url, tab, background, window)
def _navigate_up(self, url, tab): def _navigate_up(self, url, tab, background, window):
"""Helper method for :navigate when `where' is up. """Helper method for :navigate when `where' is up.
Args: Args:
url: The current url. url: The current url.
tab: Whether to open the link in a new tab. tab: Whether to open the link in a new tab.
background: Open the link in a new background tab.
window: Open the link in a new window.
""" """
path = url.path() path = url.path()
if not path or path == '/': if not path or path == '/':
raise cmdexc.CommandError("Can't go up!") raise cmdexc.CommandError("Can't go up!")
new_path = posixpath.join(path, posixpath.pardir) new_path = posixpath.join(path, posixpath.pardir)
url.setPath(new_path) url.setPath(new_path)
self._open(url, tab, background=False) self._open(url, tab, background, window)
@cmdutils.register(instance='command-dispatcher') @cmdutils.register(instance='command-dispatcher', scope='window')
def navigate(self, where: ('prev', 'next', 'up', 'increment', 'decrement'), def navigate(self, where: ('prev', 'next', 'up', 'increment', 'decrement'),
tab=False): tab=False, bg=False, window=False):
"""Open typical prev/next links or navigate using the URL path. """Open typical prev/next links or navigate using the URL path.
This tries to automatically click on typical _Previous Page_ or This tries to automatically click on typical _Previous Page_ or
@ -405,7 +444,11 @@ class CommandDispatcher:
- `decrement`: Decrement the last number in the URL. - `decrement`: Decrement the last number in the URL.
tab: Open in a new tab. tab: Open in a new tab.
bg: Open in a background tab.
window: Open in a new window.
""" """
if sum(1 for e in (tab, bg, window) if e) > 1:
raise cmdexc.CommandError("Only one of -t/-b/-w can be given!")
widget = self._current_widget() widget = self._current_widget()
frame = widget.page().currentFrame() frame = widget.page().currentFrame()
url = self._current_url() url = self._current_url()
@ -413,18 +456,21 @@ class CommandDispatcher:
raise cmdexc.CommandError("No frame focused!") raise cmdexc.CommandError("No frame focused!")
hintmanager = objreg.get('hintmanager', scope='tab') hintmanager = objreg.get('hintmanager', scope='tab')
if where == 'prev': if where == 'prev':
hintmanager.follow_prevnext(frame, url, prev=True, newtab=tab) hintmanager.follow_prevnext(frame, url, prev=True, tab=tab,
background=bg, window=window)
elif where == 'next': elif where == 'next':
hintmanager.follow_prevnext(frame, url, prev=False, newtab=tab) hintmanager.follow_prevnext(frame, url, prev=False, tab=tab,
background=bg, window=window)
elif where == 'up': elif where == 'up':
self._navigate_up(url, tab) self._navigate_up(url, tab, bg, window)
elif where in ('decrement', 'increment'): elif where in ('decrement', 'increment'):
self._navigate_incdec(url, tab, where) self._navigate_incdec(url, where, tab, bg, window)
else: else:
raise ValueError("Got called with invalid value {} for " raise ValueError("Got called with invalid value {} for "
"`where'.".format(where)) "`where'.".format(where))
@cmdutils.register(instance='command-dispatcher', hide=True) @cmdutils.register(instance='command-dispatcher', hide=True,
scope='window')
def scroll(self, dx: float, dy: float, count=1): def scroll(self, dx: float, dy: float, count=1):
"""Scroll the current tab by 'count * dx/dy'. """Scroll the current tab by 'count * dx/dy'.
@ -439,7 +485,8 @@ class CommandDispatcher:
cmdutils.check_overflow(dy, 'int') cmdutils.check_overflow(dy, 'int')
self._current_widget().page().currentFrame().scroll(dx, dy) self._current_widget().page().currentFrame().scroll(dx, dy)
@cmdutils.register(instance='command-dispatcher', hide=True) @cmdutils.register(instance='command-dispatcher', hide=True,
scope='window')
def scroll_perc(self, perc: float=None, def scroll_perc(self, perc: float=None,
horizontal: {'flag': 'x'}=False, count=None): horizontal: {'flag': 'x'}=False, count=None):
"""Scroll to a specific percentage of the page. """Scroll to a specific percentage of the page.
@ -455,7 +502,8 @@ class CommandDispatcher:
self._scroll_percent(perc, count, self._scroll_percent(perc, count,
Qt.Horizontal if horizontal else Qt.Vertical) Qt.Horizontal if horizontal else Qt.Vertical)
@cmdutils.register(instance='command-dispatcher', hide=True) @cmdutils.register(instance='command-dispatcher', hide=True,
scope='window')
def scroll_page(self, x: float, y: float, count=1): def scroll_page(self, x: float, y: float, count=1):
"""Scroll the frame page-wise. """Scroll the frame page-wise.
@ -472,7 +520,7 @@ class CommandDispatcher:
cmdutils.check_overflow(dy, 'int') cmdutils.check_overflow(dy, 'int')
frame.scroll(dx, dy) frame.scroll(dx, dy)
@cmdutils.register(instance='command-dispatcher') @cmdutils.register(instance='command-dispatcher', scope='window')
def yank(self, title=False, sel=False): def yank(self, title=False, sel=False):
"""Yank the current URL/title to the clipboard or primary selection. """Yank the current URL/title to the clipboard or primary selection.
@ -482,7 +530,7 @@ class CommandDispatcher:
""" """
clipboard = QApplication.clipboard() clipboard = QApplication.clipboard()
if title: if title:
s = objreg.get('tabbed-browser').tabText(self._current_index()) s = self._tabbed_browser().tabText(self._current_index())
else: else:
s = self._current_url().toString( s = self._current_url().toString(
QUrl.FullyEncoded | QUrl.RemovePassword) QUrl.FullyEncoded | QUrl.RemovePassword)
@ -495,9 +543,9 @@ class CommandDispatcher:
log.misc.debug("Yanking to {}: '{}'".format(target, s)) log.misc.debug("Yanking to {}: '{}'".format(target, s))
clipboard.setText(s, mode) clipboard.setText(s, mode)
what = 'Title' if title else 'URL' what = 'Title' if title else 'URL'
message.info("{} yanked to {}".format(what, target)) message.info(self._win_id, "{} yanked to {}".format(what, target))
@cmdutils.register(instance='command-dispatcher') @cmdutils.register(instance='command-dispatcher', scope='window')
def zoom_in(self, count=1): def zoom_in(self, count=1):
"""Increase the zoom level for the current tab. """Increase the zoom level for the current tab.
@ -507,7 +555,7 @@ class CommandDispatcher:
tab = self._current_widget() tab = self._current_widget()
tab.zoom(count) tab.zoom(count)
@cmdutils.register(instance='command-dispatcher') @cmdutils.register(instance='command-dispatcher', scope='window')
def zoom_out(self, count=1): def zoom_out(self, count=1):
"""Decrease the zoom level for the current tab. """Decrease the zoom level for the current tab.
@ -517,7 +565,7 @@ class CommandDispatcher:
tab = self._current_widget() tab = self._current_widget()
tab.zoom(-count) tab.zoom(-count)
@cmdutils.register(instance='command-dispatcher') @cmdutils.register(instance='command-dispatcher', scope='window')
def zoom(self, zoom=None, count=None): def zoom(self, zoom=None, count=None):
"""Set the zoom level for the current tab. """Set the zoom level for the current tab.
@ -535,24 +583,24 @@ class CommandDispatcher:
tab = self._current_widget() tab = self._current_widget()
tab.zoom_perc(level) tab.zoom_perc(level)
@cmdutils.register(instance='command-dispatcher') @cmdutils.register(instance='command-dispatcher', scope='window')
def tab_only(self): def tab_only(self):
"""Close all tabs except for the current one.""" """Close all tabs except for the current one."""
tabbed_browser = objreg.get('tabbed-browser') tabbed_browser = self._tabbed_browser()
for tab in tabbed_browser.widgets(): for tab in tabbed_browser.widgets():
if tab is self._current_widget(): if tab is self._current_widget():
continue continue
tabbed_browser.close_tab(tab) tabbed_browser.close_tab(tab)
@cmdutils.register(instance='command-dispatcher') @cmdutils.register(instance='command-dispatcher', scope='window')
def undo(self): def undo(self):
"""Re-open a closed tab (optionally skipping [count] closed tabs).""" """Re-open a closed tab (optionally skipping [count] closed tabs)."""
try: try:
objreg.get('tabbed-browser').undo() self._tabbed_browser().undo()
except IndexError: except IndexError:
raise cmdexc.CommandError("Nothing to undo!") raise cmdexc.CommandError("Nothing to undo!")
@cmdutils.register(instance='command-dispatcher') @cmdutils.register(instance='command-dispatcher', scope='window')
def tab_prev(self, count=1): def tab_prev(self, count=1):
"""Switch to the previous tab, or switch [count] tabs back. """Switch to the previous tab, or switch [count] tabs back.
@ -567,7 +615,7 @@ class CommandDispatcher:
else: else:
raise cmdexc.CommandError("First tab") raise cmdexc.CommandError("First tab")
@cmdutils.register(instance='command-dispatcher') @cmdutils.register(instance='command-dispatcher', scope='window')
def tab_next(self, count=1): def tab_next(self, count=1):
"""Switch to the next tab, or switch [count] tabs forward. """Switch to the next tab, or switch [count] tabs forward.
@ -582,14 +630,15 @@ class CommandDispatcher:
else: else:
raise cmdexc.CommandError("Last tab") raise cmdexc.CommandError("Last tab")
@cmdutils.register(instance='command-dispatcher') @cmdutils.register(instance='command-dispatcher', scope='window')
def paste(self, sel=False, tab=False, bg=False): def paste(self, sel=False, tab=False, bg=False, window=False):
"""Open a page from the clipboard. """Open a page from the clipboard.
Args: Args:
sel: Use the primary selection instead of the clipboard. sel: Use the primary selection instead of the clipboard.
tab: Open in a new tab. tab: Open in a new tab.
bg: Open in a background tab. bg: Open in a background tab.
window: Open in new window.
""" """
clipboard = QApplication.clipboard() clipboard = QApplication.clipboard()
if sel and clipboard.supportsSelection(): if sel and clipboard.supportsSelection():
@ -606,9 +655,9 @@ class CommandDispatcher:
url = urlutils.fuzzy_url(text) url = urlutils.fuzzy_url(text)
except urlutils.FuzzyUrlError as e: except urlutils.FuzzyUrlError as e:
raise cmdexc.CommandError(e) raise cmdexc.CommandError(e)
self._open(url, tab, bg) self._open(url, tab, bg, window)
@cmdutils.register(instance='command-dispatcher') @cmdutils.register(instance='command-dispatcher', scope='window')
def tab_focus(self, index: (int, 'last')=None, count=None): def tab_focus(self, index: (int, 'last')=None, count=None):
"""Select the tab given as argument/[count]. """Select the tab given as argument/[count].
@ -632,7 +681,7 @@ class CommandDispatcher:
raise cmdexc.CommandError("There's no tab with index {}!".format( raise cmdexc.CommandError("There's no tab with index {}!".format(
idx)) idx))
@cmdutils.register(instance='command-dispatcher') @cmdutils.register(instance='command-dispatcher', scope='window')
def tab_move(self, direction: ('+', '-')=None, count=None): def tab_move(self, direction: ('+', '-')=None, count=None):
"""Move the current tab. """Move the current tab.
@ -656,7 +705,7 @@ class CommandDispatcher:
if not 0 <= new_idx < self._count(): if not 0 <= new_idx < self._count():
raise cmdexc.CommandError("Can't move tab to position {}!".format( raise cmdexc.CommandError("Can't move tab to position {}!".format(
new_idx)) new_idx))
tabbed_browser = objreg.get('tabbed-browser') tabbed_browser = self._tabbed_browser()
tab = self._current_widget() tab = self._current_widget()
cur_idx = self._current_index() cur_idx = self._current_index()
icon = tabbed_browser.tabIcon(cur_idx) icon = tabbed_browser.tabIcon(cur_idx)
@ -671,7 +720,8 @@ class CommandDispatcher:
finally: finally:
tabbed_browser.setUpdatesEnabled(True) tabbed_browser.setUpdatesEnabled(True)
@cmdutils.register(instance='command-dispatcher', split=False) @cmdutils.register(instance='command-dispatcher', split=False,
scope='window')
def spawn(self, *args): def spawn(self, *args):
"""Spawn a command in a shell. """Spawn a command in a shell.
@ -689,12 +739,12 @@ class CommandDispatcher:
log.procs.debug("Executing: {}".format(args)) log.procs.debug("Executing: {}".format(args))
subprocess.Popen(args) subprocess.Popen(args)
@cmdutils.register(instance='command-dispatcher') @cmdutils.register(instance='command-dispatcher', scope='window')
def home(self): def home(self):
"""Open main startpage in current tab.""" """Open main startpage in current tab."""
self.openurl(config.get('general', 'startpage')[0]) self.openurl(config.get('general', 'startpage')[0])
@cmdutils.register(instance='command-dispatcher') @cmdutils.register(instance='command-dispatcher', scope='window')
def run_userscript(self, cmd, *args: {'nargs': '*'}): def run_userscript(self, cmd, *args: {'nargs': '*'}):
"""Run an userscript given as argument. """Run an userscript given as argument.
@ -702,27 +752,30 @@ class CommandDispatcher:
cmd: The userscript to run. cmd: The userscript to run.
args: Arguments to pass to the userscript. args: Arguments to pass to the userscript.
""" """
userscripts.run(cmd, *args, url=self._current_url()) userscripts.run(cmd, *args, url=self._current_url(),
win_id=self._win_id)
@cmdutils.register(instance='command-dispatcher') @cmdutils.register(instance='command-dispatcher', scope='window')
def quickmark_save(self): def quickmark_save(self):
"""Save the current page as a quickmark.""" """Save the current page as a quickmark."""
quickmarks.prompt_save(self._current_url()) quickmarks.prompt_save(self._win_id, self._current_url())
@cmdutils.register(instance='command-dispatcher') @cmdutils.register(instance='command-dispatcher', scope='window')
def quickmark_load(self, name, tab=False, bg=False): def quickmark_load(self, name, tab=False, bg=False, window=False):
"""Load a quickmark. """Load a quickmark.
Args: Args:
name: The name of the quickmark to load. name: The name of the quickmark to load.
tab: Load the quickmark in a new tab. tab: Load the quickmark in a new tab.
bg: Load the quickmark in a new background tab. bg: Load the quickmark in a new background tab.
window: Load the quickmark in a new window.
""" """
urlstr = quickmarks.get(name) urlstr = quickmarks.get(name)
url = QUrl(urlstr) url = QUrl(urlstr)
self._open(url, tab, bg) self._open(url, tab, bg, window)
@cmdutils.register(instance='command-dispatcher', name='inspector') @cmdutils.register(instance='command-dispatcher', name='inspector',
scope='window')
def toggle_inspector(self): def toggle_inspector(self):
"""Toggle the web inspector.""" """Toggle the web inspector."""
cur = self._current_widget() cur = self._current_widget()
@ -744,13 +797,13 @@ class CommandDispatcher:
else: else:
cur.inspector.show() cur.inspector.show()
@cmdutils.register(instance='command-dispatcher') @cmdutils.register(instance='command-dispatcher', scope='window')
def download_page(self): def download_page(self):
"""Download the current page.""" """Download the current page."""
page = self._current_widget().page() page = self._current_widget().page()
objreg.get('download-manager').get(self._current_url(), page) objreg.get('download-manager').get(self._current_url(), page)
@cmdutils.register(instance='command-dispatcher') @cmdutils.register(instance='command-dispatcher', scope='window')
def view_source(self): def view_source(self):
"""Show the source of the current page.""" """Show the source of the current page."""
# pylint doesn't seem to like pygments... # pylint doesn't seem to like pygments...
@ -766,15 +819,20 @@ class CommandDispatcher:
highlighted = pygments.highlight(html, lexer, formatter) highlighted = pygments.highlight(html, lexer, formatter)
current_url = self._current_url() current_url = self._current_url()
tab = objreg.get('tabbed-browser').tabopen(explicit=True) tab = objreg.get('tabbed-browser').tabopen(explicit=True)
tab = self._tabbed_browser().tabopen(explicit=True)
tab.setHtml(highlighted, current_url) tab.setHtml(highlighted, current_url)
tab.viewing_source = True tab.viewing_source = True
@cmdutils.register(instance='command-dispatcher', name='help', @cmdutils.register(instance='command-dispatcher', name='help',
completion=[usertypes.Completion.helptopic]) completion=[usertypes.Completion.helptopic],
def show_help(self, topic=None): scope='window')
def show_help(self, tab=False, bg=False, window=False, topic=None):
r"""Show help about a command or setting. r"""Show help about a command or setting.
Args: Args:
tab: Open in a new tab.
bg: Open in a background tab.
window: Open in a new window.
topic: The topic to show help for. topic: The topic to show help for.
- :__command__ for commands. - :__command__ for commands.
@ -804,11 +862,12 @@ class CommandDispatcher:
path = 'settings.html#{}'.format(topic.replace('->', '-')) path = 'settings.html#{}'.format(topic.replace('->', '-'))
else: else:
raise cmdexc.CommandError("Invalid help topic {}!".format(topic)) raise cmdexc.CommandError("Invalid help topic {}!".format(topic))
self.openurl('qute://help/{}'.format(path)) url = QUrl('qute://help/{}'.format(path))
self._open(url, tab, bg, window)
@cmdutils.register(instance='command-dispatcher', @cmdutils.register(instance='command-dispatcher',
modes=[usertypes.KeyMode.insert], modes=[usertypes.KeyMode.insert],
hide=True) hide=True, scope='window')
def open_editor(self): def open_editor(self):
"""Open an external editor with the currently selected form field. """Open an external editor with the currently selected form field.
@ -832,9 +891,10 @@ class CommandDispatcher:
text = str(elem) text = str(elem)
else: else:
text = elem.evaluateJavaScript('this.value') text = elem.evaluateJavaScript('this.value')
self._editor = editor.ExternalEditor(objreg.get('tabbed-browser')) self._editor = editor.ExternalEditor(
self._win_id, self._tabbed_browser())
self._editor.editing_finished.connect( self._editor.editing_finished.connect(
partial(self.on_editing_finished, elem)) functools.partial(self.on_editing_finished, elem))
self._editor.edit(text) self._editor.edit(text)
def on_editing_finished(self, elem, text): def on_editing_finished(self, elem, text):

View File

@ -26,6 +26,8 @@ import collections
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject, QTimer, QStandardPaths from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject, QTimer, QStandardPaths
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
# We need this import so PyQt can use it inside pyqtSlot
from PyQt5.QtWebKitWidgets import QWebPage # pylint: disable=unused-import
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.commands import cmdexc, cmdutils
@ -412,7 +414,9 @@ class DownloadManager(QObject):
q.destroyed.connect(functools.partial(self.questions.remove, q)) q.destroyed.connect(functools.partial(self.questions.remove, q))
self.questions.append(q) self.questions.append(q)
download.cancelled.connect(q.abort) download.cancelled.connect(q.abort)
objreg.get('message-bridge').ask(q, blocking=False) message_bridge = objreg.get('message-bridge', scope='window',
window='current')
message_bridge.ask(q, blocking=False)
@pyqtSlot(DownloadItem) @pyqtSlot(DownloadItem)
def on_finished(self, download): def on_finished(self, download):
@ -433,4 +437,4 @@ class DownloadManager(QObject):
@pyqtSlot(str) @pyqtSlot(str)
def on_error(self, msg): def on_error(self, msg):
"""Display error message on download errors.""" """Display error message on download errors."""
message.error("Download error: {}".format(msg)) message.error('current', "Download error: {}".format(msg))

View File

@ -37,16 +37,17 @@ from qutebrowser.utils import usertypes, log, qtutils, message, objreg
ElemTuple = collections.namedtuple('ElemTuple', ['elem', 'label']) ElemTuple = collections.namedtuple('ElemTuple', ['elem', 'label'])
Target = usertypes.enum('Target', ['normal', 'tab', 'tab_bg', 'yank', Target = usertypes.enum('Target', ['normal', 'tab', 'tab_bg', 'window', 'yank',
'yank_primary', 'fill', 'rapid', 'download', 'yank_primary', 'fill', 'rapid',
'userscript', 'spawn']) 'rapid_win', 'download', 'userscript',
'spawn'])
@pyqtSlot(usertypes.KeyMode) @pyqtSlot(usertypes.KeyMode)
def on_mode_entered(mode): def on_mode_entered(mode, win_id):
"""Stop hinting when insert mode was entered.""" """Stop hinting when insert mode was entered."""
if mode == usertypes.KeyMode.insert: if mode == usertypes.KeyMode.insert:
modeman.maybe_leave(usertypes.KeyMode.hint, 'insert mode') modeman.maybe_leave(win_id, usertypes.KeyMode.hint, 'insert mode')
class HintContext: class HintContext:
@ -58,7 +59,7 @@ class HintContext:
elems: A mapping from keystrings to (elem, label) namedtuples. elems: A mapping from keystrings to (elem, label) namedtuples.
baseurl: The URL of the current page. baseurl: The URL of the current page.
target: What to do with the opened links. target: What to do with the opened links.
normal/tab/tab_bg: Get passed to BrowserTab. normal/tab/tab_bg/window: Get passed to BrowserTab.
yank/yank_primary: Yank to clipboard/primary selection yank/yank_primary: Yank to clipboard/primary selection
fill: Fill commandline with link. fill: Fill commandline with link.
rapid: Rapid mode with background tabs rapid: Rapid mode with background tabs
@ -100,6 +101,8 @@ class HintManager(QObject):
Attributes: Attributes:
_context: The HintContext for the current invocation. _context: The HintContext for the current invocation.
_win_id: The window ID this HintManager is associated with.
_tab_id: The tab ID this HintManager is associated with.
Signals: Signals:
mouse_event: Mouse event to be posted in the web view. mouse_event: Mouse event to be posted in the web view.
@ -125,10 +128,12 @@ class HintManager(QObject):
Target.normal: "Follow hint...", Target.normal: "Follow hint...",
Target.tab: "Follow hint in new tab...", Target.tab: "Follow hint in new tab...",
Target.tab_bg: "Follow hint in background tab...", Target.tab_bg: "Follow hint in background tab...",
Target.window: "Follow hint in new window...",
Target.yank: "Yank hint to clipboard...", Target.yank: "Yank hint to clipboard...",
Target.yank_primary: "Yank hint to primary selection...", Target.yank_primary: "Yank hint to primary selection...",
Target.fill: "Set hint in commandline...", Target.fill: "Set hint in commandline...",
Target.rapid: "Follow hint (rapid mode)...", Target.rapid: "Follow hint (rapid mode)...",
Target.rapid_win: "Follow hint in new window (rapid mode)...",
Target.download: "Download hint...", Target.download: "Download hint...",
Target.userscript: "Call userscript via hint...", Target.userscript: "Call userscript via hint...",
Target.spawn: "Spawn command via hint...", Target.spawn: "Spawn command via hint...",
@ -137,15 +142,15 @@ class HintManager(QObject):
mouse_event = pyqtSignal('QMouseEvent') mouse_event = pyqtSignal('QMouseEvent')
set_open_target = pyqtSignal(str) set_open_target = pyqtSignal(str)
def __init__(self, parent=None): def __init__(self, win_id, tab_id, parent=None):
"""Constructor. """Constructor."""
Args:
frame: The QWebFrame to use for finding elements and drawing.
"""
super().__init__(parent) super().__init__(parent)
self._win_id = win_id
self._tab_id = tab_id
self._context = None self._context = None
objreg.get('mode-manager').left.connect(self.on_mode_left) mode_manager = objreg.get('mode-manager', scope='window',
window=win_id)
mode_manager.left.connect(self.on_mode_left)
def _cleanup(self): def _cleanup(self):
"""Clean up after hinting.""" """Clean up after hinting."""
@ -155,7 +160,9 @@ class HintManager(QObject):
except webelem.IsNullError: except webelem.IsNullError:
pass pass
text = self.HINT_TEXTS[self._context.target] text = self.HINT_TEXTS[self._context.target]
objreg.get('message-bridge').maybe_reset_text(text) message_bridge = objreg.get('message-bridge', scope='window',
window=self._win_id)
message_bridge.maybe_reset_text(text)
self._context = None self._context = None
def _hint_strings(self, elems): def _hint_strings(self, elems):
@ -300,6 +307,8 @@ class HintManager(QObject):
""" """
if self._context.target == Target.rapid: if self._context.target == Target.rapid:
target = Target.tab_bg target = Target.tab_bg
elif self._context.target == Target.rapid_win:
target = Target.window
else: else:
target = self._context.target target = self._context.target
self.set_open_target.emit(target.name) self.set_open_target.emit(target.name)
@ -331,8 +340,8 @@ class HintManager(QObject):
mode = QClipboard.Selection if sel else QClipboard.Clipboard mode = QClipboard.Selection if sel else QClipboard.Clipboard
urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword) urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword)
QApplication.clipboard().setText(urlstr, mode) QApplication.clipboard().setText(urlstr, mode)
message.info("URL yanked to {}".format("primary selection" if sel message.info(self._win_id, "URL yanked to {}".format(
else "clipboard")) "primary selection" if sel else "clipboard"))
def _preset_cmd_text(self, url): def _preset_cmd_text(self, url):
"""Preset a commandline text based on a hint URL. """Preset a commandline text based on a hint URL.
@ -342,7 +351,7 @@ class HintManager(QObject):
""" """
urlstr = url.toDisplayString(QUrl.FullyEncoded) urlstr = url.toDisplayString(QUrl.FullyEncoded)
args = self._context.get_args(urlstr) args = self._context.get_args(urlstr)
message.set_cmd_text(' '.join(args)) message.set_cmd_text(self._win_id, ' '.join(args))
def _download(self, elem): def _download(self, elem):
"""Download a hint URL. """Download a hint URL.
@ -352,7 +361,8 @@ class HintManager(QObject):
""" """
url = self._resolve_url(elem) url = self._resolve_url(elem)
if url is None: if url is None:
message.error("No suitable link found for this element.", message.error(self._win_id,
"No suitable link found for this element.",
immediately=True) immediately=True)
return return
objreg.get('download-manager').get(url, elem.webFrame().page()) objreg.get('download-manager').get(url, elem.webFrame().page())
@ -361,7 +371,7 @@ class HintManager(QObject):
"""Call an userscript from a hint.""" """Call an userscript from a hint."""
cmd = self._context.args[0] cmd = self._context.args[0]
args = self._context.args[1:] args = self._context.args[1:]
userscripts.run(cmd, *args, url=url) userscripts.run(cmd, *args, url=url, win_id=self._win_id)
def _spawn(self, url): def _spawn(self, url):
"""Spawn a simple command from a hint.""" """Spawn a simple command from a hint."""
@ -487,17 +497,22 @@ class HintManager(QObject):
for e, string in zip(elems, strings): for e, string in zip(elems, strings):
label = self._draw_label(e, string) label = self._draw_label(e, string)
self._context.elems[string] = ElemTuple(e, label) self._context.elems[string] = ElemTuple(e, label)
keyparser = objreg.get('keyparsers')[usertypes.KeyMode.hint] keyparsers = objreg.get('keyparsers', scope='window',
window=self._win_id)
keyparser = keyparsers[usertypes.KeyMode.hint]
keyparser.update_bindings(strings) keyparser.update_bindings(strings)
def follow_prevnext(self, frame, baseurl, prev=False, newtab=False): def follow_prevnext(self, frame, baseurl, prev=False, tab=False,
background=False, window=False):
"""Click a "previous"/"next" element on the page. """Click a "previous"/"next" element on the page.
Args: Args:
frame: The frame where the element is in. frame: The frame where the element is in.
baseurl: The base URL of the current tab. baseurl: The base URL of the current tab.
prev: True to open a "previous" link, False to open a "next" link. prev: True to open a "previous" link, False to open a "next" link.
newtab: True to open in a new tab, False for the current tab. tab: True to open in a new tab, False for the current tab.
background: True to open in a background tab.
window: True to open in a new window, False for the current one.
""" """
elem = self._find_prevnext(frame, prev) elem = self._find_prevnext(frame, prev)
if elem is None: if elem is None:
@ -507,10 +522,22 @@ class HintManager(QObject):
if url is None: if url is None:
raise cmdexc.CommandError("No {} links found!".format( raise cmdexc.CommandError("No {} links found!".format(
"prev" if prev else "forward")) "prev" if prev else "forward"))
if newtab: qtutils.ensure_valid(url)
objreg.get('tabbed-browser').tabopen(url, background=False) if window:
main_window = objreg.get('main-window', scope='window',
window=self._win_id)
win_id = main_window.spawn()
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
tabbed_browser.tabopen(url, background=False)
elif tab:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
tabbed_browser.tabopen(url, background=background)
else: else:
objreg.get('webview', scope='tab').openurl(url) webview = objreg.get('webview', scope='tab', window=self._win_id,
tab=self._tab_id)
webview.openurl(url)
@cmdutils.register(instance='hintmanager', scope='tab', name='hint') @cmdutils.register(instance='hintmanager', scope='tab', name='hint')
def start(self, group=webelem.Group.all, target=Target.normal, def start(self, group=webelem.Group.all, target=Target.normal,
@ -529,11 +556,14 @@ class HintManager(QObject):
- `normal`: Open the link in the current tab. - `normal`: Open the link in the current tab.
- `tab`: Open the link in a new tab. - `tab`: Open the link in a new tab.
- `tab-bg`: Open the link in a new background tab. - `tab-bg`: Open the link in a new background tab.
- `window`: Open the link in a new window.
- `yank`: Yank the link to the clipboard. - `yank`: Yank the link to the clipboard.
- `yank-primary`: Yank the link to the primary selection. - `yank-primary`: Yank the link to the primary selection.
- `fill`: Fill the commandline with the command given as - `fill`: Fill the commandline with the command given as
argument. argument.
- `rapid`: Open the link in a new tab and stay in hinting mode. - `rapid`: Open the link in a new tab and stay in hinting mode.
- `rapid-win`: Open the link in a new window and stay in
hinting mode.
- `download`: Download the link. - `download`: Download the link.
- `userscript`: Call an userscript with `$QUTE_URL` set to the - `userscript`: Call an userscript with `$QUTE_URL` set to the
link. link.
@ -549,7 +579,8 @@ class HintManager(QObject):
`{hint-url}` will get replaced by the selected `{hint-url}` will get replaced by the selected
URL. URL.
""" """
tabbed_browser = objreg.get('tabbed-browser') tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
widget = tabbed_browser.currentWidget() widget = tabbed_browser.currentWidget()
if widget is None: if widget is None:
raise cmdexc.CommandError("No WebView available yet!") raise cmdexc.CommandError("No WebView available yet!")
@ -563,10 +594,13 @@ class HintManager(QObject):
self._context.frames = webelem.get_child_frames(mainframe) self._context.frames = webelem.get_child_frames(mainframe)
self._context.args = args self._context.args = args
self._init_elements(mainframe, group) self._init_elements(mainframe, group)
objreg.get('message-bridge').set_text(self.HINT_TEXTS[target]) message_bridge = objreg.get('message-bridge', scope='window',
window=self._win_id)
message_bridge.set_text(self.HINT_TEXTS[target])
self._connect_frame_signals() self._connect_frame_signals()
try: try:
modeman.enter(usertypes.KeyMode.hint, 'HintManager.start') modeman.enter(self._win_id, usertypes.KeyMode.hint,
'HintManager.start')
except modeman.ModeLockedError: except modeman.ModeLockedError:
self._cleanup() self._cleanup()
@ -610,7 +644,7 @@ class HintManager(QObject):
visible[k] = e visible[k] = e
if not visible: if not visible:
# Whoops, filtered all hints # Whoops, filtered all hints
modeman.leave(usertypes.KeyMode.hint, 'all filtered') modeman.leave(self._win_id, usertypes.KeyMode.hint, 'all filtered')
elif len(visible) == 1 and config.get('hints', 'auto-follow'): elif len(visible) == 1 and config.get('hints', 'auto-follow'):
# unpacking gets us the first (and only) key in the dict. # unpacking gets us the first (and only) key in the dict.
self.fire(*visible) self.fire(*visible)
@ -631,7 +665,9 @@ class HintManager(QObject):
Target.normal: self._click, Target.normal: self._click,
Target.tab: self._click, Target.tab: self._click,
Target.tab_bg: self._click, Target.tab_bg: self._click,
Target.window: self._click,
Target.rapid: self._click, Target.rapid: self._click,
Target.rapid_win: self._click,
# _download needs a QWebElement to get the frame. # _download needs a QWebElement to get the frame.
Target.download: self._download, Target.download: self._download,
} }
@ -649,14 +685,16 @@ class HintManager(QObject):
elif self._context.target in url_handlers: elif self._context.target in url_handlers:
url = self._resolve_url(elem) url = self._resolve_url(elem)
if url is None: if url is None:
message.error("No suitable link found for this element.", message.error(self._win_id,
"No suitable link found for this element.",
immediately=True) immediately=True)
return return
url_handlers[self._context.target](url) url_handlers[self._context.target](url)
else: else:
raise ValueError("No suitable handler found!") raise ValueError("No suitable handler found!")
if self._context.target != Target.rapid: if self._context.target not in (Target.rapid, Target.rapid_win):
modeman.maybe_leave(usertypes.KeyMode.hint, 'followed') modeman.maybe_leave(self._win_id, usertypes.KeyMode.hint,
'followed')
@cmdutils.register(instance='hintmanager', scope='tab', hide=True) @cmdutils.register(instance='hintmanager', scope='tab', hide=True)
def follow_hint(self): def follow_hint(self):

View File

@ -47,7 +47,7 @@ def init():
try: try:
key, url = line.split(maxsplit=1) key, url = line.split(maxsplit=1)
except ValueError: except ValueError:
message.error("Invalid quickmark '{}'".format(line)) message.error(0, "Invalid quickmark '{}'".format(line))
else: else:
marks[key] = url marks[key] = url
@ -58,22 +58,23 @@ def save():
linecp.save() linecp.save()
def prompt_save(url): def prompt_save(win_id, 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():
urlutils.invalid_url_error(url, "save quickmark") urlutils.invalid_url_error(url, "save quickmark")
return return
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded) urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
message.ask_async("Add quickmark:", usertypes.PromptMode.text, message.ask_async(win_id, "Add quickmark:", usertypes.PromptMode.text,
functools.partial(quickmark_add, urlstr)) functools.partial(quickmark_add, urlstr))
@cmdutils.register() @cmdutils.register()
def quickmark_add(url, name): def quickmark_add(url, name, win_id):
"""Add a new quickmark. """Add a new quickmark.
Args: Args:
@ -83,10 +84,10 @@ def quickmark_add(url, name):
# We don't raise cmdexc.CommandError here as this can be called async via # We don't raise cmdexc.CommandError here as this can be called async via
# prompt_save. # prompt_save.
if not name: if not name:
message.error("Can't set mark with empty name!") message.error(win_id, "Can't set mark with empty name!")
return return
if not url: if not url:
message.error("Can't set mark with empty URL!") message.error(win_id, "Can't set mark with empty URL!")
return return
def set_mark(): def set_mark():
@ -94,7 +95,7 @@ def quickmark_add(url, name):
marks[name] = url marks[name] = url
if name in marks: if name in marks:
message.confirm_async("Override existing quickmark?", set_mark, message.confirm_async(win_id, "Override existing quickmark?", set_mark,
default=True) default=True)
else: else:
set_mark() set_mark()

View File

@ -34,12 +34,19 @@ class SignalFilter(QObject):
Signals are only passed to the parent TabbedBrowser if they originated in Signals are only passed to the parent TabbedBrowser if they originated in
the currently shown widget. the currently shown widget.
Attributes:
_win_id: The window ID this SignalFilter is associated with.
Class attributes: Class attributes:
BLACKLIST: List of signal names which should not be logged. BLACKLIST: List of signal names which should not be logged.
""" """
BLACKLIST = ['cur_scroll_perc_changed', 'cur_progress'] BLACKLIST = ['cur_scroll_perc_changed', 'cur_progress']
def __init__(self, win_id, parent=None):
super().__init__(parent)
self._win_id = win_id
def create(self, signal, tab): def create(self, signal, tab):
"""Factory for partial _filter_signals functions. """Factory for partial _filter_signals functions.
@ -73,7 +80,8 @@ class SignalFilter(QObject):
The target signal if the sender was the current widget. The target signal if the sender was the current widget.
""" """
log_signal = debug.signal_name(signal) not in self.BLACKLIST log_signal = debug.signal_name(signal) not in self.BLACKLIST
tabbed_browser = objreg.get('tabbed-browser') tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
try: try:
tabidx = tabbed_browser.indexOf(tab) tabidx = tabbed_browser.indexOf(tab)
except RuntimeError: except RuntimeError:

View File

@ -41,6 +41,7 @@ class BrowserPage(QWebPage):
Attributes: Attributes:
_extension_handlers: Mapping of QWebPage extensions to their handlers. _extension_handlers: Mapping of QWebPage extensions to their handlers.
_networkmnager: The NetworkManager used. _networkmnager: The NetworkManager used.
_win_id: The window ID this BrowserPage is associated with.
Signals: Signals:
start_download: Emitted when a file should be downloaded. start_download: Emitted when a file should be downloaded.
@ -48,13 +49,14 @@ class BrowserPage(QWebPage):
start_download = pyqtSignal('QNetworkReply*') start_download = pyqtSignal('QNetworkReply*')
def __init__(self, parent=None): def __init__(self, win_id, parent=None):
super().__init__(parent) super().__init__(parent)
self._win_id = win_id
self._extension_handlers = { self._extension_handlers = {
QWebPage.ErrorPageExtension: self._handle_errorpage, QWebPage.ErrorPageExtension: self._handle_errorpage,
QWebPage.ChooseMultipleFilesExtension: self._handle_multiple_files, QWebPage.ChooseMultipleFilesExtension: self._handle_multiple_files,
} }
self._networkmanager = networkmanager.NetworkManager(self) self._networkmanager = networkmanager.NetworkManager(win_id, self)
self.setNetworkAccessManager(self._networkmanager) self.setNetworkAccessManager(self._networkmanager)
self.setForwardUnsupportedContent(True) self.setForwardUnsupportedContent(True)
self.printRequested.connect(self.on_print_requested) self.printRequested.connect(self.on_print_requested)
@ -75,8 +77,8 @@ class BrowserPage(QWebPage):
http://www.riverbankcomputing.com/pipermail/pyqt/2014-June/034385.html http://www.riverbankcomputing.com/pipermail/pyqt/2014-June/034385.html
""" """
answer = message.ask("js: {}".format(msg), usertypes.PromptMode.text, answer = message.ask(self._win_id, "js: {}".format(msg),
default) usertypes.PromptMode.text, default)
if answer is None: if answer is None:
return (False, "") return (False, "")
else: else:
@ -155,8 +157,8 @@ class BrowserPage(QWebPage):
def on_print_requested(self, frame): def on_print_requested(self, frame):
"""Handle printing when requested via javascript.""" """Handle printing when requested via javascript."""
if not qtutils.check_print_compat(): if not qtutils.check_print_compat():
message.error("Printing on Qt < 5.3.0 on Windows is broken, " message.error(self._win_id, "Printing on Qt < 5.3.0 on Windows is "
"please upgrade!", immediately=True) "broken, please upgrade!", immediately=True)
return return
printdiag = QPrintDialog() printdiag = QPrintDialog()
printdiag.setAttribute(Qt.WA_DeleteOnClose) printdiag.setAttribute(Qt.WA_DeleteOnClose)
@ -245,11 +247,12 @@ class BrowserPage(QWebPage):
def javaScriptAlert(self, _frame, msg): def javaScriptAlert(self, _frame, msg):
"""Override javaScriptAlert to use the statusbar.""" """Override javaScriptAlert to use the statusbar."""
message.ask("[js alert] {}".format(msg), usertypes.PromptMode.alert) message.ask(self._win_id, "[js alert] {}".format(msg),
usertypes.PromptMode.alert)
def javaScriptConfirm(self, _frame, msg): def javaScriptConfirm(self, _frame, msg):
"""Override javaScriptConfirm to use the statusbar.""" """Override javaScriptConfirm to use the statusbar."""
ans = message.ask("[js confirm] {}".format(msg), ans = message.ask(self._win_id, "[js confirm] {}".format(msg),
usertypes.PromptMode.yesno) usertypes.PromptMode.yesno)
return bool(ans) return bool(ans)
@ -269,8 +272,8 @@ class BrowserPage(QWebPage):
def shouldInterruptJavaScript(self): def shouldInterruptJavaScript(self):
"""Override shouldInterruptJavaScript to use the statusbar.""" """Override shouldInterruptJavaScript to use the statusbar."""
answer = message.ask("Interrupt long-running javascript?", answer = message.ask(self._win_id, "Interrupt long-running "
usertypes.PromptMode.yesno) "javascript?", usertypes.PromptMode.yesno)
if answer is None: if answer is None:
answer = True answer = True
return answer return answer
@ -295,14 +298,26 @@ class BrowserPage(QWebPage):
url = request.url() url = request.url()
urlstr = url.toDisplayString() urlstr = url.toDisplayString()
if not url.isValid(): if not url.isValid():
message.error("Invalid link {} clicked!".format(urlstr)) message.error(self._win_id, "Invalid link {} clicked!".format(
urlstr))
log.webview.debug(url.errorString()) log.webview.debug(url.errorString())
return False return False
if self.view().open_target == usertypes.ClickTarget.tab: tabbed_browser = objreg.get('tabbed-browser', scope='window',
objreg.get('tabbed-browser').tabopen(url, False) window=self._win_id)
open_target = self.view().open_target
if open_target == usertypes.ClickTarget.tab:
tabbed_browser.tabopen(url, False)
return False return False
elif self.view().open_target == usertypes.ClickTarget.tab_bg: elif open_target == usertypes.ClickTarget.tab_bg:
objreg.get('tabbed-browser').tabopen(url, True) tabbed_browser.tabopen(url, True)
return False
elif open_target == usertypes.ClickTarget.window:
main_window = objreg.get('main-window', scope='window',
window=self._win_id)
win_id = main_window.spawn()
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
tabbed_browser.tabopen(url, False)
return False return False
else: else:
return True return True

View File

@ -58,7 +58,9 @@ class HelpAction(argparse.Action):
""" """
def __call__(self, parser, _namespace, _values, _option_string=None): def __call__(self, parser, _namespace, _values, _option_string=None):
objreg.get('tabbed-browser').tabopen( tabbed_browser = objreg.get('tabbed-browser', scope='window',
window='current')
tabbed_browser.tabopen(
QUrl('qute://help/commands.html#{}'.format(parser.name))) QUrl('qute://help/commands.html#{}'.format(parser.name)))
parser.exit() parser.exit()

View File

@ -95,13 +95,18 @@ class Command:
self._type_conv = type_conv self._type_conv = type_conv
self._name_conv = name_conv self._name_conv = name_conv
def _check_prerequisites(self): def _check_prerequisites(self, win_id):
"""Check if the command is permitted to run currently. """Check if the command is permitted to run currently.
Args:
win_id: The window ID the command is run in.
Raise: Raise:
PrerequisitesError if the command can't be called currently. PrerequisitesError if the command can't be called currently.
""" """
curmode = objreg.get('mode-manager').mode() mode_manager = objreg.get('mode-manager', scope='window',
window=win_id)
curmode = mode_manager.mode()
if self._modes is not None and curmode not in self._modes: if self._modes is not None and curmode not in self._modes:
mode_names = '/'.join(mode.name for mode in self._modes) mode_names = '/'.join(mode.name for mode in self._modes)
raise cmdexc.PrerequisitesError( raise cmdexc.PrerequisitesError(
@ -179,7 +184,7 @@ class Command:
desc = "" desc = ""
if not self.ignore_args: if not self.ignore_args:
for param in signature.parameters.values(): for param in signature.parameters.values():
if param.name in ('self', 'count'): if param.name in ('self', 'count', 'win_id'):
continue continue
annotation_info = self._parse_annotation(param) annotation_info = self._parse_annotation(param)
typ = self._get_type(param, annotation_info) typ = self._get_type(param, annotation_info)
@ -294,15 +299,19 @@ class Command:
else: else:
return type(param.default) return type(param.default)
def _get_self_arg(self, param, args): def _get_self_arg(self, win_id, param, args):
"""Get the self argument for a function call. """Get the self argument for a function call.
Arguments: Arguments:
win_id: The window id this command should be executed in.
param: The count parameter. param: The count parameter.
args: The positional argument list. Gets modified directly. args: The positional argument list. Gets modified directly.
""" """
assert param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD assert param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
obj = objreg.get(self._instance, scope=self._scope) if self._scope is not 'window':
win_id = None
obj = objreg.get(self._instance, scope=self._scope,
window=win_id)
args.append(obj) args.append(obj)
def _get_count_arg(self, param, args, kwargs): def _get_count_arg(self, param, args, kwargs):
@ -328,6 +337,23 @@ class Command:
raise TypeError("{}: invalid parameter type {} for argument " raise TypeError("{}: invalid parameter type {} for argument "
"'count'!".format(self.name, param.kind)) "'count'!".format(self.name, param.kind))
def _get_win_id_arg(self, win_id, param, args, kwargs):
"""Add the win_id argument to a function call.
Arguments:
win_id: The window ID to add.
param: The count parameter.
args: The positional argument list. Gets modified directly.
kwargs: The keyword argument dict. Gets modified directly.
"""
if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
args.append(win_id)
elif param.kind == inspect.Parameter.KEYWORD_ONLY:
kwargs['win_id'] = win_id
else:
raise TypeError("{}: invalid parameter type {} for argument "
"'count'!".format(self.name, param.kind))
def _get_param_name_and_value(self, param): def _get_param_name_and_value(self, param):
"""Get the converted name and value for an inspect.Parameter.""" """Get the converted name and value for an inspect.Parameter."""
name = self._name_conv.get(param.name, param.name) name = self._name_conv.get(param.name, param.name)
@ -340,9 +366,12 @@ class Command:
value = self._type_conv[param.name](value) value = self._type_conv[param.name](value)
return name, value return name, value
def _get_call_args(self): def _get_call_args(self, win_id): # noqa
"""Get arguments for a function call. """Get arguments for a function call.
Args:
win_id: The window id this command should be executed in.
Return: Return:
An (args, kwargs) tuple. An (args, kwargs) tuple.
""" """
@ -354,18 +383,22 @@ class Command:
if self.ignore_args: if self.ignore_args:
if self._instance is not None: if self._instance is not None:
param = list(signature.parameters.values())[0] param = list(signature.parameters.values())[0]
self._get_self_arg(param, args) self._get_self_arg(win_id, param, args)
return args, kwargs return args, kwargs
for i, param in enumerate(signature.parameters.values()): for i, param in enumerate(signature.parameters.values()):
if i == 0 and self._instance is not None: if i == 0 and self._instance is not None:
# Special case for 'self'. # Special case for 'self'.
self._get_self_arg(param, args) self._get_self_arg(win_id, param, args)
continue continue
elif param.name == 'count': elif param.name == 'count':
# Special case for 'count'. # Special case for 'count'.
self._get_count_arg(param, args, kwargs) self._get_count_arg(param, args, kwargs)
continue continue
elif param.name == 'win_id':
# Special case for 'win_id'.
self._get_win_id_arg(win_id, param, args, kwargs)
continue
name, value = self._get_param_name_and_value(param) name, value = self._get_param_name_and_value(param)
if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD: if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
args.append(value) args.append(value)
@ -380,12 +413,13 @@ class Command:
self.name, param.kind, param.name)) self.name, param.kind, param.name))
return args, kwargs return args, kwargs
def run(self, args=None, count=None): def run(self, win_id, args=None, count=None):
"""Run the command. """Run the command.
Note we don't catch CommandError here as it might happen async. Note we don't catch CommandError here as it might happen async.
Args: Args:
win_id: The window ID the command is run in.
args: Arguments to the command. args: Arguments to the command.
count: Command repetition count. count: Command repetition count.
""" """
@ -398,15 +432,15 @@ class Command:
try: try:
self.namespace = self.parser.parse_args(args) self.namespace = self.parser.parse_args(args)
except argparser.ArgumentParserError as e: except argparser.ArgumentParserError as e:
message.error('{}: {}'.format(self.name, e)) message.error(win_id, '{}: {}'.format(self.name, e))
return return
except argparser.ArgumentParserExit as e: except argparser.ArgumentParserExit as e:
log.commands.debug("argparser exited with status {}: {}".format( log.commands.debug("argparser exited with status {}: {}".format(
e.status, e)) e.status, e))
return return
self._count = count self._count = count
posargs, kwargs = self._get_call_args() posargs, kwargs = self._get_call_args(win_id)
self._check_prerequisites() self._check_prerequisites(win_id)
log.commands.debug('Calling {}'.format( log.commands.debug('Calling {}'.format(
debug.format_call(self.handler, posargs, kwargs))) debug.format_call(self.handler, posargs, kwargs)))
self.handler(*posargs, **kwargs) self.handler(*posargs, **kwargs)

View File

@ -27,13 +27,17 @@ from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.utils import message, log, utils, objreg from qutebrowser.utils import message, log, utils, objreg
def replace_variables(arglist): def replace_variables(win_id, arglist):
"""Utility function to replace variables like {url} in a list of args.""" """Utility function to replace variables like {url} in a list of args."""
args = [] args = []
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
for arg in arglist: for arg in arglist:
if arg == '{url}': if arg == '{url}':
url = objreg.get('tabbed-browser').current_url().toString( # Note we have to do this in here as the user gets an error message
QUrl.FullyEncoded | QUrl.RemovePassword) # by current_url if no URL is open yet.
url = tabbed_browser.current_url().toString(QUrl.FullyEncoded |
QUrl.RemovePassword)
args.append(url) args.append(url)
else: else:
args.append(arg) args.append(arg)
@ -114,7 +118,7 @@ class SearchRunner(QObject):
""" """
self._search(text, rev=True) self._search(text, rev=True)
@cmdutils.register(instance='search-runner', hide=True) @cmdutils.register(instance='search-runner', hide=True, scope='window')
def search_next(self, count=1): def search_next(self, count=1):
"""Continue the search to the ([count]th) next term. """Continue the search to the ([count]th) next term.
@ -128,7 +132,7 @@ class SearchRunner(QObject):
for _ in range(count): for _ in range(count):
self.do_search.emit(self._text, self._flags) self.do_search.emit(self._text, self._flags)
@cmdutils.register(instance='search-runner', hide=True) @cmdutils.register(instance='search-runner', hide=True, scope='window')
def search_prev(self, count=1): def search_prev(self, count=1):
"""Continue the search to the ([count]th) previous term. """Continue the search to the ([count]th) previous term.
@ -152,18 +156,21 @@ class SearchRunner(QObject):
self.do_search.emit(self._text, flags) self.do_search.emit(self._text, flags)
class CommandRunner: class CommandRunner(QObject):
"""Parse and run qutebrowser commandline commands. """Parse and run qutebrowser commandline commands.
Attributes: Attributes:
_cmd: The command which was parsed. _cmd: The command which was parsed.
_args: The arguments which were parsed. _args: The arguments which were parsed.
_win_id: The window this CommandRunner is associated with.
""" """
def __init__(self): def __init__(self, win_id, parent=None):
super().__init__(parent)
self._cmd = None self._cmd = None
self._args = [] self._args = []
self._win_id = win_id
def _get_alias(self, text, alias_no_args): def _get_alias(self, text, alias_no_args):
"""Get an alias from the config. """Get an alias from the config.
@ -278,11 +285,11 @@ class CommandRunner:
self.run(sub, count) self.run(sub, count)
return return
self.parse(text) self.parse(text)
args = replace_variables(self._args) args = replace_variables(self._win_id, self._args)
if count is not None: if count is not None:
self._cmd.run(args, count=count) self._cmd.run(self._win_id, args, count=count)
else: else:
self._cmd.run(args) self._cmd.run(self._win_id, args)
@pyqtSlot(str, int) @pyqtSlot(str, int)
def run_safely(self, text, count=None): def run_safely(self, text, count=None):
@ -290,7 +297,7 @@ class CommandRunner:
try: try:
self.run(text, count) self.run(text, count)
except (cmdexc.CommandMetaError, cmdexc.CommandError) as e: except (cmdexc.CommandMetaError, cmdexc.CommandError) as e:
message.error(e, immediately=True) message.error(self._win_id, e, immediately=True)
@pyqtSlot(str, int) @pyqtSlot(str, int)
def run_safely_init(self, text, count=None): def run_safely_init(self, text, count=None):
@ -301,4 +308,4 @@ class CommandRunner:
try: try:
self.run(text, count) self.run(text, count)
except (cmdexc.CommandMetaError, cmdexc.CommandError) as e: except (cmdexc.CommandMetaError, cmdexc.CommandError) as e:
message.error(e) message.error(self._win_id, e)

View File

@ -17,29 +17,20 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Functions to execute an userscript. """Functions to execute an userscript."""
Module attributes:
_runners: Active userscript runners from run_userscript.
"""
import os import os
import os.path import os.path
import tempfile import tempfile
import select import select
import functools
from PyQt5.QtCore import (pyqtSignal, QObject, QThread, QStandardPaths, from PyQt5.QtCore import (pyqtSignal, QObject, QThread, QStandardPaths,
QProcessEnvironment, QProcess, QUrl) QProcessEnvironment, QProcess, QUrl)
from qutebrowser.utils import message, log, utils from qutebrowser.utils import message, log, utils, objreg
from qutebrowser.commands import runners, cmdexc from qutebrowser.commands import runners, cmdexc
_runners = []
_commandrunner = None
class _BlockingFIFOReader(QObject): class _BlockingFIFOReader(QObject):
"""A worker which reads commands from a FIFO endlessly. """A worker which reads commands from a FIFO endlessly.
@ -97,6 +88,7 @@ class _BaseUserscriptRunner(QObject):
Attributes: Attributes:
_filepath: The path of the file/FIFO which is being read. _filepath: The path of the file/FIFO which is being read.
_proc: The QProcess which is being executed. _proc: The QProcess which is being executed.
_win_id: The window ID this runner is associated with.
Class attributes: Class attributes:
PROCESS_MESSAGES: A mapping of QProcess::ProcessError members to PROCESS_MESSAGES: A mapping of QProcess::ProcessError members to
@ -121,8 +113,9 @@ class _BaseUserscriptRunner(QObject):
QProcess.UnknownError: "An unknown error occurred.", QProcess.UnknownError: "An unknown error occurred.",
} }
def __init__(self, parent=None): def __init__(self, win_id, parent=None):
super().__init__(parent) super().__init__(parent)
self._win_id = win_id
self._filepath = None self._filepath = None
self._proc = None self._proc = None
@ -152,7 +145,8 @@ class _BaseUserscriptRunner(QObject):
except PermissionError as e: except PermissionError as e:
# NOTE: Do not replace this with "raise CommandError" as it's # NOTE: Do not replace this with "raise CommandError" as it's
# executed async. # executed async.
message.error("Failed to delete tempfile... ({})".format(e)) message.error(self._win_id,
"Failed to delete tempfile... ({})".format(e))
self._filepath = None self._filepath = None
self._proc = None self._proc = None
@ -180,7 +174,8 @@ class _BaseUserscriptRunner(QObject):
msg = self.PROCESS_MESSAGES[error] msg = self.PROCESS_MESSAGES[error]
# NOTE: Do not replace this with "raise CommandError" as it's # NOTE: Do not replace this with "raise CommandError" as it's
# executed async. # executed async.
message.error("Error while calling userscript: {}".format(msg)) message.error(self._win_id,
"Error while calling userscript: {}".format(msg))
class _POSIXUserscriptRunner(_BaseUserscriptRunner): class _POSIXUserscriptRunner(_BaseUserscriptRunner):
@ -195,8 +190,8 @@ class _POSIXUserscriptRunner(_BaseUserscriptRunner):
_thread: The QThread where reader runs. _thread: The QThread where reader runs.
""" """
def __init__(self, parent=None): def __init__(self, win_id, parent=None):
super().__init__(parent) super().__init__(win_id, parent)
self._reader = None self._reader = None
self._thread = None self._thread = None
@ -262,8 +257,8 @@ class _WindowsUserscriptRunner(_BaseUserscriptRunner):
_oshandle: The oshandle of the temp file. _oshandle: The oshandle of the temp file.
""" """
def __init__(self, parent=None): def __init__(self, win_id, parent=None):
super().__init__(parent) super().__init__(win_id, parent)
self._oshandle = None self._oshandle = None
def _cleanup(self): def _cleanup(self):
@ -326,19 +321,16 @@ else:
UserscriptRunner = _DummyUserscriptRunner UserscriptRunner = _DummyUserscriptRunner
def init(): def run(cmd, *args, url, win_id):
"""Initialize the global _commandrunner."""
global _commandrunner
_commandrunner = runners.CommandRunner()
def run(cmd, *args, url):
"""Convenience method to run an userscript.""" """Convenience method to run an userscript."""
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
# We don't remove the password in the URL here, as it's probably safe to # We don't remove the password in the URL here, as it's probably safe to
# pass via env variable.. # pass via env variable..
urlstr = url.toString(QUrl.FullyEncoded) urlstr = url.toString(QUrl.FullyEncoded)
runner = UserscriptRunner() commandrunner = runners.CommandRunner(win_id, tabbed_browser)
runner.got_cmd.connect(_commandrunner.run_safely) runner = UserscriptRunner(win_id, tabbed_browser)
runner.got_cmd.connect(commandrunner.run_safely)
runner.run(cmd, *args, env={'QUTE_URL': urlstr}) runner.run(cmd, *args, env={'QUTE_URL': urlstr})
_runners.append(runner) runner.finished.connect(commandrunner.deleteLater)
runner.finished.connect(functools.partial(_runners.remove, runner)) runner.finished.connect(runner.deleteLater)

View File

@ -435,7 +435,7 @@ class ConfigManager(QObject):
@cmdutils.register(name='set', instance='config', @cmdutils.register(name='set', instance='config',
completion=[Completion.section, Completion.option, completion=[Completion.section, Completion.option,
Completion.value]) Completion.value])
def set_command(self, sectname: {'name': 'section'}, def set_command(self, win_id, sectname: {'name': 'section'},
optname: {'name': 'option'}, value=None, temp=False): optname: {'name': 'option'}, value=None, temp=False):
"""Set an option. """Set an option.
@ -455,8 +455,8 @@ class ConfigManager(QObject):
try: try:
if optname.endswith('?'): if optname.endswith('?'):
val = self.get(sectname, optname[:-1], transformed=False) val = self.get(sectname, optname[:-1], transformed=False)
message.info("{} {} = {}".format(sectname, optname[:-1], val), message.info(win_id, "{} {} = {}".format(
immediately=True) sectname, optname[:-1], val), immediately=True)
else: else:
if value is None: if value is None:
raise cmdexc.CommandError("set: The following arguments " raise cmdexc.CommandError("set: The following arguments "

View File

@ -878,6 +878,8 @@ KEY_DATA = collections.OrderedDict([
('set-cmd-text ":open -t {url}"', ['gO']), ('set-cmd-text ":open -t {url}"', ['gO']),
('set-cmd-text ":open -b "', ['xo']), ('set-cmd-text ":open -b "', ['xo']),
('set-cmd-text ":open -b {url}"', ['xO']), ('set-cmd-text ":open -b {url}"', ['xO']),
('set-cmd-text ":open -w "', ['wo']),
('set-cmd-text ":open -w {url}"', ['wO']),
('open -t about:blank', ['ga']), ('open -t about:blank', ['ga']),
('tab-close', ['d', '<Ctrl-W>']), ('tab-close', ['d', '<Ctrl-W>']),
('tab-only', ['co']), ('tab-only', ['co']),
@ -891,10 +893,13 @@ KEY_DATA = collections.OrderedDict([
('reload', ['r']), ('reload', ['r']),
('back', ['H', '<Backspace>']), ('back', ['H', '<Backspace>']),
('back -t', ['th']), ('back -t', ['th']),
('back -w', ['wh']),
('forward', ['L']), ('forward', ['L']),
('forward -t', ['tl']), ('forward -t', ['tl']),
('forward -w', ['wl']),
('hint', ['f']), ('hint', ['f']),
('hint all tab', ['F']), ('hint all tab', ['F']),
('hint all window', ['wf']),
('hint all tab-bg', [';b']), ('hint all tab-bg', [';b']),
('hint images', [';i']), ('hint images', [';i']),
('hint images tab', [';I']), ('hint images tab', [';I']),
@ -905,6 +910,7 @@ KEY_DATA = collections.OrderedDict([
('hint links yank', [';y']), ('hint links yank', [';y']),
('hint links yank-primary', [';Y']), ('hint links yank-primary', [';Y']),
('hint links rapid', [';r']), ('hint links rapid', [';r']),
('hint links rapid-win', [';R']),
('hint links download', [';d']), ('hint links download', [';d']),
('scroll -50 0', ['h']), ('scroll -50 0', ['h']),
('scroll 0 50', ['j']), ('scroll 0 50', ['j']),
@ -924,9 +930,12 @@ KEY_DATA = collections.OrderedDict([
('paste -s', ['pP']), ('paste -s', ['pP']),
('paste -t', ['Pp']), ('paste -t', ['Pp']),
('paste -ts', ['PP']), ('paste -ts', ['PP']),
('paste -w', ['wp']),
('paste -ws', ['wP']),
('quickmark-save', ['m']), ('quickmark-save', ['m']),
('set-cmd-text ":quickmark-load "', ['b']), ('set-cmd-text ":quickmark-load "', ['b']),
('set-cmd-text ":quickmark-load -t "', ['B']), ('set-cmd-text ":quickmark-load -t "', ['B']),
('set-cmd-text ":quickmark-load -w"', ['wb']),
('save', ['sf']), ('save', ['sf']),
('set-cmd-text ":set "', ['ss']), ('set-cmd-text ":set "', ['ss']),
('set-cmd-text ":set -t "', ['sl']), ('set-cmd-text ":set -t "', ['sl']),

View File

@ -21,12 +21,13 @@
import os import os
import os.path import os.path
import collections
from qutebrowser.utils import log, utils from qutebrowser.utils import log, utils
from qutebrowser.config import config from qutebrowser.config import config
class LineConfigParser: class LineConfigParser(collections.UserList):
"""Parser for configuration files which are simply line-based. """Parser for configuration files which are simply line-based.
@ -47,6 +48,7 @@ class LineConfigParser:
limit: Config tuple (section, option) which contains a limit. limit: Config tuple (section, option) which contains a limit.
binary: Whether to open the file in binary mode. binary: Whether to open the file in binary mode.
""" """
super().__init__()
self._configdir = configdir self._configdir = configdir
self._configfile = os.path.join(self._configdir, fname) self._configfile = os.path.join(self._configdir, fname)
self._fname = fname self._fname = fname
@ -65,10 +67,6 @@ class LineConfigParser:
configdir=self._configdir, fname=self._fname, configdir=self._configdir, fname=self._fname,
limit=self._limit, binary=self._binary) limit=self._limit, binary=self._binary)
def __iter__(self):
"""Iterate over the set data."""
return self.data.__iter__()
def read(self, filename): def read(self, filename):
"""Read the data from a file.""" """Read the data from a file."""
if self._binary: if self._binary:

View File

@ -53,6 +53,7 @@ class BaseKeyParser(QObject):
Attributes: Attributes:
bindings: Bound keybindings bindings: Bound keybindings
special_bindings: Bound special bindings (<Foo>). special_bindings: Bound special bindings (<Foo>).
_win_id: The window ID this keyparser is associated with.
_warn_on_keychains: Whether a warning should be logged when binding _warn_on_keychains: Whether a warning should be logged when binding
keychains in a section which does not support them. keychains in a section which does not support them.
_keystring: The currently entered key sequence _keystring: The currently entered key sequence
@ -73,9 +74,10 @@ class BaseKeyParser(QObject):
'none']) 'none'])
Type = usertypes.enum('Type', ['chain', 'special']) Type = usertypes.enum('Type', ['chain', 'special'])
def __init__(self, parent=None, supports_count=None, def __init__(self, win_id, parent=None, supports_count=None,
supports_chains=False): supports_chains=False):
super().__init__(parent) super().__init__(parent)
self._win_id = win_id
self._timer = None self._timer = None
self._modename = None self._modename = None
self._keystring = '' self._keystring = ''

View File

@ -32,16 +32,16 @@ class CommandKeyParser(BaseKeyParser):
_commandrunner: CommandRunner instance. _commandrunner: CommandRunner instance.
""" """
def __init__(self, parent=None, supports_count=None, def __init__(self, win_id, parent=None, supports_count=None,
supports_chains=False): supports_chains=False):
super().__init__(parent, supports_count, supports_chains) super().__init__(win_id, parent, supports_count, supports_chains)
self._commandrunner = runners.CommandRunner() self._commandrunner = runners.CommandRunner(win_id)
def execute(self, cmdstr, _keytype, count=None): def execute(self, cmdstr, _keytype, count=None):
try: try:
self._commandrunner.run(cmdstr, count) self._commandrunner.run(cmdstr, count)
except (cmdexc.CommandMetaError, cmdexc.CommandError) as e: except (cmdexc.CommandMetaError, cmdexc.CommandError) as e:
message.error(e, immediately=True) message.error(self._win_id, e, immediately=True)
class PassthroughKeyParser(CommandKeyParser): class PassthroughKeyParser(CommandKeyParser):
@ -56,7 +56,7 @@ class PassthroughKeyParser(CommandKeyParser):
do_log = False do_log = False
def __init__(self, mode, parent=None, warn=True): def __init__(self, win_id, mode, parent=None, warn=True):
"""Constructor. """Constructor.
Args: Args:
@ -64,7 +64,7 @@ class PassthroughKeyParser(CommandKeyParser):
parent: Qt parent. parent: Qt parent.
warn: Whether to warn if an ignored key was bound. warn: Whether to warn if an ignored key was bound.
""" """
super().__init__(parent, supports_chains=False) super().__init__(win_id, parent, supports_chains=False)
self._warn_on_keychains = warn self._warn_on_keychains = warn
self.read_config(mode) self.read_config(mode)
self._mode = mode self._mode = mode

View File

@ -23,6 +23,8 @@ Module attributes:
manager: The ModeManager instance. manager: The ModeManager instance.
""" """
import functools
from PyQt5.QtGui import QWindow from PyQt5.QtGui import QWindow
from PyQt5.QtCore import pyqtSignal, QObject, QEvent from PyQt5.QtCore import pyqtSignal, QObject, QEvent
from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QApplication
@ -43,22 +45,26 @@ class NotInModeError(Exception):
"""Exception raised when we want to leave a mode we're not in.""" """Exception raised when we want to leave a mode we're not in."""
def init(): def init(win_id, parent):
"""Inizialize the mode manager and the keyparsers.""" """Inizialize the mode manager and the keyparsers for the given win_id."""
KM = usertypes.KeyMode # pylint: disable=invalid-name KM = usertypes.KeyMode # pylint: disable=invalid-name
modeman = ModeManager(objreg.get('app')) modeman = ModeManager(win_id, parent)
objreg.register('mode-manager', modeman) objreg.register('mode-manager', modeman, scope='window', window=win_id)
keyparsers = { keyparsers = {
KM.normal: modeparsers.NormalKeyParser(modeman), KM.normal: modeparsers.NormalKeyParser(win_id, modeman),
KM.hint: modeparsers.HintKeyParser(modeman), KM.hint: modeparsers.HintKeyParser(win_id, modeman),
KM.insert: keyparser.PassthroughKeyParser('insert', modeman), KM.insert: keyparser.PassthroughKeyParser(win_id, 'insert', modeman),
KM.passthrough: keyparser.PassthroughKeyParser('passthrough', modeman), KM.passthrough: keyparser.PassthroughKeyParser(win_id, 'passthrough',
KM.command: keyparser.PassthroughKeyParser('command', modeman), modeman),
KM.prompt: keyparser.PassthroughKeyParser('prompt', modeman, KM.command: keyparser.PassthroughKeyParser(win_id, 'command', modeman),
KM.prompt: keyparser.PassthroughKeyParser(win_id, 'prompt', modeman,
warn=False), warn=False),
KM.yesno: modeparsers.PromptKeyParser(modeman), KM.yesno: modeparsers.PromptKeyParser(win_id, modeman),
} }
objreg.register('keyparsers', keyparsers) objreg.register('keyparsers', keyparsers, scope='window', window=win_id)
modeman.destroyed.connect(
functools.partial(objreg.delete, 'keyparsers', scope='window',
window=win_id))
modeman.register(KM.normal, keyparsers[KM.normal].handle) modeman.register(KM.normal, keyparsers[KM.normal].handle)
modeman.register(KM.hint, keyparsers[KM.hint].handle) modeman.register(KM.hint, keyparsers[KM.hint].handle)
modeman.register(KM.insert, keyparsers[KM.insert].handle, passthrough=True) modeman.register(KM.insert, keyparsers[KM.insert].handle, passthrough=True)
@ -68,35 +74,66 @@ def init():
passthrough=True) passthrough=True)
modeman.register(KM.prompt, keyparsers[KM.prompt].handle, passthrough=True) modeman.register(KM.prompt, keyparsers[KM.prompt].handle, passthrough=True)
modeman.register(KM.yesno, keyparsers[KM.yesno].handle) modeman.register(KM.yesno, keyparsers[KM.yesno].handle)
return modeman
def enter(mode, reason=None, override=False): def _get_modeman(win_id):
"""Get a modemanager object."""
return objreg.get('mode-manager', scope='window', window=win_id)
def enter(win_id, mode, reason=None, override=False):
"""Enter the mode 'mode'.""" """Enter the mode 'mode'."""
objreg.get('mode-manager').enter(mode, reason, override) _get_modeman(win_id).enter(mode, reason, override)
def leave(mode, reason=None): def leave(win_id, mode, reason=None):
"""Leave the mode 'mode'.""" """Leave the mode 'mode'."""
objreg.get('mode-manager').leave(mode, reason) _get_modeman(win_id).leave(mode, reason)
def maybe_enter(mode, reason=None, override=False): def maybe_enter(win_id, mode, reason=None, override=False):
"""Convenience method to enter 'mode' without exceptions.""" """Convenience method to enter 'mode' without exceptions."""
try: try:
objreg.get('mode-manager').enter(mode, reason, override) _get_modeman(win_id).enter(mode, reason, override)
except ModeLockedError: except ModeLockedError:
pass pass
def maybe_leave(mode, reason=None): def maybe_leave(win_id, mode, reason=None):
"""Convenience method to leave 'mode' without exceptions.""" """Convenience method to leave 'mode' without exceptions."""
try: try:
objreg.get('mode-manager').leave(mode, reason) _get_modeman(win_id).leave(mode, reason)
except NotInModeError as e: except NotInModeError as e:
# This is rather likely to happen, so we only log to debug log. # This is rather likely to happen, so we only log to debug log.
log.modes.debug("{} (leave reason: {})".format(e, reason)) log.modes.debug("{} (leave reason: {})".format(e, reason))
class EventFilter(QObject):
"""Event filter which passes the event to the corrent ModeManager."""
def __init__(self, parent=None):
super().__init__(parent)
self._activated = True
def eventFilter(self, obj, event):
"""Forward events to the correct modeman."""
if not self._activated:
return False
try:
modeman = objreg.get('mode-manager', scope='window',
window='current')
return modeman.eventFilter(obj, event)
except objreg.RegistryUnavailableError:
# No window available yet, or not a MainWindow
return False
except:
# If there is an exception in here and we leave the eventfilter
# activated, we'll get an infinite loop and a stack overflow.
self._activated = False
raise
class ModeManager(QObject): class ModeManager(QObject):
"""Manager for keyboard modes. """Manager for keyboard modes.
@ -106,6 +143,7 @@ class ModeManager(QObject):
locked: Whether current mode is locked. This means the current mode can locked: Whether current mode is locked. This means the current mode can
still be left (then locked will be reset), but no new mode can still be left (then locked will be reset), but no new mode can
be entered while the current mode is active. be entered while the current mode is active.
_win_id: The window ID of this ModeManager
_handlers: A dictionary of modes and their handlers. _handlers: A dictionary of modes and their handlers.
_mode_stack: A list of the modes we're currently in, with the active _mode_stack: A list of the modes we're currently in, with the active
one on the right. one on the right.
@ -116,16 +154,19 @@ class ModeManager(QObject):
Signals: Signals:
entered: Emitted when a mode is entered. entered: Emitted when a mode is entered.
arg: The mode which has been entered. arg1: The mode which has been entered.
arg2: The window ID of this mode manager.
left: Emitted when a mode is left. left: Emitted when a mode is left.
arg: The mode which has been left. arg1: The mode which has been left.
arg2: The window ID of this mode manager.
""" """
entered = pyqtSignal(usertypes.KeyMode) entered = pyqtSignal(usertypes.KeyMode, int)
left = pyqtSignal(usertypes.KeyMode) left = pyqtSignal(usertypes.KeyMode, int)
def __init__(self, parent=None): def __init__(self, win_id, parent=None):
super().__init__(parent) super().__init__(parent)
self._win_id = win_id
self.locked = False self.locked = False
self._handlers = {} self._handlers = {}
self.passthrough = [] self.passthrough = []
@ -251,9 +292,9 @@ class ModeManager(QObject):
return return
self._mode_stack.append(mode) self._mode_stack.append(mode)
log.modes.debug("New mode stack: {}".format(self._mode_stack)) log.modes.debug("New mode stack: {}".format(self._mode_stack))
self.entered.emit(mode) self.entered.emit(mode, self._win_id)
@cmdutils.register(instance='mode-manager', hide=True) @cmdutils.register(instance='mode-manager', hide=True, scope='window')
def enter_mode(self, mode): def enter_mode(self, mode):
"""Enter a key mode. """Enter a key mode.
@ -284,10 +325,11 @@ class ModeManager(QObject):
log.modes.debug("Leaving mode {}{}, new mode stack {}".format( log.modes.debug("Leaving mode {}{}, new mode stack {}".format(
mode, '' if reason is None else ' (reason: {})'.format(reason), mode, '' if reason is None else ' (reason: {})'.format(reason),
self._mode_stack)) self._mode_stack))
self.left.emit(mode) self.left.emit(mode, self._win_id)
@cmdutils.register(instance='mode-manager', name='leave-mode', @cmdutils.register(instance='mode-manager', name='leave-mode',
not_modes=[usertypes.KeyMode.normal], hide=True) not_modes=[usertypes.KeyMode.normal], hide=True,
scope='window')
def leave_current_mode(self): def leave_current_mode(self):
"""Leave the mode we're currently in.""" """Leave the mode we're currently in."""
if self.mode() == usertypes.KeyMode.normal: if self.mode() == usertypes.KeyMode.normal:
@ -321,8 +363,8 @@ class ModeManager(QObject):
# We already handled this same event at some point earlier, so # We already handled this same event at some point earlier, so
# we're not interested in it anymore. # we're not interested in it anymore.
return False return False
if (QApplication.instance().activeWindow() is not if (QApplication.instance().activeWindow() not in
objreg.get('main-window')): objreg.window_registry.values()):
# Some other window (print dialog, etc.) is focused so we pass # Some other window (print dialog, etc.) is focused so we pass
# the event through. # the event through.
return False return False

View File

@ -39,8 +39,9 @@ class NormalKeyParser(keyparser.CommandKeyParser):
"""KeyParser for normalmode with added STARTCHARS detection.""" """KeyParser for normalmode with added STARTCHARS detection."""
def __init__(self, parent=None): def __init__(self, win_id, parent=None):
super().__init__(parent, supports_count=True, supports_chains=True) super().__init__(win_id, parent, supports_count=True,
supports_chains=True)
self.read_config('normal') self.read_config('normal')
def __repr__(self): def __repr__(self):
@ -57,7 +58,7 @@ class NormalKeyParser(keyparser.CommandKeyParser):
""" """
txt = e.text().strip() txt = e.text().strip()
if not self._keystring and any(txt == c for c in STARTCHARS): if not self._keystring and any(txt == c for c in STARTCHARS):
message.set_cmd_text(txt) message.set_cmd_text(self._win_id, txt)
return True return True
return super()._handle_single_key(e) return super()._handle_single_key(e)
@ -66,8 +67,9 @@ class PromptKeyParser(keyparser.CommandKeyParser):
"""KeyParser for yes/no prompts.""" """KeyParser for yes/no prompts."""
def __init__(self, parent=None): def __init__(self, win_id, parent=None):
super().__init__(parent, supports_count=False, supports_chains=True) super().__init__(win_id, parent, supports_count=False,
supports_chains=True)
# We don't want an extra section for this in the config, so we just # We don't want an extra section for this in the config, so we just
# abuse the prompt section. # abuse the prompt section.
self.read_config('prompt') self.read_config('prompt')
@ -85,8 +87,9 @@ class HintKeyParser(keyparser.CommandKeyParser):
_last_press: The nature of the last keypress, a LastPress member. _last_press: The nature of the last keypress, a LastPress member.
""" """
def __init__(self, parent=None): def __init__(self, win_id, parent=None):
super().__init__(parent, supports_count=False, supports_chains=True) super().__init__(win_id, parent, supports_count=False,
supports_chains=True)
self._filtertext = '' self._filtertext = ''
self._last_press = LastPress.none self._last_press = LastPress.none
self.read_config('hint') self.read_config('hint')
@ -108,7 +111,8 @@ class HintKeyParser(keyparser.CommandKeyParser):
""" """
log.keyboard.debug("Got special key 0x{:x} text {}".format( log.keyboard.debug("Got special key 0x{:x} text {}".format(
e.key(), e.text())) e.key(), e.text()))
hintmanager = objreg.get('hintmanager', scope='tab') hintmanager = objreg.get('hintmanager', scope='tab',
window=self._win_id, tab='current')
if e.key() == Qt.Key_Backspace: if e.key() == Qt.Key_Backspace:
log.keyboard.debug("Got backspace, mode {}, filtertext '{}', " log.keyboard.debug("Got backspace, mode {}, filtertext '{}', "
"keystring '{}'".format(self._last_press, "keystring '{}'".format(self._last_press,
@ -163,7 +167,9 @@ class HintKeyParser(keyparser.CommandKeyParser):
if not isinstance(keytype, self.Type): if not isinstance(keytype, self.Type):
raise TypeError("Type {} is no Type member!".format(keytype)) raise TypeError("Type {} is no Type member!".format(keytype))
if keytype == self.Type.chain: if keytype == self.Type.chain:
objreg.get('hintmanager', scope='tab').fire(cmdstr) hintmanager = objreg.get('hintmanager', scope='tab',
window=self._win_id, tab='current')
hintmanager.fire(cmdstr)
else: else:
# execute as command # execute as command
super().execute(cmdstr, keytype, count) super().execute(cmdstr, keytype, count)
@ -180,4 +186,6 @@ class HintKeyParser(keyparser.CommandKeyParser):
@pyqtSlot(str) @pyqtSlot(str)
def on_keystring_updated(self, keystr): def on_keystring_updated(self, keystr):
"""Update hintmanager when the keystring was updated.""" """Update hintmanager when the keystring was updated."""
objreg.get('hintmanager', scope='tab').handle_partial_key(keystr) hintmanager = objreg.get('hintmanager', scope='tab',
window=self._win_id, tab='current')
hintmanager.handle_partial_key(keystr)

View File

@ -42,14 +42,16 @@ class NetworkManager(QNetworkAccessManager):
_requests: Pending requests. _requests: Pending requests.
_scheme_handlers: A dictionary (scheme -> handler) of supported custom _scheme_handlers: A dictionary (scheme -> handler) of supported custom
schemes. schemes.
_win_id: The window ID this NetworkManager is associated with.
""" """
def __init__(self, parent=None): def __init__(self, win_id, parent=None):
log.init.debug("Initializing NetworkManager") log.init.debug("Initializing NetworkManager")
super().__init__(parent) super().__init__(parent)
self._win_id = win_id
self._requests = [] self._requests = []
self._scheme_handlers = { self._scheme_handlers = {
'qute': qutescheme.QuteSchemeHandler(), 'qute': qutescheme.QuteSchemeHandler(win_id),
} }
# We have a shared cookie jar and cache - we restore their parents so # We have a shared cookie jar and cache - we restore their parents so
@ -101,20 +103,22 @@ class NetworkManager(QNetworkAccessManager):
for err in errors: for err in errors:
# FIXME we might want to use warn here (non-fatal error) # FIXME we might want to use warn here (non-fatal error)
# https://github.com/The-Compiler/qutebrowser/issues/114 # https://github.com/The-Compiler/qutebrowser/issues/114
message.error('SSL error: {}'.format(err.errorString())) message.error(self._win_id,
'SSL error: {}'.format(err.errorString()))
reply.ignoreSslErrors() reply.ignoreSslErrors()
@pyqtSlot('QNetworkReply', 'QAuthenticator') @pyqtSlot('QNetworkReply', 'QAuthenticator')
def on_authentication_required(self, _reply, authenticator): def on_authentication_required(self, _reply, authenticator):
"""Called when a website needs authentication.""" """Called when a website needs authentication."""
answer = message.ask("Username ({}):".format(authenticator.realm()), answer = message.ask(self._win_id,
"Username ({}):".format(authenticator.realm()),
mode=usertypes.PromptMode.user_pwd) mode=usertypes.PromptMode.user_pwd)
self._fill_authenticator(authenticator, answer) self._fill_authenticator(authenticator, answer)
@pyqtSlot('QNetworkProxy', 'QAuthenticator') @pyqtSlot('QNetworkProxy', 'QAuthenticator')
def on_proxy_authentication_required(self, _proxy, authenticator): def on_proxy_authentication_required(self, _proxy, authenticator):
"""Called when a proxy needs authentication.""" """Called when a proxy needs authentication."""
answer = message.ask("Proxy username ({}):".format( answer = message.ask(self._win_id, "Proxy username ({}):".format(
authenticator.realm()), mode=usertypes.PromptMode.user_pwd) authenticator.realm()), mode=usertypes.PromptMode.user_pwd)
self._fill_authenticator(authenticator, answer) self._fill_authenticator(authenticator, answer)

View File

@ -69,7 +69,7 @@ class QuteSchemeHandler(schemehandler.SchemeHandler):
request, errorstr, QNetworkReply.ContentNotFoundError, request, errorstr, QNetworkReply.ContentNotFoundError,
self.parent()) self.parent())
try: try:
data = handler(request) data = handler(self._win_id, request)
except IOError as e: except IOError as e:
return schemehandler.ErrorNetworkReply( return schemehandler.ErrorNetworkReply(
request, str(e), QNetworkReply.ContentNotFoundError, request, str(e), QNetworkReply.ContentNotFoundError,
@ -78,14 +78,14 @@ class QuteSchemeHandler(schemehandler.SchemeHandler):
request, data, 'text/html', self.parent()) request, data, 'text/html', self.parent())
def qute_pyeval(_request): def qute_pyeval(_win_id, _request):
"""Handler for qute:pyeval. Return HTML content as bytes.""" """Handler for qute:pyeval. Return HTML content as bytes."""
html = jinja.env.get_template('pre.html').render( html = jinja.env.get_template('pre.html').render(
title='pyeval', content=pyeval_output) title='pyeval', content=pyeval_output)
return html.encode('UTF-8', errors='xmlcharrefreplace') return html.encode('UTF-8', errors='xmlcharrefreplace')
def qute_version(_request): def qute_version(_win_id, _request):
"""Handler for qute:version. Return HTML content as bytes.""" """Handler for qute:version. Return HTML content as bytes."""
html = jinja.env.get_template('version.html').render( html = jinja.env.get_template('version.html').render(
title='Version info', version=version.version(), title='Version info', version=version.version(),
@ -93,7 +93,7 @@ def qute_version(_request):
return html.encode('UTF-8', errors='xmlcharrefreplace') return html.encode('UTF-8', errors='xmlcharrefreplace')
def qute_plainlog(_request): def qute_plainlog(_win_id, _request):
"""Handler for qute:plainlog. Return HTML content as bytes.""" """Handler for qute:plainlog. Return HTML content as bytes."""
if log.ram_handler is None: if log.ram_handler is None:
text = "Log output was disabled." text = "Log output was disabled."
@ -103,7 +103,7 @@ def qute_plainlog(_request):
return html.encode('UTF-8', errors='xmlcharrefreplace') return html.encode('UTF-8', errors='xmlcharrefreplace')
def qute_log(_request): def qute_log(_win_id, _request):
"""Handler for qute:log. Return HTML content as bytes.""" """Handler for qute:log. Return HTML content as bytes."""
if log.ram_handler is None: if log.ram_handler is None:
html_log = None html_log = None
@ -114,12 +114,12 @@ def qute_log(_request):
return html.encode('UTF-8', errors='xmlcharrefreplace') return html.encode('UTF-8', errors='xmlcharrefreplace')
def qute_gpl(_request): def qute_gpl(_win_id, _request):
"""Handler for qute:gpl. Return HTML content as bytes.""" """Handler for qute:gpl. Return HTML content as bytes."""
return utils.read_file('html/COPYING.html').encode('ASCII') return utils.read_file('html/COPYING.html').encode('ASCII')
def qute_help(request): def qute_help(win_id, request):
"""Handler for qute:help. Return HTML content as bytes.""" """Handler for qute:help. Return HTML content as bytes."""
try: try:
utils.read_file('html/doc/index.html') utils.read_file('html/doc/index.html')
@ -140,8 +140,8 @@ def qute_help(request):
else: else:
urlpath = urlpath.lstrip('/') urlpath = urlpath.lstrip('/')
if not docutils.docs_up_to_date(urlpath): if not docutils.docs_up_to_date(urlpath):
message.error("Your documentation is outdated! Please re-run scripts/" message.error(win_id, "Your documentation is outdated! Please re-run "
"asciidoc2html.py.") "scripts/asciidoc2html.py.")
path = 'html/doc/{}'.format(urlpath) path = 'html/doc/{}'.format(urlpath)
return utils.read_file(path).encode('UTF-8', errors='xmlcharrefreplace') return utils.read_file(path).encode('UTF-8', errors='xmlcharrefreplace')

View File

@ -28,7 +28,15 @@ from PyQt5.QtCore import pyqtSlot, QObject, QIODevice, QByteArray, QTimer
class SchemeHandler(QObject): class SchemeHandler(QObject):
"""Abstract base class for custom scheme handlers.""" """Abstract base class for custom scheme handlers.
Attributes:
_win_id: The window ID this scheme handler is associated with.
"""
def __init__(self, win_id, parent=None):
super().__init__(parent)
self._win_id = win_id
def createRequest(self, op, request, outgoing_data): def createRequest(self, op, request, outgoing_data):
"""Create a new request. """Create a new request.

View File

@ -91,7 +91,8 @@ def get_argparser():
parser.add_argument('command', nargs='*', help="Commands to execute on " parser.add_argument('command', nargs='*', help="Commands to execute on "
"startup.", metavar=':command') "startup.", metavar=':command')
# URLs will actually be in command # URLs will actually be in command
parser.add_argument('url', nargs='*', help="URLs to open on startup.") parser.add_argument('url', nargs='*', help="URLs to open on startup "
"(empty as a window separator).")
return parser return parser

View File

@ -67,7 +67,7 @@ class SplitCountTests(unittest.TestCase):
""" """
def setUp(self): def setUp(self):
self.kp = basekeyparser.BaseKeyParser(supports_count=True) self.kp = basekeyparser.BaseKeyParser(0, supports_count=True)
def test_onlycount(self): def test_onlycount(self):
"""Test split_count with only a count.""" """Test split_count with only a count."""
@ -114,13 +114,13 @@ class ReadConfigTests(unittest.TestCase):
def test_read_config_invalid(self): def test_read_config_invalid(self):
"""Test reading config without setting it before.""" """Test reading config without setting it before."""
kp = basekeyparser.BaseKeyParser() kp = basekeyparser.BaseKeyParser(0)
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
kp.read_config() kp.read_config()
def test_read_config_valid(self): def test_read_config_valid(self):
"""Test reading config.""" """Test reading config."""
kp = basekeyparser.BaseKeyParser(supports_count=True, kp = basekeyparser.BaseKeyParser(0, supports_count=True,
supports_chains=True) supports_chains=True)
kp.read_config('test') kp.read_config('test')
self.assertIn('ccc', kp.bindings) self.assertIn('ccc', kp.bindings)
@ -147,7 +147,7 @@ class SpecialKeysTests(unittest.TestCase):
patcher.start() patcher.start()
objreg.register('key-config', fake_keyconfig) objreg.register('key-config', fake_keyconfig)
self.addCleanup(patcher.stop) self.addCleanup(patcher.stop)
self.kp = basekeyparser.BaseKeyParser() self.kp = basekeyparser.BaseKeyParser(0)
self.kp.execute = mock.Mock() self.kp.execute = mock.Mock()
self.kp.read_config('test') self.kp.read_config('test')
@ -187,7 +187,7 @@ class KeyChainTests(unittest.TestCase):
objreg.register('key-config', fake_keyconfig) objreg.register('key-config', fake_keyconfig)
self.timermock = mock.Mock() self.timermock = mock.Mock()
basekeyparser.usertypes.Timer = mock.Mock(return_value=self.timermock) basekeyparser.usertypes.Timer = mock.Mock(return_value=self.timermock)
self.kp = basekeyparser.BaseKeyParser(supports_chains=True, self.kp = basekeyparser.BaseKeyParser(0, supports_chains=True,
supports_count=False) supports_count=False)
self.kp.execute = mock.Mock() self.kp.execute = mock.Mock()
self.kp.read_config('test') self.kp.read_config('test')
@ -254,7 +254,7 @@ class CountTests(unittest.TestCase):
def setUp(self): def setUp(self):
objreg.register('key-config', fake_keyconfig) objreg.register('key-config', fake_keyconfig)
basekeyparser.usertypes.Timer = mock.Mock() basekeyparser.usertypes.Timer = mock.Mock()
self.kp = basekeyparser.BaseKeyParser(supports_chains=True, self.kp = basekeyparser.BaseKeyParser(0, supports_chains=True,
supports_count=True) supports_count=True)
self.kp.execute = mock.Mock() self.kp.execute = mock.Mock()
self.kp.read_config('test') self.kp.read_config('test')

View File

@ -54,7 +54,7 @@ class ArgTests(unittest.TestCase):
""" """
def setUp(self): def setUp(self):
self.editor = editor.ExternalEditor() self.editor = editor.ExternalEditor(0)
def test_simple_start_args(self): def test_simple_start_args(self):
"""Test starting editor without arguments.""" """Test starting editor without arguments."""
@ -102,7 +102,7 @@ class FileHandlingTests(unittest.TestCase):
""" """
def setUp(self): def setUp(self):
self.editor = editor.ExternalEditor() self.editor = editor.ExternalEditor(0)
editor.config = stubs.ConfigStub( editor.config = stubs.ConfigStub(
{'general': {'editor': [''], 'editor-encoding': 'utf-8'}}) {'general': {'editor': [''], 'editor-encoding': 'utf-8'}})
@ -141,7 +141,7 @@ class TextModifyTests(unittest.TestCase):
""" """
def setUp(self): def setUp(self):
self.editor = editor.ExternalEditor() self.editor = editor.ExternalEditor(0)
self.editor.editing_finished = mock.Mock() self.editor.editing_finished = mock.Mock()
editor.config = stubs.ConfigStub( editor.config = stubs.ConfigStub(
{'general': {'editor': [''], 'editor-encoding': 'utf-8'}}) {'general': {'editor': [''], 'editor-encoding': 'utf-8'}})
@ -211,7 +211,7 @@ class ErrorMessageTests(unittest.TestCase):
# pylint: disable=maybe-no-member # pylint: disable=maybe-no-member
def setUp(self): def setUp(self):
self.editor = editor.ExternalEditor() self.editor = editor.ExternalEditor(0)
editor.config = stubs.ConfigStub( editor.config = stubs.ConfigStub(
{'general': {'editor': [''], 'editor-encoding': 'utf-8'}}) {'general': {'editor': [''], 'editor-encoding': 'utf-8'}})

View File

@ -35,6 +35,7 @@ class Completer(QObject):
Attributes: Attributes:
_ignore_change: Whether to ignore the next completion update. _ignore_change: Whether to ignore the next completion update.
_models: dict of available completion models. _models: dict of available completion models.
_win_id: The window ID this completer is in.
Signals: Signals:
change_completed_part: Text which should be substituted for the word change_completed_part: Text which should be substituted for the word
@ -47,8 +48,9 @@ class Completer(QObject):
change_completed_part = pyqtSignal(str, bool) change_completed_part = pyqtSignal(str, bool)
def __init__(self, parent=None): def __init__(self, win_id, parent=None):
super().__init__(parent) super().__init__(parent)
self._win_id = win_id
self._ignore_change = False self._ignore_change = False
self._models = { self._models = {
@ -63,7 +65,9 @@ class Completer(QObject):
def _model(self): def _model(self):
"""Convienience method to get the current completion model.""" """Convienience method to get the current completion model."""
return objreg.get('completion').model() completion = objreg.get('completion', scope='window',
window=self._win_id)
return completion.model()
def _init_static_completions(self): def _init_static_completions(self):
"""Initialize the static completion models.""" """Initialize the static completion models."""
@ -192,7 +196,8 @@ class Completer(QObject):
log.completion.debug("Ignoring completion update") log.completion.debug("Ignoring completion update")
return return
completion = objreg.get('completion') completion = objreg.get('completion', scope='window',
window=self._win_id)
if prefix != ':': if prefix != ':':
# This is a search or gibberish, so we don't need to complete # This is a search or gibberish, so we don't need to complete

View File

@ -37,16 +37,18 @@ class ExternalEditor(QObject):
_oshandle: The OS level handle to the tmpfile. _oshandle: The OS level handle to the tmpfile.
_filehandle: The file handle to the tmpfile. _filehandle: The file handle to the tmpfile.
_proc: The QProcess of the editor. _proc: The QProcess of the editor.
_win_id: The window ID the ExternalEditor is associated with.
""" """
editing_finished = pyqtSignal(str) editing_finished = pyqtSignal(str)
def __init__(self, parent=None): def __init__(self, win_id, parent=None):
super().__init__(parent) super().__init__(parent)
self._text = None self._text = None
self._oshandle = None self._oshandle = None
self._filename = None self._filename = None
self._proc = None self._proc = None
self._win_id = win_id
def _cleanup(self): def _cleanup(self):
"""Clean up temporary files after the editor closed.""" """Clean up temporary files after the editor closed."""
@ -56,7 +58,8 @@ class ExternalEditor(QObject):
except PermissionError as e: except PermissionError as e:
# NOTE: Do not replace this with "raise CommandError" as it's # NOTE: Do not replace this with "raise CommandError" as it's
# executed async. # executed async.
message.error("Failed to delete tempfile... ({})".format(e)) message.error(self._win_id,
"Failed to delete tempfile... ({})".format(e))
def on_proc_closed(self, exitcode, exitstatus): def on_proc_closed(self, exitcode, exitstatus):
"""Write the editor text into the form field and clean up tempfile. """Write the editor text into the form field and clean up tempfile.
@ -75,8 +78,9 @@ class ExternalEditor(QObject):
if exitcode != 0: if exitcode != 0:
# NOTE: Do not replace this with "raise CommandError" as it's # NOTE: Do not replace this with "raise CommandError" as it's
# executed async. # executed async.
message.error("Editor did quit abnormally (status {})!".format( message.error(
exitcode)) self._win_id, "Editor did quit abnormally (status "
"{})!".format(exitcode))
return return
encoding = config.get('general', 'editor-encoding') encoding = config.get('general', 'editor-encoding')
with open(self._filename, 'r', encoding=encoding) as f: with open(self._filename, 'r', encoding=encoding) as f:
@ -100,7 +104,8 @@ class ExternalEditor(QObject):
} }
# NOTE: Do not replace this with "raise CommandError" as it's # NOTE: Do not replace this with "raise CommandError" as it's
# executed async. # executed async.
message.error("Error while calling editor: {}".format(messages[error])) message.error(self._win_id,
"Error while calling editor: {}".format(messages[error]))
self._cleanup() self._cleanup()
def edit(self, text): def edit(self, text):

View File

@ -24,33 +24,41 @@ from PyQt5.QtCore import pyqtSignal, QObject, QTimer
from qutebrowser.utils import usertypes, log, objreg, utils from qutebrowser.utils import usertypes, log, objreg, utils
def error(message, immediately=False): def _get_bridge(win_id):
"""Get the correct MessageBridge instance for a window."""
return objreg.get('message-bridge', scope='window', window=win_id)
def error(win_id, message, immediately=False):
"""Convienience function to display an error message in the statusbar. """Convienience function to display an error message in the statusbar.
Args: Args:
See MessageBridge.error. win_id: The ID of the window which is calling this function.
others: See MessageBridge.error.
""" """
objreg.get('message-bridge').error(message, immediately) _get_bridge(win_id).error(message, immediately)
def info(message, immediately=True): def info(win_id, message, immediately=True):
"""Convienience function to display an info message in the statusbar. """Convienience function to display an info message in the statusbar.
Args: Args:
See MessageBridge.info. win_id: The ID of the window which is calling this function.
others: See MessageBridge.info.
""" """
objreg.get('message-bridge').info(message, immediately) _get_bridge(win_id).info(message, immediately)
def set_cmd_text(txt): def set_cmd_text(win_id, txt):
"""Convienience function to Set the statusbar command line to a text.""" """Convienience function to Set the statusbar command line to a text."""
objreg.get('message-bridge').set_cmd_text(txt) _get_bridge(win_id).set_cmd_text(txt)
def ask(message, mode, default=None): def ask(win_id, message, mode, default=None):
"""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.
@ -62,24 +70,30 @@ def ask(message, mode, default=None):
q.text = message q.text = message
q.mode = mode q.mode = mode
q.default = default q.default = default
objreg.get('message-bridge').ask(q, blocking=True) _get_bridge(win_id).ask(q, blocking=True)
q.deleteLater() q.deleteLater()
return q.answer return q.answer
def alert(message): def alert(win_id, message):
"""Display an alert which needs to be confirmed.""" """Display an alert which needs to be confirmed.
Args:
win_id: The ID of the window which is calling this function.
message: The message to show.
"""
q = usertypes.Question() q = usertypes.Question()
q.text = message q.text = message
q.mode = usertypes.PromptMode.alert q.mode = usertypes.PromptMode.alert
objreg.get('message-bridge').ask(q, blocking=True) _get_bridge(win_id).ask(q, blocking=True)
q.deleteLater() q.deleteLater()
def ask_async(message, mode, handler, default=None): def ask_async(win_id, message, mode, handler, default=None):
"""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.
@ -87,7 +101,7 @@ def ask_async(message, mode, handler, default=None):
""" """
if not isinstance(mode, usertypes.PromptMode): if not isinstance(mode, usertypes.PromptMode):
raise TypeError("Mode {} is no PromptMode member!".format(mode)) raise TypeError("Mode {} is no PromptMode member!".format(mode))
bridge = objreg.get('message-bridge') bridge = _get_bridge(win_id)
q = usertypes.Question(bridge) q = usertypes.Question(bridge)
q.text = message q.text = message
q.mode = mode q.mode = mode
@ -97,16 +111,17 @@ def ask_async(message, mode, handler, default=None):
bridge.ask(q, blocking=False) bridge.ask(q, blocking=False)
def confirm_async(message, yes_action, no_action=None, default=None): def confirm_async(win_id, message, yes_action, no_action=None, default=None):
"""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.
default: True/False to set a default value, or None. default: True/False to set a default value, or None.
""" """
bridge = objreg.get('message-bridge') bridge = _get_bridge(win_id)
q = usertypes.Question(bridge) q = usertypes.Question(bridge)
q.text = message q.text = message
q.mode = usertypes.PromptMode.yesno q.mode = usertypes.PromptMode.yesno

View File

@ -23,7 +23,9 @@
import collections import collections
import functools import functools
from PyQt5.QtCore import QObject from PyQt5.QtCore import QObject, QTimer
from qutebrowser.utils import log
class UnsetObject: class UnsetObject:
@ -58,12 +60,28 @@ class ObjectRegistry(collections.UserDict):
Sets a slot to remove QObjects when they are destroyed. Sets a slot to remove QObjects when they are destroyed.
""" """
if name is None:
raise TypeError("Registering '{}' with name 'None'!".format(obj))
if obj is None:
raise TypeError("Registering object None with name '{}'!".format(
name))
if isinstance(obj, QObject): if isinstance(obj, QObject):
obj.destroyed.connect(functools.partial(self.on_destroyed, name)) obj.destroyed.connect(functools.partial(self.on_destroyed, name))
super().__setitem__(name, obj) super().__setitem__(name, obj)
def on_destroyed(self, name): def on_destroyed(self, name):
"""Schedule removing of a destroyed QObject.
We don't remove the destroyed object immediately because it might still
be destroying its children, which might still use the object
registry.
"""
log.misc.debug("schedule destroyed: {}".format(name))
QTimer.singleShot(0, functools.partial(self._on_destroyed, name))
def _on_destroyed(self, name):
"""Remove a destroyed QObject.""" """Remove a destroyed QObject."""
log.misc.debug("destroyed: {}".format(name))
try: try:
del self[name] del self[name]
except KeyError: except KeyError:
@ -79,33 +97,79 @@ class ObjectRegistry(collections.UserDict):
# The registry for global objects # The registry for global objects
global_registry = ObjectRegistry() global_registry = ObjectRegistry()
# The object registry of object registries. # The window registry.
meta_registry = ObjectRegistry() window_registry = ObjectRegistry()
meta_registry['global'] = global_registry
def _get_registry(scope): def _get_tab_registry(win_id, tab_id):
"""Get the registry of a tab."""
if tab_id is None:
tab_id = 'current'
if tab_id == 'current' and win_id is None:
app = get('app')
window = app.activeWindow()
if window is None or not hasattr(window, 'win_id'):
raise RegistryUnavailableError('tab')
win_id = window.win_id
elif win_id is not None:
window = window_registry[win_id]
if tab_id == 'current':
tabbed_browser = get('tabbed-browser', scope='window', window=win_id)
tab = tabbed_browser.currentWidget()
if tab is None:
raise RegistryUnavailableError('window')
tab_id = tab.tab_id
tab_registry = get('tab-registry', scope='window', window=win_id)
try:
return tab_registry[tab_id].registry
except AttributeError:
raise RegistryUnavailableError('tab')
def _get_window_registry(window):
"""Get the registry of a window."""
if window is None:
raise TypeError("window is None with scope window!")
if window == 'current':
app = get('app')
win = app.activeWindow()
if win is None or not hasattr(win, 'win_id'):
raise RegistryUnavailableError('window')
else:
try:
win = window_registry[window]
except KeyError:
raise RegistryUnavailableError('window')
try:
return win.registry
except AttributeError:
raise RegistryUnavailableError('window')
def _get_registry(scope, window=None, tab=None):
"""Get the correct registry for a given scope.""" """Get the correct registry for a given scope."""
if window is not None and scope not in ('window', 'tab'):
raise TypeError("window is set with scope {}".format(scope))
if tab is not None and scope != 'tab':
raise TypeError("tab is set with scope {}".format(scope))
if scope == 'global': if scope == 'global':
return global_registry return global_registry
elif scope == 'tab': elif scope == 'tab':
widget = get('tabbed-browser').currentWidget() return _get_tab_registry(window, tab)
if widget is None: elif scope == 'window':
raise RegistryUnavailableError(scope) return _get_window_registry(window)
return widget.registry
elif scope == 'meta':
return meta_registry
else: else:
raise ValueError("Invalid scope '{}'!".format(scope)) raise ValueError("Invalid scope '{}'!".format(scope))
def get(name, default=_UNSET, scope='global'): def get(name, default=_UNSET, scope='global', window=None, tab=None):
"""Helper function to get an object. """Helper function to get an object.
Args: Args:
default: A default to return if the object does not exist. default: A default to return if the object does not exist.
""" """
reg = _get_registry(scope) reg = _get_registry(scope, window, tab)
try: try:
return reg[name] return reg[name]
except KeyError: except KeyError:
@ -115,7 +179,8 @@ def get(name, default=_UNSET, scope='global'):
raise raise
def register(name, obj, update=False, scope=None, registry=None): def register(name, obj, update=False, scope=None, registry=None, window=None,
tab=None):
"""Helper function to register an object. """Helper function to register an object.
Args: Args:
@ -131,14 +196,36 @@ def register(name, obj, update=False, scope=None, registry=None):
else: else:
if scope is None: if scope is None:
scope = 'global' scope = 'global'
reg = _get_registry(scope) reg = _get_registry(scope, window, tab)
if not update and name in reg: if not update and name in reg:
raise KeyError("Object '{}' is already registered ({})!".format( raise KeyError("Object '{}' is already registered ({})!".format(
name, repr(reg[name]))) name, repr(reg[name])))
reg[name] = obj reg[name] = obj
def delete(name, scope='global'): def delete(name, scope='global', window=None, tab=None):
"""Helper function to unregister an object.""" """Helper function to unregister an object."""
reg = _get_registry(scope) reg = _get_registry(scope, window, tab)
del reg[name] del reg[name]
def dump_objects():
"""Get all registered objects in all registries as a string."""
blocks = []
lines = []
blocks.append(('global', global_registry.dump_objects()))
for win_id in window_registry:
registry = _get_registry('window', window=win_id)
blocks.append(('window-{}'.format(win_id), registry.dump_objects()))
tab_registry = get('tab-registry', scope='window', window=win_id)
for tab_id, tab in tab_registry.items():
dump = tab.registry.dump_objects()
data = [' ' + line for line in dump]
blocks.append((' tab-{}'.format(tab_id), data))
for name, data in blocks:
lines.append("")
lines.append("{} object registry - {} objects:".format(
name, len(data)))
for line in data:
lines.append(" {}".format(line))
return lines

View File

@ -228,7 +228,7 @@ PromptMode = enum('PromptMode', ['yesno', 'text', 'user_pwd', 'alert'])
# Where to open a clicked link. # Where to open a clicked link.
ClickTarget = enum('ClickTarget', ['normal', 'tab', 'tab_bg']) ClickTarget = enum('ClickTarget', ['normal', 'tab', 'tab_bg', 'window'])
# Key input modes # Key input modes

View File

@ -19,36 +19,26 @@
"""Misc. utility commands exposed to the user.""" """Misc. utility commands exposed to the user."""
import types
import functools import functools
import types
from PyQt5.QtCore import QCoreApplication from PyQt5.QtCore import QCoreApplication
from qutebrowser.utils import usertypes, log, objreg from qutebrowser.utils import log, objreg, usertypes
from qutebrowser.commands import runners, cmdexc, cmdutils from qutebrowser.commands import cmdutils, runners, cmdexc
from qutebrowser.config import style from qutebrowser.config import style
_timers = [] @cmdutils.register(scope='window')
_commandrunner = None def later(ms: int, *command, win_id):
def init():
"""Initialize the global _commandrunner."""
global _commandrunner
_commandrunner = runners.CommandRunner()
@cmdutils.register()
def later(ms: int, *command):
"""Execute a command after some time. """Execute a command after some time.
Args: Args:
ms: How many milliseconds to wait. ms: How many milliseconds to wait.
*command: The command to run, with optional args. *command: The command to run, with optional args.
""" """
timer = usertypes.Timer(name='later') app = objreg.get('app')
timer = usertypes.Timer(name='later', parent=app)
timer.setSingleShot(True) timer.setSingleShot(True)
if ms < 0: if ms < 0:
raise cmdexc.CommandError("I can't run something in the past!") raise cmdexc.CommandError("I can't run something in the past!")
@ -57,11 +47,11 @@ def later(ms: int, *command):
except OverflowError: except OverflowError:
raise cmdexc.CommandError("Numeric argument is too large for internal " raise cmdexc.CommandError("Numeric argument is too large for internal "
"int representation.") "int representation.")
_timers.append(timer)
cmdline = ' '.join(command) cmdline = ' '.join(command)
timer.timeout.connect(functools.partial( commandrunner = runners.CommandRunner(win_id)
_commandrunner.run_safely, cmdline)) timer.timeout.connect(
timer.timeout.connect(lambda: _timers.remove(timer)) functools.partial(commandrunner.run_safely, cmdline))
timer.timeout.connect(timer.deleteLater)
timer.start() timer.start()

View File

@ -46,6 +46,7 @@ class CompletionView(QTreeView):
Attributes: Attributes:
enabled: Whether showing the CompletionView is enabled. enabled: Whether showing the CompletionView is enabled.
_win_id: The ID of the window this CompletionView is associated with.
_height: The height to use for the CompletionView. _height: The height to use for the CompletionView.
_height_perc: Either None or a percentage if height should be relative. _height_perc: Either None or a percentage if height should be relative.
_delegate: The item delegate used. _delegate: The item delegate used.
@ -90,11 +91,13 @@ class CompletionView(QTreeView):
resize_completion = pyqtSignal() resize_completion = pyqtSignal()
def __init__(self, parent=None): def __init__(self, win_id, parent=None):
super().__init__(parent) super().__init__(parent)
objreg.register('completion', self) self._win_id = win_id
completer_obj = completer.Completer() objreg.register('completion', self, scope='window', window=win_id)
objreg.register('completer', completer_obj) completer_obj = completer.Completer(win_id, self)
objreg.register('completer', completer_obj, scope='window',
window=win_id)
self.enabled = config.get('completion', 'show') self.enabled = config.get('completion', 'show')
config.on_change(self.set_enabled, 'completion', 'show') config.on_change(self.set_enabled, 'completion', 'show')
# FIXME # FIXME
@ -213,13 +216,13 @@ class CompletionView(QTreeView):
selmod.clearCurrentIndex() selmod.clearCurrentIndex()
@cmdutils.register(instance='completion', hide=True, @cmdutils.register(instance='completion', hide=True,
modes=[usertypes.KeyMode.command]) modes=[usertypes.KeyMode.command], scope='window')
def completion_item_prev(self): def completion_item_prev(self):
"""Select the previous completion item.""" """Select the previous completion item."""
self._next_prev_item(prev=True) self._next_prev_item(prev=True)
@cmdutils.register(instance='completion', hide=True, @cmdutils.register(instance='completion', hide=True,
modes=[usertypes.KeyMode.command]) modes=[usertypes.KeyMode.command], scope='window')
def completion_item_next(self): def completion_item_next(self):
"""Select the next completion item.""" """Select the next completion item."""
self._next_prev_item(prev=False) self._next_prev_item(prev=False)
@ -227,7 +230,9 @@ class CompletionView(QTreeView):
def selectionChanged(self, selected, deselected): def selectionChanged(self, selected, deselected):
"""Extend selectionChanged to call completers selection_changed.""" """Extend selectionChanged to call completers selection_changed."""
super().selectionChanged(selected, deselected) super().selectionChanged(selected, deselected)
objreg.get('completer').selection_changed(selected, deselected) completer_obj = objreg.get('completer', scope='window',
window=self._win_id)
completer_obj.selection_changed(selected, deselected)
def resizeEvent(self, e): def resizeEvent(self, e):
"""Extend resizeEvent to adjust column size.""" """Extend resizeEvent to adjust column size."""

View File

@ -113,7 +113,7 @@ class _CrashDialog(QDialog):
"""Gather crash information to display. """Gather crash information to display.
Args: Args:
pages: A list of the open pages (URLs as strings) pages: A list of lists of the open pages (URLs as strings)
cmdhist: A list with the command history (as strings) cmdhist: A list with the command history (as strings)
exc: An exception tuple (type, value, traceback) exc: An exception tuple (type, value, traceback)
""" """
@ -172,7 +172,7 @@ class ExceptionCrashDialog(_CrashDialog):
_btn_quit: The quit button _btn_quit: The quit button
_btn_restore: the restore button _btn_restore: the restore button
_btn_pastebin: the pastebin button _btn_pastebin: the pastebin button
_pages: A list of the open pages (URLs as strings) _pages: A list of lists of the open pages (URLs as strings)
_cmdhist: A list with the command history (as strings) _cmdhist: A list with the command history (as strings)
_exc: An exception tuple (type, value, traceback) _exc: An exception tuple (type, value, traceback)
_objects: A list of all QObjects as string. _objects: A list of all QObjects as string.
@ -220,7 +220,7 @@ class ExceptionCrashDialog(_CrashDialog):
self._crash_info += [ self._crash_info += [
("Exception", ''.join(traceback.format_exception(*self._exc))), ("Exception", ''.join(traceback.format_exception(*self._exc))),
("Commandline args", ' '.join(sys.argv[1:])), ("Commandline args", ' '.join(sys.argv[1:])),
("Open Pages", '\n'.join(self._pages)), ("Open Pages", '\n\n'.join('\n'.join(e) for e in self._pages)),
("Command history", '\n'.join(self._cmdhist)), ("Command history", '\n'.join(self._cmdhist)),
("Objects", self._objects), ("Objects", self._objects),
] ]
@ -316,7 +316,7 @@ class ReportDialog(_CrashDialog):
super()._gather_crash_info() super()._gather_crash_info()
self._crash_info += [ self._crash_info += [
("Commandline args", ' '.join(sys.argv[1:])), ("Commandline args", ' '.join(sys.argv[1:])),
("Open Pages", '\n'.join(self._pages)), ("Open Pages", '\n\n'.join('\n'.join(e) for e in self._pages)),
("Command history", '\n'.join(self._cmdhist)), ("Command history", '\n'.join(self._cmdhist)),
("Objects", self._objects), ("Objects", self._objects),
] ]

View File

@ -21,15 +21,22 @@
import binascii import binascii
import base64 import base64
import itertools
import functools
from PyQt5.QtCore import pyqtSlot, QRect, QPoint, QTimer, QEventLoop from PyQt5.QtCore import pyqtSlot, QRect, QPoint, QTimer, Qt
from PyQt5.QtWidgets import QWidget, QVBoxLayout from PyQt5.QtWidgets import QWidget, QVBoxLayout
from qutebrowser.commands import 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.widgets import tabbedbrowser, completion, downloads from qutebrowser.widgets import tabbedbrowser, completion, downloads
from qutebrowser.widgets.statusbar import bar from qutebrowser.widgets.statusbar import bar
from qutebrowser.keyinput import modeman
from qutebrowser.browser import hints
win_id_gen = itertools.count(0)
class MainWindow(QWidget): class MainWindow(QWidget):
@ -44,12 +51,102 @@ class MainWindow(QWidget):
_downloadview: The DownloadView widget. _downloadview: The DownloadView widget.
_tabbed_browser: The TabbedBrowser widget. _tabbed_browser: The TabbedBrowser widget.
_vbox: The main QVBoxLayout. _vbox: The main QVBoxLayout.
_commandrunner: The main CommandRunner instance.
""" """
def __init__(self, parent=None): def __init__(self, win_id, parent=None):
super().__init__(parent) super().__init__(parent)
self.setAttribute(Qt.WA_DeleteOnClose)
self._commandrunner = None
self.win_id = win_id
self.registry = objreg.ObjectRegistry()
objreg.window_registry[win_id] = self
objreg.register('main-window', self, scope='window', window=win_id)
tab_registry = objreg.ObjectRegistry()
objreg.register('tab-registry', tab_registry, scope='window',
window=win_id)
message_bridge = message.MessageBridge(self)
objreg.register('message-bridge', message_bridge, scope='window',
window=win_id)
self.setWindowTitle('qutebrowser') self.setWindowTitle('qutebrowser')
if win_id == 0:
self._load_geometry()
else:
self._set_default_geometry()
log.init.debug("Initial mainwindow geometry: {}".format(
self.geometry()))
self._vbox = QVBoxLayout(self)
self._vbox.setContentsMargins(0, 0, 0, 0)
self._vbox.setSpacing(0)
self._downloadview = downloads.DownloadView()
self._vbox.addWidget(self._downloadview)
self._downloadview.show()
self._tabbed_browser = tabbedbrowser.TabbedBrowser(win_id)
self._tabbed_browser.title_changed.connect(self.setWindowTitle)
objreg.register('tabbed-browser', self._tabbed_browser, scope='window',
window=win_id)
self._vbox.addWidget(self._tabbed_browser)
self._completion = completion.CompletionView(win_id, self)
self.status = bar.StatusBar(win_id)
self._vbox.addWidget(self.status)
objreg.register('status-command', self.status.cmd, scope='window',
window=win_id)
self._commandrunner = runners.CommandRunner(win_id)
log.init.debug("Initializing search...")
search_runner = runners.SearchRunner(self)
objreg.register('search-runner', search_runner, scope='window',
window=win_id)
log.init.debug("Initializing modes...")
mode_manager = modeman.init(self.win_id, self)
log.init.debug("Initializing eventfilter...")
self.installEventFilter(mode_manager)
self._connect_signals()
QTimer.singleShot(0, functools.partial(
modeman.enter, win_id, usertypes.KeyMode.normal, 'init'))
# When we're here the statusbar might not even really exist yet, so
# resizing will fail. Therefore, we use singleShot QTimers to make sure
# we defer this until everything else is initialized.
QTimer.singleShot(0, self._connect_resize_completion)
config.on_change(self.resize_completion, 'completion', 'height')
config.on_change(self.resize_completion, 'completion', 'shrink')
#self.retranslateUi(MainWindow)
#self.tabWidget.setCurrentIndex(0)
#QtCore.QMetaObject.connectSlotsByName(MainWindow)
def __repr__(self):
return utils.get_repr(self)
@classmethod
def spawn(cls, show=True):
"""Create a new main window.
Args:
show: Show the window after creating.
Return:
The new window id.
"""
win_id = next(win_id_gen)
win = MainWindow(win_id)
if show:
win.show()
return win_id
def _load_geometry(self):
"""Load the geometry from the state file."""
state_config = objreg.get('state-config') state_config = objreg.get('state-config')
try: try:
data = state_config['geometry']['mainwindow'] data = state_config['geometry']['mainwindow']
@ -71,40 +168,6 @@ class MainWindow(QWidget):
log.init.warning("Error while restoring geometry.") log.init.warning("Error while restoring geometry.")
self._set_default_geometry() self._set_default_geometry()
log.init.debug("Initial mainwindow geometry: {}".format(
self.geometry()))
self._vbox = QVBoxLayout(self)
self._vbox.setContentsMargins(0, 0, 0, 0)
self._vbox.setSpacing(0)
self._downloadview = downloads.DownloadView()
self._vbox.addWidget(self._downloadview)
self._downloadview.show()
self._tabbed_browser = tabbedbrowser.TabbedBrowser()
self._tabbed_browser.title_changed.connect(self.setWindowTitle)
objreg.register('tabbed-browser', self._tabbed_browser)
self._vbox.addWidget(self._tabbed_browser)
self._completion = completion.CompletionView(self)
self.status = bar.StatusBar()
self._vbox.addWidget(self.status)
# When we're here the statusbar might not even really exist yet, so
# resizing will fail. Therefore, we use singleShot QTimers to make sure
# we defer this until everything else is initialized.
QTimer.singleShot(0, self._connect_resize_completion)
config.on_change(self.resize_completion, 'completion', 'height')
config.on_change(self.resize_completion, 'completion', 'shrink')
#self.retranslateUi(MainWindow)
#self.tabWidget.setCurrentIndex(0)
#QtCore.QMetaObject.connectSlotsByName(MainWindow)
def __repr__(self):
return utils.get_repr(self)
def _connect_resize_completion(self): def _connect_resize_completion(self):
"""Connect the resize_completion signal and resize it once.""" """Connect the resize_completion signal and resize it once."""
self._completion.resize_completion.connect(self.resize_completion) self._completion.resize_completion.connect(self.resize_completion)
@ -114,6 +177,93 @@ class MainWindow(QWidget):
"""Set some sensible default geometry.""" """Set some sensible default geometry."""
self.setGeometry(QRect(50, 50, 800, 600)) self.setGeometry(QRect(50, 50, 800, 600))
def _get_object(self, name):
"""Get an object for this window in the object registry."""
return objreg.get(name, scope='window', window=self.win_id)
def _connect_signals(self):
"""Connect all mainwindow signals."""
# pylint: disable=too-many-locals,too-many-statements
app = objreg.get('app')
download_manager = objreg.get('download-manager')
key_config = objreg.get('key-config')
status = self._get_object('statusbar')
keyparsers = self._get_object('keyparsers')
completion_obj = self._get_object('completion')
tabs = self._get_object('tabbed-browser')
cmd = self._get_object('status-command')
completer = self._get_object('completer')
search_runner = self._get_object('search-runner')
message_bridge = self._get_object('message-bridge')
mode_manager = self._get_object('mode-manager')
prompter = self._get_object('prompter')
# misc
self._tabbed_browser.quit.connect(app.shutdown)
mode_manager.entered.connect(hints.on_mode_entered)
# status bar
mode_manager.entered.connect(status.on_mode_entered)
mode_manager.left.connect(status.on_mode_left)
mode_manager.left.connect(cmd.on_mode_left)
mode_manager.left.connect(prompter.on_mode_left)
# commands
keyparsers[usertypes.KeyMode.normal].keystring_updated.connect(
status.keystring.setText)
cmd.got_cmd.connect(self._commandrunner.run_safely)
cmd.got_search.connect(search_runner.search)
cmd.got_search_rev.connect(search_runner.search_rev)
cmd.returnPressed.connect(tabs.setFocus)
search_runner.do_search.connect(tabs.search)
tabs.got_cmd.connect(self._commandrunner.run_safely)
# config
for obj in keyparsers.values():
key_config.changed.connect(obj.on_keyconfig_changed)
# messages
message_bridge.s_error.connect(status.disp_error)
message_bridge.s_info.connect(status.disp_temp_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_set_cmd_text.connect(cmd.set_cmd_text)
message_bridge.s_question.connect(prompter.ask_question,
Qt.DirectConnection)
# statusbar
# FIXME some of these probably only should be triggered on mainframe
# loadStarted.
# https://github.com/The-Compiler/qutebrowser/issues/112
tabs.current_tab_changed.connect(status.prog.on_tab_changed)
tabs.cur_progress.connect(status.prog.setValue)
tabs.cur_load_finished.connect(status.prog.hide)
tabs.cur_load_started.connect(status.prog.on_load_started)
tabs.current_tab_changed.connect(status.percentage.on_tab_changed)
tabs.cur_scroll_perc_changed.connect(status.percentage.set_perc)
tabs.current_tab_changed.connect(status.txt.on_tab_changed)
tabs.cur_statusbar_message.connect(status.txt.on_statusbar_message)
tabs.cur_load_started.connect(status.txt.on_load_started)
tabs.current_tab_changed.connect(status.url.on_tab_changed)
tabs.cur_url_text_changed.connect(status.url.set_url)
tabs.cur_link_hovered.connect(status.url.set_hover_url)
tabs.cur_load_status_changed.connect(status.url.on_load_status_changed)
# command input / completion
mode_manager.left.connect(tabs.on_mode_left)
cmd.clear_completion_selection.connect(
completion_obj.on_clear_completion_selection)
cmd.hide_completion.connect(completion_obj.hide)
cmd.update_completion.connect(completer.on_update_completion)
completer.change_completed_part.connect(cmd.on_change_completed_part)
# downloads
tabs.start_download.connect(download_manager.fetch)
@pyqtSlot() @pyqtSlot()
def resize_completion(self): def resize_completion(self):
"""Adjust completion according to config.""" """Adjust completion according to config."""
@ -143,9 +293,9 @@ class MainWindow(QWidget):
if rect.isValid(): if rect.isValid():
self._completion.setGeometry(rect) self._completion.setGeometry(rect)
@cmdutils.register(instance='main-window', name=['quit', 'q']) @cmdutils.register(instance='main-window', scope='window')
def close(self): def close(self):
"""Quit qutebrowser. """Close the current window.
// //
@ -169,15 +319,19 @@ class MainWindow(QWidget):
confirm_quit = config.get('ui', 'confirm-quit') confirm_quit = config.get('ui', 'confirm-quit')
count = self._tabbed_browser.count() count = self._tabbed_browser.count()
if confirm_quit == 'never': if confirm_quit == 'never':
e.accept() pass
elif confirm_quit == 'multiple-tabs' and count <= 1: elif confirm_quit == 'multiple-tabs' and count <= 1:
e.accept() pass
else: else:
text = "Close {} {}?".format( text = "Close {} {}?".format(
count, "tab" if count == 1 else "tabs") count, "tab" if count == 1 else "tabs")
confirmed = message.ask(text, usertypes.PromptMode.yesno, confirmed = message.ask(self.win_id, text,
default=True) usertypes.PromptMode.yesno, default=True)
if confirmed: if not confirmed:
e.accept()
else:
e.ignore() e.ignore()
return
e.accept()
mode_manager = objreg.get('mode-manager', scope='window',
window=self.win_id)
log.destroy.debug("Removing eventfilter...")
self.removeEventFilter(mode_manager)

View File

@ -46,7 +46,7 @@ class StatusBar(QWidget):
percentage: The Percentage widget in the statusbar. percentage: The Percentage widget in the statusbar.
url: The UrlText widget in the statusbar. url: The UrlText widget in the statusbar.
prog: The Progress widget in the statusbar. prog: The Progress widget in the statusbar.
_cmd: The Command widget in the statusbar. cmd: The Command widget in the statusbar.
_hbox: The main QHBoxLayout. _hbox: The main QHBoxLayout.
_stack: The QStackedLayout with cmd/txt widgets. _stack: The QStackedLayout with cmd/txt widgets.
_text_queue: A deque of (error, text) tuples to be displayed. _text_queue: A deque of (error, text) tuples to be displayed.
@ -57,6 +57,7 @@ class StatusBar(QWidget):
the command widget. the command widget.
_previous_widget: A PreviousWidget member - the widget which was _previous_widget: A PreviousWidget member - the widget which was
displayed when an error interrupted it. displayed when an error interrupted it.
_win_id: The window ID the statusbar is associated with.
Class attributes: Class attributes:
_error: If there currently is an error, accessed through the error _error: If there currently is an error, accessed through the error
@ -113,14 +114,16 @@ class StatusBar(QWidget):
} }
""" """
def __init__(self, parent=None): def __init__(self, win_id, parent=None):
super().__init__(parent) super().__init__(parent)
objreg.register('statusbar', self, scope='window', window=win_id)
self.setObjectName(self.__class__.__name__) self.setObjectName(self.__class__.__name__)
self.setAttribute(Qt.WA_StyledBackground) self.setAttribute(Qt.WA_StyledBackground)
style.set_register_stylesheet(self) style.set_register_stylesheet(self)
self.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed) self.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed)
self._win_id = win_id
self._option = None self._option = None
self._last_text_time = None self._last_text_time = None
@ -132,9 +135,8 @@ class StatusBar(QWidget):
self._hbox.addLayout(self._stack) self._hbox.addLayout(self._stack)
self._stack.setContentsMargins(0, 0, 0, 0) self._stack.setContentsMargins(0, 0, 0, 0)
self._cmd = command.Command() self.cmd = command.Command(win_id)
objreg.register('status-command', self._cmd) self._stack.addWidget(self.cmd)
self._stack.addWidget(self._cmd)
self.txt = textwidget.Text() self.txt = textwidget.Text()
self._stack.addWidget(self.txt) self._stack.addWidget(self.txt)
@ -145,14 +147,14 @@ class StatusBar(QWidget):
self.set_pop_timer_interval() self.set_pop_timer_interval()
config.on_change(self.set_pop_timer_interval, 'ui', 'message-timeout') config.on_change(self.set_pop_timer_interval, 'ui', 'message-timeout')
self.prompt = prompt.Prompt() self.prompt = prompt.Prompt(win_id)
self._stack.addWidget(self.prompt) self._stack.addWidget(self.prompt)
self._previous_widget = PreviousWidget.none self._previous_widget = PreviousWidget.none
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') prompter = objreg.get('prompter', scope='window', window=self._win_id)
prompter.show_prompt.connect(self._show_prompt_widget) prompter.show_prompt.connect(self._show_prompt_widget)
prompter.hide_prompt.connect(self._hide_prompt_widget) prompter.hide_prompt.connect(self._hide_prompt_widget)
self._hide_prompt_widget() self._hide_prompt_widget()
@ -263,7 +265,7 @@ class StatusBar(QWidget):
if self._text_pop_timer.isActive(): if self._text_pop_timer.isActive():
self._timer_was_active = True self._timer_was_active = True
self._text_pop_timer.stop() self._text_pop_timer.stop()
self._stack.setCurrentWidget(self._cmd) self._stack.setCurrentWidget(self.cmd)
def _hide_cmd_widget(self): def _hide_cmd_widget(self):
"""Show temporary text instead of command widget.""" """Show temporary text instead of command widget."""
@ -382,7 +384,9 @@ class StatusBar(QWidget):
@pyqtSlot(usertypes.KeyMode) @pyqtSlot(usertypes.KeyMode)
def on_mode_entered(self, mode): def on_mode_entered(self, mode):
"""Mark certain modes in the commandline.""" """Mark certain modes in the commandline."""
if mode in objreg.get('mode-manager').passthrough: mode_manager = objreg.get('mode-manager', scope='window',
window=self._win_id)
if mode in mode_manager.passthrough:
text = "-- {} MODE --".format(mode.name.upper()) text = "-- {} MODE --".format(mode.name.upper())
self.txt.set_text(self.txt.Text.normal, text) self.txt.set_text(self.txt.Text.normal, text)
if mode == usertypes.KeyMode.insert: if mode == usertypes.KeyMode.insert:
@ -391,7 +395,9 @@ class StatusBar(QWidget):
@pyqtSlot(usertypes.KeyMode) @pyqtSlot(usertypes.KeyMode)
def on_mode_left(self, mode): def on_mode_left(self, mode):
"""Clear marked mode.""" """Clear marked mode."""
if mode in objreg.get('mode-manager').passthrough: mode_manager = objreg.get('mode-manager', scope='window',
window=self._win_id)
if mode in mode_manager.passthrough:
self.txt.set_text(self.txt.Text.normal, '') self.txt.set_text(self.txt.Text.normal, '')
if mode == usertypes.KeyMode.insert: if mode == usertypes.KeyMode.insert:
self._set_insert_active(False) self._set_insert_active(False)

View File

@ -35,6 +35,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
Attributes: Attributes:
_cursor_part: The part the cursor is currently over. _cursor_part: The part the cursor is currently over.
_win_id: The window ID this widget is associated with.
Signals: Signals:
got_cmd: Emitted when a command is triggered by the user. got_cmd: Emitted when a command is triggered by the user.
@ -64,9 +65,10 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
show_cmd = pyqtSignal() show_cmd = pyqtSignal()
hide_cmd = pyqtSignal() hide_cmd = pyqtSignal()
def __init__(self, parent=None): def __init__(self, win_id, parent=None):
misc.CommandLineEdit.__init__(self, parent) misc.CommandLineEdit.__init__(self, parent)
misc.MinimalLineEditMixin.__init__(self) misc.MinimalLineEditMixin.__init__(self)
self._win_id = win_id
self._cursor_part = 0 self._cursor_part = 0
self.history.history = objreg.get('command-history').data self.history.history = objreg.get('command-history').data
self._empty_item_idx = None self._empty_item_idx = None
@ -96,7 +98,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
# Text is only whitespace so we treat this as a single element with # Text is only whitespace so we treat this as a single element with
# the whitespace. # the whitespace.
return [text] return [text]
runner = runners.CommandRunner() runner = runners.CommandRunner(self._win_id)
parts = runner.parse(text, fallback=True, alias_no_args=False) parts = runner.parse(text, fallback=True, alias_no_args=False)
if self._empty_item_idx is not None: if self._empty_item_idx is not None:
log.completion.debug("Empty element queued at {}, " log.completion.debug("Empty element queued at {}, "
@ -156,7 +158,8 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
self.setFocus() self.setFocus()
self.show_cmd.emit() self.show_cmd.emit()
@cmdutils.register(instance='status-command', name='set-cmd-text') @cmdutils.register(instance='status-command', name='set-cmd-text',
scope='window')
def set_cmd_text_command(self, text): def set_cmd_text_command(self, text):
"""Preset the statusbar to some text. """Preset the statusbar to some text.
@ -168,7 +171,9 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
Args: Args:
text: The commandline to set. text: The commandline to set.
""" """
url = objreg.get('tabbed-browser').current_url().toString( tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
url = tabbed_browser.current_url().toString(
QUrl.FullyEncoded | QUrl.RemovePassword) QUrl.FullyEncoded | QUrl.RemovePassword)
# FIXME we currently replace the URL in any place in the arguments, # FIXME we currently replace the URL in any place in the arguments,
# rather than just replacing it if it is a dedicated argument. We could # rather than just replacing it if it is a dedicated argument. We could
@ -213,7 +218,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
self.show_cmd.emit() self.show_cmd.emit()
@cmdutils.register(instance='status-command', hide=True, @cmdutils.register(instance='status-command', hide=True,
modes=[usertypes.KeyMode.command]) modes=[usertypes.KeyMode.command], scope='window')
def command_history_prev(self): def command_history_prev(self):
"""Go back in the commandline history.""" """Go back in the commandline history."""
try: try:
@ -228,7 +233,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
self.set_cmd_text(item) self.set_cmd_text(item)
@cmdutils.register(instance='status-command', hide=True, @cmdutils.register(instance='status-command', hide=True,
modes=[usertypes.KeyMode.command]) modes=[usertypes.KeyMode.command], scope='window')
def command_history_next(self): def command_history_next(self):
"""Go forward in the commandline history.""" """Go forward in the commandline history."""
if not self.history.is_browsing(): if not self.history.is_browsing():
@ -241,7 +246,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
self.set_cmd_text(item) self.set_cmd_text(item)
@cmdutils.register(instance='status-command', hide=True, @cmdutils.register(instance='status-command', hide=True,
modes=[usertypes.KeyMode.command]) modes=[usertypes.KeyMode.command], scope='window')
def command_accept(self): def command_accept(self):
"""Execute the command currently in the commandline. """Execute the command currently in the commandline.
@ -257,7 +262,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
} }
text = self.text() text = self.text()
self.history.append(text) self.history.append(text)
modeman.leave(usertypes.KeyMode.command, 'cmd accept') modeman.leave(self._win_id, usertypes.KeyMode.command, 'cmd accept')
if text[0] in signals: if text[0] in signals:
signals[text[0]].emit(text.lstrip(text[0])) signals[text[0]].emit(text.lstrip(text[0]))
@ -294,7 +299,8 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
def focusInEvent(self, e): def focusInEvent(self, e):
"""Extend focusInEvent to enter command mode.""" """Extend focusInEvent to enter command mode."""
modeman.maybe_enter(usertypes.KeyMode.command, 'cmd focus') modeman.maybe_enter(self._win_id, usertypes.KeyMode.command,
'cmd focus')
super().focusInEvent(e) super().focusInEvent(e)
def setText(self, text): def setText(self, text):

View File

@ -19,6 +19,8 @@
"""Prompt shown in the statusbar.""" """Prompt shown in the statusbar."""
import functools
from PyQt5.QtWidgets import QHBoxLayout, QWidget, QLineEdit from PyQt5.QtWidgets import QHBoxLayout, QWidget, QLineEdit
from qutebrowser.widgets import misc from qutebrowser.widgets import misc
@ -45,9 +47,9 @@ class Prompt(QWidget):
_hbox: The QHBoxLayout used to display the text and prompt. _hbox: The QHBoxLayout used to display the text and prompt.
""" """
def __init__(self, parent=None): def __init__(self, win_id, parent=None):
super().__init__(parent) super().__init__(parent)
objreg.register('prompt', self) objreg.register('prompt', self, scope='window', window=win_id)
self._hbox = QHBoxLayout(self) self._hbox = QHBoxLayout(self)
self._hbox.setContentsMargins(0, 0, 0, 0) self._hbox.setContentsMargins(0, 0, 0, 0)
self._hbox.setSpacing(5) self._hbox.setSpacing(5)
@ -58,8 +60,12 @@ class Prompt(QWidget):
self.lineedit = PromptLineEdit() self.lineedit = PromptLineEdit()
self._hbox.addWidget(self.lineedit) self._hbox.addWidget(self.lineedit)
prompter_obj = prompter.Prompter(self) prompter_obj = prompter.Prompter(win_id)
objreg.register('prompter', prompter_obj) 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): def __repr__(self):
return utils.get_repr(self) return utils.get_repr(self)

View File

@ -60,6 +60,7 @@ class Prompter(QObject):
_question: A Question object with the question to be asked to the user. _question: A Question object with the question to be asked to the user.
_loops: A list of local EventLoops to spin in when blocking. _loops: A list of local EventLoops to spin in when blocking.
_queue: A deque of waiting questions. _queue: A deque of waiting questions.
_win_id: The window ID this object is associated with.
Signals: Signals:
show_prompt: Emitted when the prompt widget should be shown. show_prompt: Emitted when the prompt widget should be shown.
@ -69,12 +70,13 @@ class Prompter(QObject):
show_prompt = pyqtSignal() show_prompt = pyqtSignal()
hide_prompt = pyqtSignal() hide_prompt = pyqtSignal()
def __init__(self, parent=None): def __init__(self, win_id, parent=None):
super().__init__(parent) super().__init__(parent)
self._question = None self._question = None
self._loops = [] self._loops = []
self._queue = collections.deque() self._queue = collections.deque()
self._busy = False self._busy = False
self._win_id = win_id
def __repr__(self): def __repr__(self):
return utils.get_repr(self, loops=len(self._loops), return utils.get_repr(self, loops=len(self._loops),
@ -95,7 +97,7 @@ class Prompter(QObject):
"""Get a PromptContext based on the current state.""" """Get a PromptContext based on the current state."""
if not self._busy: if not self._busy:
return None return None
prompt = objreg.get('prompt') prompt = objreg.get('prompt', scope='window', window=self._win_id)
ctx = PromptContext(question=self._question, ctx = PromptContext(question=self._question,
text=prompt.txt.text(), text=prompt.txt.text(),
input_text=prompt.lineedit.text(), input_text=prompt.lineedit.text(),
@ -112,7 +114,7 @@ class Prompter(QObject):
Return: True if a context was restored, False otherwise. Return: True if a context was restored, False otherwise.
""" """
log.statusbar.debug("Restoring context {}".format(ctx)) log.statusbar.debug("Restoring context {}".format(ctx))
prompt = objreg.get('prompt') prompt = objreg.get('prompt', scope='window', window=self._win_id)
if ctx is None: if ctx is None:
self.hide_prompt.emit() self.hide_prompt.emit()
self._busy = False self._busy = False
@ -134,7 +136,7 @@ class Prompter(QObject):
Raise: Raise:
ValueError if the set PromptMode is invalid. ValueError if the set PromptMode is invalid.
""" """
prompt = objreg.get('prompt') prompt = objreg.get('prompt', scope='window', window=self._win_id)
if self._question.mode == usertypes.PromptMode.yesno: if self._question.mode == usertypes.PromptMode.yesno:
if self._question.default is None: if self._question.default is None:
suffix = "" suffix = ""
@ -188,7 +190,7 @@ class Prompter(QObject):
@pyqtSlot(usertypes.KeyMode) @pyqtSlot(usertypes.KeyMode)
def on_mode_left(self, mode): def on_mode_left(self, mode):
"""Clear and reset input when the mode was left.""" """Clear and reset input when the mode was left."""
prompt = objreg.get('prompt') prompt = objreg.get('prompt', scope='window', window=self._win_id)
if mode in (usertypes.KeyMode.prompt, usertypes.KeyMode.yesno): if mode in (usertypes.KeyMode.prompt, usertypes.KeyMode.yesno):
prompt.txt.setText('') prompt.txt.setText('')
prompt.lineedit.clear() prompt.lineedit.clear()
@ -198,7 +200,7 @@ class Prompter(QObject):
if self._question.answer is None and not self._question.is_aborted: if self._question.answer is None and not self._question.is_aborted:
self._question.cancel() self._question.cancel()
@cmdutils.register(instance='prompter', hide=True, @cmdutils.register(instance='prompter', hide=True, scope='window',
modes=[usertypes.KeyMode.prompt, modes=[usertypes.KeyMode.prompt,
usertypes.KeyMode.yesno]) usertypes.KeyMode.yesno])
def prompt_accept(self): def prompt_accept(self):
@ -209,7 +211,7 @@ class Prompter(QObject):
This executes the next action depending on the question mode, e.g. asks This executes the next action depending on the question mode, e.g. asks
for the password or leaves the mode. for the password or leaves the mode.
""" """
prompt = objreg.get('prompt') prompt = objreg.get('prompt', scope='window', window=self._win_id)
if (self._question.mode == usertypes.PromptMode.user_pwd and if (self._question.mode == usertypes.PromptMode.user_pwd and
self._question.user is None): self._question.user is None):
# User just entered an username # User just entered an username
@ -221,27 +223,31 @@ class Prompter(QObject):
# User just entered a password # User just entered a password
password = prompt.lineedit.text() password = prompt.lineedit.text()
self._question.answer = (self._question.user, password) self._question.answer = (self._question.user, password)
modeman.leave(usertypes.KeyMode.prompt, 'prompt accept') modeman.leave(self._win_id, usertypes.KeyMode.prompt,
'prompt accept')
self._question.done() self._question.done()
elif self._question.mode == usertypes.PromptMode.text: elif self._question.mode == usertypes.PromptMode.text:
# User just entered text. # User just entered text.
self._question.answer = prompt.lineedit.text() self._question.answer = prompt.lineedit.text()
modeman.leave(usertypes.KeyMode.prompt, 'prompt accept') modeman.leave(self._win_id, usertypes.KeyMode.prompt,
'prompt accept')
self._question.done() self._question.done()
elif self._question.mode == usertypes.PromptMode.yesno: elif self._question.mode == usertypes.PromptMode.yesno:
# User wants to accept the default of a yes/no question. # User wants to accept the default of a yes/no question.
self._question.answer = self._question.default self._question.answer = self._question.default
modeman.leave(usertypes.KeyMode.yesno, 'yesno accept') modeman.leave(self._win_id, usertypes.KeyMode.yesno,
'yesno accept')
self._question.done() self._question.done()
elif self._question.mode == usertypes.PromptMode.alert: elif self._question.mode == usertypes.PromptMode.alert:
# User acknowledged an alert # User acknowledged an alert
self._question.answer = None self._question.answer = None
modeman.leave(usertypes.KeyMode.prompt, 'alert accept') modeman.leave(self._win_id, usertypes.KeyMode.prompt,
'alert accept')
self._question.done() self._question.done()
else: else:
raise ValueError("Invalid question mode!") raise ValueError("Invalid question mode!")
@cmdutils.register(instance='prompter', hide=True, @cmdutils.register(instance='prompter', hide=True, scope='window',
modes=[usertypes.KeyMode.yesno]) modes=[usertypes.KeyMode.yesno])
def prompt_yes(self): def prompt_yes(self):
"""Answer yes to a yes/no prompt.""" """Answer yes to a yes/no prompt."""
@ -249,10 +255,10 @@ class Prompter(QObject):
# We just ignore this if we don't have a yes/no question. # We just ignore this if we don't have a yes/no question.
return return
self._question.answer = True self._question.answer = True
modeman.leave(usertypes.KeyMode.yesno, 'yesno accept') modeman.leave(self._win_id, usertypes.KeyMode.yesno, 'yesno accept')
self._question.done() self._question.done()
@cmdutils.register(instance='prompter', hide=True, @cmdutils.register(instance='prompter', hide=True, scope='window',
modes=[usertypes.KeyMode.yesno]) modes=[usertypes.KeyMode.yesno])
def prompt_no(self): def prompt_no(self):
"""Answer no to a yes/no prompt.""" """Answer no to a yes/no prompt."""
@ -260,7 +266,7 @@ class Prompter(QObject):
# We just ignore this if we don't have a yes/no question. # We just ignore this if we don't have a yes/no question.
return return
self._question.answer = False self._question.answer = False
modeman.leave(usertypes.KeyMode.yesno, 'prompt accept') modeman.leave(self._win_id, usertypes.KeyMode.yesno, 'prompt accept')
self._question.done() self._question.done()
@pyqtSlot(usertypes.Question, bool) @pyqtSlot(usertypes.Question, bool)
@ -293,10 +299,12 @@ class Prompter(QObject):
self._question = question self._question = question
mode = self._display_question() mode = self._display_question()
question.aborted.connect(lambda: modeman.maybe_leave(mode, 'aborted')) question.aborted.connect(
mode_manager = objreg.get('mode-manager') lambda: modeman.maybe_leave(self._win_id, mode, 'aborted'))
mode_manager = objreg.get('mode-manager', scope='window',
window=self._win_id)
try: try:
modeman.enter(mode, 'question asked', override=True) modeman.enter(self._win_id, mode, 'question asked', override=True)
except modeman.ModeLockedError: except modeman.ModeLockedError:
if mode_manager.mode() != usertypes.KeyMode.prompt: if mode_manager.mode() != usertypes.KeyMode.prompt:
question.abort() question.abort()

View File

@ -51,7 +51,7 @@ class TabbedBrowser(tabwidget.TabWidget):
emitted if the signal occured in the current tab. emitted if the signal occured in the current tab.
Attributes: Attributes:
_tabs: A list of open tabs. _win_id: The window ID this tabbedbrowser is associated with.
_filter: A SignalFilter instance. _filter: A SignalFilter instance.
_now_focused: The tab which is focused now. _now_focused: The tab which is focused now.
_tab_insert_idx_left: Where to insert a new tab with _tab_insert_idx_left: Where to insert a new tab with
@ -97,19 +97,23 @@ class TabbedBrowser(tabwidget.TabWidget):
current_tab_changed = pyqtSignal(webview.WebView) current_tab_changed = pyqtSignal(webview.WebView)
title_changed = pyqtSignal(str) title_changed = pyqtSignal(str)
def __init__(self, parent=None): def __init__(self, win_id, parent=None):
super().__init__(parent) super().__init__(win_id, parent)
self._win_id = win_id
self._tab_insert_idx_left = 0 self._tab_insert_idx_left = 0
self._tab_insert_idx_right = -1 self._tab_insert_idx_right = -1
self.tabCloseRequested.connect(self.on_tab_close_requested) self.tabCloseRequested.connect(self.on_tab_close_requested)
self.currentChanged.connect(self.on_current_changed) self.currentChanged.connect(self.on_current_changed)
self.cur_load_started.connect(self.on_cur_load_started) self.cur_load_started.connect(self.on_cur_load_started)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self._tabs = []
self._undo_stack = [] self._undo_stack = []
self._filter = signalfilter.SignalFilter(self) self._filter = signalfilter.SignalFilter(win_id, self)
dispatcher = commands.CommandDispatcher() dispatcher = commands.CommandDispatcher(win_id)
objreg.register('command-dispatcher', dispatcher) objreg.register('command-dispatcher', dispatcher, scope='window',
window=win_id)
self.destroyed.connect(
functools.partial(objreg.delete, 'command-dispatcher',
scope='window', window=win_id))
self._now_focused = None self._now_focused = None
# FIXME adjust this to font size # FIXME adjust this to font size
# https://github.com/The-Compiler/qutebrowser/issues/119 # https://github.com/The-Compiler/qutebrowser/issues/119
@ -245,8 +249,10 @@ class TabbedBrowser(tabwidget.TabWidget):
tab)) tab))
if tab is self._now_focused: if tab is self._now_focused:
self._now_focused = None self._now_focused = None
if tab is objreg.get('last-focused-tab', None): if tab is objreg.get('last-focused-tab', None, scope='window',
objreg.delete('last-focused-tab') window=self._win_id):
objreg.delete('last-focused-tab', scope='window',
window=self._win_id)
if tab.cur_url.isValid(): if tab.cur_url.isValid():
history_data = qtutils.serialize(tab.history()) history_data = qtutils.serialize(tab.history())
entry = UndoEntry(tab.cur_url, history_data) entry = UndoEntry(tab.cur_url, history_data)
@ -255,7 +261,6 @@ class TabbedBrowser(tabwidget.TabWidget):
urlutils.invalid_url_error(url, "saving tab") urlutils.invalid_url_error(url, "saving tab")
return return
tab.shutdown() tab.shutdown()
self._tabs.remove(tab)
self.removeTab(idx) self.removeTab(idx)
tab.deleteLater() tab.deleteLater()
@ -318,9 +323,8 @@ class TabbedBrowser(tabwidget.TabWidget):
if url is not None: if url is not None:
qtutils.ensure_valid(url) qtutils.ensure_valid(url)
log.webview.debug("Creating new tab with URL {}".format(url)) log.webview.debug("Creating new tab with URL {}".format(url))
tab = webview.WebView(self) tab = webview.WebView(self._win_id, self)
self._connect_tab_signals(tab) self._connect_tab_signals(tab)
self._tabs.append(tab)
if explicit: if explicit:
pos = config.get('tabs', 'new-tab-position-explicit') pos = config.get('tabs', 'new-tab-position-explicit')
else: else:
@ -369,19 +373,19 @@ class TabbedBrowser(tabwidget.TabWidget):
old_scroll_pos = widget.scroll_pos old_scroll_pos = widget.scroll_pos
found = widget.findText(text, flags) found = widget.findText(text, flags)
if not found and not flags & QWebPage.HighlightAllOccurrences and text: if not found and not flags & QWebPage.HighlightAllOccurrences and text:
message.error("Text '{}' not found on page!".format(text), message.error(self._win_id, "Text '{}' not found on "
immediately=True) "page!".format(text), immediately=True)
else: else:
backward = int(flags) & QWebPage.FindBackward backward = int(flags) & QWebPage.FindBackward
def check_scroll_pos(): def check_scroll_pos():
"""Check if the scroll position got smaller and show info.""" """Check if the scroll position got smaller and show info."""
if not backward and widget.scroll_pos < old_scroll_pos: if not backward and widget.scroll_pos < old_scroll_pos:
message.info("Search hit BOTTOM, continuing at TOP", message.info(self._win_id, "Search hit BOTTOM, continuing "
immediately=True) "at TOP", immediately=True)
elif backward and widget.scroll_pos > old_scroll_pos: elif backward and widget.scroll_pos > old_scroll_pos:
message.info("Search hit TOP, continuing at BOTTOM", message.info(self._win_id, "Search hit TOP, continuing at "
immediately=True) "BOTTOM", immediately=True)
# We first want QWebPage to refresh. # We first want QWebPage to refresh.
QTimer.singleShot(0, check_scroll_pos) QTimer.singleShot(0, check_scroll_pos)
@ -414,8 +418,10 @@ class TabbedBrowser(tabwidget.TabWidget):
@pyqtSlot() @pyqtSlot()
def on_cur_load_started(self): def on_cur_load_started(self):
"""Leave insert/hint mode when loading started.""" """Leave insert/hint mode when loading started."""
modeman.maybe_leave(usertypes.KeyMode.insert, 'load started') modeman.maybe_leave(self._win_id, usertypes.KeyMode.insert,
modeman.maybe_leave(usertypes.KeyMode.hint, 'load started') 'load started')
modeman.maybe_leave(self._win_id, usertypes.KeyMode.hint,
'load started')
@pyqtSlot(webview.WebView, str) @pyqtSlot(webview.WebView, str)
def on_title_changed(self, tab, text): def on_title_changed(self, tab, text):
@ -501,9 +507,11 @@ class TabbedBrowser(tabwidget.TabWidget):
return return
tab = self.widget(idx) tab = self.widget(idx)
tab.setFocus() tab.setFocus()
modeman.maybe_leave(usertypes.KeyMode.hint, 'tab changed') modeman.maybe_leave(self._win_id, usertypes.KeyMode.hint,
'tab changed')
if self._now_focused is not None: if self._now_focused is not None:
objreg.register('last-focused-tab', self._now_focused, update=True) objreg.register('last-focused-tab', self._now_focused, update=True,
scope='window', window=self._win_id)
self._now_focused = tab self._now_focused = tab
self.current_tab_changed.emit(tab) self.current_tab_changed.emit(tab)
self._change_app_title(self.tabText(idx)) self._change_app_title(self.tabText(idx))

View File

@ -42,9 +42,9 @@ class TabWidget(QTabWidget):
"""The tabwidget used for TabbedBrowser.""" """The tabwidget used for TabbedBrowser."""
def __init__(self, parent=None): def __init__(self, win_id, parent=None):
super().__init__(parent) super().__init__(parent)
bar = TabBar() bar = TabBar(win_id)
self.setTabBar(bar) self.setTabBar(bar)
bar.tabCloseRequested.connect(self.tabCloseRequested) bar.tabCloseRequested.connect(self.tabCloseRequested)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
@ -92,10 +92,12 @@ class TabBar(QTabBar):
Attributes: Attributes:
vertical: When the tab bar is currently vertical. vertical: When the tab bar is currently vertical.
win_id: The window ID this TabBar belongs to.
""" """
def __init__(self, parent=None): def __init__(self, win_id, parent=None):
super().__init__(parent) super().__init__(parent)
self._win_id = win_id
self.setStyle(TabBarStyle(self.style())) self.setStyle(TabBarStyle(self.style()))
self.set_font() self.set_font()
config.on_change(self.set_font, 'fonts', 'tabbar') config.on_change(self.set_font, 'fonts', 'tabbar')
@ -202,8 +204,10 @@ class TabBar(QTabBar):
if self.vertical: if self.vertical:
confwidth = str(config.get('tabs', 'width')) confwidth = str(config.get('tabs', 'width'))
if confwidth.endswith('%'): if confwidth.endswith('%'):
main_window = objreg.get('main-window', scope='window',
window=self._win_id)
perc = int(confwidth.rstrip('%')) perc = int(confwidth.rstrip('%'))
width = objreg.get('main-window').width() * perc / 100 width = main_window.width() * perc / 100
else: else:
width = int(confwidth) width = int(confwidth)
size = QSize(max(minimum_size.width(), width), height) size = QSize(max(minimum_size.width(), width), height)

View File

@ -57,6 +57,7 @@ class WebView(QWebView):
viewing_source: Whether the webview is currently displaying source viewing_source: Whether the webview is currently displaying source
code. code.
registry: The ObjectRegistry associated with this tab. registry: The ObjectRegistry associated with this tab.
tab_id: The tab ID of the view.
_cur_url: The current URL (accessed via cur_url property). _cur_url: The current URL (accessed via cur_url property).
_has_ssl_errors: Whether SSL errors occured during loading. _has_ssl_errors: Whether SSL errors occured during loading.
_zoom: A NeighborList with the zoom levels. _zoom: A NeighborList with the zoom levels.
@ -64,6 +65,7 @@ class WebView(QWebView):
_force_open_target: Override for open_target. _force_open_target: Override for open_target.
_check_insertmode: If True, in mouseReleaseEvent we should check if we _check_insertmode: If True, in mouseReleaseEvent we should check if we
need to enter/leave insert mode. need to enter/leave insert mode.
_win_id: The window ID of the view.
Signals: Signals:
scroll_pos_changed: Scroll percentage of current tab changed. scroll_pos_changed: Scroll percentage of current tab changed.
@ -79,8 +81,9 @@ class WebView(QWebView):
load_status_changed = pyqtSignal(str) load_status_changed = pyqtSignal(str)
url_text_changed = pyqtSignal(str) url_text_changed = pyqtSignal(str)
def __init__(self, parent=None): def __init__(self, win_id, parent=None):
super().__init__(parent) super().__init__(parent)
self._win_id = win_id
self.load_status = LoadStatus.none self.load_status = LoadStatus.none
self._check_insertmode = False self._check_insertmode = False
self.inspector = None self.inspector = None
@ -99,15 +102,16 @@ class WebView(QWebView):
self.progress = 0 self.progress = 0
self.registry = objreg.ObjectRegistry() self.registry = objreg.ObjectRegistry()
self.tab_id = next(tab_id_gen) self.tab_id = next(tab_id_gen)
tab_registry = objreg.get('tab-registry', scope='window',
window=win_id)
tab_registry[self.tab_id] = self
objreg.register('webview', self, registry=self.registry) objreg.register('webview', self, registry=self.registry)
page = webpage.BrowserPage(self) page = webpage.BrowserPage(win_id, self)
self.setPage(page) self.setPage(page)
hintmanager = hints.HintManager(self) hintmanager = hints.HintManager(win_id, self.tab_id, self)
hintmanager.mouse_event.connect(self.on_mouse_event) hintmanager.mouse_event.connect(self.on_mouse_event)
hintmanager.set_open_target.connect(self.set_force_open_target) hintmanager.set_open_target.connect(self.set_force_open_target)
objreg.register('hintmanager', hintmanager, registry=self.registry) objreg.register('hintmanager', hintmanager, registry=self.registry)
objreg.register('tab-{}'.format(self.tab_id),
self.registry, scope='meta')
page.linkHovered.connect(self.linkHovered) page.linkHovered.connect(self.linkHovered)
page.mainFrame().loadStarted.connect(self.on_load_started) page.mainFrame().loadStarted.connect(self.on_load_started)
self.urlChanged.connect(self.on_url_changed) self.urlChanged.connect(self.on_url_changed)
@ -150,16 +154,18 @@ class WebView(QWebView):
""" """
if e.button() == Qt.XButton1: if e.button() == Qt.XButton1:
# Back button on mice which have it. # Back button on mice which have it.
try: if self.page().history().canGoBack():
self.go_back() self.back()
except cmdexc.CommandError as ex: else:
message.error(ex, immediately=True) message.error(self._win_id, "At beginning of history.",
immediately=True)
elif e.button() == Qt.XButton2: elif e.button() == Qt.XButton2:
# Forward button on mice which have it. # Forward button on mice which have it.
try: if self.page().history().canGoForward():
self.go_forward() self.forward()
except cmdexc.CommandError as ex: else:
message.error(ex, immediately=True) message.error(self._win_id, "At end of history.",
immediately=True)
def _mousepress_insertmode(self, e): def _mousepress_insertmode(self, e):
"""Switch to insert mode when an editable element was clicked. """Switch to insert mode when an editable element was clicked.
@ -200,11 +206,13 @@ class WebView(QWebView):
if ((hitresult.isContentEditable() and elem.is_writable()) or if ((hitresult.isContentEditable() and elem.is_writable()) or
elem.is_editable()): elem.is_editable()):
log.mouse.debug("Clicked editable element!") log.mouse.debug("Clicked editable element!")
modeman.maybe_enter(usertypes.KeyMode.insert, 'click') modeman.maybe_enter(self._win_id, usertypes.KeyMode.insert,
'click')
else: else:
log.mouse.debug("Clicked non-editable element!") log.mouse.debug("Clicked non-editable element!")
if config.get('input', 'auto-leave-insert-mode'): if config.get('input', 'auto-leave-insert-mode'):
modeman.maybe_leave(usertypes.KeyMode.insert, 'click') modeman.maybe_leave(self._win_id, usertypes.KeyMode.insert,
'click')
def mouserelease_insertmode(self): def mouserelease_insertmode(self):
"""If we have an insertmode check scheduled, handle it.""" """If we have an insertmode check scheduled, handle it."""
@ -218,11 +226,13 @@ class WebView(QWebView):
return return
if elem.is_editable(): if elem.is_editable():
log.mouse.debug("Clicked editable element (delayed)!") log.mouse.debug("Clicked editable element (delayed)!")
modeman.maybe_enter(usertypes.KeyMode.insert, 'click-delayed') modeman.maybe_enter(self._win_id, usertypes.KeyMode.insert,
'click-delayed')
else: else:
log.mouse.debug("Clicked non-editable element (delayed)!") log.mouse.debug("Clicked non-editable element (delayed)!")
if config.get('input', 'auto-leave-insert-mode'): if config.get('input', 'auto-leave-insert-mode'):
modeman.maybe_leave(usertypes.KeyMode.insert, 'click-delayed') modeman.maybe_leave(self._win_id, usertypes.KeyMode.insert,
'click-delayed')
def _mousepress_opentarget(self, e): def _mousepress_opentarget(self, e):
"""Set the open target when something was clicked. """Set the open target when something was clicked.
@ -293,7 +303,7 @@ class WebView(QWebView):
if perc < 0: if perc < 0:
raise cmdexc.CommandError("Can't zoom {}%!".format(perc)) raise cmdexc.CommandError("Can't zoom {}%!".format(perc))
self.setZoomFactor(float(perc) / 100) self.setZoomFactor(float(perc) / 100)
message.info("Zoom level: {}%".format(perc)) message.info(self._win_id, "Zoom level: {}%".format(perc))
def zoom(self, offset): def zoom(self, offset):
"""Increase/Decrease the zoom level. """Increase/Decrease the zoom level.
@ -304,30 +314,6 @@ class WebView(QWebView):
level = self._zoom.getitem(offset) level = self._zoom.getitem(offset)
self.zoom_perc(level, fuzzyval=False) self.zoom_perc(level, fuzzyval=False)
@pyqtSlot(str, int)
def search(self, text, flags):
"""Search for text in the current page.
Args:
text: The text to search for.
flags: The QWebPage::FindFlags.
"""
self._tabs.currentWidget().findText(text, flags)
def go_back(self):
"""Go back a page in the history."""
if self.page().history().canGoBack():
self.back()
else:
raise cmdexc.CommandError("At beginning of history.")
def go_forward(self):
"""Go forward a page in the history."""
if self.page().history().canGoForward():
self.forward()
else:
raise cmdexc.CommandError("At end of history.")
@pyqtSlot('QUrl') @pyqtSlot('QUrl')
def on_url_changed(self, url): def on_url_changed(self, url):
"""Update cur_url when URL has changed. """Update cur_url when URL has changed.
@ -363,7 +349,9 @@ class WebView(QWebView):
self._set_load_status(LoadStatus.error) self._set_load_status(LoadStatus.error)
if not config.get('input', 'auto-insert-mode'): if not config.get('input', 'auto-insert-mode'):
return return
cur_mode = objreg.get('mode-manager').mode() mode_manager = objreg.get('mode-manager', scope='window',
window=self._win_id)
cur_mode = mode_manager.mode()
if cur_mode == usertypes.KeyMode.insert or not ok: if cur_mode == usertypes.KeyMode.insert or not ok:
return return
frame = self.page().currentFrame() frame = self.page().currentFrame()
@ -374,7 +362,8 @@ class WebView(QWebView):
return return
log.modes.debug("focus element: {}".format(repr(elem))) log.modes.debug("focus element: {}".format(repr(elem)))
if elem.is_editable(): if elem.is_editable():
modeman.maybe_enter(usertypes.KeyMode.insert, 'load finished') modeman.maybe_enter(self._win_id, usertypes.KeyMode.insert,
'load finished')
@pyqtSlot(str) @pyqtSlot(str)
def set_force_open_target(self, target): def set_force_open_target(self, target):
@ -409,7 +398,9 @@ class WebView(QWebView):
if wintype == QWebPage.WebModalDialog: if wintype == QWebPage.WebModalDialog:
log.webview.warning("WebModalDialog requested, but we don't " log.webview.warning("WebModalDialog requested, but we don't "
"support that!") "support that!")
return objreg.get('tabbed-browser').tabopen() tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
return tabbed_browser.tabopen()
def paintEvent(self, e): def paintEvent(self, e):
"""Extend paintEvent to emit a signal if the scroll position changed. """Extend paintEvent to emit a signal if the scroll position changed.