From 76de3d0c51ae1b99eed2b023526b9e60c978c49a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 18 Oct 2014 19:50:10 +0200 Subject: [PATCH] Use signals and decorators for configs again. This basically rolls back 64a119afb26eaf37f95d3748af75ff35678a54e7 and b7f2a6d1435810434282ce8a6887fc744e08dbfe. Fixes #188. Breaks #156 again but the next commit will fix this. --- qutebrowser/browser/cookies.py | 6 +- qutebrowser/config/config.py | 107 +++++++++++++++----------- qutebrowser/config/lineparser.py | 11 ++- qutebrowser/config/style.py | 3 +- qutebrowser/config/websettings.py | 4 +- qutebrowser/keyinput/modeman.py | 4 +- qutebrowser/models/completion.py | 25 ++++-- qutebrowser/widgets/completion.py | 7 +- qutebrowser/widgets/console.py | 6 +- qutebrowser/widgets/mainwindow.py | 10 ++- qutebrowser/widgets/statusbar/bar.py | 3 +- qutebrowser/widgets/statusbar/text.py | 5 +- qutebrowser/widgets/tabbedbrowser.py | 3 +- qutebrowser/widgets/tabwidget.py | 13 +++- qutebrowser/widgets/webview.py | 9 ++- 15 files changed, 137 insertions(+), 79 deletions(-) diff --git a/qutebrowser/browser/cookies.py b/qutebrowser/browser/cookies.py index bb759580f..a6e929ac4 100644 --- a/qutebrowser/browser/cookies.py +++ b/qutebrowser/browser/cookies.py @@ -23,7 +23,7 @@ from PyQt5.QtNetwork import QNetworkCookie, QNetworkCookieJar from PyQt5.QtCore import QStandardPaths, QDateTime from qutebrowser.config import config, lineparser -from qutebrowser.utils import utils, standarddir +from qutebrowser.utils import utils, standarddir, objreg class CookieJar(QNetworkCookieJar): @@ -39,8 +39,7 @@ class CookieJar(QNetworkCookieJar): for line in self._linecp: cookies += QNetworkCookie.parseCookies(line) self.setAllCookies(cookies) - config.on_change(self.cookies_store_changed, - 'permissions', 'cookies-store') + objreg.get('config').changed.connect(self.cookies_store_changed) def __repr__(self): return utils.get_repr(self, count=len(self.allCookies())) @@ -81,6 +80,7 @@ class CookieJar(QNetworkCookieJar): self._linecp.data = lines self._linecp.save() + @config.change_filter('permissions', 'cookies-store') def cookies_store_changed(self): """Delete stored cookies if cookies-store changed.""" if not config.get('permissions', 'cookies-store'): diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index ac921350a..4a27a8073 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -27,14 +27,12 @@ we borrow some methods and classes from there where it makes sense. import os import sys import os.path -import inspect import functools -import weakref import configparser import collections import collections.abc -from PyQt5.QtCore import pyqtSignal, QObject, QStandardPaths +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QStandardPaths from PyQt5.QtWidgets import QMessageBox from qutebrowser.config import (configdata, iniparsers, configtypes, @@ -44,31 +42,67 @@ from qutebrowser.utils import message, objreg, utils, standarddir, log from qutebrowser.utils.usertypes import Completion -ChangeHandler = collections.namedtuple( - 'ChangeHandler', ['func_ref', 'section', 'option']) +class change_filter: # pylint: disable=invalid-name + """Decorator to register a new command handler. -change_handlers = [] + This could also be a function, but as a class (with a "wrong" name) it's + much cleaner to implement. - -def on_change(func, sectname=None, optname=None): - """Register a new change handler. - - Args: - func: The function to be called on change. - sectname: Filter for the config section. - If None, the handler gets called for all sections. - optname: Filter for the config option. - If None, the handler gets called for all options. + Attributes: + _sectname: The section to be filtered. + _optname: The option to be filtered. """ - if optname is not None and sectname is None: - raise TypeError("option is {} but section is None!".format(optname)) - if sectname is not None and sectname not in configdata.DATA: - raise NoSectionError("Section '{}' does not exist!".format(sectname)) - if optname is not None and optname not in configdata.DATA[sectname]: - raise NoOptionError("Option '{}' does not exist in section " - "'{}'!".format(optname, sectname)) - change_handlers.append(ChangeHandler(weakref.ref(func), sectname, optname)) + + def __init__(self, sectname, optname=None): + """Save decorator arguments. + + Gets called on parse-time with the decorator arguments. + + Args: + See class attributes. + """ + if sectname not in configdata.DATA: + raise NoSectionError("Section '{}' does not exist!".format( + sectname)) + if optname is not None and optname not in configdata.DATA[sectname]: + raise NoOptionError("Option '{}' does not exist in section " + "'{}'!".format(optname, sectname)) + self._sectname = sectname + self._optname = optname + + def __call__(self, func): + """Register the command before running the function. + + Gets called when a function should be decorated. + + Adds a filter which returns if we're not interested in the change-event + and calls the wrapped function if we are. + + We assume the function passed doesn't take any parameters. + + Args: + func: The function to be decorated. + + Return: + The decorated function. + """ + + @pyqtSlot(str, str) + @functools.wraps(func) + def wrapper(wrapper_self, sectname=None, optname=None): + # pylint: disable=missing-docstring + if sectname is None and optname is None: + # Called directly, not from a config change event. + return func(wrapper_self) + elif sectname != self._sectname: + return + elif self._optname is not None and optname != self._optname: + return + else: + return func(wrapper_self) + + return wrapper def get(*args, **kwargs): @@ -181,6 +215,7 @@ class ConfigManager(QObject): _initialized: Whether the ConfigManager is fully initialized yet. Signals: + changed: Emitted when a config option changed. style_changed: When style caches need to be invalidated. Args: the changed section and option. """ @@ -188,6 +223,7 @@ class ConfigManager(QObject): KEY_ESCAPE = r'\#[' ESCAPE_CHAR = '\\' + changed = pyqtSignal(str, str) style_changed = pyqtSignal(str, str) def __init__(self, configdir, fname, parent=None): @@ -316,28 +352,7 @@ class ConfigManager(QObject): sectname, optname)) if sectname in ('colors', 'fonts'): self.style_changed.emit(sectname, optname) - to_delete = [] - for handler in change_handlers: - func = handler.func_ref() - if func is None: - to_delete.append(handler) - continue - elif handler.section is not None and handler.section != sectname: - continue - elif handler.option is not None and handler.option != optname: - continue - param_count = len(inspect.signature(func).parameters) - if param_count == 2: - func(sectname, optname) - elif param_count == 1: - func(sectname) - elif param_count == 0: - func() - else: - raise TypeError("Handler {} has invalid signature.".format( - utils.qualname(func))) - for handler in to_delete: - change_handlers.remove(handler) + self.changed.emit(sectname, optname) def _after_set(self, changed_sect, changed_opt): """Clean up caches and emit signals after an option has been set.""" diff --git a/qutebrowser/config/lineparser.py b/qutebrowser/config/lineparser.py index 9496c1681..9d6ac538f 100644 --- a/qutebrowser/config/lineparser.py +++ b/qutebrowser/config/lineparser.py @@ -23,7 +23,9 @@ import os import os.path import collections -from qutebrowser.utils import log, utils +from PyQt5.QtCore import pyqtSlot + +from qutebrowser.utils import log, utils, objreg from qutebrowser.config import config @@ -37,6 +39,8 @@ class LineConfigParser(collections.UserList): _configfile: The config file path. _fname: Filename of the config. _binary: Whether to open the file in binary mode. + _limit: The config section/option used to limit the maximum number of + lines. """ def __init__(self, configdir, fname, limit=None, binary=False): @@ -60,7 +64,7 @@ class LineConfigParser(collections.UserList): log.init.debug("Reading config from {}".format(self._configfile)) self.read(self._configfile) if limit is not None: - config.on_change(self.cleanup_file, *limit) + objreg.get('config').changed.connect(self.cleanup_file) def __repr__(self): return utils.get_repr(self, constructor=True, @@ -107,8 +111,11 @@ class LineConfigParser(collections.UserList): with open(self._configfile, 'w', encoding='utf-8') as f: self.write(f, limit) + @pyqtSlot(str, str) def cleanup_file(self, section, option): """Delete the file if the limit was changed to 0.""" + if (section, option) != self._limit: + return value = config.get(section, option) if value == 0: if os.path.exists(self._configfile): diff --git a/qutebrowser/config/style.py b/qutebrowser/config/style.py index 6484cb3c9..e7b1a1465 100644 --- a/qutebrowser/config/style.py +++ b/qutebrowser/config/style.py @@ -58,7 +58,8 @@ def set_register_stylesheet(obj): log.style.vdebug("stylesheet for {}: {}".format( obj.__class__.__name__, qss)) obj.setStyleSheet(qss) - config.on_change(functools.partial(update_stylesheet, obj)) + objreg.get('config').changed.connect( + functools.partial(update_stylesheet, obj)) def update_stylesheet(obj): diff --git a/qutebrowser/config/websettings.py b/qutebrowser/config/websettings.py index f21c37509..804060205 100644 --- a/qutebrowser/config/websettings.py +++ b/qutebrowser/config/websettings.py @@ -32,7 +32,7 @@ from PyQt5.QtWebKit import QWebSettings from PyQt5.QtCore import QStandardPaths from qutebrowser.config import config -from qutebrowser.utils import usertypes, standarddir +from qutebrowser.utils import usertypes, standarddir, objreg MapType = usertypes.enum('MapType', ['attribute', 'setter', 'static_setter']) @@ -192,7 +192,7 @@ def init(): for optname, (typ, arg) in section.items(): value = config.get(sectname, optname) _set_setting(typ, arg, value) - config.on_change(update_settings) + objreg.get('config').changed.connect(update_settings) def update_settings(section, option): diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index d9624bcfe..075e6d51e 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -175,8 +175,7 @@ class ModeManager(QObject): self._releaseevents_to_pass = [] self._forward_unbound_keys = config.get( 'input', 'forward-unbound-keys') - config.on_change(self.set_forward_unbound_keys, 'input', - 'forward-unbound-keys') + objreg.get('config').changed.connect(self.set_forward_unbound_keys) def __repr__(self): return utils.get_repr(self, mode=self.mode(), locked=self.locked, @@ -331,6 +330,7 @@ class ModeManager(QObject): raise ValueError("Can't leave normal mode!") self.leave(self.mode(), 'leave current') + @config.change_filter('input', 'forward-unbound-keys') def set_forward_unbound_keys(self): """Update local setting when config changed.""" self._forward_unbound_keys = config.get( diff --git a/qutebrowser/models/completion.py b/qutebrowser/models/completion.py index 98bf46635..b463b2fe7 100644 --- a/qutebrowser/models/completion.py +++ b/qutebrowser/models/completion.py @@ -19,7 +19,7 @@ """CompletionModels for different usages.""" -from PyQt5.QtCore import Qt +from PyQt5.QtCore import pyqtSlot, Qt from qutebrowser.config import config, configdata from qutebrowser.models import basecompletion @@ -47,6 +47,7 @@ class SettingOptionCompletionModel(basecompletion.BaseCompletionModel): Attributes: _misc_items: A dict of the misc. column items which will be set later. + _section: The config section this model shows. """ # pylint: disable=abstract-method @@ -56,7 +57,8 @@ class SettingOptionCompletionModel(basecompletion.BaseCompletionModel): cat = self.new_category(section) sectdata = configdata.DATA[section] self._misc_items = {} - config.on_change(self.update_misc_column, section) + self._section = section + objreg.get('config').changed.connect(self.update_misc_column) for name in sectdata.keys(): try: desc = sectdata.descriptions[name] @@ -71,8 +73,11 @@ class SettingOptionCompletionModel(basecompletion.BaseCompletionModel): value) self._misc_items[name] = miscitem + @pyqtSlot(str, str) def update_misc_column(self, section, option): """Update misc column when config changed.""" + if section != self._section: + return try: item = self._misc_items[option] except KeyError: @@ -91,13 +96,20 @@ class SettingOptionCompletionModel(basecompletion.BaseCompletionModel): class SettingValueCompletionModel(basecompletion.BaseCompletionModel): - """A CompletionModel filled with setting values.""" + """A CompletionModel filled with setting values. + + Attributes: + _section: The config section this model shows. + _option: The config option this model shows. + """ # pylint: disable=abstract-method - def __init__(self, section, option=None, parent=None): + def __init__(self, section, option, parent=None): super().__init__(parent) - config.on_change(self.update_current_value, section, option) + self._section = section + self._option = option + objreg.get('config').changed.connect(self.update_current_value) cur_cat = self.new_category("Current", sort=0) value = config.get(section, option, raw=True) if not value: @@ -118,8 +130,11 @@ class SettingValueCompletionModel(basecompletion.BaseCompletionModel): for (val, desc) in vals: self.new_item(cat, val, desc) + @pyqtSlot(str, str) def update_current_value(self, section, option): """Update current value when config changed.""" + if (section, option) != (self._section, self._option): + return value = config.get(section, option, raw=True) if not value: value = '""' diff --git a/qutebrowser/widgets/completion.py b/qutebrowser/widgets/completion.py index 61576d734..2b03a2ba2 100644 --- a/qutebrowser/widgets/completion.py +++ b/qutebrowser/widgets/completion.py @@ -99,9 +99,9 @@ class CompletionView(QTreeView): 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 - #config.on_change(self.init_command_completion, 'aliases') + objreg.get('config').changed.connect(self.set_enabled) + # FIXME handle new aliases. + #objreg.get('config').changed.connect(self.init_command_completion) self._delegate = completiondelegate.CompletionItemDelegate(self) self.setItemDelegate(self._delegate) @@ -206,6 +206,7 @@ class CompletionView(QTreeView): if config.get('completion', 'shrink'): self.resize_completion.emit() + @config.change_filter('completion', 'show') def set_enabled(self): """Update self.enabled when the config changed.""" self.enabled = config.get('completion', 'show') diff --git a/qutebrowser/widgets/console.py b/qutebrowser/widgets/console.py index f004cd188..9dce75096 100644 --- a/qutebrowser/widgets/console.py +++ b/qutebrowser/widgets/console.py @@ -57,7 +57,7 @@ class ConsoleLineEdit(misc.CommandLineEdit): """ super().__init__(parent) self.update_font() - config.on_change(self.update_font, 'fonts', 'debug-console') + objreg.get('config').changed.connect(self.update_font) self.textChanged.connect(self.on_text_changed) self._rlcompleter = rlcompleter.Completer(namespace) @@ -135,6 +135,7 @@ class ConsoleLineEdit(misc.CommandLineEdit): else: super().keyPressEvent(e) + @config.change_filter('fonts', 'debug-console') def update_font(self): """Set the correct font.""" self.setFont(config.get('fonts', 'debug-console')) @@ -148,13 +149,14 @@ class ConsoleTextEdit(QTextEdit): super().__init__(parent) self.setAcceptRichText(False) self.setReadOnly(True) - config.on_change(self.update_font, 'fonts', 'debug-console') + objreg.get('config').changed.connect(self.update_font) self.update_font() self.setFocusPolicy(Qt.ClickFocus) def __repr__(self): return utils.get_repr(self) + @config.change_filter('fonts', 'debug-console') def update_font(self): """Update font when config changed.""" self.setFont(config.get('fonts', 'debug-console')) diff --git a/qutebrowser/widgets/mainwindow.py b/qutebrowser/widgets/mainwindow.py index c565cbec9..484c92a76 100644 --- a/qutebrowser/widgets/mainwindow.py +++ b/qutebrowser/widgets/mainwindow.py @@ -116,9 +116,7 @@ class MainWindow(QWidget): # 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') - + objreg.get('config').changed.connect(self.on_config_changed) #self.retranslateUi(MainWindow) #self.tabWidget.setCurrentIndex(0) #QtCore.QMetaObject.connectSlotsByName(MainWindow) @@ -126,6 +124,12 @@ class MainWindow(QWidget): def __repr__(self): return utils.get_repr(self) + @pyqtSlot(str, str) + def on_config_changed(self, section, option): + """Resize the completion if related config options changed.""" + if section == 'completion' and option in ('height', 'shrink'): + self.resize_completion() + @classmethod def spawn(cls, show=True): """Create a new main window. diff --git a/qutebrowser/widgets/statusbar/bar.py b/qutebrowser/widgets/statusbar/bar.py index 65d36d0d5..32deedf39 100644 --- a/qutebrowser/widgets/statusbar/bar.py +++ b/qutebrowser/widgets/statusbar/bar.py @@ -145,7 +145,7 @@ class StatusBar(QWidget): self._text_pop_timer = usertypes.Timer(self, 'statusbar_text_pop') self._text_pop_timer.timeout.connect(self._pop_text) self.set_pop_timer_interval() - config.on_change(self.set_pop_timer_interval, 'ui', 'message-timeout') + objreg.get('config').changed.connect(self.set_pop_timer_interval) self.prompt = prompt.Prompt(win_id) self._stack.addWidget(self.prompt) @@ -402,6 +402,7 @@ class StatusBar(QWidget): if mode == usertypes.KeyMode.insert: self._set_insert_active(False) + @config.change_filter('ui', 'message-timeout') def set_pop_timer_interval(self): """Update message timeout when config changed.""" self._text_pop_timer.setInterval(config.get('ui', 'message-timeout')) diff --git a/qutebrowser/widgets/statusbar/text.py b/qutebrowser/widgets/statusbar/text.py index fc26bd9a3..6be442956 100644 --- a/qutebrowser/widgets/statusbar/text.py +++ b/qutebrowser/widgets/statusbar/text.py @@ -23,7 +23,7 @@ from PyQt5.QtCore import pyqtSlot from qutebrowser.config import config from qutebrowser.widgets.statusbar import textbase -from qutebrowser.utils import usertypes, log +from qutebrowser.utils import usertypes, log, objreg class Text(textbase.TextBase): @@ -46,7 +46,7 @@ class Text(textbase.TextBase): self._normaltext = '' self._temptext = '' self._jstext = '' - config.on_change(self.update_text, 'ui', 'display-statusbar-messages') + objreg.get('config').changed.connect(self.update_text) def set_text(self, which, text): """Set a text. @@ -76,6 +76,7 @@ class Text(textbase.TextBase): else: log.misc.debug("Ignoring reset: '{}'".format(text)) + @config.change_filter('ui', 'display-statusbar-messages') def update_text(self): """Update QLabel text when needed.""" if self._temptext: diff --git a/qutebrowser/widgets/tabbedbrowser.py b/qutebrowser/widgets/tabbedbrowser.py index c53090c33..e8320b910 100644 --- a/qutebrowser/widgets/tabbedbrowser.py +++ b/qutebrowser/widgets/tabbedbrowser.py @@ -116,7 +116,7 @@ class TabbedBrowser(tabwidget.TabWidget): # FIXME adjust this to font size # https://github.com/The-Compiler/qutebrowser/issues/119 self.setIconSize(QSize(12, 12)) - config.on_change(self.update_favicons, 'tabs', 'show-favicons') + objreg.get('config').changed.connect(self.update_favicons) def __repr__(self): return utils.get_repr(self, count=self.count()) @@ -386,6 +386,7 @@ class TabbedBrowser(tabwidget.TabWidget): # We first want QWebPage to refresh. QTimer.singleShot(0, check_scroll_pos) + @config.change_filter('tabs', 'show-favicons') def update_favicons(self): """Update favicons when config was changed.""" show = config.get('tabs', 'show-favicons') diff --git a/qutebrowser/widgets/tabwidget.py b/qutebrowser/widgets/tabwidget.py index b71c66f9b..3e9dd72c3 100644 --- a/qutebrowser/widgets/tabwidget.py +++ b/qutebrowser/widgets/tabwidget.py @@ -53,8 +53,9 @@ class TabWidget(QTabWidget): self.setUsesScrollButtons(True) bar.setDrawBase(False) self.init_config() - config.on_change(self.init_config, 'tabs') + objreg.get('config').changed.connect(self.init_config) + @config.change_filter('tabs') def init_config(self): """Initialize attributes based on the config.""" tabbar = self.tabBar() @@ -88,17 +89,19 @@ class TabBar(QTabBar): self._win_id = win_id self.setStyle(TabBarStyle(self.style())) self.set_font() - config.on_change(self.set_font, 'fonts', 'tabbar') + config_obj = objreg.get('config') + config_obj.changed.connect(self.set_font) self.vertical = False self.setAutoFillBackground(True) self.set_colors() - config.on_change(self.set_colors, 'colors', 'tab.bg.bar') + config_obj.changed.connect(self.set_colors) QTimer.singleShot(0, self.autohide) - config.on_change(self.autohide, 'tabs', 'auto-hide') + config_obj.changed.connect(self.autohide) def __repr__(self): return utils.get_repr(self, count=self.count()) + @config.change_filter('tabs', 'auto-hide') def autohide(self): """Auto-hide the tabbar if needed.""" auto_hide = config.get('tabs', 'auto-hide') @@ -123,10 +126,12 @@ class TabBar(QTabBar): self.setTabData(idx, color) self.update(self.tabRect(idx)) + @config.change_filter('fonts', 'tabbar') def set_font(self): """Set the tabbar font.""" self.setFont(config.get('fonts', 'tabbar')) + @config.change_filter('colors', 'tab.bg.bar') def set_colors(self): """Set the tabbar colors.""" p = self.palette() diff --git a/qutebrowser/widgets/webview.py b/qutebrowser/widgets/webview.py index 87b504cc4..4f390e69e 100644 --- a/qutebrowser/widgets/webview.py +++ b/qutebrowser/widgets/webview.py @@ -95,8 +95,7 @@ class WebView(QWebView): self._zoom = None self._has_ssl_errors = False self.init_neighborlist() - config.on_change(self.init_neighborlist, 'ui', 'zoom-levels') - config.on_change(self.init_neighborlist, 'ui', 'default-zoom') + objreg.get('config').changed.connect(self.init_neighborlist) self._cur_url = None self.cur_url = QUrl() self.progress = 0 @@ -142,6 +141,12 @@ class WebView(QWebView): self.load_status = val self.load_status_changed.emit(val.name) + @pyqtSlot(str, str) + def on_config_changed(self, section, option): + """Reinitialize the zoom neighborlist if related config changed.""" + if section == 'ui' and option in ('zoom-levels', 'default-zoom'): + self.init_neighborlist() + def init_neighborlist(self): """Initialize the _zoom neighborlist.""" levels = config.get('ui', 'zoom-levels')