<commit_before>#-------------------------------------------------------------------------------
#
# This file is part of libgimpplugin.
#
# Copyright (C) 2014 khalim19 <<EMAIL>>
# 
# libgimpplugin is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# libgimpplugin 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 General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with libgimpplugin.  If not, see <http://www.gnu.org/licenses/>.
#
#-------------------------------------------------------------------------------

"""
This module:
* defines API for settings
* defines the means to load/save settings:
  * permanently - to a JSON file
    - settings persist even after closing GIMP
  * semi-permanently - to the GIMP shelf
    - settings persist during one GIMP session
"""

#=============================================================================== 

import errno
import abc
from collections import OrderedDict
import json

import gimp
from gimpshelf import shelf
import gimpenums

import container

#===============================================================================

pdb = gimp.pdb

#===============================================================================

class Setting(object):
  
  """
  Attributes:
  
  * name (read-only): Setting name to uniquely identify a setting.
  
  * default_value: Default value of a setting when the setting is instantiated
    or when the reset() method was called.
  
  * value: The setting value. Subclasses of Setting can override the value.setter
    property to e.g. validate input value and raise ValueError if the value assigned is invalid.
  
  * gimp_pdb_type: GIMP Procedural Database (PDB) type, used when registering
    parameters in plug-ins. `_allowed_pdb_types` list, which is class-specific, determines
    whether the PDB type assigned is valid. `_allowed_pdb_types` in this class is None,
    which means that any PDB type can be assigned.
  
  * can_be_registered_to_pdb: Indicates whether a setting can be registered
    as a parameter to a plug-in. Automatically set to True if gimp_pdb_type is
    assigned to a valid value that is not None.
  
  * display_name: Setting name in human-readable format. Useful as GUI labels.
  
  * short_description (read-only): Usually `display_name` plus additional information
    in parentheses. Useful as setting description when registering parameters in plug-ins.
  
  * description: Describes a setting in more detail. Useful for documentation
    purposes as well as GUI tooltips.
  
  * error_messages: A dict of error messages, which can be used e.g. when a value
    assigned to a setting is invalid. You can add your own error messages and
    assign them to one of the "default" error messages (such as 'invalid_value'
    in ImageSetting) depending on the context in which the value assigned is invalid.
  
  * ui_enabled: Indicates whether a setting should be enabled (respond to user input)
    in the GUI. True by default.
  
  * ui_visible: Indicates whether a setting should be visible in the GUI. True by default.
  
  * can_be_reset_by_container: If True, setting is reset to its default value if
    the reset() method from the corresponding SettingContainer is called. False by default.
    
  * changed_attributes (read-only): Contains a set of Setting attribute names that were changed.
    Currently, it can contain the following attribute names: `value`, `ui_enabled` or `ui_visible`.
    Assigning to any of the attributes causes the corresponding attribute name
    to be added to the set. `changed_attributes` is cleared if streamline() is called.
  """
  
  def __init__(self, name, default_value):
    
    self._attrs_that_trigger_change = { 'value', 'ui_enabled', 'ui_visible' }
    self._changed_attributes = set()
    
    self._name = name
    self.default_value = default_value
    
    self._value = self.default_value
    
    self._gimp_pdb_type = None
    self._can_be_registered_to_pdb = False
    self._allowed_pdb_types = None
    
    self._display_name = ""
    self._description = ""
    
    self._error_messages = {}
    
    self.ui_enabled = True
    self.ui_visible = True
    
    self.can_be_reset_by_container = True
    
    self._streamline_func = None
    self._streamline_args = []
    
    # Some attributes may now be in _changed_attributes because of __setattr__,
    # hence it must be cleared.
    self._changed_attributes.clear()
  
  def __setattr__(self, name, value):
    super(Setting, self).__setattr__(name, value)
    if name in self._attrs_that_trigger_change:
      self._changed_attributes.add(name)
  
  @property
  def name(self):
    return self._name
  
  @property
  def value(self):
    return self._value
  @value.setter
  def value(self, value_):
    self._set_value(value_)
  
  def _set_value(self, value_):
    self._value = value_
  
  @property
  def gimp_pdb_type(self):
    return self._gimp_pdb_type
  @gimp_pdb_type.setter
  def gimp_pdb_type(self, value):
    if self._allowed_pdb_types is None or value in self._allowed_pdb_types:
      self._gimp_pdb_type = value
      self.can_be_registered_to_pdb = value is not None
    else:
      raise ValueError("GIMP PDB type " + str(value) + " not allowed")
  
  @property
  def can_be_registered_to_pdb(self):
    return self._can_be_registered_to_pdb
  @can_be_registered_to_pdb.setter
  def can_be_registered_to_pdb(self, value):
    if value and self._gimp_pdb_type is None:
      raise ValueError("setting cannot be registered to PDB because it has no PDB type set (attribute gimp_pdb_type)")
    self._can_be_registered_to_pdb = value
  
  @property
  def display_name(self):
    return self._display_name
  @display_name.setter
  def display_name(self, value):
    self._display_name = value if value is not None else ""
  
  @property
  def description(self):
    return self._description
  @description.setter
  def description(self, value):
    self._description = value if value is not None else ""
  
  @property
  def changed_attributes(self):
    return self._changed_attributes
  
  @property
  def short_description(self):
    return self.display_name
  
  @property
  def error_messages(self):
    return self._error_messages
  
  def reset(self):
    """
    Set the setting value to its default value.
    
    This is different from
      setting.value = setting.default_value
    in that this method does not raise an exception if the default value is invalid.
    """
    self._value = self.default_value
  
  def streamline(self, force=False):
    """
    Adjust attributes of this setting based on the attributes of other settings or arguments.
    Return a list of changed settings. A setting is considered changed if at least one of the
    following attributes were assigned a value:
    * `value`
    * `ui_enabled`
    * `ui_visible`
    
    If force is True, streamline settings even if the values of the other
    settings were not changed. This is useful when initializing GUI elements -
    setting up proper values, enabled/disabled state or visibility.
    """
    
    if self._streamline_func is None:
      raise TypeError("streamline() cannot be called because there is no streamline function set")
    
    changed_settings = OrderedDict()
    
    if self._changed_attributes or force:
      self._streamline_func(self, *self._streamline_args)
      
      # Create copies of the changed attributes since the sets are cleared
      # in the objects afterwards.
      changed_settings[self] = set(self._changed_attributes)
      self._changed_attributes.clear()
      
      for arg in self._streamline_args:
        if isinstance(arg, Setting) and arg.changed_attributes:
          changed_settings[arg] = set(arg.changed_attributes)
          arg.changed_attributes.clear()
    
    return changed_settings
  
  def set_streamline_func(self, streamline_func, *streamline_args):
    """
    Set a function to be called by the streamline() method.
    
    streamline_args are additional arguments to the streamline function.
    
    A streamline function must always contain at least one argument. The first
    argument is the setting from which the streamline function is invoked.
    This argument should not be specified in streamline_args.
    """
    
    if not callable(streamline_func):
      raise TypeError("not a function")
    
    self._streamline_func = streamline_func
    self._streamline_args = streamline_args
  
  def remove_streamline_func(self):
    if self._streamline_func is None:
      raise TypeError("no streamline function was previously set")
    
    self._streamline_func = None
    self._streamline_args = []
  
  def can_streamline(self):
    return self._streamline_func is not None


class NumericSetting(Setting):
  
  """
  Additional attributes:
  
  * min_value: Minimum numeric value. If not None and the value assigned is less
    than min_value, ValueError is raised.
    
  * max_value: Maximum numeric value. If not None and the value assigned is greater
    than max_value, ValueError is raised.
  
  
  Error messages:
  
  * below_min: Used if a value assigned is less than min_value.
  
  * above_max: Used if a value assigned is greater than min_value.
  """
  
  __metaclass__ = abc.ABCMeta
  
  def __init__(self, name, default_value):
    super(NumericSetting, self).__init__(name, default_value)
    
    self.min_value = None
    self.max_value = None
    
    self.error_messages['below_min'] = "value cannot be less than the minimum value " + str(self.min_value)
    self.error_messages['above_max'] = "value cannot be greater than the maximum value " + str(self.max_value)
  
  @property
  def value(self):
    return self._value

  @value.setter
  def value(self, val):
    if self.min_value is not None and val < self.min_value:
      raise ValueError(self.error_messages['below_min'])
    if self.max_value is not None and val > self.max_value:
      raise ValueError(self.error_messages['above_max'])
    
    super(NumericSetting, self)._set_value(val)


class IntSetting(NumericSetting):
  
  def __init__(self, name, default_value):
    super(IntSetting, self).__init__(name, default_value)
    
    self._allowed_pdb_types = [gimpenums.PDB_INT8, gimpenums.PDB_INT16, gimpenums.PDB_INT32]
    self.gimp_pdb_type = gimpenums.PDB_INT32


# Use BoolSetting to indicate that a setting is a boolean.
# No need for a separate subclass, since the functionality is the same as IntSetting.
BoolSetting = IntSetting


class FloatSetting(NumericSetting):
  
  def __init__(self, name, default_value):
    super(FloatSetting, self).__init__(name, default_value)
    
    self._allowed_pdb_types = [gimpenums.PDB_FLOAT]
    self.gimp_pdb_type = gimpenums.PDB_FLOAT


class EnumSetting(Setting):
  
  """
  This class can be used for settings with a limited number of values.
  
  Additional attributes:
  
  * options (read-only): A dict of <option name, option value> pairs. Option name
    uniquely identifies each option. Option value is the corresponding integer value.
  
  * options_display_names (read-only): A dict of <option name, option display name> pairs.
    Option display names can be used e.g. as combo box items in the GUI.
  
  To access an option value:
    setting.options[option name]
  
  To access an option display name:
    setting.options_display_names[option name]
  
  
  Error messages:
  
  * invalid_value: Used if a value assigned is invalid.
  
  * invalid_default_value: Option name is invalid (not found in the `options` parameter).
  
  * wrong_options_len: Wrong number of elements in tuples in the `options` parameter.
  
  * duplicate_option_value: Some option values in the 3-element tuples were specified multiple times.
  """
  
  def __init__(self, name, default_value, options):
    
    """
    Parameters:
    
    * name: Setting name.
    
    * default_value: Option name (identifier). Unlike other Setting classes, where
      the value is specified directly, EnumSetting accepts a valid option
      identifier instead.
    
    * options: A list of either (option name, option display name) tuples
      or (option name, option display name, option value) tuples.
      For 2-element tuples, option values are assigned automatically, starting with 0.
      Use 3-element tuples to assign explicit option values. Values must be unique
      and specified in each tuple.
    """
    
    super(EnumSetting, self).__init__(name, default_value)
    
    self._allowed_pdb_types = [gimpenums.PDB_INT8, gimpenums.PDB_INT16, gimpenums.PDB_INT32]
    self.gimp_pdb_type = gimpenums.PDB_INT32
    
    self._options = OrderedDict()
    self._options_display_names = OrderedDict()
    self._option_values = set()
    
    self.error_messages['wrong_options_len'] = "Wrong number of tuple elements in options - must be 2 or 3"
    self.error_messages['duplicate_option_value'] = ("Cannot set the same value for multiple options "
                                                     "- they must be unique")
    
    if len(options[0]) == 2:
      for i, (option_name, option_display_name) in enumerate(options):
        self._options[option_name] = i
        self._options_display_names[option_name] = option_display_name
        self._option_values.add(i)
    elif len(options[0]) == 3:
      for option_name, option_display_name, option_value in options:
        if option_value in self._option_values:
          raise ValueError(self.error_messages['duplicate_option_value'])
        
        self._options[option_name] = option_value
        self._options_display_names[option_name] = option_display_name
        self._option_values.add(option_value)
    else:
      raise ValueError(self.error_messages['wrong_options_len'])
    
    self.error_messages['invalid_value'] = "invalid option value; valid values: " + str(list(self._option_values))
    self.error_messages['invalid_default_value'] = ("invalid identifier for the default value; "
                                                    "must be one of " + str(self._options.keys()))
    
    if default_value in self._options:
      self.default_value = self._options[default_value]
      self._value = self.default_value
    else:
      raise ValueError(self.error_messages['invalid_default_value'])
    
    self._options_str = self._stringify_options()
  
  @property
  def value(self):
    return self._value
  @value.setter
  def value(self, value_):
    if value_ not in self._option_values:
      raise ValueError(self.error_messages['invalid_value'])
    
    super(EnumSetting, self)._set_value(value_)
  
  @property
  def short_description(self):
    return self.display_name + " " + self._options_str
  
  @property
  def options(self):
    return self._options
  
  @property
  def options_display_names(self):
    return self._options_display_names
  
  def get_option_display_names_and_values(self):
    display_names_and_values = []
    for option_name, option_value in zip(self._options_display_names.values(), self._options.values()):
      display_names_and_values.extend((option_name, option_value))
    return display_names_and_values
  
  def _stringify_options(self):
    options_str = ""
    options_sep = ", "
    
    for value, display_name in zip(self._options.values(), self._options_display_names.values()):
      options_str += '{0} ({1})'.format(display_name, str(value)) + options_sep
    options_str = options_str[:-len(options_sep)]
    
    return "{ " + options_str + " }"


class ImageSetting(Setting):
  
  """
  Error messages:
  
  * invalid_value: Used if a value assigned is invalid.
  """
  
  def __init__(self, name, default_value):
    super(ImageSetting, self).__init__(name, default_value)
    
    self._allowed_pdb_types = [gimpenums.PDB_IMAGE]
    self.gimp_pdb_type = gimpenums.PDB_IMAGE
    
    self.error_messages['invalid_value'] = "invalid image"
  
  @property
  def value(self):
    return self._value
  
  @value.setter
  def value(self, image):
    if not pdb.gimp_image_is_valid(image):
      raise ValueError(self.error_messages['invalid_value'])
    
    super(ImageSetting, self)._set_value(image)


class StringSetting(Setting):
  
  def __init__(self, name, default_value):
    super(StringSetting, self).__init__(name, default_value)
    
    self._allowed_pdb_types = [gimpenums.PDB_STRING]
    self.gimp_pdb_type = gimpenums.PDB_STRING


class NonEmptyStringSetting(StringSetting):
  
  """
  Error messages:
  
  * invalid_value: Used if a value assigned is invalid.<commit_msg>Add DrawableSetting class<commit_after>#-------------------------------------------------------------------------------
#
# This file is part of libgimpplugin.
#
# Copyright (C) 2014 khalim19 <<EMAIL>>
# 
# libgimpplugin is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# libgimpplugin 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 General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with libgimpplugin.  If not, see <http://www.gnu.org/licenses/>.
#
#-------------------------------------------------------------------------------

"""
This module:
* defines API for settings
* defines the means to load/save settings:
  * permanently - to a JSON file
    - settings persist even after closing GIMP
  * semi-permanently - to the GIMP shelf
    - settings persist during one GIMP session
"""

#=============================================================================== 

import errno
import abc
from collections import OrderedDict
import json

import gimp
from gimpshelf import shelf
import gimpenums

import container

#===============================================================================

pdb = gimp.pdb

#===============================================================================

class Setting(object):
  
  """
  Attributes:
  
  * name (read-only): Setting name to uniquely identify a setting.
  
  * default_value: Default value of a setting when the setting is instantiated
    or when the reset() method was called.
  
  * value: The setting value. Subclasses of Setting can override the value.setter
    property to e.g. validate input value and raise ValueError if the value assigned is invalid.
  
  * gimp_pdb_type: GIMP Procedural Database (PDB) type, used when registering
    parameters in plug-ins. `_allowed_pdb_types` list, which is class-specific, determines
    whether the PDB type assigned is valid. `_allowed_pdb_types` in this class is None,
    which means that any PDB type can be assigned.
  
  * can_be_registered_to_pdb: Indicates whether a setting can be registered
    as a parameter to a plug-in. Automatically set to True if gimp_pdb_type is
    assigned to a valid value that is not None.
  
  * display_name: Setting name in human-readable format. Useful as GUI labels.
  
  * short_description (read-only): Usually `display_name` plus additional information
    in parentheses. Useful as setting description when registering parameters in plug-ins.
  
  * description: Describes a setting in more detail. Useful for documentation
    purposes as well as GUI tooltips.
  
  * error_messages: A dict of error messages, which can be used e.g. when a value
    assigned to a setting is invalid. You can add your own error messages and
    assign them to one of the "default" error messages (such as 'invalid_value'
    in ImageSetting) depending on the context in which the value assigned is invalid.
  
  * ui_enabled: Indicates whether a setting should be enabled (respond to user input)
    in the GUI. True by default.
  
  * ui_visible: Indicates whether a setting should be visible in the GUI. True by default.
  
  * can_be_reset_by_container: If True, setting is reset to its default value if
    the reset() method from the corresponding SettingContainer is called. False by default.
    
  * changed_attributes (read-only): Contains a set of Setting attribute names that were changed.
    Currently, it can contain the following attribute names: `value`, `ui_enabled` or `ui_visible`.
    Assigning to any of the attributes causes the corresponding attribute name
    to be added to the set. `changed_attributes` is cleared if streamline() is called.
  """
  
  def __init__(self, name, default_value):
    
    self._attrs_that_trigger_change = { 'value', 'ui_enabled', 'ui_visible' }
    self._changed_attributes = set()
    
    self._name = name
    self.default_value = default_value
    
    self._value = self.default_value
    
    self._gimp_pdb_type = None
    self._can_be_registered_to_pdb = False
    self._allowed_pdb_types = None
    
    self._display_name = ""
    self._description = ""
    
    self._error_messages = {}
    
    self.ui_enabled = True
    self.ui_visible = True
    
    self.can_be_reset_by_container = True
    
    self._streamline_func = None
    self._streamline_args = []
    
    # Some attributes may now be in _changed_attributes because of __setattr__,
    # hence it must be cleared.
    self._changed_attributes.clear()
  
  def __setattr__(self, name, value):
    super(Setting, self).__setattr__(name, value)
    if name in self._attrs_that_trigger_change:
      self._changed_attributes.add(name)
  
  @property
  def name(self):
    return self._name
  
  @property
  def value(self):
    return self._value
  @value.setter
  def value(self, value_):
    self._set_value(value_)
  
  def _set_value(self, value_):
    self._value = value_
  
  @property
  def gimp_pdb_type(self):
    return self._gimp_pdb_type
  @gimp_pdb_type.setter
  def gimp_pdb_type(self, value):
    if self._allowed_pdb_types is None or value in self._allowed_pdb_types:
      self._gimp_pdb_type = value
      self.can_be_registered_to_pdb = value is not None
    else:
      raise ValueError("GIMP PDB type " + str(value) + " not allowed")
  
  @property
  def can_be_registered_to_pdb(self):
    return self._can_be_registered_to_pdb
  @can_be_registered_to_pdb.setter
  def can_be_registered_to_pdb(self, value):
    if value and self._gimp_pdb_type is None:
      raise ValueError("setting cannot be registered to PDB because it has no PDB type set (attribute gimp_pdb_type)")
    self._can_be_registered_to_pdb = value
  
  @property
  def display_name(self):
    return self._display_name
  @display_name.setter
  def display_name(self, value):
    self._display_name = value if value is not None else ""
  
  @property
  def description(self):
    return self._description
  @description.setter
  def description(self, value):
    self._description = value if value is not None else ""
  
  @property
  def changed_attributes(self):
    return self._changed_attributes
  
  @property
  def short_description(self):
    return self.display_name
  
  @property
  def error_messages(self):
    return self._error_messages
  
  def reset(self):
    """
    Set the setting value to its default value.
    
    This is different from
      setting.value = setting.default_value
    in that this method does not raise an exception if the default value is invalid.
    """
    self._value = self.default_value
  
  def streamline(self, force=False):
    """
    Adjust attributes of this setting based on the attributes of other settings or arguments.
    Return a list of changed settings. A setting is considered changed if at least one of the
    following attributes were assigned a value:
    * `value`
    * `ui_enabled`
    * `ui_visible`
    
    If force is True, streamline settings even if the values of the other
    settings were not changed. This is useful when initializing GUI elements -
    setting up proper values, enabled/disabled state or visibility.
    """
    
    if self._streamline_func is None:
      raise TypeError("streamline() cannot be called because there is no streamline function set")
    
    changed_settings = OrderedDict()
    
    if self._changed_attributes or force:
      self._streamline_func(self, *self._streamline_args)
      
      # Create copies of the changed attributes since the sets are cleared
      # in the objects afterwards.
      changed_settings[self] = set(self._changed_attributes)
      self._changed_attributes.clear()
      
      for arg in self._streamline_args:
        if isinstance(arg, Setting) and arg.changed_attributes:
          changed_settings[arg] = set(arg.changed_attributes)
          arg.changed_attributes.clear()
    
    return changed_settings
  
  def set_streamline_func(self, streamline_func, *streamline_args):
    """
    Set a function to be called by the streamline() method.
    
    streamline_args are additional arguments to the streamline function.
    
    A streamline function must always contain at least one argument. The first
    argument is the setting from which the streamline function is invoked.
    This argument should not be specified in streamline_args.
    """
    
    if not callable(streamline_func):
      raise TypeError("not a function")
    
    self._streamline_func = streamline_func
    self._streamline_args = streamline_args
  
  def remove_streamline_func(self):
    if self._streamline_func is None:
      raise TypeError("no streamline function was previously set")
    
    self._streamline_func = None
    self._streamline_args = []
  
  def can_streamline(self):
    return self._streamline_func is not None


class NumericSetting(Setting):
  
  """
  Additional attributes:
  
  * min_value: Minimum numeric value. If not None and the value assigned is less
    than min_value, ValueError is raised.
    
  * max_value: Maximum numeric value. If not None and the value assigned is greater
    than max_value, ValueError is raised.
  
  
  Error messages:
  
  * below_min: Used if a value assigned is less than min_value.
  
  * above_max: Used if a value assigned is greater than min_value.
  """
  
  __metaclass__ = abc.ABCMeta
  
  def __init__(self, name, default_value):
    super(NumericSetting, self).__init__(name, default_value)
    
    self.min_value = None
    self.max_value = None
    
    self.error_messages['below_min'] = "value cannot be less than the minimum value " + str(self.min_value)
    self.error_messages['above_max'] = "value cannot be greater than the maximum value " + str(self.max_value)
  
  @property
  def value(self):
    return self._value

  @value.setter
  def value(self, val):
    if self.min_value is not None and val < self.min_value:
      raise ValueError(self.error_messages['below_min'])
    if self.max_value is not None and val > self.max_value:
      raise ValueError(self.error_messages['above_max'])
    
    super(NumericSetting, self)._set_value(val)


class IntSetting(NumericSetting):
  
  def __init__(self, name, default_value):
    super(IntSetting, self).__init__(name, default_value)
    
    self._allowed_pdb_types = [gimpenums.PDB_INT8, gimpenums.PDB_INT16, gimpenums.PDB_INT32]
    self.gimp_pdb_type = gimpenums.PDB_INT32


# Use BoolSetting to indicate that a setting is a boolean.
# No need for a separate subclass, since the functionality is the same as IntSetting.
BoolSetting = IntSetting


class FloatSetting(NumericSetting):
  
  def __init__(self, name, default_value):
    super(FloatSetting, self).__init__(name, default_value)
    
    self._allowed_pdb_types = [gimpenums.PDB_FLOAT]
    self.gimp_pdb_type = gimpenums.PDB_FLOAT


class EnumSetting(Setting):
  
  """
  This class can be used for settings with a limited number of values.
  
  Additional attributes:
  
  * options (read-only): A dict of <option name, option value> pairs. Option name
    uniquely identifies each option. Option value is the corresponding integer value.
  
  * options_display_names (read-only): A dict of <option name, option display name> pairs.
    Option display names can be used e.g. as combo box items in the GUI.
  
  To access an option value:
    setting.options[option name]
  
  To access an option display name:
    setting.options_display_names[option name]
  
  
  Error messages:
  
  * invalid_value: Used if a value assigned is invalid.
  
  * invalid_default_value: Option name is invalid (not found in the `options` parameter).
  
  * wrong_options_len: Wrong number of elements in tuples in the `options` parameter.
  
  * duplicate_option_value: Some option values in the 3-element tuples were specified multiple times.
  """
  
  def __init__(self, name, default_value, options):
    
    """
    Parameters:
    
    * name: Setting name.
    
    * default_value: Option name (identifier). Unlike other Setting classes, where
      the value is specified directly, EnumSetting accepts a valid option
      identifier instead.
    
    * options: A list of either (option name, option display name) tuples
      or (option name, option display name, option value) tuples.
      For 2-element tuples, option values are assigned automatically, starting with 0.
      Use 3-element tuples to assign explicit option values. Values must be unique
      and specified in each tuple.
    """
    
    super(EnumSetting, self).__init__(name, default_value)
    
    self._allowed_pdb_types = [gimpenums.PDB_INT8, gimpenums.PDB_INT16, gimpenums.PDB_INT32]
    self.gimp_pdb_type = gimpenums.PDB_INT32
    
    self._options = OrderedDict()
    self._options_display_names = OrderedDict()
    self._option_values = set()
    
    self.error_messages['wrong_options_len'] = "Wrong number of tuple elements in options - must be 2 or 3"
    self.error_messages['duplicate_option_value'] = ("Cannot set the same value for multiple options "
                                                     "- they must be unique")
    
    if len(options[0]) == 2:
      for i, (option_name, option_display_name) in enumerate(options):
        self._options[option_name] = i
        self._options_display_names[option_name] = option_display_name
        self._option_values.add(i)
    elif len(options[0]) == 3:
      for option_name, option_display_name, option_value in options:
        if option_value in self._option_values:
          raise ValueError(self.error_messages['duplicate_option_value'])
        
        self._options[option_name] = option_value
        self._options_display_names[option_name] = option_display_name
        self._option_values.add(option_value)
    else:
      raise ValueError(self.error_messages['wrong_options_len'])
    
    self.error_messages['invalid_value'] = "invalid option value; valid values: " + str(list(self._option_values))
    self.error_messages['invalid_default_value'] = ("invalid identifier for the default value; "
                                                    "must be one of " + str(self._options.keys()))
    
    if default_value in self._options:
      self.default_value = self._options[default_value]
      self._value = self.default_value
    else:
      raise ValueError(self.error_messages['invalid_default_value'])
    
    self._options_str = self._stringify_options()
  
  @property
  def value(self):
    return self._value
  @value.setter
  def value(self, value_):
    if value_ not in self._option_values:
      raise ValueError(self.error_messages['invalid_value'])
    
    super(EnumSetting, self)._set_value(value_)
  
  @property
  def short_description(self):
    return self.display_name + " " + self._options_str
  
  @property
  def options(self):
    return self._options
  
  @property
  def options_display_names(self):
    return self._options_display_names
  
  def get_option_display_names_and_values(self):
    display_names_and_values = []
    for option_name, option_value in zip(self._options_display_names.values(), self._options.values()):
      display_names_and_values.extend((option_name, option_value))
    return display_names_and_values
  
  def _stringify_options(self):
    options_str = ""
    options_sep = ", "
    
    for value, display_name in zip(self._options.values(), self._options_display_names.values()):
      options_str += '{0} ({1})'.format(display_name, str(value)) + options_sep
    options_str = options_str[:-len(options_sep)]
    
    return "{ " + options_str + " }"


class ImageSetting(Setting):
  
  """
  Error messages:
  
  * invalid_value: Used if a value assigned is invalid.
  """
  
  def __init__(self, name, default_value):
    super(ImageSetting, self).__init__(name, default_value)
    
    self._allowed_pdb_types = [gimpenums.PDB_IMAGE]
    self.gimp_pdb_type = gimpenums.PDB_IMAGE
    
    self.error_messages['invalid_value'] = "invalid image"
  
  @property
  def value(self):
    return self._value
  
  @value.setter
  def value(self, image):
    if not pdb.gimp_image_is_valid(image):
      raise ValueError(self.error_messages['invalid_value'])
    
    super(ImageSetting, self)._set_value(image)


class DrawableSetting(Setting):
  
  """
  Error messages:
  
  * invalid_value: Used if a value assigned is invalid.
  """
  
  def __init__(self, name, default_value):
    super(DrawableSetting, self).__init__(name, default_value)
    
    self._allowed_pdb_types = [gimpenums.PDB_DRAWABLE]
    self.gimp_pdb_type = gimpenums.PDB_DRAWABLE
    
    self.error_messages['invalid_value'] = "invalid drawable"
  
  @property
  def value(self):
    return self._value
  
  @value.setter
  def value(self, drawable):
    if not pdb.gimp_item_is_valid(drawable):
      raise ValueError(self.error_messages['invalid_value'])
    
    super(DrawableSetting, self)._set_value(drawable)


class StringSetting(Setting):
  
  def __init__(self, name, default_value):
    super(StringSetting, self).__init__(name, default_value)
    
    self._allowed_pdb_types = [gimpenums.PDB_STRING]
    self.gimp_pdb_type = gimpenums.PDB_STRING


class NonEmptyStringSetting(StringSetting):
  
  """
  Error messages:
  
  * invalid_value: Used if a value assigned is invalid.
{'fraction_non_alphanumeric': 0.09548802412573218, 'fraction_numerical': 0.0019138201009105144, 'mean_word_length': 3.2899614379898, 'pattern_counts': {'":': 0, '<': 15, '<?xml version=': 0, '>': 15, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 8, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3651584', 'n_tokens_mistral': 10074, 'n_tokens_neox': 9945, 'n_words': 3177}