settings.py 14.2 KB
Newer Older
1
# -*- coding: utf-8 -*-
2
# Pitivi video editor
3 4 5 6 7 8 9 10 11 12 13 14 15 16
# Copyright (c) 2005, Edward Hervey <bilboed@bilboed.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program; if not, write to the
Hicham HAOUARI's avatar
Hicham HAOUARI committed
17 18
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301, USA.
19
import configparser
20
import os
21

22
from gi.repository import Gdk
23
from gi.repository import GLib
24
from gi.repository import GObject
25

26
from pitivi.utils.loggable import Loggable
27 28
from pitivi.utils.misc import unicode_error_dialog

29

30 31 32 33 34 35
def get_bool_env(var):
    value = os.getenv(var)
    if not value:
        return False
    value = value.lower()
    if value == 'False':
Edward Hervey's avatar
Edward Hervey committed
36
        return False
37
    if value == '0':
Edward Hervey's avatar
Edward Hervey committed
38
        return False
39 40 41
    else:
        return bool(value)

42

43
def get_env_by_type(type_, var):
44 45 46 47 48
    """Gets an environment variable.

    Args:
        type_ (type): The type of the variable.
        var (str): The name of the environment variable.
49

50 51
    Returns:
        The contents of the environment variable, or None if it doesn't exist.
52
    """
53 54
    if var is None:
        return None
Brandon Lewis's avatar
Brandon Lewis committed
55
    if type_ == bool:
56
        return get_bool_env(var)
57 58 59 60
    value = os.getenv(var)
    if value:
        return type_(os.getenv(var))
    return None
61

62

63 64
def get_dir(path, autocreate=True):
    if autocreate and not os.path.exists(path):
65
        os.makedirs(path)
66 67
    return path

68

69
def xdg_config_home(autocreate=True):
70
    """Gets the directory for storing the user's Pitivi configuration."""
71 72 73
    default = os.path.join(GLib.get_user_config_dir(), "pitivi")
    path = os.getenv("PITIVI_USER_CONFIG_DIR", default)
    return get_dir(path, autocreate)
74

75

76
def xdg_data_home(autocreate=True):
77
    """Gets the directory for storing the user's data: presets, plugins, etc."""
78 79 80
    default = os.path.join(GLib.get_user_data_dir(), "pitivi")
    path = os.getenv("PITIVI_USER_DATA_DIR", default)
    return get_dir(path, autocreate)
81

82

83
def xdg_cache_home(autocreate=True):
84
    """Gets the Pitivi cache directory."""
85 86 87
    default = os.path.join(GLib.get_user_cache_dir(), "pitivi")
    path = os.getenv("PITIVI_USER_CACHE_DIR", default)
    return get_dir(path, autocreate)
88

89

90 91
class ConfigError(Exception):
    pass
Edward Hervey's avatar
Edward Hervey committed
92

93

94
class Notification(object):
95
    """A descriptor which emits a signal when set."""
96 97 98

    def __init__(self, attrname):
        self.attrname = "_" + attrname
99 100 101 102 103
        self.signame = self.signalName(attrname)

    @staticmethod
    def signalName(attrname):
        return attrname + "Changed"
104 105 106 107 108 109 110 111

    def __get__(self, instance, unused):
        return getattr(instance, self.attrname)

    def __set__(self, instance, value):
        setattr(instance, self.attrname, value)
        instance.emit(self.signame)

112

113
class GlobalSettings(GObject.Object, Loggable):
114
    """Pitivi app settings.
115

116
    Loads settings from different sources, currently:
117 118
    - the local configuration file,
    - environment variables.
Brandon Lewis's avatar
Brandon Lewis committed
119 120 121

    Modules declare which settings they wish to access by calling the
    addConfigOption() class method during initialization.
122

123 124 125
    Attributes:
        options (dict): The available settings.
        environment (set): The controlled environment variables.
Edward Hervey's avatar
Edward Hervey committed
126 127
    """

128 129
    options = {}
    environment = set()
130
    defaults = {}
131

132
    def __init__(self):
133
        GObject.Object.__init__(self)
134
        Loggable.__init__(self)
135 136

        self.conf_file_path = os.path.join(xdg_config_home(), "pitivi.conf")
137
        self._config = configparser.ConfigParser()
Edward Hervey's avatar
Edward Hervey committed
138 139 140
        self._readSettingsFromConfigurationFile()
        self._readSettingsFromEnvironmentVariables()

141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
    def reload_attribute_from_file(self, section, attrname):
        """Reads and sets an attribute from the configuration file.

        Pitivi's default behavior is to set attributes from the configuration
        file when starting and to save those attributes back to the file when
        exiting the application. You can use this method when you need to
        read an attribute during runtime (in the middle of the process).
        """
        if section in self.options:
            if attrname in self.options[section]:
                type_, key, _ = self.options[section][attrname]
                try:
                    value = self._read_value(section, key, type_)
                except configparser.NoSectionError:
                    return
                setattr(self, attrname, value)

158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
    def _read_value(self, section, key, type_):
        if type_ == int:
            try:
                value = self._config.getint(section, key)
            except ValueError:
                # In previous configurations we incorrectly stored
                # ints using float values.
                value = int(self._config.getfloat(section, key))
        elif type_ == float:
            value = self._config.getfloat(section, key)
        elif type_ == bool:
            value = self._config.getboolean(section, key)
        elif type_ == list:
            tmp_value = self._config.get(section, key)
            value = [token.strip() for token in tmp_value.split("\n") if token]
173 174
        elif type_ == Gdk.RGBA:
            value = self.get_rgba(section, key)
175 176 177 178 179 180 181 182
        else:
            value = self._config.get(section, key)
        return value

    def _write_value(self, section, key, value):
        if type(value) == list:
            value = "\n" + "\n".join(value)
            self._config.set(section, key, value)
183 184
        elif type(value) == Gdk.RGBA:
            self.set_rgba(section, key, value)
185 186 187
        else:
            self._config.set(section, key, str(value))

Edward Hervey's avatar
Edward Hervey committed
188
    def _readSettingsFromConfigurationFile(self):
189
        """Reads the settings from the user configuration file."""
190
        try:
191
            self._config.read(self.conf_file_path)
192
        except UnicodeDecodeError as e:
193
            self.error("Failed to read %s: %s", self.conf_file_path, e)
194 195
            unicode_error_dialog()
            return
196
        except configparser.ParsingError as e:
197
            self.error("Failed to parse %s: %s", self.conf_file_path, e)
198 199
            return

200
        for (section, attrname, typ, key, env, value) in self.iterAllOptions():
201 202 203
            if not self._config.has_section(section):
                continue
            if key and self._config.has_option(section, key):
204
                value = self._read_value(section, key, typ)
Brandon Lewis's avatar
Brandon Lewis committed
205
                setattr(self, attrname, value)
Edward Hervey's avatar
Edward Hervey committed
206

207
    @classmethod
208
    def readSettingSectionFromFile(self, cls, section):
209
        """Reads a particular section of the settings file.
210

211 212 213 214 215
        Use this if you dynamically determine settings sections/keys at runtime.
        Otherwise, the settings file would be read only once, at the
        initialization phase of your module, and your config sections would
        never be read, thus values would be reset to defaults on every startup
        because GlobalSettings would think they don't exist.
216
        """
217
        if cls._config.has_section(section):
218
            for option in cls._config.options(section):
219 220 221 222 223 224 225 226 227 228 229 230
                # We don't know the value type in advance, just try them all.
                try:
                    value = cls._config.getfloat(section, option)
                except:
                    try:
                        value = cls._config.getint(section, option)
                    except:
                        try:
                            value = cls._config.getboolean(section, option)
                        except:
                            value = cls._config.get(section, option)

231
                setattr(cls, section + option, value)
232

Edward Hervey's avatar
Edward Hervey committed
233
    def _readSettingsFromEnvironmentVariables(self):
234
        """Reads settings from their registered environment variables."""
235 236 237 238
        for section, attrname, typ, key, env, value in self.iterAllOptions():
            if not env:
                # This option does not have an environment variable name.
                continue
Edward Hervey's avatar
Edward Hervey committed
239
            var = get_env_by_type(typ, env)
240
            if var is not None:
241
                setattr(self, attrname, var)
242 243

    def _writeSettingsToConfigurationFile(self):
244
        for (section, attrname, typ, key, env_var, value) in self.iterAllOptions():
245 246 247
            if not self._config.has_section(section):
                self._config.add_section(section)
            if key:
248
                if value is not None:
249
                    self._write_value(section, key, value)
250 251
                else:
                    self._config.remove_option(section, key)
252
        try:
253
            with open(self.conf_file_path, 'w') as file:
254
                self._config.write(file)
255
        except (IOError, OSError) as e:
256
            self.error("Failed to write to %s: %s", self.conf_file_path, e)
257 258

    def storeSettings(self):
259 260 261
        """Writes settings to the user's local configuration file.

        Only those settings which were added with a section and a key value are
Brandon Lewis's avatar
Brandon Lewis committed
262 263
        stored.
        """
264 265 266
        self._writeSettingsToConfigurationFile()

    def iterAllOptions(self):
267
        """Iterates over all registered options."""
268 269
        for section, options in list(self.options.items()):
            for attrname, (typ, key, environment) in list(options.items()):
Edward Hervey's avatar
Edward Hervey committed
270
                yield section, attrname, typ, key, environment, getattr(self, attrname)
271

272 273 274 275
    def isDefault(self, attrname):
        return getattr(self, attrname) == self.defaults[attrname]

    def setDefault(self, attrname):
276
        """Resets the specified setting to its default value."""
277 278
        setattr(self, attrname, self.defaults[attrname])

279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305
    def get_rgba(self, section, option):
        """Gets the option value from the configuration file parsed as a RGBA.

        Args:
            section (str): The section.
            option (str): The option that belongs to the `section`.

        Returns:
            Gdk.RGBA: The value for the `option` at the given `section`.
        """
        value = self._config.get(section, option)
        color = Gdk.RGBA()
        if not color.parse(value):
            raise Exception("Value cannot be parsed as Gdk.RGBA: %s" % value)
        return color

    def set_rgba(self, section, option, value):
        """Sets the option value to the configuration file as a RGBA.

        Args:
            section (str): The section.
            option (str): The option that belongs to the `section`.
            value (Gdk.RGBA): The color.
        """
        value = value.to_string()
        self._config.set(section, option, value)

306
    @classmethod
307
    def addConfigOption(cls, attrname, type_=None, section=None, key=None,
308
                        environment=None, default=None, notify=False,):
309
        """Adds a configuration option.
310

Brandon Lewis's avatar
Brandon Lewis committed
311
        This function should be called during module initialization, before
312 313
        the config file is actually read. By default, only options registered
        beforehand will be loaded.
Brandon Lewis's avatar
Brandon Lewis committed
314

315
        If you want to add configuration options after initialization,
316 317 318 319 320 321 322 323 324 325 326 327 328
        use the `readSettingSectionFromFile` method to force reading later on.

        Args:
            attrname (str): The attribute of this class for accessing the option.
            type_ (Optional[type]): The type of the attribute. Unnecessary if a
                `default` value is specified.
            section (Optional[str]): The section of the config file under which
                this option is saved. This section must have been added with
                addConfigSection(). Not necessary if `key` is not given.
            key (Optional[str]): The key under which this option is to be saved.
                By default the option will not be saved.
            notify (Optional[bool]): Whether this attribute should emit
                signals when modified. By default signals are not emitted.
Brandon Lewis's avatar
Brandon Lewis committed
329
        """
330
        if section and section not in cls.options:
331
            raise ConfigError("You must add the section `%s` first" % section)
Brandon Lewis's avatar
Brandon Lewis committed
332
        if key and not section:
333
            raise ConfigError("You must specify a section for key `%s`" % key)
Brandon Lewis's avatar
Brandon Lewis committed
334
        if section and key in cls.options[section]:
335
            raise ConfigError("Key `%s` is already in use" % key)
336
        if hasattr(cls, attrname):
337
            raise ConfigError("Attribute `%s` is already in use" % attrname)
338
        if environment and environment in cls.environment:
339
            raise ConfigError("Env var `%s` is already in use" % environment)
340
        if not type_ and default is None:
341 342
            raise ConfigError("Attribute `%s` must have a type or a default" %
                              attrname)
Brandon Lewis's avatar
Brandon Lewis committed
343 344
        if not type_:
            type_ = type(default)
345
        if notify:
346 347
            notification = Notification(attrname)
            setattr(cls, attrname, notification)
348
            setattr(cls, "_" + attrname, default)
349 350 351 352 353
            GObject.signal_new(notification.signame,
                               cls,
                               GObject.SIGNAL_RUN_LAST,
                               None,
                               ())
354 355
        else:
            setattr(cls, attrname, default)
Brandon Lewis's avatar
Brandon Lewis committed
356
        if section and key:
357
            cls.options[section][attrname] = type_, key, environment
358
        cls.environment.add(environment)
359
        cls.defaults[attrname] = default
360 361 362

    @classmethod
    def addConfigSection(cls, section):
363 364 365 366
        """Adds a section to the local config file.

        Args:
            section (str): The section name.
Brandon Lewis's avatar
Brandon Lewis committed
367

368 369
        Raises:
            ConfigError: If the section already exists.
Brandon Lewis's avatar
Brandon Lewis committed
370
        """
371 372 373
        if section in cls.options:
            raise ConfigError("Duplicate Section \"%s\"." % section)
        cls.options[section] = {}
374 375 376

    @classmethod
    def notifiesConfigOption(cls, attrname):
377 378 379 380 381 382 383 384
        """Checks whether a signal is emitted when the setting is changed.

        Args:
            attrname (str): The attribute name used to access the setting.

        Returns:
            bool: True when the setting emits a signal when changed,
                False otherwise.
385
        """
386
        signal_name = Notification.signalName(attrname)
387
        return bool(GObject.signal_lookup(signal_name, cls))