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`,
`cookie-jar`, etc.)
* The `tab` scope with objects which are per-tab (`hintmanager`, `webview`,
etc.). Passing this scope to `objreg.get()` always selects the object in the
currently focused tab.
* The `meta` scope which is an object registry of all other object registries,
mainly intended for debugging.
etc.). Passing this scope to `objreg.get()` selects the object in the currently
focused tab by default. A tab can be explicitely selected by passing
+tab=_tab-id_, window=_win-id_+ to it.
A new object can be registered by using
+objreg.register(_name_, _object_[, scope=_scope_])+. An object should not be
registered twice. To update it, `update=True` has to be given.
+objreg.register(_name_, _object_[, scope=_scope_, window=_win-id_,
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
default scope is `global`.
An object can be retrieved by using +objreg.get(_name_[, scope=_scope_,
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
`:debug-all-objects` command.

View File

@ -8,6 +8,7 @@
|<<back,back>>|Go back in the history of the current tab.
|<<bind,bind>>|Bind a key to a command.
|<<cancel-download,cancel-download>>|Cancel the first/[count]th download.
|<<close,close>>|Close the current window.
|<<download-page,download-page>>|Download the current page.
|<<forward,forward>>|Go forward in the history of the current tab.
|<<help,help>>|Show help about a command or setting.
@ -49,13 +50,14 @@
|==============
[[back]]
=== back
Syntax: +:back [*--tab*] [*--bg*]+
Syntax: +:back [*--tab*] [*--bg*] [*--window*]+
Go back in the history of the current tab.
==== optional arguments
* +*-t*+, +*--tab*+: Go back in a new tab.
* +*-b*+, +*--bg*+: Go back in a background tab.
* +*-w*+, +*--window*+: Go back in a new window.
==== count
How many pages to go back.
@ -81,26 +83,31 @@ Cancel the first/[count]th download.
==== count
The index of the download to cancel.
[[close]]
=== close
Close the current window.
[[download-page]]
=== download-page
Download the current page.
[[forward]]
=== forward
Syntax: +:forward [*--tab*] [*--bg*]+
Syntax: +:forward [*--tab*] [*--bg*] [*--window*]+
Go forward in the history of the current tab.
==== optional arguments
* +*-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
How many pages to go forward.
[[help]]
=== help
Syntax: +:help ['topic']+
Syntax: +:help [*--tab*] [*--bg*] [*--window*] ['topic']+
Show help about a command or setting.
@ -111,6 +118,11 @@ Show help about a command or setting.
- __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
Syntax: +:hint ['group'] ['target'] ['args' ['args' ...]]+
@ -131,11 +143,14 @@ Start hinting.
- `normal`: Open the link in the current tab.
- `tab`: Open the link in a new 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-primary`: Yank the link to the primary selection.
- `fill`: Fill the commandline with the command given as
argument.
- `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.
- `userscript`: Call an userscript with `$QUTE_URL` set to the
link.
@ -174,7 +189,7 @@ Execute a command after some time.
[[navigate]]
=== navigate
Syntax: +:navigate [*--tab*] 'where'+
Syntax: +:navigate [*--tab*] [*--bg*] [*--window*] 'where'+
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
* +*-t*+, +*--tab*+: Open in a new tab.
* +*-b*+, +*--bg*+: Open in a background tab.
* +*-w*+, +*--window*+: Open in a new window.
[[open]]
=== open
Syntax: +:open [*--bg*] [*--tab*] 'url'+
Syntax: +:open [*--bg*] [*--tab*] [*--window*] 'url'+
Open a URL in the current/[count]th tab.
@ -207,13 +224,14 @@ Open a URL in the current/[count]th tab.
==== optional arguments
* +*-b*+, +*--bg*+: Open in a new background tab.
* +*-t*+, +*--tab*+: Open in a new tab.
* +*-w*+, +*--window*+: Open in a new window.
==== count
The tab index to open the URL in.
[[paste]]
=== paste
Syntax: +:paste [*--sel*] [*--tab*] [*--bg*]+
Syntax: +:paste [*--sel*] [*--tab*] [*--bg*] [*--window*]+
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.
* +*-t*+, +*--tab*+: Open in a new tab.
* +*-b*+, +*--bg*+: Open in a background tab.
* +*-w*+, +*--window*+: Open in new window.
[[print]]
=== print
@ -246,7 +265,7 @@ Add a new quickmark.
[[quickmark-load]]
=== quickmark-load
Syntax: +:quickmark-load [*--tab*] [*--bg*] 'name'+
Syntax: +:quickmark-load [*--tab*] [*--bg*] [*--window*] 'name'+
Load a quickmark.
@ -256,6 +275,7 @@ Load a quickmark.
==== optional arguments
* +*-t*+, +*--tab*+: Load the quickmark in a new tab.
* +*-b*+, +*--bg*+: Load the quickmark in a new background tab.
* +*-w*+, +*--window*+: Load the quickmark in a new window.
[[quickmark-save]]
=== quickmark-save
@ -339,12 +359,13 @@ The tab index to stop.
[[tab-clone]]
=== tab-clone
Syntax: +:tab-clone [*--bg*]+
Syntax: +:tab-clone [*--bg*] [*--window*]+
Duplicate the current tab.
==== optional arguments
* +*-b*+, +*--bg*+: Open in a background tab.
* +*-w*+, +*--window*+: Open in a new window.
[[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.
*'URL'*::
URLs to open on startup.
URLs to open on startup (empty as a window separator).
=== optional arguments
*-h*, *--help*::

View File

@ -36,15 +36,16 @@ from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QTimer, Qt, QUrl,
QStandardPaths, QObject)
import qutebrowser
from qutebrowser.commands import userscripts, runners, cmdutils
from qutebrowser.commands import cmdutils, runners
from qutebrowser.config import style, config, websettings
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.keyinput import modeman
from qutebrowser.utils import (log, version, message, utilcmds, readline,
utils, qtutils, urlutils, debug, objreg,
usertypes)
from qutebrowser.utils import (log, version, message, readline, utils, qtutils,
urlutils, debug, objreg, usertypes)
# We import utilcmds to run the cmdutils.register decorators.
from qutebrowser.utils import utilcmds # pylint: disable=unused-import
class Application(QApplication):
@ -53,11 +54,11 @@ class Application(QApplication):
Attributes:
_args: ArgumentParser instance.
_commandrunner: The main CommandRunner instance.
_shutting_down: True if we're currently shutting down.
_quit_status: The current quitting status.
_crashdlg: The crash dialog currently open.
_crashlogfile: A file handler to the fatal crash logfile.
_event_filter: The EventFilter for the application.
"""
def __init__(self, args):
@ -74,7 +75,6 @@ class Application(QApplication):
self._shutting_down = False
self._crashdlg = None
self._crashlogfile = None
self._commandrunner = None
if args.debug:
# We don't enable this earlier because some imports trigger
@ -110,16 +110,11 @@ class Application(QApplication):
self._init_modules()
log.init.debug("Initializing eventfilter...")
mode_manager = objreg.get('mode-manager')
self.installEventFilter(mode_manager)
self._event_filter = modeman.EventFilter(self)
self.installEventFilter(self._event_filter)
log.init.debug("Connecting 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...")
self._python_hacks()
@ -134,10 +129,6 @@ class Application(QApplication):
def _init_modules(self):
"""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...")
readline_bridge = readline.ReadlineBridge()
objreg.register('readline-bridge', readline_bridge)
@ -146,35 +137,23 @@ class Application(QApplication):
config.init(self._args)
log.init.debug("Initializing crashlog...")
self._handle_segfault()
log.init.debug("Initializing modes...")
modeman.init()
log.init.debug("Initializing websettings...")
websettings.init()
log.init.debug("Initializing quickmarks...")
quickmarks.init()
log.init.debug("Initializing proxy...")
proxy.init()
log.init.debug("Initializing userscripts...")
userscripts.init()
log.init.debug("Initializing utility commands...")
utilcmds.init()
log.init.debug("Initializing cookies...")
cookie_jar = cookies.CookieJar(self)
objreg.register('cookie-jar', cookie_jar)
log.init.debug("Initializing cache...")
diskcache = cache.DiskCache(self)
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...")
download_manager = downloads.DownloadManager(self)
objreg.register('download-manager', download_manager)
log.init.debug("Initializing main window...")
main_window = mainwindow.MainWindow()
objreg.register('main-window', main_window)
mainwindow.MainWindow.spawn(False if self._args.nowindow else True)
log.init.debug("Initializing debug console...")
debug_console = console.ConsoleWidget()
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.
"""
tabbed_browser = objreg.get('tabbed-browser')
win_id = 0
for cmd in self._args.command:
if cmd.startswith(':'):
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:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
log.init.debug("Startup URL {}".format(cmd))
try:
url = urlutils.fuzzy_url(cmd)
except urlutils.FuzzyUrlError as e:
message.error("Error in startup argument '{}': {}".format(
cmd, e))
message.error(0, "Error in startup argument '{}': "
"{}".format(cmd, e))
else:
tabbed_browser.tabopen(url)
if tabbed_browser.count() == 0:
log.init.debug("Opening startpage")
for urlstr in config.get('general', 'startpage'):
try:
url = urlutils.fuzzy_url(urlstr)
except urlutils.FuzzyUrlError as e:
message.error("Error when opening startpage: {}".format(e))
else:
tabbed_browser.tabopen(url)
for win_id in objreg.window_registry:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
if tabbed_browser.count() == 0:
log.init.debug("Opening startpage")
for urlstr in config.get('general', 'startpage'):
try:
url = urlutils.fuzzy_url(urlstr)
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
state_config = objreg.get('state-config')
@ -294,89 +284,9 @@ class Application(QApplication):
def _connect_signals(self):
"""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')
key_config = objreg.get('key-config')
# misc
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)
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):
"""Get a string list of all widgets."""
@ -390,20 +300,6 @@ class Application(QApplication):
lines.append(' ' * depth + repr(kid))
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):
"""Get all children of an object recursively as a string."""
output = ['']
@ -419,36 +315,47 @@ class Application(QApplication):
len(pyqt_lines)))
output += pyqt_lines
output += ['']
output += self._get_registered_objects()
output += objreg.dump_objects()
return '\n'.join(output)
def _recover_pages(self):
def _recover_pages(self, forgiving=False):
"""Try to recover all open pages.
Called from _exception_hook, so as forgiving as possible.
Args:
forgiving: Whether to ignore exceptions.
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 = []
for tab in tabbed_browser.widgets():
try:
url = tab.cur_url.toString(
QUrl.RemovePassword | QUrl.FullyEncoded)
if url:
pages.append(url)
except Exception: # pylint: disable=broad-except
log.destroy.exception("Error while recovering tab")
for win_id in objreg.window_registry:
win_pages = []
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
for tab in tabbed_browser.widgets():
try:
urlstr = tab.cur_url.toString(
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
def _save_geometry(self):
"""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')
data = bytes(objreg.get('main-window').saveGeometry())
data = bytes(main_window.saveGeometry())
geom = base64.b64encode(data).decode('ASCII')
try:
state_config.add_section('geometry')
@ -497,13 +404,13 @@ class Application(QApplication):
self._quit_status['crash'] = False
try:
pages = self._recover_pages()
pages = self._recover_pages(forgiving=True)
except Exception:
log.destroy.exception("Error while recovering pages")
pages = []
try:
history = objreg.get('status-command').history[-5:]
history = objreg.get('command-history')[-5:]
except Exception:
log.destroy.exception("Error while getting history: {}")
history = []
@ -531,18 +438,16 @@ class Application(QApplication):
self._destroy_crashlogfile()
sys.exit(1)
@cmdutils.register(instance='app', name=['quit', 'q'])
def quit(self):
"""Quit qutebrowser."""
QApplication.closeAllWindows()
@cmdutils.register(instance='app', ignore_args=True)
def restart(self, shutdown=True, pages=None):
"""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:
pages = []
for tab in objreg.get('tabbed-browser').widgets():
urlstr = tab.cur_url.toString(
QUrl.RemovePassword | QUrl.FullyEncoded)
if urlstr:
pages.append(urlstr)
pages = self._recover_pages()
log.destroy.debug("sys.executable: {}".format(sys.executable))
log.destroy.debug("sys.path: {}".format(sys.path))
log.destroy.debug("sys.argv: {}".format(sys.argv))
@ -563,7 +468,12 @@ class Application(QApplication):
# We only want to preserve options on a restart.
args.append(arg)
# 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("cwd: {}".format(cwd))
# Open a new process and immediately shutdown the existing one
@ -592,13 +502,15 @@ class Application(QApplication):
except Exception: # pylint: disable=broad-except
out = traceback.format_exc()
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')
def report(self):
"""Report a bug in qutebrowser."""
pages = self._recover_pages()
history = objreg.get('status-command').history[-5:]
history = objreg.get('command-history')[-5:]
objects = self.get_all_objects()
self._crashdlg = crash.ReportDialog(pages, history, objects)
self._crashdlg.show()
@ -654,8 +566,13 @@ class Application(QApplication):
return
self._shutting_down = True
log.destroy.debug("Shutting down with status {}...".format(status))
prompter = objreg.get('prompter', None)
if prompter is not None and prompter.shutdown():
deferrer = False
for win_id in objreg.window_registry:
prompter = objreg.get('prompter', None, scope='window',
window=win_id)
if prompter is not None and prompter.shutdown():
deferrer = True
if deferrer:
# If shutdown was called while we were asking a question, we're in
# a still sub-eventloop (which gets quitted now) and not in the
# main one.
@ -678,15 +595,15 @@ class Application(QApplication):
# Remove eventfilter
try:
log.destroy.debug("Removing eventfilter...")
self.removeEventFilter(objreg.get('mode-manager'))
self.removeEventFilter(self._event_filter)
except KeyError:
pass
# Close all tabs
try:
log.destroy.debug("Closing tabs...")
objreg.get('tabbed-browser').shutdown()
except KeyError:
pass
for win_id in objreg.window_registry:
log.destroy.debug("Closing tabs in window {}...".format(win_id))
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
tabbed_browser.shutdown()
# Save everything
try:
config_obj = objreg.get('config')

View File

@ -23,7 +23,7 @@ import re
import os
import subprocess
import posixpath
from functools import partial
import functools
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import Qt, QUrl
@ -53,53 +53,73 @@ class CommandDispatcher:
Attributes:
_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._win_id = win_id
def __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):
"""Convenience method to get the widget count."""
return objreg.get('tabbed-browser').count()
return self._tabbed_browser().count()
def _set_current_index(self, idx):
"""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):
"""Convenience method to get the current widget index."""
return objreg.get('tabbed-browser').currentIndex()
return self._tabbed_browser().currentIndex()
def _current_url(self):
"""Convenience method to get the current url."""
return objreg.get('tabbed-browser').current_url()
return self._tabbed_browser().current_url()
def _current_widget(self):
"""Get the currently active widget from a command."""
widget = objreg.get('tabbed-browser').currentWidget()
widget = self._tabbed_browser().currentWidget()
if widget is None:
raise cmdexc.CommandError("No WebView available yet!")
return widget
def _open(self, url, tab, background):
def _open(self, url, tab, background, window):
"""Helper function to open a page.
Args:
url: The URL to open as QUrl.
tab: Whether to open in a new tab.
background: Whether to open in the background.
window: Whether to open in a new window
"""
if not url.isValid():
errstr = "Invalid URL {}"
if url.errorString():
errstr += " - {}".format(url.errorString())
raise cmdexc.CommandError(errstr)
tabbed_browser = objreg.get('tabbed-browser')
if tab and background:
raise cmdexc.CommandError("Only one of -t/-b can be given!")
tabbed_browser = self._tabbed_browser()
if sum(1 for e in (tab, background, window) if e) > 1:
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:
tabbed_browser.tabopen(url, background=False, explicit=True)
elif background:
@ -119,7 +139,7 @@ class CommandDispatcher:
The widget with the given tab ID if count is given.
None if no widget was found.
"""
tabbed_browser = objreg.get('tabbed-browser')
tabbed_browser = self._tabbed_browser()
if count is None:
return tabbed_browser.currentWidget()
elif 1 <= count <= self._count():
@ -179,10 +199,11 @@ class CommandDispatcher:
def _tab_focus_last(self):
"""Select the tab which was last focused."""
try:
tab = objreg.get('last-focused-tab')
tab = objreg.get('last-focused-tab', scope='window',
window=self._win_id)
except KeyError:
raise cmdexc.CommandError("No last focused tab!")
idx = objreg.get('tabbed-browser').indexOf(tab)
idx = self._tabbed_browser().indexOf(tab)
if idx == -1:
raise cmdexc.CommandError("Last focused tab vanished!")
self._set_current_index(idx)
@ -195,7 +216,7 @@ class CommandDispatcher:
except PermissionError:
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):
"""Close the current/[count]th tab.
@ -209,39 +230,41 @@ class CommandDispatcher:
tab = self._cntwidget(count)
if tab is None:
return
objreg.get('tabbed-browser').close_tab(tab)
self._tabbed_browser().close_tab(tab)
@cmdutils.register(instance='command-dispatcher', name='open',
split=False)
def openurl(self, url, bg=False, tab=False, count=None):
split=False, scope='window')
def openurl(self, url, bg=False, tab=False, window=False, count=None):
"""Open a URL in the current/[count]th tab.
Args:
url: The URL to open.
bg: Open in a new background tab.
tab: Open in a new tab.
window: Open in a new window.
count: The tab index to open the URL in, or None.
"""
try:
url = urlutils.fuzzy_url(url)
except urlutils.FuzzyUrlError as e:
raise cmdexc.CommandError(e)
if tab or bg:
self._open(url, tab, bg)
if tab or bg or window:
self._open(url, tab, bg, window)
else:
curtab = self._cntwidget(count)
if curtab is None:
if count is None:
# We want to open a URL in the current tab, but none exists
# yet.
objreg.get('tabbed-browser').tabopen(url)
self._tabbed_browser().tabopen(url)
else:
# Explicit count with a tab that doesn't exist.
return
else:
curtab.openurl(url)
@cmdutils.register(instance='command-dispatcher', name='reload')
@cmdutils.register(instance='command-dispatcher', name='reload',
scope='window')
def reloadpage(self, count=None):
"""Reload the current/[count]th tab.
@ -252,7 +275,7 @@ class CommandDispatcher:
if tab is not None:
tab.reload()
@cmdutils.register(instance='command-dispatcher')
@cmdutils.register(instance='command-dispatcher', scope='window')
def stop(self, count=None):
"""Stop loading in the current/[count]th tab.
@ -263,7 +286,8 @@ class CommandDispatcher:
if tab is not None:
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):
"""Print the current/[count]th tab.
@ -287,64 +311,77 @@ class CommandDispatcher:
diag.setAttribute(Qt.WA_DeleteOnClose)
diag.open(lambda: tab.print(diag.printer()))
@cmdutils.register(instance='command-dispatcher')
def tab_clone(self, bg=False):
@cmdutils.register(instance='command-dispatcher', scope='window')
def tab_clone(self, bg=False, window=False):
"""Duplicate the current tab.
Args:
bg: Open in a background tab.
window: Open in a new window.
Return:
The new QWebView.
"""
if bg and window:
raise cmdexc.CommandError("Only one of -b/-w can be given!")
curtab = self._current_widget()
tabbed_browser = objreg.get('tabbed-browser')
tabbed_browser = self._tabbed_browser(window)
newtab = tabbed_browser.tabopen(background=bg, explicit=True)
history = qtutils.serialize(curtab.history())
qtutils.deserialize(history, newtab.history())
return newtab
def _back_forward(self, tab, bg, count, forward):
def _back_forward(self, tab, bg, window, count, forward):
"""Helper function for :back/:forward."""
if tab or bg:
widget = self.tab_clone(bg)
if (not forward and not
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:
widget = self._current_widget()
for _ in range(count):
if forward:
widget.go_forward()
widget.forward()
else:
widget.go_back()
widget.back()
@cmdutils.register(instance='command-dispatcher')
def back(self, tab=False, bg=False, count=1):
@cmdutils.register(instance='command-dispatcher', scope='window')
def back(self, tab=False, bg=False, window=False, count=1):
"""Go back in the history of the current tab.
Args:
tab: Go back in a new tab.
bg: Go back in a background tab.
window: Go back in a new window.
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')
def forward(self, tab=False, bg=False, count=1):
@cmdutils.register(instance='command-dispatcher', scope='window')
def forward(self, tab=False, bg=False, window=False, count=1):
"""Go forward in the history of the current tab.
Args:
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.
"""
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.
Args:
url: The current url.
tab: Whether to open the link in a new tab.
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')
# Get the last number in a string
@ -369,25 +406,27 @@ class CommandDispatcher:
raise ValueError("Invalid value {} for indec!".format(incdec))
urlstr = ''.join([pre, str(val), post]).encode('ascii')
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.
Args:
url: The current url.
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()
if not path or path == '/':
raise cmdexc.CommandError("Can't go up!")
new_path = posixpath.join(path, posixpath.pardir)
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'),
tab=False):
tab=False, bg=False, window=False):
"""Open typical prev/next links or navigate using the URL path.
This tries to automatically click on typical _Previous Page_ or
@ -405,7 +444,11 @@ class CommandDispatcher:
- `decrement`: Decrement the last number in the URL.
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()
frame = widget.page().currentFrame()
url = self._current_url()
@ -413,18 +456,21 @@ class CommandDispatcher:
raise cmdexc.CommandError("No frame focused!")
hintmanager = objreg.get('hintmanager', scope='tab')
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':
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':
self._navigate_up(url, tab)
self._navigate_up(url, tab, bg, window)
elif where in ('decrement', 'increment'):
self._navigate_incdec(url, tab, where)
self._navigate_incdec(url, where, tab, bg, window)
else:
raise ValueError("Got called with invalid value {} for "
"`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):
"""Scroll the current tab by 'count * dx/dy'.
@ -439,7 +485,8 @@ class CommandDispatcher:
cmdutils.check_overflow(dy, 'int')
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,
horizontal: {'flag': 'x'}=False, count=None):
"""Scroll to a specific percentage of the page.
@ -455,7 +502,8 @@ class CommandDispatcher:
self._scroll_percent(perc, count,
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):
"""Scroll the frame page-wise.
@ -472,7 +520,7 @@ class CommandDispatcher:
cmdutils.check_overflow(dy, 'int')
frame.scroll(dx, dy)
@cmdutils.register(instance='command-dispatcher')
@cmdutils.register(instance='command-dispatcher', scope='window')
def yank(self, title=False, sel=False):
"""Yank the current URL/title to the clipboard or primary selection.
@ -482,7 +530,7 @@ class CommandDispatcher:
"""
clipboard = QApplication.clipboard()
if title:
s = objreg.get('tabbed-browser').tabText(self._current_index())
s = self._tabbed_browser().tabText(self._current_index())
else:
s = self._current_url().toString(
QUrl.FullyEncoded | QUrl.RemovePassword)
@ -495,9 +543,9 @@ class CommandDispatcher:
log.misc.debug("Yanking to {}: '{}'".format(target, s))
clipboard.setText(s, mode)
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):
"""Increase the zoom level for the current tab.
@ -507,7 +555,7 @@ class CommandDispatcher:
tab = self._current_widget()
tab.zoom(count)
@cmdutils.register(instance='command-dispatcher')
@cmdutils.register(instance='command-dispatcher', scope='window')
def zoom_out(self, count=1):
"""Decrease the zoom level for the current tab.
@ -517,7 +565,7 @@ class CommandDispatcher:
tab = self._current_widget()
tab.zoom(-count)
@cmdutils.register(instance='command-dispatcher')
@cmdutils.register(instance='command-dispatcher', scope='window')
def zoom(self, zoom=None, count=None):
"""Set the zoom level for the current tab.
@ -535,24 +583,24 @@ class CommandDispatcher:
tab = self._current_widget()
tab.zoom_perc(level)
@cmdutils.register(instance='command-dispatcher')
@cmdutils.register(instance='command-dispatcher', scope='window')
def tab_only(self):
"""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():
if tab is self._current_widget():
continue
tabbed_browser.close_tab(tab)
@cmdutils.register(instance='command-dispatcher')
@cmdutils.register(instance='command-dispatcher', scope='window')
def undo(self):
"""Re-open a closed tab (optionally skipping [count] closed tabs)."""
try:
objreg.get('tabbed-browser').undo()
self._tabbed_browser().undo()
except IndexError:
raise cmdexc.CommandError("Nothing to undo!")
@cmdutils.register(instance='command-dispatcher')
@cmdutils.register(instance='command-dispatcher', scope='window')
def tab_prev(self, count=1):
"""Switch to the previous tab, or switch [count] tabs back.
@ -567,7 +615,7 @@ class CommandDispatcher:
else:
raise cmdexc.CommandError("First tab")
@cmdutils.register(instance='command-dispatcher')
@cmdutils.register(instance='command-dispatcher', scope='window')
def tab_next(self, count=1):
"""Switch to the next tab, or switch [count] tabs forward.
@ -582,14 +630,15 @@ class CommandDispatcher:
else:
raise cmdexc.CommandError("Last tab")
@cmdutils.register(instance='command-dispatcher')
def paste(self, sel=False, tab=False, bg=False):
@cmdutils.register(instance='command-dispatcher', scope='window')
def paste(self, sel=False, tab=False, bg=False, window=False):
"""Open a page from the clipboard.
Args:
sel: Use the primary selection instead of the clipboard.
tab: Open in a new tab.
bg: Open in a background tab.
window: Open in new window.
"""
clipboard = QApplication.clipboard()
if sel and clipboard.supportsSelection():
@ -606,9 +655,9 @@ class CommandDispatcher:
url = urlutils.fuzzy_url(text)
except urlutils.FuzzyUrlError as 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):
"""Select the tab given as argument/[count].
@ -632,7 +681,7 @@ class CommandDispatcher:
raise cmdexc.CommandError("There's no tab with index {}!".format(
idx))
@cmdutils.register(instance='command-dispatcher')
@cmdutils.register(instance='command-dispatcher', scope='window')
def tab_move(self, direction: ('+', '-')=None, count=None):
"""Move the current tab.
@ -656,7 +705,7 @@ class CommandDispatcher:
if not 0 <= new_idx < self._count():
raise cmdexc.CommandError("Can't move tab to position {}!".format(
new_idx))
tabbed_browser = objreg.get('tabbed-browser')
tabbed_browser = self._tabbed_browser()
tab = self._current_widget()
cur_idx = self._current_index()
icon = tabbed_browser.tabIcon(cur_idx)
@ -671,7 +720,8 @@ class CommandDispatcher:
finally:
tabbed_browser.setUpdatesEnabled(True)
@cmdutils.register(instance='command-dispatcher', split=False)
@cmdutils.register(instance='command-dispatcher', split=False,
scope='window')
def spawn(self, *args):
"""Spawn a command in a shell.
@ -689,12 +739,12 @@ class CommandDispatcher:
log.procs.debug("Executing: {}".format(args))
subprocess.Popen(args)
@cmdutils.register(instance='command-dispatcher')
@cmdutils.register(instance='command-dispatcher', scope='window')
def home(self):
"""Open main startpage in current tab."""
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': '*'}):
"""Run an userscript given as argument.
@ -702,27 +752,30 @@ class CommandDispatcher:
cmd: The userscript to run.
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):
"""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')
def quickmark_load(self, name, tab=False, bg=False):
@cmdutils.register(instance='command-dispatcher', scope='window')
def quickmark_load(self, name, tab=False, bg=False, window=False):
"""Load a quickmark.
Args:
name: The name of the quickmark to load.
tab: Load the quickmark in a new tab.
bg: Load the quickmark in a new background tab.
window: Load the quickmark in a new window.
"""
urlstr = quickmarks.get(name)
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):
"""Toggle the web inspector."""
cur = self._current_widget()
@ -744,13 +797,13 @@ class CommandDispatcher:
else:
cur.inspector.show()
@cmdutils.register(instance='command-dispatcher')
@cmdutils.register(instance='command-dispatcher', scope='window')
def download_page(self):
"""Download the current page."""
page = self._current_widget().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):
"""Show the source of the current page."""
# pylint doesn't seem to like pygments...
@ -766,15 +819,20 @@ class CommandDispatcher:
highlighted = pygments.highlight(html, lexer, formatter)
current_url = self._current_url()
tab = objreg.get('tabbed-browser').tabopen(explicit=True)
tab = self._tabbed_browser().tabopen(explicit=True)
tab.setHtml(highlighted, current_url)
tab.viewing_source = True
@cmdutils.register(instance='command-dispatcher', name='help',
completion=[usertypes.Completion.helptopic])
def show_help(self, topic=None):
completion=[usertypes.Completion.helptopic],
scope='window')
def show_help(self, tab=False, bg=False, window=False, topic=None):
r"""Show help about a command or setting.
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.
- :__command__ for commands.
@ -804,11 +862,12 @@ class CommandDispatcher:
path = 'settings.html#{}'.format(topic.replace('->', '-'))
else:
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',
modes=[usertypes.KeyMode.insert],
hide=True)
hide=True, scope='window')
def open_editor(self):
"""Open an external editor with the currently selected form field.
@ -832,9 +891,10 @@ class CommandDispatcher:
text = str(elem)
else:
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(
partial(self.on_editing_finished, elem))
functools.partial(self.on_editing_finished, elem))
self._editor.edit(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.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.commands import cmdexc, cmdutils
@ -412,7 +414,9 @@ class DownloadManager(QObject):
q.destroyed.connect(functools.partial(self.questions.remove, q))
self.questions.append(q)
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)
def on_finished(self, download):
@ -433,4 +437,4 @@ class DownloadManager(QObject):
@pyqtSlot(str)
def on_error(self, msg):
"""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'])
Target = usertypes.enum('Target', ['normal', 'tab', 'tab_bg', 'yank',
'yank_primary', 'fill', 'rapid', 'download',
'userscript', 'spawn'])
Target = usertypes.enum('Target', ['normal', 'tab', 'tab_bg', 'window', 'yank',
'yank_primary', 'fill', 'rapid',
'rapid_win', 'download', 'userscript',
'spawn'])
@pyqtSlot(usertypes.KeyMode)
def on_mode_entered(mode):
def on_mode_entered(mode, win_id):
"""Stop hinting when insert mode was entered."""
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:
@ -58,7 +59,7 @@ class HintContext:
elems: A mapping from keystrings to (elem, label) namedtuples.
baseurl: The URL of the current page.
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
fill: Fill commandline with link.
rapid: Rapid mode with background tabs
@ -100,6 +101,8 @@ class HintManager(QObject):
Attributes:
_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:
mouse_event: Mouse event to be posted in the web view.
@ -125,10 +128,12 @@ class HintManager(QObject):
Target.normal: "Follow hint...",
Target.tab: "Follow hint in new tab...",
Target.tab_bg: "Follow hint in background tab...",
Target.window: "Follow hint in new window...",
Target.yank: "Yank hint to clipboard...",
Target.yank_primary: "Yank hint to primary selection...",
Target.fill: "Set hint in commandline...",
Target.rapid: "Follow hint (rapid mode)...",
Target.rapid_win: "Follow hint in new window (rapid mode)...",
Target.download: "Download hint...",
Target.userscript: "Call userscript via hint...",
Target.spawn: "Spawn command via hint...",
@ -137,15 +142,15 @@ class HintManager(QObject):
mouse_event = pyqtSignal('QMouseEvent')
set_open_target = pyqtSignal(str)
def __init__(self, parent=None):
"""Constructor.
Args:
frame: The QWebFrame to use for finding elements and drawing.
"""
def __init__(self, win_id, tab_id, parent=None):
"""Constructor."""
super().__init__(parent)
self._win_id = win_id
self._tab_id = tab_id
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):
"""Clean up after hinting."""
@ -155,7 +160,9 @@ class HintManager(QObject):
except webelem.IsNullError:
pass
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
def _hint_strings(self, elems):
@ -300,6 +307,8 @@ class HintManager(QObject):
"""
if self._context.target == Target.rapid:
target = Target.tab_bg
elif self._context.target == Target.rapid_win:
target = Target.window
else:
target = self._context.target
self.set_open_target.emit(target.name)
@ -331,8 +340,8 @@ class HintManager(QObject):
mode = QClipboard.Selection if sel else QClipboard.Clipboard
urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword)
QApplication.clipboard().setText(urlstr, mode)
message.info("URL yanked to {}".format("primary selection" if sel
else "clipboard"))
message.info(self._win_id, "URL yanked to {}".format(
"primary selection" if sel else "clipboard"))
def _preset_cmd_text(self, url):
"""Preset a commandline text based on a hint URL.
@ -342,7 +351,7 @@ class HintManager(QObject):
"""
urlstr = url.toDisplayString(QUrl.FullyEncoded)
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):
"""Download a hint URL.
@ -352,7 +361,8 @@ class HintManager(QObject):
"""
url = self._resolve_url(elem)
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)
return
objreg.get('download-manager').get(url, elem.webFrame().page())
@ -361,7 +371,7 @@ class HintManager(QObject):
"""Call an userscript from a hint."""
cmd = self._context.args[0]
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):
"""Spawn a simple command from a hint."""
@ -487,17 +497,22 @@ class HintManager(QObject):
for e, string in zip(elems, strings):
label = self._draw_label(e, string)
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)
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.
Args:
frame: The frame where the element is in.
baseurl: The base URL of the current tab.
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)
if elem is None:
@ -507,10 +522,22 @@ class HintManager(QObject):
if url is None:
raise cmdexc.CommandError("No {} links found!".format(
"prev" if prev else "forward"))
if newtab:
objreg.get('tabbed-browser').tabopen(url, background=False)
qtutils.ensure_valid(url)
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:
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')
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.
- `tab`: Open the link in a new 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-primary`: Yank the link to the primary selection.
- `fill`: Fill the commandline with the command given as
argument.
- `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.
- `userscript`: Call an userscript with `$QUTE_URL` set to the
link.
@ -549,7 +579,8 @@ class HintManager(QObject):
`{hint-url}` will get replaced by the selected
URL.
"""
tabbed_browser = objreg.get('tabbed-browser')
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
widget = tabbed_browser.currentWidget()
if widget is None:
raise cmdexc.CommandError("No WebView available yet!")
@ -563,10 +594,13 @@ class HintManager(QObject):
self._context.frames = webelem.get_child_frames(mainframe)
self._context.args = args
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()
try:
modeman.enter(usertypes.KeyMode.hint, 'HintManager.start')
modeman.enter(self._win_id, usertypes.KeyMode.hint,
'HintManager.start')
except modeman.ModeLockedError:
self._cleanup()
@ -610,7 +644,7 @@ class HintManager(QObject):
visible[k] = e
if not visible:
# 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'):
# unpacking gets us the first (and only) key in the dict.
self.fire(*visible)
@ -631,7 +665,9 @@ class HintManager(QObject):
Target.normal: self._click,
Target.tab: self._click,
Target.tab_bg: self._click,
Target.window: self._click,
Target.rapid: self._click,
Target.rapid_win: self._click,
# _download needs a QWebElement to get the frame.
Target.download: self._download,
}
@ -649,14 +685,16 @@ class HintManager(QObject):
elif self._context.target in url_handlers:
url = self._resolve_url(elem)
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)
return
url_handlers[self._context.target](url)
else:
raise ValueError("No suitable handler found!")
if self._context.target != Target.rapid:
modeman.maybe_leave(usertypes.KeyMode.hint, 'followed')
if self._context.target not in (Target.rapid, Target.rapid_win):
modeman.maybe_leave(self._win_id, usertypes.KeyMode.hint,
'followed')
@cmdutils.register(instance='hintmanager', scope='tab', hide=True)
def follow_hint(self):

View File

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

View File

@ -34,12 +34,19 @@ class SignalFilter(QObject):
Signals are only passed to the parent TabbedBrowser if they originated in
the currently shown widget.
Attributes:
_win_id: The window ID this SignalFilter is associated with.
Class attributes:
BLACKLIST: List of signal names which should not be logged.
"""
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):
"""Factory for partial _filter_signals functions.
@ -73,7 +80,8 @@ class SignalFilter(QObject):
The target signal if the sender was the current widget.
"""
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:
tabidx = tabbed_browser.indexOf(tab)
except RuntimeError:

View File

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

View File

@ -58,7 +58,9 @@ class HelpAction(argparse.Action):
"""
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)))
parser.exit()

View File

@ -95,13 +95,18 @@ class Command:
self._type_conv = type_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.
Args:
win_id: The window ID the command is run in.
Raise:
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:
mode_names = '/'.join(mode.name for mode in self._modes)
raise cmdexc.PrerequisitesError(
@ -179,7 +184,7 @@ class Command:
desc = ""
if not self.ignore_args:
for param in signature.parameters.values():
if param.name in ('self', 'count'):
if param.name in ('self', 'count', 'win_id'):
continue
annotation_info = self._parse_annotation(param)
typ = self._get_type(param, annotation_info)
@ -294,15 +299,19 @@ class Command:
else:
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.
Arguments:
win_id: The window id this command should be executed in.
param: The count parameter.
args: The positional argument list. Gets modified directly.
"""
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)
def _get_count_arg(self, param, args, kwargs):
@ -328,6 +337,23 @@ class Command:
raise TypeError("{}: invalid parameter type {} for argument "
"'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):
"""Get the converted name and value for an inspect.Parameter."""
name = self._name_conv.get(param.name, param.name)
@ -340,9 +366,12 @@ class Command:
value = self._type_conv[param.name](value)
return name, value
def _get_call_args(self):
def _get_call_args(self, win_id): # noqa
"""Get arguments for a function call.
Args:
win_id: The window id this command should be executed in.
Return:
An (args, kwargs) tuple.
"""
@ -354,18 +383,22 @@ class Command:
if self.ignore_args:
if self._instance is not None:
param = list(signature.parameters.values())[0]
self._get_self_arg(param, args)
self._get_self_arg(win_id, param, args)
return args, kwargs
for i, param in enumerate(signature.parameters.values()):
if i == 0 and self._instance is not None:
# Special case for 'self'.
self._get_self_arg(param, args)
self._get_self_arg(win_id, param, args)
continue
elif param.name == 'count':
# Special case for 'count'.
self._get_count_arg(param, args, kwargs)
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)
if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
args.append(value)
@ -380,12 +413,13 @@ class Command:
self.name, param.kind, param.name))
return args, kwargs
def run(self, args=None, count=None):
def run(self, win_id, args=None, count=None):
"""Run the command.
Note we don't catch CommandError here as it might happen async.
Args:
win_id: The window ID the command is run in.
args: Arguments to the command.
count: Command repetition count.
"""
@ -398,15 +432,15 @@ class Command:
try:
self.namespace = self.parser.parse_args(args)
except argparser.ArgumentParserError as e:
message.error('{}: {}'.format(self.name, e))
message.error(win_id, '{}: {}'.format(self.name, e))
return
except argparser.ArgumentParserExit as e:
log.commands.debug("argparser exited with status {}: {}".format(
e.status, e))
return
self._count = count
posargs, kwargs = self._get_call_args()
self._check_prerequisites()
posargs, kwargs = self._get_call_args(win_id)
self._check_prerequisites(win_id)
log.commands.debug('Calling {}'.format(
debug.format_call(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
def replace_variables(arglist):
def replace_variables(win_id, arglist):
"""Utility function to replace variables like {url} in a list of args."""
args = []
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
for arg in arglist:
if arg == '{url}':
url = objreg.get('tabbed-browser').current_url().toString(
QUrl.FullyEncoded | QUrl.RemovePassword)
# Note we have to do this in here as the user gets an error message
# by current_url if no URL is open yet.
url = tabbed_browser.current_url().toString(QUrl.FullyEncoded |
QUrl.RemovePassword)
args.append(url)
else:
args.append(arg)
@ -114,7 +118,7 @@ class SearchRunner(QObject):
"""
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):
"""Continue the search to the ([count]th) next term.
@ -128,7 +132,7 @@ class SearchRunner(QObject):
for _ in range(count):
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):
"""Continue the search to the ([count]th) previous term.
@ -152,18 +156,21 @@ class SearchRunner(QObject):
self.do_search.emit(self._text, flags)
class CommandRunner:
class CommandRunner(QObject):
"""Parse and run qutebrowser commandline commands.
Attributes:
_cmd: The command which was 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._args = []
self._win_id = win_id
def _get_alias(self, text, alias_no_args):
"""Get an alias from the config.
@ -278,11 +285,11 @@ class CommandRunner:
self.run(sub, count)
return
self.parse(text)
args = replace_variables(self._args)
args = replace_variables(self._win_id, self._args)
if count is not None:
self._cmd.run(args, count=count)
self._cmd.run(self._win_id, args, count=count)
else:
self._cmd.run(args)
self._cmd.run(self._win_id, args)
@pyqtSlot(str, int)
def run_safely(self, text, count=None):
@ -290,7 +297,7 @@ class CommandRunner:
try:
self.run(text, count)
except (cmdexc.CommandMetaError, cmdexc.CommandError) as e:
message.error(e, immediately=True)
message.error(self._win_id, e, immediately=True)
@pyqtSlot(str, int)
def run_safely_init(self, text, count=None):
@ -301,4 +308,4 @@ class CommandRunner:
try:
self.run(text, count)
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
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Functions to execute an userscript.
Module attributes:
_runners: Active userscript runners from run_userscript.
"""
"""Functions to execute an userscript."""
import os
import os.path
import tempfile
import select
import functools
from PyQt5.QtCore import (pyqtSignal, QObject, QThread, QStandardPaths,
QProcessEnvironment, QProcess, QUrl)
from qutebrowser.utils import message, log, utils
from qutebrowser.utils import message, log, utils, objreg
from qutebrowser.commands import runners, cmdexc
_runners = []
_commandrunner = None
class _BlockingFIFOReader(QObject):
"""A worker which reads commands from a FIFO endlessly.
@ -97,6 +88,7 @@ class _BaseUserscriptRunner(QObject):
Attributes:
_filepath: The path of the file/FIFO which is being read.
_proc: The QProcess which is being executed.
_win_id: The window ID this runner is associated with.
Class attributes:
PROCESS_MESSAGES: A mapping of QProcess::ProcessError members to
@ -121,8 +113,9 @@ class _BaseUserscriptRunner(QObject):
QProcess.UnknownError: "An unknown error occurred.",
}
def __init__(self, parent=None):
def __init__(self, win_id, parent=None):
super().__init__(parent)
self._win_id = win_id
self._filepath = None
self._proc = None
@ -152,7 +145,8 @@ class _BaseUserscriptRunner(QObject):
except PermissionError as e:
# NOTE: Do not replace this with "raise CommandError" as it's
# 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._proc = None
@ -180,7 +174,8 @@ class _BaseUserscriptRunner(QObject):
msg = self.PROCESS_MESSAGES[error]
# NOTE: Do not replace this with "raise CommandError" as it's
# executed async.
message.error("Error while calling userscript: {}".format(msg))
message.error(self._win_id,
"Error while calling userscript: {}".format(msg))
class _POSIXUserscriptRunner(_BaseUserscriptRunner):
@ -195,8 +190,8 @@ class _POSIXUserscriptRunner(_BaseUserscriptRunner):
_thread: The QThread where reader runs.
"""
def __init__(self, parent=None):
super().__init__(parent)
def __init__(self, win_id, parent=None):
super().__init__(win_id, parent)
self._reader = None
self._thread = None
@ -262,8 +257,8 @@ class _WindowsUserscriptRunner(_BaseUserscriptRunner):
_oshandle: The oshandle of the temp file.
"""
def __init__(self, parent=None):
super().__init__(parent)
def __init__(self, win_id, parent=None):
super().__init__(win_id, parent)
self._oshandle = None
def _cleanup(self):
@ -326,19 +321,16 @@ else:
UserscriptRunner = _DummyUserscriptRunner
def init():
"""Initialize the global _commandrunner."""
global _commandrunner
_commandrunner = runners.CommandRunner()
def run(cmd, *args, url):
def run(cmd, *args, url, win_id):
"""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
# pass via env variable..
urlstr = url.toString(QUrl.FullyEncoded)
runner = UserscriptRunner()
runner.got_cmd.connect(_commandrunner.run_safely)
commandrunner = runners.CommandRunner(win_id, tabbed_browser)
runner = UserscriptRunner(win_id, tabbed_browser)
runner.got_cmd.connect(commandrunner.run_safely)
runner.run(cmd, *args, env={'QUTE_URL': urlstr})
_runners.append(runner)
runner.finished.connect(functools.partial(_runners.remove, runner))
runner.finished.connect(commandrunner.deleteLater)
runner.finished.connect(runner.deleteLater)

View File

@ -435,7 +435,7 @@ class ConfigManager(QObject):
@cmdutils.register(name='set', instance='config',
completion=[Completion.section, Completion.option,
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):
"""Set an option.
@ -455,8 +455,8 @@ class ConfigManager(QObject):
try:
if optname.endswith('?'):
val = self.get(sectname, optname[:-1], transformed=False)
message.info("{} {} = {}".format(sectname, optname[:-1], val),
immediately=True)
message.info(win_id, "{} {} = {}".format(
sectname, optname[:-1], val), immediately=True)
else:
if value is None:
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 -b "', ['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']),
('tab-close', ['d', '<Ctrl-W>']),
('tab-only', ['co']),
@ -891,10 +893,13 @@ KEY_DATA = collections.OrderedDict([
('reload', ['r']),
('back', ['H', '<Backspace>']),
('back -t', ['th']),
('back -w', ['wh']),
('forward', ['L']),
('forward -t', ['tl']),
('forward -w', ['wl']),
('hint', ['f']),
('hint all tab', ['F']),
('hint all window', ['wf']),
('hint all tab-bg', [';b']),
('hint images', [';i']),
('hint images tab', [';I']),
@ -905,6 +910,7 @@ KEY_DATA = collections.OrderedDict([
('hint links yank', [';y']),
('hint links yank-primary', [';Y']),
('hint links rapid', [';r']),
('hint links rapid-win', [';R']),
('hint links download', [';d']),
('scroll -50 0', ['h']),
('scroll 0 50', ['j']),
@ -924,9 +930,12 @@ KEY_DATA = collections.OrderedDict([
('paste -s', ['pP']),
('paste -t', ['Pp']),
('paste -ts', ['PP']),
('paste -w', ['wp']),
('paste -ws', ['wP']),
('quickmark-save', ['m']),
('set-cmd-text ":quickmark-load "', ['b']),
('set-cmd-text ":quickmark-load -t "', ['B']),
('set-cmd-text ":quickmark-load -w"', ['wb']),
('save', ['sf']),
('set-cmd-text ":set "', ['ss']),
('set-cmd-text ":set -t "', ['sl']),

View File

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

View File

@ -53,6 +53,7 @@ class BaseKeyParser(QObject):
Attributes:
bindings: Bound keybindings
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
keychains in a section which does not support them.
_keystring: The currently entered key sequence
@ -73,9 +74,10 @@ class BaseKeyParser(QObject):
'none'])
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):
super().__init__(parent)
self._win_id = win_id
self._timer = None
self._modename = None
self._keystring = ''

View File

@ -32,16 +32,16 @@ class CommandKeyParser(BaseKeyParser):
_commandrunner: CommandRunner instance.
"""
def __init__(self, parent=None, supports_count=None,
def __init__(self, win_id, parent=None, supports_count=None,
supports_chains=False):
super().__init__(parent, supports_count, supports_chains)
self._commandrunner = runners.CommandRunner()
super().__init__(win_id, parent, supports_count, supports_chains)
self._commandrunner = runners.CommandRunner(win_id)
def execute(self, cmdstr, _keytype, count=None):
try:
self._commandrunner.run(cmdstr, count)
except (cmdexc.CommandMetaError, cmdexc.CommandError) as e:
message.error(e, immediately=True)
message.error(self._win_id, e, immediately=True)
class PassthroughKeyParser(CommandKeyParser):
@ -56,7 +56,7 @@ class PassthroughKeyParser(CommandKeyParser):
do_log = False
def __init__(self, mode, parent=None, warn=True):
def __init__(self, win_id, mode, parent=None, warn=True):
"""Constructor.
Args:
@ -64,7 +64,7 @@ class PassthroughKeyParser(CommandKeyParser):
parent: Qt parent.
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.read_config(mode)
self._mode = mode

View File

@ -23,6 +23,8 @@ Module attributes:
manager: The ModeManager instance.
"""
import functools
from PyQt5.QtGui import QWindow
from PyQt5.QtCore import pyqtSignal, QObject, QEvent
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."""
def init():
"""Inizialize the mode manager and the keyparsers."""
def init(win_id, parent):
"""Inizialize the mode manager and the keyparsers for the given win_id."""
KM = usertypes.KeyMode # pylint: disable=invalid-name
modeman = ModeManager(objreg.get('app'))
objreg.register('mode-manager', modeman)
modeman = ModeManager(win_id, parent)
objreg.register('mode-manager', modeman, scope='window', window=win_id)
keyparsers = {
KM.normal: modeparsers.NormalKeyParser(modeman),
KM.hint: modeparsers.HintKeyParser(modeman),
KM.insert: keyparser.PassthroughKeyParser('insert', modeman),
KM.passthrough: keyparser.PassthroughKeyParser('passthrough', modeman),
KM.command: keyparser.PassthroughKeyParser('command', modeman),
KM.prompt: keyparser.PassthroughKeyParser('prompt', modeman,
KM.normal: modeparsers.NormalKeyParser(win_id, modeman),
KM.hint: modeparsers.HintKeyParser(win_id, modeman),
KM.insert: keyparser.PassthroughKeyParser(win_id, 'insert', modeman),
KM.passthrough: keyparser.PassthroughKeyParser(win_id, 'passthrough',
modeman),
KM.command: keyparser.PassthroughKeyParser(win_id, 'command', modeman),
KM.prompt: keyparser.PassthroughKeyParser(win_id, 'prompt', modeman,
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.hint, keyparsers[KM.hint].handle)
modeman.register(KM.insert, keyparsers[KM.insert].handle, passthrough=True)
@ -68,35 +74,66 @@ def init():
passthrough=True)
modeman.register(KM.prompt, keyparsers[KM.prompt].handle, passthrough=True)
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'."""
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'."""
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."""
try:
objreg.get('mode-manager').enter(mode, reason, override)
_get_modeman(win_id).enter(mode, reason, override)
except ModeLockedError:
pass
def maybe_leave(mode, reason=None):
def maybe_leave(win_id, mode, reason=None):
"""Convenience method to leave 'mode' without exceptions."""
try:
objreg.get('mode-manager').leave(mode, reason)
_get_modeman(win_id).leave(mode, reason)
except NotInModeError as e:
# This is rather likely to happen, so we only log to debug log.
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):
"""Manager for keyboard modes.
@ -106,6 +143,7 @@ class ModeManager(QObject):
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
be entered while the current mode is active.
_win_id: The window ID of this ModeManager
_handlers: A dictionary of modes and their handlers.
_mode_stack: A list of the modes we're currently in, with the active
one on the right.
@ -116,16 +154,19 @@ class ModeManager(QObject):
Signals:
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.
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)
left = pyqtSignal(usertypes.KeyMode)
entered = pyqtSignal(usertypes.KeyMode, int)
left = pyqtSignal(usertypes.KeyMode, int)
def __init__(self, parent=None):
def __init__(self, win_id, parent=None):
super().__init__(parent)
self._win_id = win_id
self.locked = False
self._handlers = {}
self.passthrough = []
@ -251,9 +292,9 @@ class ModeManager(QObject):
return
self._mode_stack.append(mode)
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):
"""Enter a key mode.
@ -284,10 +325,11 @@ class ModeManager(QObject):
log.modes.debug("Leaving mode {}{}, new mode stack {}".format(
mode, '' if reason is None else ' (reason: {})'.format(reason),
self._mode_stack))
self.left.emit(mode)
self.left.emit(mode, self._win_id)
@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):
"""Leave the mode we're currently in."""
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're not interested in it anymore.
return False
if (QApplication.instance().activeWindow() is not
objreg.get('main-window')):
if (QApplication.instance().activeWindow() not in
objreg.window_registry.values()):
# Some other window (print dialog, etc.) is focused so we pass
# the event through.
return False

View File

@ -39,8 +39,9 @@ class NormalKeyParser(keyparser.CommandKeyParser):
"""KeyParser for normalmode with added STARTCHARS detection."""
def __init__(self, parent=None):
super().__init__(parent, supports_count=True, supports_chains=True)
def __init__(self, win_id, parent=None):
super().__init__(win_id, parent, supports_count=True,
supports_chains=True)
self.read_config('normal')
def __repr__(self):
@ -57,7 +58,7 @@ class NormalKeyParser(keyparser.CommandKeyParser):
"""
txt = e.text().strip()
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 super()._handle_single_key(e)
@ -66,8 +67,9 @@ class PromptKeyParser(keyparser.CommandKeyParser):
"""KeyParser for yes/no prompts."""
def __init__(self, parent=None):
super().__init__(parent, supports_count=False, supports_chains=True)
def __init__(self, win_id, parent=None):
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
# abuse the prompt section.
self.read_config('prompt')
@ -85,8 +87,9 @@ class HintKeyParser(keyparser.CommandKeyParser):
_last_press: The nature of the last keypress, a LastPress member.
"""
def __init__(self, parent=None):
super().__init__(parent, supports_count=False, supports_chains=True)
def __init__(self, win_id, parent=None):
super().__init__(win_id, parent, supports_count=False,
supports_chains=True)
self._filtertext = ''
self._last_press = LastPress.none
self.read_config('hint')
@ -108,7 +111,8 @@ class HintKeyParser(keyparser.CommandKeyParser):
"""
log.keyboard.debug("Got special key 0x{:x} text {}".format(
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:
log.keyboard.debug("Got backspace, mode {}, filtertext '{}', "
"keystring '{}'".format(self._last_press,
@ -163,7 +167,9 @@ class HintKeyParser(keyparser.CommandKeyParser):
if not isinstance(keytype, self.Type):
raise TypeError("Type {} is no Type member!".format(keytype))
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:
# execute as command
super().execute(cmdstr, keytype, count)
@ -180,4 +186,6 @@ class HintKeyParser(keyparser.CommandKeyParser):
@pyqtSlot(str)
def on_keystring_updated(self, keystr):
"""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.
_scheme_handlers: A dictionary (scheme -> handler) of supported custom
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")
super().__init__(parent)
self._win_id = win_id
self._requests = []
self._scheme_handlers = {
'qute': qutescheme.QuteSchemeHandler(),
'qute': qutescheme.QuteSchemeHandler(win_id),
}
# We have a shared cookie jar and cache - we restore their parents so
@ -101,20 +103,22 @@ class NetworkManager(QNetworkAccessManager):
for err in errors:
# FIXME we might want to use warn here (non-fatal error)
# 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()
@pyqtSlot('QNetworkReply', 'QAuthenticator')
def on_authentication_required(self, _reply, authenticator):
"""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)
self._fill_authenticator(authenticator, answer)
@pyqtSlot('QNetworkProxy', 'QAuthenticator')
def on_proxy_authentication_required(self, _proxy, authenticator):
"""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)
self._fill_authenticator(authenticator, answer)

View File

@ -69,7 +69,7 @@ class QuteSchemeHandler(schemehandler.SchemeHandler):
request, errorstr, QNetworkReply.ContentNotFoundError,
self.parent())
try:
data = handler(request)
data = handler(self._win_id, request)
except IOError as e:
return schemehandler.ErrorNetworkReply(
request, str(e), QNetworkReply.ContentNotFoundError,
@ -78,14 +78,14 @@ class QuteSchemeHandler(schemehandler.SchemeHandler):
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."""
html = jinja.env.get_template('pre.html').render(
title='pyeval', content=pyeval_output)
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."""
html = jinja.env.get_template('version.html').render(
title='Version info', version=version.version(),
@ -93,7 +93,7 @@ def qute_version(_request):
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."""
if log.ram_handler is None:
text = "Log output was disabled."
@ -103,7 +103,7 @@ def qute_plainlog(_request):
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."""
if log.ram_handler is None:
html_log = None
@ -114,12 +114,12 @@ def qute_log(_request):
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."""
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."""
try:
utils.read_file('html/doc/index.html')
@ -140,8 +140,8 @@ def qute_help(request):
else:
urlpath = urlpath.lstrip('/')
if not docutils.docs_up_to_date(urlpath):
message.error("Your documentation is outdated! Please re-run scripts/"
"asciidoc2html.py.")
message.error(win_id, "Your documentation is outdated! Please re-run "
"scripts/asciidoc2html.py.")
path = 'html/doc/{}'.format(urlpath)
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):
"""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):
"""Create a new request.

View File

@ -91,7 +91,8 @@ def get_argparser():
parser.add_argument('command', nargs='*', help="Commands to execute on "
"startup.", metavar=':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

View File

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

View File

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

View File

@ -35,6 +35,7 @@ class Completer(QObject):
Attributes:
_ignore_change: Whether to ignore the next completion update.
_models: dict of available completion models.
_win_id: The window ID this completer is in.
Signals:
change_completed_part: Text which should be substituted for the word
@ -47,8 +48,9 @@ class Completer(QObject):
change_completed_part = pyqtSignal(str, bool)
def __init__(self, parent=None):
def __init__(self, win_id, parent=None):
super().__init__(parent)
self._win_id = win_id
self._ignore_change = False
self._models = {
@ -63,7 +65,9 @@ class Completer(QObject):
def _model(self):
"""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):
"""Initialize the static completion models."""
@ -192,7 +196,8 @@ class Completer(QObject):
log.completion.debug("Ignoring completion update")
return
completion = objreg.get('completion')
completion = objreg.get('completion', scope='window',
window=self._win_id)
if prefix != ':':
# 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.
_filehandle: The file handle to the tmpfile.
_proc: The QProcess of the editor.
_win_id: The window ID the ExternalEditor is associated with.
"""
editing_finished = pyqtSignal(str)
def __init__(self, parent=None):
def __init__(self, win_id, parent=None):
super().__init__(parent)
self._text = None
self._oshandle = None
self._filename = None
self._proc = None
self._win_id = win_id
def _cleanup(self):
"""Clean up temporary files after the editor closed."""
@ -56,7 +58,8 @@ class ExternalEditor(QObject):
except PermissionError as e:
# NOTE: Do not replace this with "raise CommandError" as it's
# 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):
"""Write the editor text into the form field and clean up tempfile.
@ -75,8 +78,9 @@ class ExternalEditor(QObject):
if exitcode != 0:
# NOTE: Do not replace this with "raise CommandError" as it's
# executed async.
message.error("Editor did quit abnormally (status {})!".format(
exitcode))
message.error(
self._win_id, "Editor did quit abnormally (status "
"{})!".format(exitcode))
return
encoding = config.get('general', 'editor-encoding')
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
# 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()
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
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.
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.
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."""
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).
Args:
win_id: The ID of the window which is calling this function.
message: The message to display to the user.
mode: A PromptMode.
default: The default value to display.
@ -62,24 +70,30 @@ def ask(message, mode, default=None):
q.text = message
q.mode = mode
q.default = default
objreg.get('message-bridge').ask(q, blocking=True)
_get_bridge(win_id).ask(q, blocking=True)
q.deleteLater()
return q.answer
def alert(message):
"""Display an alert which needs to be confirmed."""
def alert(win_id, message):
"""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.text = message
q.mode = usertypes.PromptMode.alert
objreg.get('message-bridge').ask(q, blocking=True)
_get_bridge(win_id).ask(q, blocking=True)
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.
Args:
win_id: The ID of the window which is calling this function.
message: The message to display to the user.
mode: A PromptMode.
handler: The function to get called with the answer as argument.
@ -87,7 +101,7 @@ def ask_async(message, mode, handler, default=None):
"""
if not isinstance(mode, usertypes.PromptMode):
raise TypeError("Mode {} is no PromptMode member!".format(mode))
bridge = objreg.get('message-bridge')
bridge = _get_bridge(win_id)
q = usertypes.Question(bridge)
q.text = message
q.mode = mode
@ -97,16 +111,17 @@ def ask_async(message, mode, handler, default=None):
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.
Args:
win_id: The ID of the window which is calling this function.
message: The message to display to the user.
yes_action: Callable to be called when the user answered yes.
no_action: Callable to be called when the user answered no.
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.text = message
q.mode = usertypes.PromptMode.yesno

View File

@ -23,7 +23,9 @@
import collections
import functools
from PyQt5.QtCore import QObject
from PyQt5.QtCore import QObject, QTimer
from qutebrowser.utils import log
class UnsetObject:
@ -58,12 +60,28 @@ class ObjectRegistry(collections.UserDict):
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):
obj.destroyed.connect(functools.partial(self.on_destroyed, name))
super().__setitem__(name, obj)
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."""
log.misc.debug("destroyed: {}".format(name))
try:
del self[name]
except KeyError:
@ -79,33 +97,79 @@ class ObjectRegistry(collections.UserDict):
# The registry for global objects
global_registry = ObjectRegistry()
# The object registry of object registries.
meta_registry = ObjectRegistry()
meta_registry['global'] = global_registry
# The window registry.
window_registry = ObjectRegistry()
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."""
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':
return global_registry
elif scope == 'tab':
widget = get('tabbed-browser').currentWidget()
if widget is None:
raise RegistryUnavailableError(scope)
return widget.registry
elif scope == 'meta':
return meta_registry
return _get_tab_registry(window, tab)
elif scope == 'window':
return _get_window_registry(window)
else:
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.
Args:
default: A default to return if the object does not exist.
"""
reg = _get_registry(scope)
reg = _get_registry(scope, window, tab)
try:
return reg[name]
except KeyError:
@ -115,7 +179,8 @@ def get(name, default=_UNSET, scope='global'):
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.
Args:
@ -131,14 +196,36 @@ def register(name, obj, update=False, scope=None, registry=None):
else:
if scope is None:
scope = 'global'
reg = _get_registry(scope)
reg = _get_registry(scope, window, tab)
if not update and name in reg:
raise KeyError("Object '{}' is already registered ({})!".format(
name, repr(reg[name])))
reg[name] = obj
def delete(name, scope='global'):
def delete(name, scope='global', window=None, tab=None):
"""Helper function to unregister an object."""
reg = _get_registry(scope)
reg = _get_registry(scope, window, tab)
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.
ClickTarget = enum('ClickTarget', ['normal', 'tab', 'tab_bg'])
ClickTarget = enum('ClickTarget', ['normal', 'tab', 'tab_bg', 'window'])
# Key input modes

View File

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

View File

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

View File

@ -113,7 +113,7 @@ class _CrashDialog(QDialog):
"""Gather crash information to display.
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)
exc: An exception tuple (type, value, traceback)
"""
@ -172,7 +172,7 @@ class ExceptionCrashDialog(_CrashDialog):
_btn_quit: The quit button
_btn_restore: the restore 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)
_exc: An exception tuple (type, value, traceback)
_objects: A list of all QObjects as string.
@ -220,7 +220,7 @@ class ExceptionCrashDialog(_CrashDialog):
self._crash_info += [
("Exception", ''.join(traceback.format_exception(*self._exc))),
("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)),
("Objects", self._objects),
]
@ -316,7 +316,7 @@ class ReportDialog(_CrashDialog):
super()._gather_crash_info()
self._crash_info += [
("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)),
("Objects", self._objects),
]

View File

@ -21,15 +21,22 @@
import binascii
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 qutebrowser.commands import cmdutils
from qutebrowser.commands import runners, cmdutils
from qutebrowser.config import config
from qutebrowser.utils import message, log, usertypes, qtutils, objreg, utils
from qutebrowser.widgets import tabbedbrowser, completion, downloads
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):
@ -44,12 +51,102 @@ class MainWindow(QWidget):
_downloadview: The DownloadView widget.
_tabbed_browser: The TabbedBrowser widget.
_vbox: The main QVBoxLayout.
_commandrunner: The main CommandRunner instance.
"""
def __init__(self, parent=None):
def __init__(self, win_id, parent=None):
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')
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')
try:
data = state_config['geometry']['mainwindow']
@ -71,40 +168,6 @@ class MainWindow(QWidget):
log.init.warning("Error while restoring 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):
"""Connect the resize_completion signal and resize it once."""
self._completion.resize_completion.connect(self.resize_completion)
@ -114,6 +177,93 @@ class MainWindow(QWidget):
"""Set some sensible default geometry."""
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()
def resize_completion(self):
"""Adjust completion according to config."""
@ -143,9 +293,9 @@ class MainWindow(QWidget):
if rect.isValid():
self._completion.setGeometry(rect)
@cmdutils.register(instance='main-window', name=['quit', 'q'])
@cmdutils.register(instance='main-window', scope='window')
def close(self):
"""Quit qutebrowser.
"""Close the current window.
//
@ -169,15 +319,19 @@ class MainWindow(QWidget):
confirm_quit = config.get('ui', 'confirm-quit')
count = self._tabbed_browser.count()
if confirm_quit == 'never':
e.accept()
pass
elif confirm_quit == 'multiple-tabs' and count <= 1:
e.accept()
pass
else:
text = "Close {} {}?".format(
count, "tab" if count == 1 else "tabs")
confirmed = message.ask(text, usertypes.PromptMode.yesno,
default=True)
if confirmed:
e.accept()
else:
confirmed = message.ask(self.win_id, text,
usertypes.PromptMode.yesno, default=True)
if not confirmed:
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.
url: The UrlText 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.
_stack: The QStackedLayout with cmd/txt widgets.
_text_queue: A deque of (error, text) tuples to be displayed.
@ -57,6 +57,7 @@ class StatusBar(QWidget):
the command widget.
_previous_widget: A PreviousWidget member - the widget which was
displayed when an error interrupted it.
_win_id: The window ID the statusbar is associated with.
Class attributes:
_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)
objreg.register('statusbar', self, scope='window', window=win_id)
self.setObjectName(self.__class__.__name__)
self.setAttribute(Qt.WA_StyledBackground)
style.set_register_stylesheet(self)
self.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed)
self._win_id = win_id
self._option = None
self._last_text_time = None
@ -132,9 +135,8 @@ class StatusBar(QWidget):
self._hbox.addLayout(self._stack)
self._stack.setContentsMargins(0, 0, 0, 0)
self._cmd = command.Command()
objreg.register('status-command', self._cmd)
self._stack.addWidget(self._cmd)
self.cmd = command.Command(win_id)
self._stack.addWidget(self.cmd)
self.txt = textwidget.Text()
self._stack.addWidget(self.txt)
@ -145,14 +147,14 @@ class StatusBar(QWidget):
self.set_pop_timer_interval()
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._previous_widget = PreviousWidget.none
self._cmd.show_cmd.connect(self._show_cmd_widget)
self._cmd.hide_cmd.connect(self._hide_cmd_widget)
self.cmd.show_cmd.connect(self._show_cmd_widget)
self.cmd.hide_cmd.connect(self._hide_cmd_widget)
self._hide_cmd_widget()
prompter = objreg.get('prompter')
prompter = objreg.get('prompter', scope='window', window=self._win_id)
prompter.show_prompt.connect(self._show_prompt_widget)
prompter.hide_prompt.connect(self._hide_prompt_widget)
self._hide_prompt_widget()
@ -263,7 +265,7 @@ class StatusBar(QWidget):
if self._text_pop_timer.isActive():
self._timer_was_active = True
self._text_pop_timer.stop()
self._stack.setCurrentWidget(self._cmd)
self._stack.setCurrentWidget(self.cmd)
def _hide_cmd_widget(self):
"""Show temporary text instead of command widget."""
@ -382,7 +384,9 @@ class StatusBar(QWidget):
@pyqtSlot(usertypes.KeyMode)
def on_mode_entered(self, mode):
"""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())
self.txt.set_text(self.txt.Text.normal, text)
if mode == usertypes.KeyMode.insert:
@ -391,7 +395,9 @@ class StatusBar(QWidget):
@pyqtSlot(usertypes.KeyMode)
def on_mode_left(self, 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, '')
if mode == usertypes.KeyMode.insert:
self._set_insert_active(False)

View File

@ -35,6 +35,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
Attributes:
_cursor_part: The part the cursor is currently over.
_win_id: The window ID this widget is associated with.
Signals:
got_cmd: Emitted when a command is triggered by the user.
@ -64,9 +65,10 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
show_cmd = pyqtSignal()
hide_cmd = pyqtSignal()
def __init__(self, parent=None):
def __init__(self, win_id, parent=None):
misc.CommandLineEdit.__init__(self, parent)
misc.MinimalLineEditMixin.__init__(self)
self._win_id = win_id
self._cursor_part = 0
self.history.history = objreg.get('command-history').data
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
# the whitespace.
return [text]
runner = runners.CommandRunner()
runner = runners.CommandRunner(self._win_id)
parts = runner.parse(text, fallback=True, alias_no_args=False)
if self._empty_item_idx is not None:
log.completion.debug("Empty element queued at {}, "
@ -156,7 +158,8 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
self.setFocus()
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):
"""Preset the statusbar to some text.
@ -168,7 +171,9 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
Args:
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)
# 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
@ -213,7 +218,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
self.show_cmd.emit()
@cmdutils.register(instance='status-command', hide=True,
modes=[usertypes.KeyMode.command])
modes=[usertypes.KeyMode.command], scope='window')
def command_history_prev(self):
"""Go back in the commandline history."""
try:
@ -228,7 +233,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
self.set_cmd_text(item)
@cmdutils.register(instance='status-command', hide=True,
modes=[usertypes.KeyMode.command])
modes=[usertypes.KeyMode.command], scope='window')
def command_history_next(self):
"""Go forward in the commandline history."""
if not self.history.is_browsing():
@ -241,7 +246,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
self.set_cmd_text(item)
@cmdutils.register(instance='status-command', hide=True,
modes=[usertypes.KeyMode.command])
modes=[usertypes.KeyMode.command], scope='window')
def command_accept(self):
"""Execute the command currently in the commandline.
@ -257,7 +262,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
}
text = self.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:
signals[text[0]].emit(text.lstrip(text[0]))
@ -294,7 +299,8 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
def focusInEvent(self, e):
"""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)
def setText(self, text):

View File

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

View File

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

View File

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

View File

@ -42,9 +42,9 @@ class TabWidget(QTabWidget):
"""The tabwidget used for TabbedBrowser."""
def __init__(self, parent=None):
def __init__(self, win_id, parent=None):
super().__init__(parent)
bar = TabBar()
bar = TabBar(win_id)
self.setTabBar(bar)
bar.tabCloseRequested.connect(self.tabCloseRequested)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
@ -92,10 +92,12 @@ class TabBar(QTabBar):
Attributes:
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)
self._win_id = win_id
self.setStyle(TabBarStyle(self.style()))
self.set_font()
config.on_change(self.set_font, 'fonts', 'tabbar')
@ -202,8 +204,10 @@ class TabBar(QTabBar):
if self.vertical:
confwidth = str(config.get('tabs', 'width'))
if confwidth.endswith('%'):
main_window = objreg.get('main-window', scope='window',
window=self._win_id)
perc = int(confwidth.rstrip('%'))
width = objreg.get('main-window').width() * perc / 100
width = main_window.width() * perc / 100
else:
width = int(confwidth)
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
code.
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).
_has_ssl_errors: Whether SSL errors occured during loading.
_zoom: A NeighborList with the zoom levels.
@ -64,6 +65,7 @@ class WebView(QWebView):
_force_open_target: Override for open_target.
_check_insertmode: If True, in mouseReleaseEvent we should check if we
need to enter/leave insert mode.
_win_id: The window ID of the view.
Signals:
scroll_pos_changed: Scroll percentage of current tab changed.
@ -79,8 +81,9 @@ class WebView(QWebView):
load_status_changed = pyqtSignal(str)
url_text_changed = pyqtSignal(str)
def __init__(self, parent=None):
def __init__(self, win_id, parent=None):
super().__init__(parent)
self._win_id = win_id
self.load_status = LoadStatus.none
self._check_insertmode = False
self.inspector = None
@ -99,15 +102,16 @@ class WebView(QWebView):
self.progress = 0
self.registry = objreg.ObjectRegistry()
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)
page = webpage.BrowserPage(self)
page = webpage.BrowserPage(win_id, self)
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.set_open_target.connect(self.set_force_open_target)
objreg.register('hintmanager', hintmanager, registry=self.registry)
objreg.register('tab-{}'.format(self.tab_id),
self.registry, scope='meta')
page.linkHovered.connect(self.linkHovered)
page.mainFrame().loadStarted.connect(self.on_load_started)
self.urlChanged.connect(self.on_url_changed)
@ -150,16 +154,18 @@ class WebView(QWebView):
"""
if e.button() == Qt.XButton1:
# Back button on mice which have it.
try:
self.go_back()
except cmdexc.CommandError as ex:
message.error(ex, immediately=True)
if self.page().history().canGoBack():
self.back()
else:
message.error(self._win_id, "At beginning of history.",
immediately=True)
elif e.button() == Qt.XButton2:
# Forward button on mice which have it.
try:
self.go_forward()
except cmdexc.CommandError as ex:
message.error(ex, immediately=True)
if self.page().history().canGoForward():
self.forward()
else:
message.error(self._win_id, "At end of history.",
immediately=True)
def _mousepress_insertmode(self, e):
"""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
elem.is_editable()):
log.mouse.debug("Clicked editable element!")
modeman.maybe_enter(usertypes.KeyMode.insert, 'click')
modeman.maybe_enter(self._win_id, usertypes.KeyMode.insert,
'click')
else:
log.mouse.debug("Clicked non-editable element!")
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):
"""If we have an insertmode check scheduled, handle it."""
@ -218,11 +226,13 @@ class WebView(QWebView):
return
if elem.is_editable():
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:
log.mouse.debug("Clicked non-editable element (delayed)!")
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):
"""Set the open target when something was clicked.
@ -293,7 +303,7 @@ class WebView(QWebView):
if perc < 0:
raise cmdexc.CommandError("Can't zoom {}%!".format(perc))
self.setZoomFactor(float(perc) / 100)
message.info("Zoom level: {}%".format(perc))
message.info(self._win_id, "Zoom level: {}%".format(perc))
def zoom(self, offset):
"""Increase/Decrease the zoom level.
@ -304,30 +314,6 @@ class WebView(QWebView):
level = self._zoom.getitem(offset)
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')
def on_url_changed(self, url):
"""Update cur_url when URL has changed.
@ -363,7 +349,9 @@ class WebView(QWebView):
self._set_load_status(LoadStatus.error)
if not config.get('input', 'auto-insert-mode'):
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:
return
frame = self.page().currentFrame()
@ -374,7 +362,8 @@ class WebView(QWebView):
return
log.modes.debug("focus element: {}".format(repr(elem)))
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)
def set_force_open_target(self, target):
@ -409,7 +398,9 @@ class WebView(QWebView):
if wintype == QWebPage.WebModalDialog:
log.webview.warning("WebModalDialog requested, but we don't "
"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):
"""Extend paintEvent to emit a signal if the scroll position changed.