From fb6cb62f93f4ad503760483cd21638765d1af234 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 28 Sep 2014 22:13:14 +0200 Subject: [PATCH 01/47] First attempt at multi-window support. --- qutebrowser/app.py | 143 +++----------- qutebrowser/browser/commands.py | 157 +++++++++------ qutebrowser/browser/downloads.py | 3 +- qutebrowser/browser/hints.py | 42 ++-- qutebrowser/browser/quickmarks.py | 15 +- qutebrowser/browser/signalfilter.py | 10 +- qutebrowser/browser/webpage.py | 26 +-- qutebrowser/commands/command.py | 35 ++-- qutebrowser/commands/runners.py | 16 +- qutebrowser/commands/userscripts.py | 36 ++-- qutebrowser/config/config.py | 6 +- qutebrowser/keyinput/basekeyparser.py | 4 +- qutebrowser/keyinput/keyparser.py | 12 +- qutebrowser/keyinput/modeman.py | 99 +++++++--- qutebrowser/keyinput/modeparsers.py | 17 +- qutebrowser/network/networkmanager.py | 14 +- qutebrowser/network/qutescheme.py | 16 +- qutebrowser/network/schemehandler.py | 9 +- qutebrowser/test/utils/test_editor.py | 8 +- qutebrowser/utils/completer.py | 11 +- qutebrowser/utils/editor.py | 15 +- qutebrowser/utils/message.py | 49 +++-- qutebrowser/utils/objreg.py | 37 +++- qutebrowser/utils/utilcmds.py | 39 +--- qutebrowser/widgets/completion.py | 13 +- qutebrowser/widgets/mainwindow.py | 224 ++++++++++++++++++---- qutebrowser/widgets/statusbar/bar.py | 28 +-- qutebrowser/widgets/statusbar/command.py | 24 ++- qutebrowser/widgets/statusbar/prompt.py | 7 +- qutebrowser/widgets/statusbar/prompter.py | 31 +-- qutebrowser/widgets/tabbedbrowser.py | 34 ++-- qutebrowser/widgets/webview.py | 29 +-- 32 files changed, 724 insertions(+), 485 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index b28703eff..b5ce72f1b 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -36,10 +36,10 @@ from PyQt5.QtCore import (pyqtSlot, QTimer, QEventLoop, Qt, QStandardPaths, qInstallMessageHandler, QObject, QUrl) import qutebrowser -from qutebrowser.commands import userscripts, runners, cmdutils +from qutebrowser.commands import runners, cmdutils 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, @@ -53,11 +53,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 +74,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 +109,15 @@ 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() + objreg.get('main-window', scope='window', window=0).show() log.init.debug("Applying python hacks...") self._python_hacks() @@ -134,10 +132,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 +140,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.create_window() log.init.debug("Initializing debug console...") debug_console = console.ConsoleWidget() objreg.register('debug-console', debug_console) @@ -240,18 +222,18 @@ class Application(QApplication): # we make sure the GUI is refreshed here, so the start seems faster. self.processEvents(QEventLoop.ExcludeUserInputEvents | QEventLoop.ExcludeSocketNotifiers) - tabbed_browser = objreg.get('tabbed-browser') + tabbed_browser = objreg.get('tabbed-browser', scope='window', window=0) for cmd in self._args.command: if cmd.startswith(':'): log.init.debug("Startup cmd {}".format(cmd)) - self._commandrunner.run_safely_init(cmd.lstrip(':')) + self._commandrunner.run_safely_init(0, cmd.lstrip(':')) else: 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) @@ -261,7 +243,8 @@ class Application(QApplication): try: url = urlutils.fuzzy_url(urlstr) except urlutils.FuzzyUrlError as e: - message.error("Error when opening startpage: {}".format(e)) + message.error(0, "Error when opening startpage: " + "{}".format(e)) else: tabbed_browser.tabopen(url) @@ -281,88 +264,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. - 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.""" @@ -433,8 +337,11 @@ class Application(QApplication): 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') @@ -517,6 +424,11 @@ 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.""" @@ -640,8 +552,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. @@ -663,7 +580,7 @@ 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 diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index f47e29608..20c9eb35a 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,33 +53,42 @@ class CommandDispatcher: Attributes: _editor: The ExternalEditor object. + _win_id: The window ID the CommandDispatcher is associated with. + _timers: A list of timers for :later """ - def __init__(self): + def __init__(self, win_id): self._editor = None + self._win_id = win_id + self._timers = [] def __repr__(self): return utils.get_repr(self) + def _tabbed_browser(self): + """Convienence method to get the right tabbed-browser.""" + return objreg.get('tabbed-browser', scope='window', + window=self._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 @@ -92,7 +101,7 @@ class CommandDispatcher: tab: Whether to open in a new tab. background: Whether to open in the background. """ - tabbed_browser = objreg.get('tabbed-browser') + tabbed_browser = self._tabbed_browser() if tab and background: raise cmdexc.CommandError("Only one of -t/-b can be given!") elif tab: @@ -114,7 +123,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(): @@ -177,7 +186,7 @@ class CommandDispatcher: tab = objreg.get('last-focused-tab') 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) @@ -190,7 +199,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. @@ -204,10 +213,10 @@ 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) + split=False, scope='window') def openurl(self, url, bg=False, tab=False, count=None): """Open a URL in the current/[count]th tab. @@ -229,14 +238,15 @@ class CommandDispatcher: 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. @@ -247,7 +257,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. @@ -258,7 +268,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. @@ -282,7 +293,7 @@ class CommandDispatcher: diag.setAttribute(Qt.WA_DeleteOnClose) diag.open(lambda: tab.print(diag.printer())) - @cmdutils.register(instance='command-dispatcher') + @cmdutils.register(instance='command-dispatcher', scope='window') def tab_clone(self, bg=False): """Duplicate the current tab. @@ -293,7 +304,7 @@ class CommandDispatcher: The new QWebView. """ curtab = self._current_widget() - tabbed_browser = objreg.get('tabbed-browser') + tabbed_browser = self._tabbed_browser() newtab = tabbed_browser.tabopen(background=bg, explicit=True) history = qtutils.serialize(curtab.history()) qtutils.deserialize(history, newtab.history()) @@ -311,7 +322,7 @@ class CommandDispatcher: else: widget.go_back() - @cmdutils.register(instance='command-dispatcher') + @cmdutils.register(instance='command-dispatcher', scope='window') def back(self, tab=False, bg=False, count=1): """Go back in the history of the current tab. @@ -322,7 +333,7 @@ class CommandDispatcher: """ self._back_forward(tab, bg, count, forward=False) - @cmdutils.register(instance='command-dispatcher') + @cmdutils.register(instance='command-dispatcher', scope='window') def forward(self, tab=False, bg=False, count=1): """Go forward in the history of the current tab. @@ -380,7 +391,7 @@ class CommandDispatcher: url.setPath(new_path) self._open(url, tab, background=False) - @cmdutils.register(instance='command-dispatcher') + @cmdutils.register(instance='command-dispatcher', scope='window') def navigate(self, where: ('prev', 'next', 'up', 'increment', 'decrement'), tab=False): """Open typical prev/next links or navigate using the URL path. @@ -419,7 +430,8 @@ class CommandDispatcher: 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'. @@ -434,7 +446,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. @@ -450,7 +463,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. @@ -467,7 +481,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. @@ -477,7 +491,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) @@ -490,9 +504,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. @@ -502,7 +516,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. @@ -512,7 +526,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. @@ -530,24 +544,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. @@ -562,7 +576,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. @@ -577,7 +591,7 @@ class CommandDispatcher: else: raise cmdexc.CommandError("Last tab") - @cmdutils.register(instance='command-dispatcher') + @cmdutils.register(instance='command-dispatcher', scope='window') def paste(self, sel=False, tab=False, bg=False): """Open a page from the clipboard. @@ -603,7 +617,7 @@ class CommandDispatcher: raise cmdexc.CommandError(e) self._open(url, tab, bg) - @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]. @@ -627,7 +641,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. @@ -651,7 +665,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) @@ -666,7 +680,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. @@ -684,12 +699,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. @@ -697,14 +712,15 @@ 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') + @cmdutils.register(instance='command-dispatcher', scope='window') def quickmark_load(self, name, tab=False, bg=False): """Load a quickmark. @@ -720,7 +736,8 @@ class CommandDispatcher: urlstr, url.errorString())) self._open(url, tab, bg) - @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() @@ -742,13 +759,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... @@ -762,12 +779,13 @@ class CommandDispatcher: formatter = pygments.formatters.HtmlFormatter( full=True, linenos='table') highlighted = pygments.highlight(html, lexer, formatter) - tab = objreg.get('tabbed-browser').tabopen(explicit=True) + tab = self._tabbed_browser().tabopen(explicit=True) tab.setHtml(highlighted, self._current_url()) tab.viewing_source = True @cmdutils.register(instance='command-dispatcher', name='help', - completion=[usertypes.Completion.helptopic]) + completion=[usertypes.Completion.helptopic], + scope='window') def show_help(self, topic=None): r"""Show help about a command or setting. @@ -803,9 +821,35 @@ class CommandDispatcher: raise cmdexc.CommandError("Invalid help topic {}!".format(topic)) self.openurl('qute://help/{}'.format(path)) + @cmdutils.register(instance='command-dispatcher', scope='window') + def later(self, ms: int, *command): + """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') + timer.setSingleShot(True) + if ms < 0: + raise cmdexc.CommandError("I can't run something in the past!") + try: + timer.setInterval(ms) + except OverflowError: + raise cmdexc.CommandError("Numeric argument is too large for internal " + "int representation.") + self._timers.append(timer) + cmdline = ' '.join(command) + commandrunner = objreg.get('command-runner', scope='window', + window=self._win_id) + timer.timeout.connect(functools.partial(commandrunner.run_safely, cmdline)) + timer.timeout.connect(lambda: self._timers.remove(timer)) + timer.start() + + @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. @@ -829,9 +873,10 @@ class CommandDispatcher: text = str(elem) else: text = elem.evaluateJavaScript('this.value') - self._editor = editor.ExternalEditor(objreg.get('tabbed-browser')) + self._editor = editor.ExternalEditor( + 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 46c3ea7db..5c4b9282d 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -31,6 +31,7 @@ from qutebrowser.config import config from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.utils import (message, http, usertypes, log, utils, qtutils, objreg) +from qutebrowser.widgets import mainwindow class DownloadItem(QObject): @@ -431,4 +432,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 7848bd59d..f24e9a39e 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -43,10 +43,10 @@ Target = usertypes.enum('Target', ['normal', 'tab', 'tab_bg', 'yank', @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: @@ -100,6 +100,7 @@ class HintManager(QObject): Attributes: _context: The HintContext for the current invocation. + _win_id: The window ID this HintManager is associated with. Signals: mouse_event: Mouse event to be posted in the web view. @@ -137,15 +138,14 @@ 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, parent=None): + """Constructor.""" super().__init__(parent) + self._win_id = win_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 +155,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): @@ -331,8 +333,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. @@ -343,7 +345,7 @@ class HintManager(QObject): qtutils.ensure_valid(url) 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. @@ -353,7 +355,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 qtutils.ensure_valid(url) @@ -570,7 +573,8 @@ class HintManager(QObject): objreg.get('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() @@ -614,7 +618,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) @@ -653,14 +657,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') + 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 4c91438f8..19d921e91 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,20 +58,21 @@ 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. """ qtutils.ensure_valid(url) 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: @@ -81,10 +82,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(): @@ -92,7 +93,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 6f59fd05a..5d06ec05e 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: @@ -153,8 +155,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) @@ -243,11 +245,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) @@ -267,8 +270,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 @@ -293,7 +296,8 @@ 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: diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 0237c2295..06e70a8db 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( @@ -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): @@ -340,9 +349,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): """Get arguments for a function call. + Args: + win_id: The window id this command should be executed in. + Return: An (args, kwargs) tuple. """ @@ -354,13 +366,13 @@ 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'. @@ -380,12 +392,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 +411,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..9b89d8805 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -114,7 +114,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 +128,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. @@ -159,11 +159,13 @@ class CommandRunner: 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): self._cmd = None self._args = [] + self._win_id = win_id def _get_alias(self, text, alias_no_args): """Get an alias from the config. @@ -280,9 +282,9 @@ class CommandRunner: self.parse(text) args = replace_variables(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 +292,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 +303,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 47f38846c..b7945c35a 100644 --- a/qutebrowser/commands/userscripts.py +++ b/qutebrowser/commands/userscripts.py @@ -37,7 +37,7 @@ from qutebrowser.commands import runners, cmdexc _runners = [] -_commandrunner = None +_commandrunners = [] class _BlockingFIFOReader(QObject): @@ -98,6 +98,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 @@ -122,8 +123,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 @@ -153,7 +155,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 @@ -181,7 +184,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): @@ -196,8 +200,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 @@ -263,8 +267,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): @@ -327,19 +331,17 @@ 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.""" # 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) + runner = UserscriptRunner(win_id) + runner.got_cmd.connect(commandrunner.run_safely) runner.run(cmd, *args, env={'QUTE_URL': urlstr}) _runners.append(runner) + _commandrunners.append(commandrunner) runner.finished.connect(functools.partial(_runners.remove, runner)) + commandrunner.finished.connect( + functools.partial(_commandrunners.remove, commandrunner)) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 2dc6d15b7..f6ebd32ea 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -438,7 +438,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. @@ -458,8 +458,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/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 192c768e3..238891e6c 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -43,22 +43,23 @@ 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.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 +69,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): +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): """Enter the mode 'mode'.""" - objreg.get('mode-manager').enter(mode, reason) + _get_modeman(win_id).enter(mode, reason) -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): +def maybe_enter(win_id, mode, reason=None): """Convenience method to enter 'mode' without exceptions.""" try: - objreg.get('mode-manager').enter(mode, reason) + _get_modeman(win_id).enter(mode, reason) 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): + 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 + 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 +138,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 +149,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 = [] @@ -246,9 +282,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. @@ -279,10 +315,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: @@ -316,8 +353,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..26ceedb59 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') diff --git a/qutebrowser/network/networkmanager.py b/qutebrowser/network/networkmanager.py index e306af54d..36d2be860 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 @@ -100,20 +102,22 @@ class NetworkManager(QNetworkAccessManager): return for err in errors: # FIXME we might want to use warn here (non-fatal error) - 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..e8479299e 100644 --- a/qutebrowser/network/qutescheme.py +++ b/qutebrowser/network/qutescheme.py @@ -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..a85565a8b 100644 --- a/qutebrowser/network/schemehandler.py +++ b/qutebrowser/network/schemehandler.py @@ -28,7 +28,14 @@ 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): + self._win_id = win_id def createRequest(self, op, request, outgoing_data): """Create a new request. 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 99611dde8..5b4a02211 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..abec732c5 100644 --- a/qutebrowser/utils/objreg.py +++ b/qutebrowser/utils/objreg.py @@ -82,30 +82,51 @@ 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_registry(scope, window): """Get the correct registry for a given scope.""" + if window is not None and scope is not 'window': + raise TypeError("window is set with scope {}".format(scope)) if scope == 'global': return global_registry elif scope == 'tab': - widget = get('tabbed-browser').currentWidget() + app = get('app') + win = app.activeWindow() + tabbed_browser = get('tabbed-browser', scope='window', window=win) + widget = tabbed_browser.currentWidget() if widget is None: raise RegistryUnavailableError(scope) return widget.registry + elif scope == 'window': + if window is None: + raise TypeError("window is None with scope window!") + if window is 'current': + app = get('app') + win = app.activeWindow() + if win is None: + raise RegistryUnavailableError(scope) + else: + try: + win = window_registry[window] + except KeyError: + raise RegistryUnavailableError(scope) + return win.registry elif scope == 'meta': return meta_registry else: raise ValueError("Invalid scope '{}'!".format(scope)) -def get(name, default=_UNSET, scope='global'): +def get(name, default=_UNSET, scope='global', window=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) try: return reg[name] except KeyError: @@ -115,7 +136,7 @@ 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): """Helper function to register an object. Args: @@ -131,14 +152,14 @@ 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) 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): """Helper function to unregister an object.""" - reg = _get_registry(scope) + reg = _get_registry(scope, window) del reg[name] diff --git a/qutebrowser/utils/utilcmds.py b/qutebrowser/utils/utilcmds.py index 82d2a8e09..82820357a 100644 --- a/qutebrowser/utils/utilcmds.py +++ b/qutebrowser/utils/utilcmds.py @@ -25,46 +25,11 @@ import functools 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 +from qutebrowser.commands import cmdutils 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): - """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') - timer.setSingleShot(True) - if ms < 0: - raise cmdexc.CommandError("I can't run something in the past!") - try: - timer.setInterval(ms) - 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)) - timer.start() - - @cmdutils.register(debug=True) def debug_crash(typ: ('exception', 'segfault')='exception'): """Crash for debugging purposes. diff --git a/qutebrowser/widgets/completion.py b/qutebrowser/widgets/completion.py index 73d56b6f2..34c9418a6 100644 --- a/qutebrowser/widgets/completion.py +++ b/qutebrowser/widgets/completion.py @@ -89,11 +89,12 @@ 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) + objreg.register('completion', self, scope='window', window=win_id) + completer_obj = completer.Completer(win_id) + 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 @@ -211,13 +212,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) diff --git a/qutebrowser/widgets/mainwindow.py b/qutebrowser/widgets/mainwindow.py index d8dd81601..e9e1a87fb 100644 --- a/qutebrowser/widgets/mainwindow.py +++ b/qutebrowser/widgets/mainwindow.py @@ -21,15 +21,27 @@ import binascii import base64 +import itertools +import functools -from PyQt5.QtCore import pyqtSlot, QRect, QPoint, QTimer +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) + + +def create_window(): + win_id = next(win_id_gen) + win = MainWindow(win_id) class MainWindow(QWidget): @@ -44,12 +56,84 @@ 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._commandrunner = None + self.win_id = win_id + self.registry = objreg.ObjectRegistry() + objreg.register('window-{}'.format(win_id), self.registry, + scope='meta') + objreg.window_registry[win_id] = self + objreg.register('main-window', self, 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) + + 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 +155,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 +164,94 @@ 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.""" + app = objreg.get('app') + download_manager = objreg.get('download-manager') + key_config = objreg.get('key-config') + config_obj = objreg.get('config') + + status = self._get_object('statusbar') + keyparsers = self._get_object('keyparsers') + completion = 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. + 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) + + + @pyqtSlot() def resize_completion(self): """Adjust completion according to config.""" @@ -143,9 +281,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. // @@ -175,9 +313,13 @@ class MainWindow(QWidget): else: text = "Close {} {}?".format( count, "tab" if count == 1 else "tabs") - confirmed = message.ask(text, usertypes.PromptMode.yesno, - default=True) + confirmed = message.ask(self.win_id, text, + usertypes.PromptMode.yesno, default=True) if confirmed: e.accept() else: e.ignore() + 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 920221b52..107299d7d 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,12 +147,12 @@ 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() self.prompt.show_prompt.connect(self._show_prompt_widget) self.prompt.hide_prompt.connect(self._hide_prompt_widget) @@ -262,7 +264,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.""" @@ -378,7 +380,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: @@ -387,7 +391,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 f88d16b90..7c8839ff8 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. @@ -68,9 +69,10 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit): # See http://www.saltycrane.com/blog/2008/01/how-to-capture-tab-key-press-event-with/ # for a possible fix. - 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 @@ -100,7 +102,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 {}, " @@ -158,7 +160,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. @@ -170,7 +173,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 @@ -214,7 +219,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: @@ -229,7 +234,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(): @@ -242,7 +247,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. @@ -258,7 +263,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])) @@ -295,7 +300,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 e5cc21f00..c8718e760 100644 --- a/qutebrowser/widgets/statusbar/prompt.py +++ b/qutebrowser/widgets/statusbar/prompt.py @@ -53,7 +53,7 @@ class Prompt(QWidget): show_prompt = pyqtSignal() hide_prompt = pyqtSignal() - def __init__(self, parent=None): + def __init__(self, win_id, parent=None): super().__init__(parent) objreg.register('prompt', self) self._hbox = QHBoxLayout(self) @@ -66,8 +66,9 @@ class Prompt(QWidget): self.lineedit = PromptLineEdit() self._hbox.addWidget(self.lineedit) - prompter_obj = prompter.Prompter() - objreg.register('prompter', prompter_obj) + prompter_obj = prompter.Prompter(win_id) + objreg.register('prompter', prompter_obj, 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 872a957d3..72420d3ed 100644 --- a/qutebrowser/widgets/statusbar/prompter.py +++ b/qutebrowser/widgets/statusbar/prompter.py @@ -60,13 +60,15 @@ class Prompter: _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. """ - def __init__(self): + def __init__(self, win_id): 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), @@ -189,7 +191,7 @@ class Prompter: 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): @@ -212,27 +214,31 @@ class Prompter: # 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.""" @@ -240,10 +246,10 @@ class Prompter: # 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.""" @@ -251,7 +257,7 @@ class Prompter: # 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) @@ -284,10 +290,11 @@ class Prompter: self._question = question mode = self._display_question() - question.aborted.connect(lambda: modeman.maybe_leave(mode, 'aborted')) + question.aborted.connect( + lambda: modeman.maybe_leave(self._win_id, mode, 'aborted')) mode_manager = objreg.get('mode-manager') try: - modeman.enter(mode, 'question asked') + modeman.enter(self._win_id, mode, 'question asked') 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 1b814978f..634f6ee90 100644 --- a/qutebrowser/widgets/tabbedbrowser.py +++ b/qutebrowser/widgets/tabbedbrowser.py @@ -51,6 +51,7 @@ class TabbedBrowser(tabwidget.TabWidget): emitted if the signal occured in the current tab. Attributes: + _win_id: The window ID this tabbedbrowser is associated with. _tabs: A list of open tabs. _filter: A SignalFilter instance. _now_focused: The tab which is focused now. @@ -97,8 +98,9 @@ class TabbedBrowser(tabwidget.TabWidget): current_tab_changed = pyqtSignal(webview.WebView) title_changed = pyqtSignal(str) - def __init__(self, parent=None): + def __init__(self, win_id, parent=None): super().__init__(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) @@ -107,9 +109,10 @@ class TabbedBrowser(tabwidget.TabWidget): 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._now_focused = None # FIXME adjust this to font size self.setIconSize(QSize(12, 12)) @@ -315,7 +318,7 @@ 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: @@ -366,19 +369,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) @@ -411,8 +414,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): @@ -495,7 +500,8 @@ class TabbedBrowser(tabwidget.TabWidget): """Set last-focused-tab and leave hinting mode when focus changed.""" 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) self._now_focused = tab diff --git a/qutebrowser/widgets/webview.py b/qutebrowser/widgets/webview.py index 71cbfab37..033a56cae 100644 --- a/qutebrowser/widgets/webview.py +++ b/qutebrowser/widgets/webview.py @@ -64,6 +64,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 +80,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 @@ -100,9 +102,9 @@ class WebView(QWebView): self.registry = objreg.ObjectRegistry() self.tab_id = next(tab_id_gen) 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) 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) @@ -153,13 +155,13 @@ class WebView(QWebView): try: self.go_back() except cmdexc.CommandError as ex: - message.error(ex, immediately=True) + message.error(self._win_id, ex, 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) + message.error(self._win_id, ex, immediately=True) def _mousepress_insertmode(self, e): """Switch to insert mode when an editable element was clicked. @@ -200,11 +202,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 +222,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 +299,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. @@ -371,7 +377,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): From dc7b32e460a95b313ca580946314de48c89a6c48 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 28 Sep 2014 22:23:37 +0200 Subject: [PATCH 02/47] Fix lint --- qutebrowser/app.py | 9 ++++----- qutebrowser/browser/commands.py | 10 +++++----- qutebrowser/browser/downloads.py | 1 - qutebrowser/browser/hints.py | 2 +- qutebrowser/commands/userscripts.py | 2 +- qutebrowser/keyinput/modeman.py | 1 + qutebrowser/network/schemehandler.py | 3 ++- qutebrowser/utils/utilcmds.py | 2 -- qutebrowser/widgets/mainwindow.py | 16 ++++++++++------ 9 files changed, 24 insertions(+), 22 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index b5ce72f1b..0e4b152f8 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -32,19 +32,18 @@ import functools import traceback from PyQt5.QtWidgets import QApplication, QDialog -from PyQt5.QtCore import (pyqtSlot, QTimer, QEventLoop, Qt, QStandardPaths, +from PyQt5.QtCore import (pyqtSlot, QTimer, QEventLoop, QStandardPaths, qInstallMessageHandler, QObject, QUrl) import qutebrowser -from qutebrowser.commands import runners, cmdutils +from qutebrowser.commands import cmdutils from qutebrowser.config import style, config, websettings from qutebrowser.network import qutescheme, proxy 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) class Application(QApplication): diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 20c9eb35a..f3e95cb71 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -836,17 +836,17 @@ class CommandDispatcher: try: timer.setInterval(ms) except OverflowError: - raise cmdexc.CommandError("Numeric argument is too large for internal " - "int representation.") + raise cmdexc.CommandError("Numeric argument is too large for " + "internal int representation.") self._timers.append(timer) cmdline = ' '.join(command) commandrunner = objreg.get('command-runner', scope='window', window=self._win_id) - timer.timeout.connect(functools.partial(commandrunner.run_safely, cmdline)) + timer.timeout.connect( + functools.partial(commandrunner.run_safely, cmdline)) timer.timeout.connect(lambda: self._timers.remove(timer)) timer.start() - @cmdutils.register(instance='command-dispatcher', modes=[usertypes.KeyMode.insert], hide=True, scope='window') @@ -874,7 +874,7 @@ class CommandDispatcher: else: text = elem.evaluateJavaScript('this.value') self._editor = editor.ExternalEditor( - win_id, self._tabbed_browser()) + self._win_id, self._tabbed_browser()) self._editor.editing_finished.connect( functools.partial(self.on_editing_finished, elem)) self._editor.edit(text) diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index 5c4b9282d..473596e6f 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -31,7 +31,6 @@ from qutebrowser.config import config from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.utils import (message, http, usertypes, log, utils, qtutils, objreg) -from qutebrowser.widgets import mainwindow class DownloadItem(QObject): diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index f24e9a39e..6fe13c7b0 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -367,7 +367,7 @@ class HintManager(QObject): qtutils.ensure_valid(url) 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.""" diff --git a/qutebrowser/commands/userscripts.py b/qutebrowser/commands/userscripts.py index b7945c35a..bd579b2a6 100644 --- a/qutebrowser/commands/userscripts.py +++ b/qutebrowser/commands/userscripts.py @@ -343,5 +343,5 @@ def run(cmd, *args, url, win_id): _runners.append(runner) _commandrunners.append(commandrunner) runner.finished.connect(functools.partial(_runners.remove, runner)) - commandrunner.finished.connect( + runner.finished.connect( functools.partial(_commandrunners.remove, commandrunner)) diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index 238891e6c..9982b9aaf 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -113,6 +113,7 @@ class EventFilter(QObject): self._activated = True def eventFilter(self, obj, event): + """Forward events to the correct modeman.""" if not self._activated: return False try: diff --git a/qutebrowser/network/schemehandler.py b/qutebrowser/network/schemehandler.py index a85565a8b..8256162eb 100644 --- a/qutebrowser/network/schemehandler.py +++ b/qutebrowser/network/schemehandler.py @@ -34,7 +34,8 @@ class SchemeHandler(QObject): _win_id: The window ID this scheme handler is associated with. """ - def __init__(self, win_id): + def __init__(self, win_id, parent=None): + super().__init__(parent) self._win_id = win_id def createRequest(self, op, request, outgoing_data): diff --git a/qutebrowser/utils/utilcmds.py b/qutebrowser/utils/utilcmds.py index 82820357a..3a93acada 100644 --- a/qutebrowser/utils/utilcmds.py +++ b/qutebrowser/utils/utilcmds.py @@ -20,8 +20,6 @@ """Misc. utility commands exposed to the user.""" import types -import functools - from PyQt5.QtCore import QCoreApplication diff --git a/qutebrowser/widgets/mainwindow.py b/qutebrowser/widgets/mainwindow.py index e9e1a87fb..d444b2023 100644 --- a/qutebrowser/widgets/mainwindow.py +++ b/qutebrowser/widgets/mainwindow.py @@ -40,8 +40,14 @@ win_id_gen = itertools.count(0) def create_window(): + """Create a new main window. + + Return: + The MainWindow object. + """ win_id = next(win_id_gen) win = MainWindow(win_id) + return win class MainWindow(QWidget): @@ -170,14 +176,14 @@ class MainWindow(QWidget): 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') - config_obj = objreg.get('config') status = self._get_object('statusbar') keyparsers = self._get_object('keyparsers') - completion = self._get_object('completion') + completion_obj = self._get_object('completion') tabs = self._get_object('tabbed-browser') cmd = self._get_object('status-command') completer = self._get_object('completer') @@ -242,16 +248,14 @@ class MainWindow(QWidget): # 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) + 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.""" From 7e017e66a0c5ccf860bd300a7981b61277e5c738 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 28 Sep 2014 22:24:49 +0200 Subject: [PATCH 03/47] Fix basekeyparser tests. --- qutebrowser/test/keyinput/test_basekeyparser.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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') From d87b0bf3015c419022df5598a08001c1c60e0b36 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 28 Sep 2014 22:38:11 +0200 Subject: [PATCH 04/47] Fix pyqtSlot error for downloads. --- qutebrowser/browser/downloads.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index 473596e6f..9d1e8d136 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 From cc0e164dcf6347c1a5c41ee3566d3ade0f8a0796 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 28 Sep 2014 22:41:43 +0200 Subject: [PATCH 05/47] First attempt at adding a --window to commands. --- qutebrowser/browser/commands.py | 35 ++++++++++++++++++++----------- qutebrowser/widgets/mainwindow.py | 6 +++--- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index f3e95cb71..d63f4ba7b 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -93,17 +93,25 @@ class CommandDispatcher: 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 """ tabbed_browser = self._tabbed_browser() - if tab and background: - raise cmdexc.CommandError("Only one of -t/-b can be given!") + 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: + # We have to import this here to avoid a circular import. + from qutebrowser.widgets import mainwindow + win_id = mainwindow.create_window() + tabbed_browser = objreg.get('tabbed-browser', scope='window', + window=win_id) + tabbed_browser.tabopen(url) elif tab: tabbed_browser.tabopen(url, background=False, explicit=True) elif background: @@ -217,21 +225,22 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', name='open', split=False, scope='window') - def openurl(self, url, bg=False, tab=False, count=None): + def openurl(self, url, bg=False, tab=False, window=False, count=None): """Open a URL in the current/[count]th tab. 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: @@ -375,7 +384,7 @@ 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=False, window=False) def _navigate_up(self, url, tab): """Helper method for :navigate when `where' is up. @@ -389,7 +398,7 @@ class CommandDispatcher: 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=False, window=False) @cmdutils.register(instance='command-dispatcher', scope='window') def navigate(self, where: ('prev', 'next', 'up', 'increment', 'decrement'), @@ -592,13 +601,14 @@ class CommandDispatcher: raise cmdexc.CommandError("Last tab") @cmdutils.register(instance='command-dispatcher', scope='window') - def paste(self, sel=False, tab=False, bg=False): + def paste(self, sel=False, tab=False, bg=False, window=False): """Open a page from the clipboard. 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(): @@ -615,7 +625,7 @@ 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', scope='window') def tab_focus(self, index: (int, 'last')=None, count=None): @@ -721,20 +731,21 @@ class CommandDispatcher: quickmarks.prompt_save(self._win_id, self._current_url()) @cmdutils.register(instance='command-dispatcher', scope='window') - def quickmark_load(self, name, tab=False, bg=False): + def quickmark_load(self, name, tab=False, bg=False, window=False): """Load a quickmark. 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) if not url.isValid(): raise cmdexc.CommandError("Invalid URL {} ({})".format( urlstr, url.errorString())) - self._open(url, tab, bg) + self._open(url, tab, bg, window) @cmdutils.register(instance='command-dispatcher', name='inspector', scope='window') diff --git a/qutebrowser/widgets/mainwindow.py b/qutebrowser/widgets/mainwindow.py index d444b2023..11caa3414 100644 --- a/qutebrowser/widgets/mainwindow.py +++ b/qutebrowser/widgets/mainwindow.py @@ -43,11 +43,11 @@ def create_window(): """Create a new main window. Return: - The MainWindow object. + The new window id. """ win_id = next(win_id_gen) - win = MainWindow(win_id) - return win + MainWindow(win_id) + return win_id class MainWindow(QWidget): From 2dea47b162d5cb659bc2b2edd7c15a9d0a144814 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 28 Sep 2014 22:44:12 +0200 Subject: [PATCH 06/47] Register prompt per window --- qutebrowser/widgets/statusbar/prompt.py | 2 +- qutebrowser/widgets/statusbar/prompter.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/qutebrowser/widgets/statusbar/prompt.py b/qutebrowser/widgets/statusbar/prompt.py index c8718e760..4578ff3fc 100644 --- a/qutebrowser/widgets/statusbar/prompt.py +++ b/qutebrowser/widgets/statusbar/prompt.py @@ -55,7 +55,7 @@ class Prompt(QWidget): 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) diff --git a/qutebrowser/widgets/statusbar/prompter.py b/qutebrowser/widgets/statusbar/prompter.py index 72420d3ed..2fcc632fe 100644 --- a/qutebrowser/widgets/statusbar/prompter.py +++ b/qutebrowser/widgets/statusbar/prompter.py @@ -89,7 +89,7 @@ class Prompter: """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(), @@ -106,7 +106,7 @@ class Prompter: 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: prompt.hide_prompt.emit() self._busy = False @@ -127,7 +127,7 @@ class Prompter: 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 = "" @@ -181,7 +181,7 @@ class Prompter: @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() @@ -202,7 +202,7 @@ class Prompter: 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 From 16c067e32da5236d5357c086ac54a674d4e0f883 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 28 Sep 2014 22:46:37 +0200 Subject: [PATCH 07/47] Simplify showing of main windows. --- qutebrowser/app.py | 6 +----- qutebrowser/browser/commands.py | 2 +- qutebrowser/widgets/mainwindow.py | 9 +++++++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 0e4b152f8..cb7ad2dcf 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -114,10 +114,6 @@ class Application(QApplication): log.init.debug("Connecting signals...") self._connect_signals() - log.init.debug("Showing mainwindow...") - if not args.nowindow: - objreg.get('main-window', scope='window', window=0).show() - log.init.debug("Applying python hacks...") self._python_hacks() @@ -155,7 +151,7 @@ class Application(QApplication): download_manager = downloads.DownloadManager(self) objreg.register('download-manager', download_manager) log.init.debug("Initializing main window...") - mainwindow.create_window() + mainwindow.create_window(False if self._args.nowindow else True) log.init.debug("Initializing debug console...") debug_console = console.ConsoleWidget() objreg.register('debug-console', debug_console) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index d63f4ba7b..5043c2c76 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -108,7 +108,7 @@ class CommandDispatcher: elif window: # We have to import this here to avoid a circular import. from qutebrowser.widgets import mainwindow - win_id = mainwindow.create_window() + win_id = mainwindow.create_window(True) tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id) tabbed_browser.tabopen(url) diff --git a/qutebrowser/widgets/mainwindow.py b/qutebrowser/widgets/mainwindow.py index 11caa3414..5bae11c4a 100644 --- a/qutebrowser/widgets/mainwindow.py +++ b/qutebrowser/widgets/mainwindow.py @@ -39,14 +39,19 @@ from qutebrowser.browser import hints win_id_gen = itertools.count(0) -def create_window(): +def create_window(show): """Create a new main window. + Args: + show: Show the window after creating. + Return: The new window id. """ win_id = next(win_id_gen) - MainWindow(win_id) + win = MainWindow(win_id) + if show: + win.show() return win_id From 9533312e0d472bed1c74d867c59452e95ee5591b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 28 Sep 2014 22:53:24 +0200 Subject: [PATCH 08/47] Fix event filter when a non-Mainwindow dialog has an event. --- qutebrowser/keyinput/modeman.py | 2 +- qutebrowser/utils/objreg.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index 9982b9aaf..c52248c28 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -121,7 +121,7 @@ class EventFilter(QObject): window='current') return modeman.eventFilter(obj, event) except objreg.RegistryUnavailableError: - # No window available yet + # No window available yet, or not a MainWindow return False except: # If there is an exception in here and we leave the eventfilter diff --git a/qutebrowser/utils/objreg.py b/qutebrowser/utils/objreg.py index abec732c5..7889d607b 100644 --- a/qutebrowser/utils/objreg.py +++ b/qutebrowser/utils/objreg.py @@ -99,7 +99,10 @@ def _get_registry(scope, window): widget = tabbed_browser.currentWidget() if widget is None: raise RegistryUnavailableError(scope) - return widget.registry + try: + return widget.registry + except AttributeError: + raise RegistryUnavailableError(scope) elif scope == 'window': if window is None: raise TypeError("window is None with scope window!") @@ -113,7 +116,10 @@ def _get_registry(scope, window): win = window_registry[window] except KeyError: raise RegistryUnavailableError(scope) - return win.registry + try: + return win.registry + except AttributeError: + raise RegistryUnavailableError(scope) elif scope == 'meta': return meta_registry else: From 36f7ff6154f4dd9657e77815b2d3648110b21b53 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 28 Sep 2014 23:23:02 +0200 Subject: [PATCH 09/47] Fix some objreg.get calls. --- qutebrowser/browser/hints.py | 15 +++++++++++---- qutebrowser/browser/webpage.py | 6 ++++-- qutebrowser/commands/argparser.py | 4 +++- qutebrowser/commands/runners.py | 10 ++++++---- qutebrowser/widgets/completion.py | 6 +++++- qutebrowser/widgets/statusbar/prompter.py | 3 ++- qutebrowser/widgets/tabbedbrowser.py | 2 +- qutebrowser/widgets/tabwidget.py | 12 ++++++++---- qutebrowser/widgets/webview.py | 8 ++++++-- 9 files changed, 46 insertions(+), 20 deletions(-) diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 6fe13c7b0..d30e89aee 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -493,7 +493,9 @@ 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): @@ -515,7 +517,9 @@ class HintManager(QObject): "prev" if prev else "forward")) qtutils.ensure_valid(url) if newtab: - objreg.get('tabbed-browser').tabopen(url, background=False) + tabbed_browser = objreg.get('tabbed-browser', scope='window', + window=self._win_id) + tabbed_browser.tabopen(url, background=False) else: objreg.get('webview', scope='tab').openurl(url) @@ -556,7 +560,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!") @@ -570,7 +575,9 @@ 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(self._win_id, usertypes.KeyMode.hint, diff --git a/qutebrowser/browser/webpage.py b/qutebrowser/browser/webpage.py index 5d06ec05e..09571a51f 100644 --- a/qutebrowser/browser/webpage.py +++ b/qutebrowser/browser/webpage.py @@ -300,11 +300,13 @@ class BrowserPage(QWebPage): urlstr)) log.webview.debug(url.errorString()) return False + tabbed_browser = objreg.get('tabbed-browser', scope='window', + window=self._win_id) if self.view().open_target == usertypes.ClickTarget.tab: - objreg.get('tabbed-browser').tabopen(url, False) + tabbed_browser.tabopen(url, False) return False elif self.view().open_target == usertypes.ClickTarget.tab_bg: - objreg.get('tabbed-browser').tabopen(url, True) + tabbed_browser.tabopen(url, True) 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/runners.py b/qutebrowser/commands/runners.py index 9b89d8805..ccb859163 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -27,13 +27,15 @@ 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) + url = tabbed_browser.current_url().toString(QUrl.FullyEncoded | + QUrl.RemovePassword) for arg in arglist: if arg == '{url}': - url = objreg.get('tabbed-browser').current_url().toString( - QUrl.FullyEncoded | QUrl.RemovePassword) args.append(url) else: args.append(arg) @@ -280,7 +282,7 @@ 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(self._win_id, args, count=count) else: diff --git a/qutebrowser/widgets/completion.py b/qutebrowser/widgets/completion.py index 34c9418a6..f8379147d 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. @@ -91,6 +92,7 @@ class CompletionView(QTreeView): def __init__(self, win_id, parent=None): super().__init__(parent) + self._win_id = win_id objreg.register('completion', self, scope='window', window=win_id) completer_obj = completer.Completer(win_id) objreg.register('completer', completer_obj, scope='window', @@ -226,7 +228,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 = objreg.get('completer', scope='window', + window=self._win_id) + completer.selection_changed(selected, deselected) def resizeEvent(self, e): """Extend resizeEvent to adjust column size.""" diff --git a/qutebrowser/widgets/statusbar/prompter.py b/qutebrowser/widgets/statusbar/prompter.py index 2fcc632fe..6b77ccc81 100644 --- a/qutebrowser/widgets/statusbar/prompter.py +++ b/qutebrowser/widgets/statusbar/prompter.py @@ -292,7 +292,8 @@ class Prompter: mode = self._display_question() question.aborted.connect( lambda: modeman.maybe_leave(self._win_id, mode, 'aborted')) - mode_manager = objreg.get('mode-manager') + mode_manager = objreg.get('mode-manager', scope='window', + window=self._win_id) try: modeman.enter(self._win_id, mode, 'question asked') except modeman.ModeLockedError: diff --git a/qutebrowser/widgets/tabbedbrowser.py b/qutebrowser/widgets/tabbedbrowser.py index 634f6ee90..6e49d37ee 100644 --- a/qutebrowser/widgets/tabbedbrowser.py +++ b/qutebrowser/widgets/tabbedbrowser.py @@ -99,7 +99,7 @@ class TabbedBrowser(tabwidget.TabWidget): title_changed = pyqtSignal(str) def __init__(self, win_id, parent=None): - super().__init__(parent) + super().__init__(win_id, parent) self._win_id = win_id self._tab_insert_idx_left = 0 self._tab_insert_idx_right = -1 diff --git a/qutebrowser/widgets/tabwidget.py b/qutebrowser/widgets/tabwidget.py index 354519f7b..10b3b0f13 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) @@ -90,10 +90,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') @@ -200,8 +202,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 033a56cae..97a021b7d 100644 --- a/qutebrowser/widgets/webview.py +++ b/qutebrowser/widgets/webview.py @@ -366,7 +366,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() @@ -413,7 +415,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. From 895f51083d86c74cab1ef08f76f473da1ad2b5c5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 29 Sep 2014 07:17:01 +0200 Subject: [PATCH 10/47] Some more objreg fixes for multi-window. --- qutebrowser/app.py | 55 +++++++++++++++------------- qutebrowser/browser/commands.py | 3 +- qutebrowser/browser/downloads.py | 5 ++- qutebrowser/widgets/tabbedbrowser.py | 9 +++-- 4 files changed, 42 insertions(+), 30 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index cb7ad2dcf..46655c94d 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -315,19 +315,18 @@ class Application(QApplication): Return: A list of open pages, or an empty list. """ - 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: + tabbed_browser = objreg.get('tabbed-browser', scope='window', + window=win_id) + 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") return pages def _save_geometry(self): @@ -391,7 +390,7 @@ class Application(QApplication): 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 = [] @@ -429,13 +428,17 @@ class Application(QApplication): """Restart qutebrowser while keeping existing tabs open.""" # We don't use _recover_pages here as it's too forgiving when # exceptions occur. + # FIXME handle multiple windows correctly here 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) + for win_id in objreg.window_registry: + tabbed_browser = objreg.get('tabbed-browser', scope='window', + window=win_id) + for tab in tabbed_browser.widgets(): + urlstr = tab.cur_url.toString( + QUrl.RemovePassword | QUrl.FullyEncoded) + if urlstr: + pages.append(urlstr) log.destroy.debug("sys.executable: {}".format(sys.executable)) log.destroy.debug("sys.path: {}".format(sys.path)) log.destroy.debug("sys.argv: {}".format(sys.argv)) @@ -485,13 +488,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() @@ -579,11 +584,11 @@ class Application(QApplication): 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 5043c2c76..cbe6ff7be 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -191,7 +191,8 @@ 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 = self._tabbed_browser().indexOf(tab) diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index 9d1e8d136..80de0375f 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -402,6 +402,7 @@ class DownloadManager(QObject): self.downloads.append(download) self.download_added.emit() + # FIXME display this in right window q = usertypes.Question(self) q.text = "Save file to:" q.mode = usertypes.PromptMode.text @@ -412,7 +413,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): diff --git a/qutebrowser/widgets/tabbedbrowser.py b/qutebrowser/widgets/tabbedbrowser.py index 6e49d37ee..01ed01eb4 100644 --- a/qutebrowser/widgets/tabbedbrowser.py +++ b/qutebrowser/widgets/tabbedbrowser.py @@ -247,8 +247,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 not tab.cur_url.isEmpty(): qtutils.ensure_valid(tab.cur_url) history_data = qtutils.serialize(tab.history()) @@ -503,7 +505,8 @@ class TabbedBrowser(tabwidget.TabWidget): 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)) From 75ba75a0d7b15cf4bcc5a1b4c459615421e5e115 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 29 Sep 2014 07:17:11 +0200 Subject: [PATCH 11/47] Check arguments against None in objreg. --- qutebrowser/utils/objreg.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/qutebrowser/utils/objreg.py b/qutebrowser/utils/objreg.py index 7889d607b..57aefb135 100644 --- a/qutebrowser/utils/objreg.py +++ b/qutebrowser/utils/objreg.py @@ -58,6 +58,11 @@ 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) From cdb2e345136560ca501f80f525caf7d2edaff015 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 29 Sep 2014 07:41:39 +0200 Subject: [PATCH 12/47] Use collections.UserDict for LineConfigParser. --- qutebrowser/config/lineparser.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) 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: From 26604a683b661b61c6110a92e746b10b8a0ac271 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 29 Sep 2014 07:47:32 +0200 Subject: [PATCH 13/47] Fix lint --- qutebrowser/utils/objreg.py | 62 ++++++++++++++++++------------- qutebrowser/widgets/completion.py | 6 +-- 2 files changed, 39 insertions(+), 29 deletions(-) diff --git a/qutebrowser/utils/objreg.py b/qutebrowser/utils/objreg.py index 57aefb135..1aceaedef 100644 --- a/qutebrowser/utils/objreg.py +++ b/qutebrowser/utils/objreg.py @@ -91,6 +91,40 @@ meta_registry['global'] = global_registry window_registry = ObjectRegistry() +def _get_tab_registry(): + """Get the registry of a tab.""" + app = get('app') + win = app.activeWindow() + tabbed_browser = get('tabbed-browser', scope='window', window=win) + widget = tabbed_browser.currentWidget() + if widget is None: + raise RegistryUnavailableError('tab') + try: + return widget.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 is 'current': + app = get('app') + win = app.activeWindow() + if win is None: + 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): """Get the correct registry for a given scope.""" if window is not None and scope is not 'window': @@ -98,33 +132,9 @@ def _get_registry(scope, window): if scope == 'global': return global_registry elif scope == 'tab': - app = get('app') - win = app.activeWindow() - tabbed_browser = get('tabbed-browser', scope='window', window=win) - widget = tabbed_browser.currentWidget() - if widget is None: - raise RegistryUnavailableError(scope) - try: - return widget.registry - except AttributeError: - raise RegistryUnavailableError(scope) + return _get_tab_registry() elif scope == 'window': - if window is None: - raise TypeError("window is None with scope window!") - if window is 'current': - app = get('app') - win = app.activeWindow() - if win is None: - raise RegistryUnavailableError(scope) - else: - try: - win = window_registry[window] - except KeyError: - raise RegistryUnavailableError(scope) - try: - return win.registry - except AttributeError: - raise RegistryUnavailableError(scope) + return _get_window_registry(window) elif scope == 'meta': return meta_registry else: diff --git a/qutebrowser/widgets/completion.py b/qutebrowser/widgets/completion.py index f8379147d..815e04992 100644 --- a/qutebrowser/widgets/completion.py +++ b/qutebrowser/widgets/completion.py @@ -228,9 +228,9 @@ class CompletionView(QTreeView): def selectionChanged(self, selected, deselected): """Extend selectionChanged to call completers selection_changed.""" super().selectionChanged(selected, deselected) - completer = objreg.get('completer', scope='window', - window=self._win_id) - 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.""" From b84eb6aeb0bb1a93d13bc362fa74270131c53baa Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 29 Sep 2014 08:52:00 +0200 Subject: [PATCH 14/47] Add missing utilcmds import. --- qutebrowser/app.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 46655c94d..3dcfc37c3 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -44,6 +44,8 @@ from qutebrowser.widgets import mainwindow, console, crash from qutebrowser.keyinput import modeman 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): From e6fe358d73f397711d3a4c81e5dd764e877f385c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 29 Sep 2014 10:43:06 +0200 Subject: [PATCH 15/47] Clean up correctly when a window is closed. --- qutebrowser/keyinput/modeman.py | 5 +++++ qutebrowser/utils/objreg.py | 16 +++++++++++++++- qutebrowser/widgets/completion.py | 2 +- qutebrowser/widgets/mainwindow.py | 1 + qutebrowser/widgets/statusbar/prompt.py | 5 +++++ qutebrowser/widgets/tabbedbrowser.py | 3 +++ 6 files changed, 30 insertions(+), 2 deletions(-) diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index c52248c28..cf172b8a5 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 @@ -60,6 +62,9 @@ def init(win_id, parent): KM.yesno: modeparsers.PromptKeyParser(win_id, modeman), } 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) diff --git a/qutebrowser/utils/objreg.py b/qutebrowser/utils/objreg.py index 1aceaedef..e5315a387 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: @@ -68,12 +70,24 @@ class ObjectRegistry(collections.UserDict): 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: pass + def dump_objects(self): """Dump all objects as a list of strings.""" lines = [] diff --git a/qutebrowser/widgets/completion.py b/qutebrowser/widgets/completion.py index 815e04992..40813b5b7 100644 --- a/qutebrowser/widgets/completion.py +++ b/qutebrowser/widgets/completion.py @@ -94,7 +94,7 @@ class CompletionView(QTreeView): super().__init__(parent) self._win_id = win_id objreg.register('completion', self, scope='window', window=win_id) - completer_obj = completer.Completer(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') diff --git a/qutebrowser/widgets/mainwindow.py b/qutebrowser/widgets/mainwindow.py index 5bae11c4a..c0e61f8d0 100644 --- a/qutebrowser/widgets/mainwindow.py +++ b/qutebrowser/widgets/mainwindow.py @@ -72,6 +72,7 @@ class MainWindow(QWidget): 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() diff --git a/qutebrowser/widgets/statusbar/prompt.py b/qutebrowser/widgets/statusbar/prompt.py index 4578ff3fc..ddbd59416 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.QtCore import pyqtSignal from PyQt5.QtWidgets import QHBoxLayout, QWidget, QLineEdit @@ -69,6 +71,9 @@ class Prompt(QWidget): 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/tabbedbrowser.py b/qutebrowser/widgets/tabbedbrowser.py index 01ed01eb4..d113c29b6 100644 --- a/qutebrowser/widgets/tabbedbrowser.py +++ b/qutebrowser/widgets/tabbedbrowser.py @@ -113,6 +113,9 @@ class TabbedBrowser(tabwidget.TabWidget): 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 self.setIconSize(QSize(12, 12)) From 6e61f4c58610a50310e90cd48eb4e059d3e03338 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 29 Sep 2014 18:46:20 +0200 Subject: [PATCH 16/47] Support opening multiple windows via init args. --- qutebrowser/app.py | 30 +++++++++++++++++++----------- qutebrowser/qutebrowser.py | 3 ++- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 3dcfc37c3..b3348f18a 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -219,12 +219,17 @@ class Application(QApplication): # we make sure the GUI is refreshed here, so the start seems faster. self.processEvents(QEventLoop.ExcludeUserInputEvents | QEventLoop.ExcludeSocketNotifiers) - tabbed_browser = objreg.get('tabbed-browser', scope='window', window=0) + win_id = 0 for cmd in self._args.command: if cmd.startswith(':'): log.init.debug("Startup cmd {}".format(cmd)) self._commandrunner.run_safely_init(0, cmd.lstrip(':')) + elif not cmd: + log.init.debug("Empty argument") + win_id = mainwindow.create_window(True) 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) @@ -234,16 +239,19 @@ class Application(QApplication): 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(0, "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)) + else: + tabbed_browser.tabopen(url) def _python_hacks(self): """Get around some PyQt-oddities by evil hacks. 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 From 24c48df9881b73187ad149de995843a6f06947d2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 29 Sep 2014 19:02:46 +0200 Subject: [PATCH 17/47] Open about:blank if opening startpage fails. --- qutebrowser/app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index b3348f18a..58e761e17 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -250,6 +250,7 @@ class Application(QApplication): 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) From 714c9d8b01401fa9e29bb4d14cc7f85ebebd4b86 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 29 Sep 2014 19:14:11 +0200 Subject: [PATCH 18/47] Add win_id parameter to commands which need it. --- qutebrowser/commands/command.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 06e70a8db..8b91cfcfe 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -184,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) @@ -337,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) @@ -378,6 +395,10 @@ class Command: # 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) From 0cf8abab7cbffa8e4ad6db0d0cc246908976a32a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 29 Sep 2014 19:56:13 +0200 Subject: [PATCH 19/47] Fix running of startup commands. --- qutebrowser/app.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 58e761e17..8366c8904 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -36,7 +36,7 @@ from PyQt5.QtCore import (pyqtSlot, QTimer, QEventLoop, QStandardPaths, qInstallMessageHandler, QObject, QUrl) import qutebrowser -from qutebrowser.commands import 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 @@ -223,7 +223,8 @@ class Application(QApplication): for cmd in self._args.command: if cmd.startswith(':'): log.init.debug("Startup cmd {}".format(cmd)) - self._commandrunner.run_safely_init(0, cmd.lstrip(':')) + commandrunner = runners.CommandRunner(win_id) + commandrunner.run_safely_init(cmd.lstrip(':')) elif not cmd: log.init.debug("Empty argument") win_id = mainwindow.create_window(True) From 42a1d7028db0e900dd56b3e1e7cd64fd4ff07a91 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 29 Sep 2014 19:56:25 +0200 Subject: [PATCH 20/47] Move :later back from commands to utilcmds. --- qutebrowser/browser/commands.py | 28 ---------------------------- qutebrowser/utils/utilcmds.py | 31 +++++++++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 30 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index cbe6ff7be..cc0bbe6c4 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -54,13 +54,11 @@ class CommandDispatcher: Attributes: _editor: The ExternalEditor object. _win_id: The window ID the CommandDispatcher is associated with. - _timers: A list of timers for :later """ def __init__(self, win_id): self._editor = None self._win_id = win_id - self._timers = [] def __repr__(self): return utils.get_repr(self) @@ -833,32 +831,6 @@ class CommandDispatcher: raise cmdexc.CommandError("Invalid help topic {}!".format(topic)) self.openurl('qute://help/{}'.format(path)) - @cmdutils.register(instance='command-dispatcher', scope='window') - def later(self, ms: int, *command): - """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') - timer.setSingleShot(True) - if ms < 0: - raise cmdexc.CommandError("I can't run something in the past!") - try: - timer.setInterval(ms) - except OverflowError: - raise cmdexc.CommandError("Numeric argument is too large for " - "internal int representation.") - self._timers.append(timer) - cmdline = ' '.join(command) - commandrunner = objreg.get('command-runner', scope='window', - window=self._win_id) - timer.timeout.connect( - functools.partial(commandrunner.run_safely, cmdline)) - timer.timeout.connect(lambda: self._timers.remove(timer)) - timer.start() - @cmdutils.register(instance='command-dispatcher', modes=[usertypes.KeyMode.insert], hide=True, scope='window') diff --git a/qutebrowser/utils/utilcmds.py b/qutebrowser/utils/utilcmds.py index 3a93acada..6d6990b7d 100644 --- a/qutebrowser/utils/utilcmds.py +++ b/qutebrowser/utils/utilcmds.py @@ -19,15 +19,42 @@ """Misc. utility commands exposed to the user.""" +import functools import types from PyQt5.QtCore import QCoreApplication -from qutebrowser.utils import log, objreg -from qutebrowser.commands import cmdutils +from qutebrowser.utils import log, objreg, usertypes +from qutebrowser.commands import cmdutils, runners from qutebrowser.config import style +@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. + """ + 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!") + try: + timer.setInterval(ms) + except OverflowError: + raise cmdexc.CommandError("Numeric argument is too large for " + "internal int representation.") + cmdline = ' '.join(command) + commandrunner = runners.CommandRunner(win_id) + timer.timeout.connect( + functools.partial(commandrunner.run_safely, cmdline)) + timer.timeout.connect(timer.deleteLater) + timer.start() + + @cmdutils.register(debug=True) def debug_crash(typ: ('exception', 'segfault')='exception'): """Crash for debugging purposes. From 45ac2e4835c816dfaf7c644a745600d6ecd83734 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 29 Sep 2014 19:56:37 +0200 Subject: [PATCH 21/47] Fix getting URL for startup commands. --- qutebrowser/commands/runners.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index ccb859163..0fbbba19d 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -32,10 +32,12 @@ def replace_variables(win_id, arglist): args = [] tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id) - url = tabbed_browser.current_url().toString(QUrl.FullyEncoded | - QUrl.RemovePassword) for arg in arglist: if arg == '{url}': + # 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) From 865389bb349a04ec54cf1efb22cdd8ab66f6cd37 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 29 Sep 2014 20:16:38 +0200 Subject: [PATCH 22/47] Make CommandRunner a QObject. --- qutebrowser/commands/runners.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index 0fbbba19d..e7971454e 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -156,7 +156,7 @@ class SearchRunner(QObject): self.do_search.emit(self._text, flags) -class CommandRunner: +class CommandRunner(QObject): """Parse and run qutebrowser commandline commands. @@ -166,7 +166,8 @@ class CommandRunner: _win_id: The window this CommandRunner is associated with. """ - def __init__(self, win_id): + def __init__(self, win_id, parent=None): + super().__init__(parent) self._cmd = None self._args = [] self._win_id = win_id From 106ad9f4edf605ea3358b1ce82dffdf34d070d08 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 29 Sep 2014 20:16:54 +0200 Subject: [PATCH 23/47] userscripts: Don't store a list of runners. --- qutebrowser/commands/userscripts.py | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/qutebrowser/commands/userscripts.py b/qutebrowser/commands/userscripts.py index bd579b2a6..62ba2b9c4 100644 --- a/qutebrowser/commands/userscripts.py +++ b/qutebrowser/commands/userscripts.py @@ -17,11 +17,7 @@ # 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 @@ -36,10 +32,6 @@ from qutebrowser.utils import message, log, utils from qutebrowser.commands import runners, cmdexc -_runners = [] -_commandrunners = [] - - class _BlockingFIFOReader(QObject): """A worker which reads commands from a FIFO endlessly. @@ -333,15 +325,14 @@ else: 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) - commandrunner = runners.CommandRunner(win_id) - runner = UserscriptRunner(win_id) + 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) - _commandrunners.append(commandrunner) - runner.finished.connect(functools.partial(_runners.remove, runner)) - runner.finished.connect( - functools.partial(_commandrunners.remove, commandrunner)) + runner.finished.connect(commandrunner.deleteLater) + runner.finished.connect(runner.deleteLater) From e031cc60c83470e17a65196c61bc343da86fc2a6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 29 Sep 2014 20:17:11 +0200 Subject: [PATCH 24/47] tabbedbrowser: Don't store a list of tabs. --- qutebrowser/widgets/tabbedbrowser.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/qutebrowser/widgets/tabbedbrowser.py b/qutebrowser/widgets/tabbedbrowser.py index d113c29b6..2d51535dc 100644 --- a/qutebrowser/widgets/tabbedbrowser.py +++ b/qutebrowser/widgets/tabbedbrowser.py @@ -52,7 +52,6 @@ class TabbedBrowser(tabwidget.TabWidget): Attributes: _win_id: The window ID this tabbedbrowser is associated with. - _tabs: A list of open tabs. _filter: A SignalFilter instance. _now_focused: The tab which is focused now. _tab_insert_idx_left: Where to insert a new tab with @@ -107,7 +106,6 @@ class TabbedBrowser(tabwidget.TabWidget): 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(win_id, self) dispatcher = commands.CommandDispatcher(win_id) @@ -260,7 +258,6 @@ class TabbedBrowser(tabwidget.TabWidget): entry = UndoEntry(tab.cur_url, history_data) self._undo_stack.append(entry) tab.shutdown() - self._tabs.remove(tab) self.removeTab(idx) tab.deleteLater() @@ -325,7 +322,6 @@ class TabbedBrowser(tabwidget.TabWidget): log.webview.debug("Creating new tab with URL {}".format(url)) 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: From 686f82c5c6d3a9ad2d5f0962bb1e3be8cd67096e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 29 Sep 2014 20:17:22 +0200 Subject: [PATCH 25/47] webview: Remove dead search method. --- qutebrowser/widgets/webview.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/qutebrowser/widgets/webview.py b/qutebrowser/widgets/webview.py index 97a021b7d..98d06169c 100644 --- a/qutebrowser/widgets/webview.py +++ b/qutebrowser/widgets/webview.py @@ -310,16 +310,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(): From 6aeecb3803615b8a23b2b5c9f346ba68ed270dc0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 29 Sep 2014 20:36:53 +0200 Subject: [PATCH 26/47] Fix lints/bugs --- qutebrowser/app.py | 32 +++++++++++++---------------- qutebrowser/commands/userscripts.py | 3 +-- qutebrowser/utils/objreg.py | 1 - qutebrowser/utils/utilcmds.py | 6 +++--- 4 files changed, 18 insertions(+), 24 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 8366c8904..792d716c5 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -250,7 +250,7 @@ class Application(QApplication): url = urlutils.fuzzy_url(urlstr) except urlutils.FuzzyUrlError as e: message.error(0, "Error when opening startpage: " - "{}".format(e)) + "{}".format(e)) tabbed_browser.tabopen(QUrl('about:blank')) else: tabbed_browser.tabopen(url) @@ -319,11 +319,14 @@ class Application(QApplication): output += self._get_registered_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. """ @@ -333,12 +336,15 @@ class Application(QApplication): window=win_id) for tab in tabbed_browser.widgets(): try: - url = tab.cur_url.toString( + urlstr = tab.cur_url.toString( QUrl.RemovePassword | QUrl.FullyEncoded) - if url: - pages.append(url) + if urlstr: + pages.append(urlstr) except Exception: # pylint: disable=broad-except - log.destroy.exception("Error while recovering tab") + if forgiving: + log.destroy.exception("Error while recovering tab") + else: + raise return pages def _save_geometry(self): @@ -396,7 +402,7 @@ 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 = [] @@ -438,19 +444,9 @@ class Application(QApplication): @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. # FIXME handle multiple windows correctly here if pages is None: - pages = [] - for win_id in objreg.window_registry: - tabbed_browser = objreg.get('tabbed-browser', scope='window', - window=win_id) - for tab in 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)) diff --git a/qutebrowser/commands/userscripts.py b/qutebrowser/commands/userscripts.py index 62ba2b9c4..3554e94f6 100644 --- a/qutebrowser/commands/userscripts.py +++ b/qutebrowser/commands/userscripts.py @@ -23,12 +23,11 @@ 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 diff --git a/qutebrowser/utils/objreg.py b/qutebrowser/utils/objreg.py index e5315a387..bc0bc4d62 100644 --- a/qutebrowser/utils/objreg.py +++ b/qutebrowser/utils/objreg.py @@ -87,7 +87,6 @@ class ObjectRegistry(collections.UserDict): except KeyError: pass - def dump_objects(self): """Dump all objects as a list of strings.""" lines = [] diff --git a/qutebrowser/utils/utilcmds.py b/qutebrowser/utils/utilcmds.py index 6d6990b7d..32aad6af1 100644 --- a/qutebrowser/utils/utilcmds.py +++ b/qutebrowser/utils/utilcmds.py @@ -25,7 +25,7 @@ import types from PyQt5.QtCore import QCoreApplication from qutebrowser.utils import log, objreg, usertypes -from qutebrowser.commands import cmdutils, runners +from qutebrowser.commands import cmdutils, runners, cmdexc from qutebrowser.config import style @@ -45,8 +45,8 @@ def later(ms: int, *command, win_id): try: timer.setInterval(ms) except OverflowError: - raise cmdexc.CommandError("Numeric argument is too large for " - "internal int representation.") + raise cmdexc.CommandError("Numeric argument is too large for internal " + "int representation.") cmdline = ' '.join(command) commandrunner = runners.CommandRunner(win_id) timer.timeout.connect( From ce8409feb298616e561e24fdeca747fc0daa8a6e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 29 Sep 2014 22:37:46 +0200 Subject: [PATCH 27/47] Add multi window support to :restore/:restart. --- qutebrowser/app.py | 15 +++++++++++---- qutebrowser/widgets/crash.py | 8 ++++---- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 792d716c5..cf0b74151 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -328,10 +328,12 @@ class Application(QApplication): 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. """ pages = [] 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(): @@ -339,12 +341,13 @@ class Application(QApplication): urlstr = tab.cur_url.toString( QUrl.RemovePassword | QUrl.FullyEncoded) if urlstr: - pages.append(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): @@ -444,7 +447,6 @@ class Application(QApplication): @cmdutils.register(instance='app', ignore_args=True) def restart(self, shutdown=True, pages=None): """Restart qutebrowser while keeping existing tabs open.""" - # FIXME handle multiple windows correctly here if pages is None: pages = self._recover_pages() log.destroy.debug("sys.executable: {}".format(sys.executable)) @@ -467,7 +469,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 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), ] From b6393a1841751c8cce5934dda91d44bda987bab1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 5 Oct 2014 21:50:14 +0200 Subject: [PATCH 28/47] Get rid of meta registry --- doc/HACKING.asciidoc | 2 -- qutebrowser/app.py | 16 +--------------- qutebrowser/utils/objreg.py | 24 +++++++++++++++++++----- qutebrowser/widgets/mainwindow.py | 2 -- qutebrowser/widgets/webview.py | 2 -- 5 files changed, 20 insertions(+), 26 deletions(-) diff --git a/doc/HACKING.asciidoc b/doc/HACKING.asciidoc index 57f1d484d..cd822913c 100644 --- a/doc/HACKING.asciidoc +++ b/doc/HACKING.asciidoc @@ -294,8 +294,6 @@ There are currently these object registries, also called 'scopes': * 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. A new object can be registered by using +objreg.register(_name_, _object_[, scope=_scope_])+. An object should not be diff --git a/qutebrowser/app.py b/qutebrowser/app.py index cf0b74151..6ae3a630c 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -287,20 +287,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 = [''] @@ -316,7 +302,7 @@ 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, forgiving=False): diff --git a/qutebrowser/utils/objreg.py b/qutebrowser/utils/objreg.py index bc0bc4d62..205530e1f 100644 --- a/qutebrowser/utils/objreg.py +++ b/qutebrowser/utils/objreg.py @@ -97,9 +97,6 @@ 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() @@ -148,8 +145,6 @@ def _get_registry(scope, window): return _get_tab_registry() elif scope == 'window': return _get_window_registry(window) - elif scope == 'meta': - return meta_registry else: raise ValueError("Invalid scope '{}'!".format(scope)) @@ -197,3 +192,22 @@ def delete(name, scope='global', window=None): """Helper function to unregister an object.""" reg = _get_registry(scope, window) 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())) + # FIXME: Add tab registries + 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 + diff --git a/qutebrowser/widgets/mainwindow.py b/qutebrowser/widgets/mainwindow.py index c0e61f8d0..76f2e5529 100644 --- a/qutebrowser/widgets/mainwindow.py +++ b/qutebrowser/widgets/mainwindow.py @@ -76,8 +76,6 @@ class MainWindow(QWidget): self._commandrunner = None self.win_id = win_id self.registry = objreg.ObjectRegistry() - objreg.register('window-{}'.format(win_id), self.registry, - scope='meta') objreg.window_registry[win_id] = self objreg.register('main-window', self, scope='window', window=win_id) diff --git a/qutebrowser/widgets/webview.py b/qutebrowser/widgets/webview.py index 98d06169c..e6d8a986f 100644 --- a/qutebrowser/widgets/webview.py +++ b/qutebrowser/widgets/webview.py @@ -108,8 +108,6 @@ class WebView(QWebView): 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) From dfd3b3d9c446601fa30ba94488b683a72dd25673 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 5 Oct 2014 22:17:29 +0200 Subject: [PATCH 29/47] Store window registries in objreg. --- qutebrowser/keyinput/modeman.py | 6 ++++-- qutebrowser/utils/objreg.py | 20 ++++++++------------ qutebrowser/widgets/mainwindow.py | 8 ++++++-- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index cf172b8a5..350057cb7 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -359,8 +359,10 @@ 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() not in - objreg.window_registry.values()): + win = QApplication.instance().activeWindow() + if win is None: + return False + if win.objectName() != 'MainWindow': # Some other window (print dialog, etc.) is focused so we pass # the event through. return False diff --git a/qutebrowser/utils/objreg.py b/qutebrowser/utils/objreg.py index 205530e1f..b0f99f129 100644 --- a/qutebrowser/utils/objreg.py +++ b/qutebrowser/utils/objreg.py @@ -115,22 +115,19 @@ def _get_tab_registry(): raise RegistryUnavailableError('tab') -def _get_window_registry(window): +def _get_window_registry(win_id): """Get the registry of a window.""" - if window is None: - raise TypeError("window is None with scope window!") - if window is 'current': + if win_id is None: + raise TypeError("win_id is None with scope window!") + if win_id == 'current': app = get('app') win = app.activeWindow() if win is None: raise RegistryUnavailableError('window') - else: - try: - win = window_registry[window] - except KeyError: - raise RegistryUnavailableError('window') + else: + win_id = win.win_id try: - return win.registry + return window_registry[win_id] except AttributeError: raise RegistryUnavailableError('window') @@ -199,8 +196,7 @@ def dump_objects(): blocks = [] lines = [] blocks.append(('global', global_registry.dump_objects())) - for win_id in window_registry: - registry = _get_registry('window', window=win_id) + for win_id, registry in window_registry.items(): blocks.append(('window-{}'.format(win_id), registry.dump_objects())) # FIXME: Add tab registries for name, data in sorted(blocks, key=lambda e: e[0]): diff --git a/qutebrowser/widgets/mainwindow.py b/qutebrowser/widgets/mainwindow.py index 76f2e5529..0a0bb6081 100644 --- a/qutebrowser/widgets/mainwindow.py +++ b/qutebrowser/widgets/mainwindow.py @@ -72,11 +72,15 @@ class MainWindow(QWidget): def __init__(self, win_id, parent=None): super().__init__(parent) + # Note the objectname is used in the event filter to filter keypresses + # not intended for the mainwindow. If this is changed, the event filter + # needs to be changed as well! + self.setObjectName('MainWindow') self.setAttribute(Qt.WA_DeleteOnClose) self._commandrunner = None self.win_id = win_id - self.registry = objreg.ObjectRegistry() - objreg.window_registry[win_id] = self + registry = objreg.ObjectRegistry() + objreg.window_registry[win_id] = registry objreg.register('main-window', self, scope='window', window=win_id) message_bridge = message.MessageBridge(self) From d260b26105c0b308b79daeca223fb5dc72994428 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 5 Oct 2014 22:41:44 +0200 Subject: [PATCH 30/47] Fix objreg._get_window_registry when non-mainwindow window is focused. --- qutebrowser/utils/objreg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/utils/objreg.py b/qutebrowser/utils/objreg.py index b0f99f129..481eec7e6 100644 --- a/qutebrowser/utils/objreg.py +++ b/qutebrowser/utils/objreg.py @@ -122,7 +122,7 @@ def _get_window_registry(win_id): if win_id == 'current': app = get('app') win = app.activeWindow() - if win is None: + if win is None or not hasattr(win, 'win_id'): raise RegistryUnavailableError('window') else: win_id = win.win_id From 180d6e45ef28c327b5632d19b06e91844afaada3 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 5 Oct 2014 23:09:04 +0200 Subject: [PATCH 31/47] Fix closeEvent of MainWindow. --- qutebrowser/widgets/mainwindow.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/qutebrowser/widgets/mainwindow.py b/qutebrowser/widgets/mainwindow.py index 0a0bb6081..c216828e9 100644 --- a/qutebrowser/widgets/mainwindow.py +++ b/qutebrowser/widgets/mainwindow.py @@ -319,18 +319,18 @@ 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(self.win_id, text, usertypes.PromptMode.yesno, default=True) - if confirmed: - e.accept() - else: + 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...") From d3121034dfd568f335332af0ef44e99ca43e3019 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 5 Oct 2014 23:09:35 +0200 Subject: [PATCH 32/47] Revert "Store window registries in objreg." This reverts commit dfd3b3d9c446601fa30ba94488b683a72dd25673. It turns out this makes it very hard to remove the window from the window registry at the right time. --- qutebrowser/keyinput/modeman.py | 6 ++---- qutebrowser/utils/objreg.py | 20 ++++++++++++-------- qutebrowser/widgets/mainwindow.py | 8 ++------ 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index 350057cb7..cf172b8a5 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -359,10 +359,8 @@ class ModeManager(QObject): # We already handled this same event at some point earlier, so # we're not interested in it anymore. return False - win = QApplication.instance().activeWindow() - if win is None: - return False - if win.objectName() != 'MainWindow': + 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/utils/objreg.py b/qutebrowser/utils/objreg.py index 481eec7e6..8ac8282fc 100644 --- a/qutebrowser/utils/objreg.py +++ b/qutebrowser/utils/objreg.py @@ -115,19 +115,22 @@ def _get_tab_registry(): raise RegistryUnavailableError('tab') -def _get_window_registry(win_id): +def _get_window_registry(window): """Get the registry of a window.""" - if win_id is None: - raise TypeError("win_id is None with scope window!") - if win_id == 'current': + if window is None: + raise TypeError("window is None with scope window!") + if window is 'current': app = get('app') win = app.activeWindow() if win is None or not hasattr(win, 'win_id'): raise RegistryUnavailableError('window') - else: - win_id = win.win_id + else: + try: + win = window_registry[window] + except KeyError: + raise RegistryUnavailableError('window') try: - return window_registry[win_id] + return win.registry except AttributeError: raise RegistryUnavailableError('window') @@ -196,7 +199,8 @@ def dump_objects(): blocks = [] lines = [] blocks.append(('global', global_registry.dump_objects())) - for win_id, registry in window_registry.items(): + for win_id in window_registry: + registry = _get_registry('window', window=win_id) blocks.append(('window-{}'.format(win_id), registry.dump_objects())) # FIXME: Add tab registries for name, data in sorted(blocks, key=lambda e: e[0]): diff --git a/qutebrowser/widgets/mainwindow.py b/qutebrowser/widgets/mainwindow.py index c216828e9..c3376ad68 100644 --- a/qutebrowser/widgets/mainwindow.py +++ b/qutebrowser/widgets/mainwindow.py @@ -72,15 +72,11 @@ class MainWindow(QWidget): def __init__(self, win_id, parent=None): super().__init__(parent) - # Note the objectname is used in the event filter to filter keypresses - # not intended for the mainwindow. If this is changed, the event filter - # needs to be changed as well! - self.setObjectName('MainWindow') self.setAttribute(Qt.WA_DeleteOnClose) self._commandrunner = None self.win_id = win_id - registry = objreg.ObjectRegistry() - objreg.window_registry[win_id] = registry + self.registry = objreg.ObjectRegistry() + objreg.window_registry[win_id] = self objreg.register('main-window', self, scope='window', window=win_id) message_bridge = message.MessageBridge(self) From 85ead4273b82416467c7b0ed98fce4c24caeed8b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 6 Oct 2014 07:41:05 +0200 Subject: [PATCH 33/47] Register tab registry under window registry with ID. --- qutebrowser/browser/hints.py | 8 +++-- qutebrowser/keyinput/modeparsers.py | 11 +++++-- qutebrowser/utils/objreg.py | 49 +++++++++++++++++++---------- qutebrowser/widgets/mainwindow.py | 3 ++ qutebrowser/widgets/webview.py | 6 +++- 5 files changed, 54 insertions(+), 23 deletions(-) diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index d30e89aee..30ce73179 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -101,6 +101,7 @@ 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. @@ -138,10 +139,11 @@ class HintManager(QObject): mouse_event = pyqtSignal('QMouseEvent') set_open_target = pyqtSignal(str) - def __init__(self, win_id, parent=None): + 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 mode_manager = objreg.get('mode-manager', scope='window', window=win_id) @@ -521,7 +523,9 @@ class HintManager(QObject): window=self._win_id) tabbed_browser.tabopen(url, background=False) 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, diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 26ceedb59..592948b7e 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -111,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, @@ -166,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) @@ -183,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/utils/objreg.py b/qutebrowser/utils/objreg.py index 8ac8282fc..d55b19342 100644 --- a/qutebrowser/utils/objreg.py +++ b/qutebrowser/utils/objreg.py @@ -101,16 +101,28 @@ global_registry = ObjectRegistry() window_registry = ObjectRegistry() -def _get_tab_registry(): +def _get_tab_registry(win_id, tab_id): """Get the registry of a tab.""" - app = get('app') - win = app.activeWindow() - tabbed_browser = get('tabbed-browser', scope='window', window=win) - widget = tabbed_browser.currentWidget() - if widget is None: - raise RegistryUnavailableError('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 widget.registry + return tab_registry[tab_id].registry except AttributeError: raise RegistryUnavailableError('tab') @@ -135,27 +147,29 @@ def _get_window_registry(window): raise RegistryUnavailableError('window') -def _get_registry(scope, window): +def _get_registry(scope, window=None, tab=None): """Get the correct registry for a given scope.""" - if window is not None and scope is not 'window': + 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': - return _get_tab_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', window=None): +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, window) + reg = _get_registry(scope, window, tab) try: return reg[name] except KeyError: @@ -165,7 +179,8 @@ def get(name, default=_UNSET, scope='global', window=None): raise -def register(name, obj, update=False, scope=None, registry=None, window=None): +def register(name, obj, update=False, scope=None, registry=None, window=None, + tab=None): """Helper function to register an object. Args: @@ -181,16 +196,16 @@ def register(name, obj, update=False, scope=None, registry=None, window=None): else: if scope is None: scope = 'global' - reg = _get_registry(scope, window) + 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', window=None): +def delete(name, scope='global', window=None, tab=None): """Helper function to unregister an object.""" - reg = _get_registry(scope, window) + reg = _get_registry(scope, window, tab) del reg[name] diff --git a/qutebrowser/widgets/mainwindow.py b/qutebrowser/widgets/mainwindow.py index c3376ad68..e4d6a348a 100644 --- a/qutebrowser/widgets/mainwindow.py +++ b/qutebrowser/widgets/mainwindow.py @@ -78,6 +78,9 @@ class MainWindow(QWidget): 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', diff --git a/qutebrowser/widgets/webview.py b/qutebrowser/widgets/webview.py index e6d8a986f..d8665a0ea 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. @@ -101,10 +102,13 @@ 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(win_id, self) self.setPage(page) - hintmanager = hints.HintManager(win_id, 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) From d8cd599096a85bb870c9c4ed9a6650ef7ffba675 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 6 Oct 2014 07:41:26 +0200 Subject: [PATCH 34/47] Fix wrong 'is' check. --- qutebrowser/utils/objreg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/utils/objreg.py b/qutebrowser/utils/objreg.py index d55b19342..e0dcfa129 100644 --- a/qutebrowser/utils/objreg.py +++ b/qutebrowser/utils/objreg.py @@ -131,7 +131,7 @@ 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 is 'current': + if window == 'current': app = get('app') win = app.activeWindow() if win is None or not hasattr(win, 'win_id'): From 7e209dc285d478a358fd5afa1b91eeaabaf77b33 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 6 Oct 2014 07:41:34 +0200 Subject: [PATCH 35/47] Print tab registries in :debug-all-objects. --- qutebrowser/utils/objreg.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/qutebrowser/utils/objreg.py b/qutebrowser/utils/objreg.py index e0dcfa129..2cb3f0cb9 100644 --- a/qutebrowser/utils/objreg.py +++ b/qutebrowser/utils/objreg.py @@ -217,8 +217,12 @@ def dump_objects(): for win_id in window_registry: registry = _get_registry('window', window=win_id) blocks.append(('window-{}'.format(win_id), registry.dump_objects())) - # FIXME: Add tab registries - for name, data in sorted(blocks, key=lambda e: e[0]): + 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))) From 2cd4642ece0a056361df728021e30c455393c9b9 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 6 Oct 2014 08:27:15 +0200 Subject: [PATCH 36/47] Pass window ID to scheme handlers. --- qutebrowser/network/qutescheme.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/network/qutescheme.py b/qutebrowser/network/qutescheme.py index e8479299e..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, From 6324751af6e6a81181b14fc00ea294749b8de2e3 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 6 Oct 2014 17:58:40 +0200 Subject: [PATCH 37/47] More window support. --- doc/qutebrowser.1.asciidoc | 2 +- qutebrowser/browser/commands.py | 78 +++++++++++++++++++++------------ qutebrowser/browser/hints.py | 34 +++++++++----- qutebrowser/browser/webpage.py | 13 +++++- qutebrowser/utils/usertypes.py | 2 +- 5 files changed, 86 insertions(+), 43 deletions(-) diff --git a/doc/qutebrowser.1.asciidoc b/doc/qutebrowser.1.asciidoc index 7761b790d..fa33b39d3 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/browser/commands.py b/qutebrowser/browser/commands.py index cc0bbe6c4..91d4cc7a3 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -63,10 +63,19 @@ class CommandDispatcher: def __repr__(self): return utils.get_repr(self) - def _tabbed_browser(self): - """Convienence method to get the right tabbed-browser.""" - return objreg.get('tabbed-browser', scope='window', - window=self._win_id) + def _tabbed_browser(self, window=False): + """Convienence method to get the right tabbed-browser. + + Args: + window: If True, open a new window. + """ + if window: + # We have to import this here to avoid a circular import. + from qutebrowser.widgets import mainwindow + win_id = mainwindow.create_window(True) + 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.""" @@ -104,11 +113,7 @@ class CommandDispatcher: 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: - # We have to import this here to avoid a circular import. - from qutebrowser.widgets import mainwindow - win_id = mainwindow.create_window(True) - tabbed_browser = objreg.get('tabbed-browser', scope='window', - window=win_id) + tabbed_browser = self._tabbed_browser(window=True) tabbed_browser.tabopen(url) elif tab: tabbed_browser.tabopen(url, background=False, explicit=True) @@ -302,26 +307,29 @@ class CommandDispatcher: diag.open(lambda: tab.print(diag.printer())) @cmdutils.register(instance='command-dispatcher', scope='window') - def tab_clone(self, bg=False): + def tab_clone(self, bg=False, window=False): """Duplicate the current tab. 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 = self._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 tab or bg or window: + widget = self.tab_clone(bg, window) else: widget = self._current_widget() for _ in range(count): @@ -331,34 +339,38 @@ class CommandDispatcher: widget.go_back() @cmdutils.register(instance='command-dispatcher', scope='window') - def back(self, tab=False, bg=False, count=1): + def back(self, tab=False, bg=False, window=False, count=1): """Go back in the history of the current tab. 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', scope='window') - def forward(self, tab=False, bg=False, count=1): + def forward(self, tab=False, bg=False, window=False, count=1): """Go forward in the history of the current tab. 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 @@ -383,25 +395,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, window=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, window=False) + self._open(url, tab, background, window) @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 @@ -419,7 +433,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() @@ -427,13 +445,15 @@ 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=background, window=window) elif where == 'next': - hintmanager.follow_prevnext(frame, url, prev=False, newtab=tab) + hintmanager.follow_prevnext(frame, url, prev=False, tab=tab, + background=background, window=window) elif where == 'up': - self._navigate_up(url, tab) + self._navigate_up(url, tab, background, window) elif where in ('decrement', 'increment'): - self._navigate_incdec(url, tab, where) + self._navigate_incdec(url, where, tab, background, window) else: raise ValueError("Got called with invalid value {} for " "`where'.".format(where)) diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 30ce73179..567085dbd 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -37,7 +37,7 @@ from qutebrowser.utils import usertypes, log, qtutils, message, objreg ElemTuple = collections.namedtuple('ElemTuple', ['elem', 'label']) -Target = usertypes.enum('Target', ['normal', 'tab', 'tab_bg', 'yank', +Target = usertypes.enum('Target', ['normal', 'tab', 'tab_bg', 'window', 'yank', 'yank_primary', 'fill', 'rapid', 'download', 'userscript', 'spawn']) @@ -58,7 +58,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 @@ -127,6 +127,7 @@ 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...", @@ -500,14 +501,17 @@ class HintManager(QObject): 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: @@ -518,13 +522,21 @@ class HintManager(QObject): raise cmdexc.CommandError("No {} links found!".format( "prev" if prev else "forward")) qtutils.ensure_valid(url) - if newtab: - tabbed_browser = objreg.get('tabbed-browser', scope='window', - window=self._win_id) - tabbed_browser.tabopen(url, background=False) + if window: + # We have to import this here to avoid a circular import. + from qutebrowser.widgets import mainwindow + win_id = mainwindow.create_window(True) + tab_id = 0 else: - webview = objreg.get('webview', scope='tab', window=self._win_id, - tab=self._tab_id) + win_id = self._win_id + tab_id = self._tab_id + if tab: + tabbed_browser = objreg.get('tabbed-browser', scope='window', + window=win_id) + tabbed_browser.tabopen(url, background=background) + else: + webview = objreg.get('webview', scope='tab', window=win_id, + tab=tab_id) webview.openurl(url) @cmdutils.register(instance='hintmanager', scope='tab', name='hint') @@ -544,6 +556,7 @@ 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 @@ -650,6 +663,7 @@ class HintManager(QObject): Target.normal: self._click, Target.tab: self._click, Target.tab_bg: self._click, + Target.window: self._click, Target.rapid: self._click, # _download needs a QWebElement to get the frame. Target.download: self._download, diff --git a/qutebrowser/browser/webpage.py b/qutebrowser/browser/webpage.py index 09571a51f..e0dcabd61 100644 --- a/qutebrowser/browser/webpage.py +++ b/qutebrowser/browser/webpage.py @@ -302,11 +302,20 @@ class BrowserPage(QWebPage): return False tabbed_browser = objreg.get('tabbed-browser', scope='window', window=self._win_id) - if self.view().open_target == usertypes.ClickTarget.tab: + 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: + elif open_target == usertypes.ClickTarget.tab_bg: tabbed_browser.tabopen(url, True) return False + elif open_target == usertypes.ClickTarget.window: + # We have to import this here to avoid a circular import. + from qutebrowser.widgets import mainwindow + win_id = mainwindow.create_window(True) + tabbed_browser = objreg.get('tabbed-browser', scope='window', + window=win_id) + tabbed_browser.openurl(url, False) + return False else: return True diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index e3d354eed..1cdc14d9e 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 From 52e72a8bece1797fa4325c778a1466f58e3b5622 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 6 Oct 2014 18:47:55 +0200 Subject: [PATCH 38/47] Fix window spawning with hints. --- qutebrowser/browser/hints.py | 13 ++++++------- qutebrowser/browser/webpage.py | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 567085dbd..54a4d0387 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -526,17 +526,16 @@ class HintManager(QObject): # We have to import this here to avoid a circular import. from qutebrowser.widgets import mainwindow win_id = mainwindow.create_window(True) - tab_id = 0 - else: - win_id = self._win_id - tab_id = self._tab_id - if tab: 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: - webview = objreg.get('webview', scope='tab', window=win_id, - tab=tab_id) + 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') diff --git a/qutebrowser/browser/webpage.py b/qutebrowser/browser/webpage.py index e0dcabd61..fa38307ea 100644 --- a/qutebrowser/browser/webpage.py +++ b/qutebrowser/browser/webpage.py @@ -315,7 +315,7 @@ class BrowserPage(QWebPage): win_id = mainwindow.create_window(True) tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id) - tabbed_browser.openurl(url, False) + tabbed_browser.tabopen(url, False) return False else: return True From 8eb04893575c7dead3f377520fc7432911a05cfd Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 6 Oct 2014 19:39:48 +0200 Subject: [PATCH 39/47] Add rapid window hinting. --- qutebrowser/browser/hints.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 54a4d0387..7e9522dcd 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -38,8 +38,9 @@ ElemTuple = collections.namedtuple('ElemTuple', ['elem', 'label']) Target = usertypes.enum('Target', ['normal', 'tab', 'tab_bg', 'window', 'yank', - 'yank_primary', 'fill', 'rapid', 'download', - 'userscript', 'spawn']) + 'yank_primary', 'fill', 'rapid', + 'rapid_win', 'download', 'userscript', + 'spawn']) @pyqtSlot(usertypes.KeyMode) @@ -132,6 +133,7 @@ class HintManager(QObject): 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...", @@ -305,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) @@ -561,6 +565,8 @@ class HintManager(QObject): - `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. @@ -664,6 +670,7 @@ class HintManager(QObject): 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, } @@ -688,7 +695,7 @@ class HintManager(QObject): url_handlers[self._context.target](url) else: raise ValueError("No suitable handler found!") - if self._context.target != Target.rapid: + if self._context.target not in (Target.rapid, Target.rapid_win): modeman.maybe_leave(self._win_id, usertypes.KeyMode.hint, 'followed') From d2abd06513e40b687d5bb05025e2119201fe5924 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 6 Oct 2014 19:40:28 +0200 Subject: [PATCH 40/47] Add missing keybindings for window actions. --- qutebrowser/config/configdata.py | 9 +++++++++ 1 file changed, 9 insertions(+) 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']), From 796dce86ae9f61a5a998eca14cf0e07f5bbcd2ea Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 6 Oct 2014 19:47:35 +0200 Subject: [PATCH 41/47] Remove go_back() and go_forward() from WebView. If we use these in commands.py, we spawn a new window before checking if we can go back/forward - but we want to check that before opening a new window. --- qutebrowser/browser/commands.py | 10 ++++++++-- qutebrowser/widgets/webview.py | 32 ++++++++++---------------------- 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 91d4cc7a3..2e29c9b31 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -328,15 +328,21 @@ class CommandDispatcher: def _back_forward(self, tab, bg, window, count, forward): """Helper function for :back/:forward.""" + 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', scope='window') def back(self, tab=False, bg=False, window=False, count=1): diff --git a/qutebrowser/widgets/webview.py b/qutebrowser/widgets/webview.py index d8665a0ea..7a5d4f3c1 100644 --- a/qutebrowser/widgets/webview.py +++ b/qutebrowser/widgets/webview.py @@ -154,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(self._win_id, 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(self._win_id, 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. @@ -312,20 +314,6 @@ class WebView(QWebView): level = self._zoom.getitem(offset) self.zoom_perc(level, fuzzyval=False) - 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.""" From 9281fa399212f2964568718c1e76557871fd4d08 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 6 Oct 2014 19:53:50 +0200 Subject: [PATCH 42/47] Fix lint/bugs --- qutebrowser/browser/commands.py | 8 ++++---- qutebrowser/commands/command.py | 2 +- qutebrowser/keyinput/modeparsers.py | 2 +- qutebrowser/utils/objreg.py | 3 +-- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 2e29c9b31..9b8c9f7d5 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -452,14 +452,14 @@ class CommandDispatcher: hintmanager = objreg.get('hintmanager', scope='tab') if where == 'prev': hintmanager.follow_prevnext(frame, url, prev=True, tab=tab, - background=background, window=window) + background=bg, window=window) elif where == 'next': hintmanager.follow_prevnext(frame, url, prev=False, tab=tab, - background=background, window=window) + background=bg, window=window) elif where == 'up': - self._navigate_up(url, tab, background, window) + self._navigate_up(url, tab, bg, window) elif where in ('decrement', 'increment'): - self._navigate_incdec(url, where, tab, background, window) + self._navigate_incdec(url, where, tab, bg, window) else: raise ValueError("Got called with invalid value {} for " "`where'.".format(where)) diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 8b91cfcfe..17dc2f8f2 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -366,7 +366,7 @@ class Command: value = self._type_conv[param.name](value) return name, value - def _get_call_args(self, win_id): + def _get_call_args(self, win_id): # noqa """Get arguments for a function call. Args: diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 592948b7e..29505f04d 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -168,7 +168,7 @@ class HintKeyParser(keyparser.CommandKeyParser): raise TypeError("Type {} is no Type member!".format(keytype)) if keytype == self.Type.chain: hintmanager = objreg.get('hintmanager', scope='tab', - window=self._win_id, tab='current') + window=self._win_id, tab='current') hintmanager.fire(cmdstr) else: # execute as command diff --git a/qutebrowser/utils/objreg.py b/qutebrowser/utils/objreg.py index 2cb3f0cb9..d6dca188f 100644 --- a/qutebrowser/utils/objreg.py +++ b/qutebrowser/utils/objreg.py @@ -151,7 +151,7 @@ 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': + if tab is not None and scope != 'tab': raise TypeError("tab is set with scope {}".format(scope)) if scope == 'global': return global_registry @@ -229,4 +229,3 @@ def dump_objects(): for line in data: lines.append(" {}".format(line)) return lines - From ecd31a6450c2eb1e6c683746879c88f541a58893 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 6 Oct 2014 20:40:00 +0200 Subject: [PATCH 43/47] Add tab/window support to :help --- qutebrowser/browser/commands.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 9b8c9f7d5..24d223987 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -822,10 +822,13 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', name='help', completion=[usertypes.Completion.helptopic], scope='window') - def show_help(self, topic=None): + 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. @@ -855,7 +858,8 @@ 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], From bb530ed9e1ceee3e86c93df329eca59751ad7243 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 6 Oct 2014 20:46:01 +0200 Subject: [PATCH 44/47] Regenerate command docs. --- doc/help/commands.asciidoc | 39 +++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) 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 From fd9a3fc5e727ce26938dc192f86212020032653c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 6 Oct 2014 21:24:07 +0200 Subject: [PATCH 45/47] Clean up mainwindow import mess. --- qutebrowser/app.py | 4 ++-- qutebrowser/browser/commands.py | 6 +++--- qutebrowser/browser/hints.py | 6 +++--- qutebrowser/browser/webpage.py | 6 +++--- qutebrowser/widgets/mainwindow.py | 32 +++++++++++++++---------------- 5 files changed, 27 insertions(+), 27 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 6ae3a630c..c77589498 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -153,7 +153,7 @@ class Application(QApplication): download_manager = downloads.DownloadManager(self) objreg.register('download-manager', download_manager) log.init.debug("Initializing main window...") - mainwindow.create_window(False if self._args.nowindow else True) + 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) @@ -227,7 +227,7 @@ class Application(QApplication): commandrunner.run_safely_init(cmd.lstrip(':')) elif not cmd: log.init.debug("Empty argument") - win_id = mainwindow.create_window(True) + win_id = mainwindow.MainWindow.spawn() else: tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 24d223987..fdec99eac 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -70,9 +70,9 @@ class CommandDispatcher: window: If True, open a new window. """ if window: - # We have to import this here to avoid a circular import. - from qutebrowser.widgets import mainwindow - win_id = mainwindow.create_window(True) + 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) diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 7e9522dcd..0b44a808c 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -527,9 +527,9 @@ class HintManager(QObject): "prev" if prev else "forward")) qtutils.ensure_valid(url) if window: - # We have to import this here to avoid a circular import. - from qutebrowser.widgets import mainwindow - win_id = mainwindow.create_window(True) + 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) diff --git a/qutebrowser/browser/webpage.py b/qutebrowser/browser/webpage.py index fa38307ea..ede0339b4 100644 --- a/qutebrowser/browser/webpage.py +++ b/qutebrowser/browser/webpage.py @@ -310,9 +310,9 @@ class BrowserPage(QWebPage): tabbed_browser.tabopen(url, True) return False elif open_target == usertypes.ClickTarget.window: - # We have to import this here to avoid a circular import. - from qutebrowser.widgets import mainwindow - win_id = mainwindow.create_window(True) + 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) diff --git a/qutebrowser/widgets/mainwindow.py b/qutebrowser/widgets/mainwindow.py index e4d6a348a..dabdb5a65 100644 --- a/qutebrowser/widgets/mainwindow.py +++ b/qutebrowser/widgets/mainwindow.py @@ -39,22 +39,6 @@ from qutebrowser.browser import hints win_id_gen = itertools.count(0) -def create_window(show): - """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 - - class MainWindow(QWidget): """The main window of qutebrowser. @@ -145,6 +129,22 @@ class MainWindow(QWidget): 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') From 4d35782cc28532bd9a9aa0cb0ae63dd2bf424a71 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 6 Oct 2014 21:28:33 +0200 Subject: [PATCH 46/47] Update HACKING. Closes #131. --- doc/HACKING.asciidoc | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/doc/HACKING.asciidoc b/doc/HACKING.asciidoc index cd822913c..2c540eb5c 100644 --- a/doc/HACKING.asciidoc +++ b/doc/HACKING.asciidoc @@ -292,15 +292,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. +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. From de37ed64ca09230adbe5609c80bd164010f21bb2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 6 Oct 2014 21:30:24 +0200 Subject: [PATCH 47/47] Remove FIXME --- qutebrowser/browser/downloads.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index 80de0375f..563e12c74 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -402,7 +402,6 @@ class DownloadManager(QObject): self.downloads.append(download) self.download_added.emit() - # FIXME display this in right window q = usertypes.Question(self) q.text = "Save file to:" q.mode = usertypes.PromptMode.text