diff --git a/doc/HACKING.asciidoc b/doc/HACKING.asciidoc index c531350b5..2f5f19bc4 100644 --- a/doc/HACKING.asciidoc +++ b/doc/HACKING.asciidoc @@ -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. diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 9a2e2bb02..659fab4d8 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -8,6 +8,7 @@ |<>|Go back in the history of the current tab. |<>|Bind a key to a command. |<>|Cancel the first/[count]th download. +|<>|Close the current window. |<>|Download the current page. |<>|Go forward in the history of the current tab. |<>|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 diff --git a/doc/qutebrowser.1.asciidoc b/doc/qutebrowser.1.asciidoc index 128f414b0..62dcdf165 100644 --- a/doc/qutebrowser.1.asciidoc +++ b/doc/qutebrowser.1.asciidoc @@ -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*:: diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 31bedbb21..cef4e6379 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -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') diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 49855d79f..928942cf6 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -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): diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index b8584a64e..12f37b775 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -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)) diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 3e6fc8a7b..8f4d4dede 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -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): diff --git a/qutebrowser/browser/quickmarks.py b/qutebrowser/browser/quickmarks.py index af357f228..266c3ea47 100644 --- a/qutebrowser/browser/quickmarks.py +++ b/qutebrowser/browser/quickmarks.py @@ -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() diff --git a/qutebrowser/browser/signalfilter.py b/qutebrowser/browser/signalfilter.py index 320f3f58f..3c59b7e63 100644 --- a/qutebrowser/browser/signalfilter.py +++ b/qutebrowser/browser/signalfilter.py @@ -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: diff --git a/qutebrowser/browser/webpage.py b/qutebrowser/browser/webpage.py index 081d3a806..cf396583b 100644 --- a/qutebrowser/browser/webpage.py +++ b/qutebrowser/browser/webpage.py @@ -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 diff --git a/qutebrowser/commands/argparser.py b/qutebrowser/commands/argparser.py index f2493825e..684361698 100644 --- a/qutebrowser/commands/argparser.py +++ b/qutebrowser/commands/argparser.py @@ -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() diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 0237c2295..17dc2f8f2 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -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) diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index 69db0067b..e7971454e 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -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) diff --git a/qutebrowser/commands/userscripts.py b/qutebrowser/commands/userscripts.py index 7c67642ab..c38ff1478 100644 --- a/qutebrowser/commands/userscripts.py +++ b/qutebrowser/commands/userscripts.py @@ -17,29 +17,20 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -"""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) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index e4336bdaa..b3f234784 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -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 " diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 27778b986..2a54c0242 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -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', '']), ('tab-only', ['co']), @@ -891,10 +893,13 @@ KEY_DATA = collections.OrderedDict([ ('reload', ['r']), ('back', ['H', '']), ('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']), diff --git a/qutebrowser/config/lineparser.py b/qutebrowser/config/lineparser.py index 755bbc699..c63cb9809 100644 --- a/qutebrowser/config/lineparser.py +++ b/qutebrowser/config/lineparser.py @@ -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: diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index 92b6148e8..b451d32d3 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -53,6 +53,7 @@ class BaseKeyParser(QObject): Attributes: bindings: Bound keybindings special_bindings: Bound special bindings (). + _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 = '' diff --git a/qutebrowser/keyinput/keyparser.py b/qutebrowser/keyinput/keyparser.py index dca4145a4..380b734a1 100644 --- a/qutebrowser/keyinput/keyparser.py +++ b/qutebrowser/keyinput/keyparser.py @@ -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 diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index d2fb44caf..2d20ea11c 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -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 diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 6de27a550..29505f04d 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -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) diff --git a/qutebrowser/network/networkmanager.py b/qutebrowser/network/networkmanager.py index 911e0bc8b..dae9a61b2 100644 --- a/qutebrowser/network/networkmanager.py +++ b/qutebrowser/network/networkmanager.py @@ -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) diff --git a/qutebrowser/network/qutescheme.py b/qutebrowser/network/qutescheme.py index 1c7f32c76..54298efe2 100644 --- a/qutebrowser/network/qutescheme.py +++ b/qutebrowser/network/qutescheme.py @@ -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') diff --git a/qutebrowser/network/schemehandler.py b/qutebrowser/network/schemehandler.py index 6cf8af365..8256162eb 100644 --- a/qutebrowser/network/schemehandler.py +++ b/qutebrowser/network/schemehandler.py @@ -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. diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py index e2f1578a8..832bf0272 100644 --- a/qutebrowser/qutebrowser.py +++ b/qutebrowser/qutebrowser.py @@ -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 diff --git a/qutebrowser/test/keyinput/test_basekeyparser.py b/qutebrowser/test/keyinput/test_basekeyparser.py index 8f8dcf660..320cbe9ed 100644 --- a/qutebrowser/test/keyinput/test_basekeyparser.py +++ b/qutebrowser/test/keyinput/test_basekeyparser.py @@ -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') diff --git a/qutebrowser/test/utils/test_editor.py b/qutebrowser/test/utils/test_editor.py index 886af719e..b6a308ebd 100644 --- a/qutebrowser/test/utils/test_editor.py +++ b/qutebrowser/test/utils/test_editor.py @@ -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'}}) diff --git a/qutebrowser/utils/completer.py b/qutebrowser/utils/completer.py index 59839cdcb..4c5256896 100644 --- a/qutebrowser/utils/completer.py +++ b/qutebrowser/utils/completer.py @@ -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 diff --git a/qutebrowser/utils/editor.py b/qutebrowser/utils/editor.py index 1d8ea3d68..32ac9affb 100644 --- a/qutebrowser/utils/editor.py +++ b/qutebrowser/utils/editor.py @@ -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): diff --git a/qutebrowser/utils/message.py b/qutebrowser/utils/message.py index 52b661c10..e30ad6ede 100644 --- a/qutebrowser/utils/message.py +++ b/qutebrowser/utils/message.py @@ -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 diff --git a/qutebrowser/utils/objreg.py b/qutebrowser/utils/objreg.py index 6f233ccf2..d6dca188f 100644 --- a/qutebrowser/utils/objreg.py +++ b/qutebrowser/utils/objreg.py @@ -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 diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index 138a946a6..845c7190f 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -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 diff --git a/qutebrowser/utils/utilcmds.py b/qutebrowser/utils/utilcmds.py index 82d2a8e09..32aad6af1 100644 --- a/qutebrowser/utils/utilcmds.py +++ b/qutebrowser/utils/utilcmds.py @@ -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() diff --git a/qutebrowser/widgets/completion.py b/qutebrowser/widgets/completion.py index 731a80b7b..733f082bb 100644 --- a/qutebrowser/widgets/completion.py +++ b/qutebrowser/widgets/completion.py @@ -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.""" diff --git a/qutebrowser/widgets/crash.py b/qutebrowser/widgets/crash.py index 1c80dfe36..a98791e78 100644 --- a/qutebrowser/widgets/crash.py +++ b/qutebrowser/widgets/crash.py @@ -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), ] diff --git a/qutebrowser/widgets/mainwindow.py b/qutebrowser/widgets/mainwindow.py index 732e154c0..e1640392c 100644 --- a/qutebrowser/widgets/mainwindow.py +++ b/qutebrowser/widgets/mainwindow.py @@ -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) diff --git a/qutebrowser/widgets/statusbar/bar.py b/qutebrowser/widgets/statusbar/bar.py index 991c616ae..c21aa93b8 100644 --- a/qutebrowser/widgets/statusbar/bar.py +++ b/qutebrowser/widgets/statusbar/bar.py @@ -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) diff --git a/qutebrowser/widgets/statusbar/command.py b/qutebrowser/widgets/statusbar/command.py index 26aab1118..06b93798a 100644 --- a/qutebrowser/widgets/statusbar/command.py +++ b/qutebrowser/widgets/statusbar/command.py @@ -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): diff --git a/qutebrowser/widgets/statusbar/prompt.py b/qutebrowser/widgets/statusbar/prompt.py index 763369463..8923082ac 100644 --- a/qutebrowser/widgets/statusbar/prompt.py +++ b/qutebrowser/widgets/statusbar/prompt.py @@ -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) diff --git a/qutebrowser/widgets/statusbar/prompter.py b/qutebrowser/widgets/statusbar/prompter.py index 91858af8a..193eb93bc 100644 --- a/qutebrowser/widgets/statusbar/prompter.py +++ b/qutebrowser/widgets/statusbar/prompter.py @@ -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() diff --git a/qutebrowser/widgets/tabbedbrowser.py b/qutebrowser/widgets/tabbedbrowser.py index c2a553efe..4d6ddafcd 100644 --- a/qutebrowser/widgets/tabbedbrowser.py +++ b/qutebrowser/widgets/tabbedbrowser.py @@ -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)) diff --git a/qutebrowser/widgets/tabwidget.py b/qutebrowser/widgets/tabwidget.py index 461c44af8..4875a8ecc 100644 --- a/qutebrowser/widgets/tabwidget.py +++ b/qutebrowser/widgets/tabwidget.py @@ -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) diff --git a/qutebrowser/widgets/webview.py b/qutebrowser/widgets/webview.py index 559cead41..5197121e0 100644 --- a/qutebrowser/widgets/webview.py +++ b/qutebrowser/widgets/webview.py @@ -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.