1"""This modules defines CSVListEditor.
2
3A CSVListEditor provides an editor for lists of simple data types.
4It allows the user to edit the list in a text field, using commas
5(or optionally some other character) to separate the elements.
6"""
7
8# ------------------------------------------------------------------------------
9#
10#  Copyright (c) 2011, Enthought, Inc.
11#  All rights reserved.
12#
13#  This software is provided without warranty under the terms of the BSD
14#  license included in LICENSE.txt and may be redistributed only
15#  under the conditions described in the aforementioned license.  The license
16#  is also available online at http://www.enthought.com/licenses/BSD.txt
17#
18#  Author: Warren Weckesser
19#  Date:   July 2011
20#
21# ------------------------------------------------------------------------------
22
23from traits.api import Str, Int, Float, Enum, Range, Bool, TraitError, Either
24from traits.trait_handlers import RangeTypes
25
26from .text_editor import TextEditor
27from ..helper import enum_values_changed
28
29
30def _eval_list_str(s, sep=",", item_eval=None, ignore_trailing_sep=True):
31    """Convert a string into a list.
32
33    Parameters
34    ----------
35    s : str
36        The string to be converted.
37    sep : str or None
38        `sep` is the text separator of list items.  If `sep` is None,
39        each contiguous stretch of whitespace is a separator.
40    item_eval : callable or None
41        `item_eval` is used to evaluate the list elements.  If `item_eval`
42        is None, the list will be a list substrings of `s`.
43    ignore_trailing_sep : bool
44        If `ignore_trailing_sep` is False, it is an error to have a separator
45        at the end of the list (i.e. 'foo, bar,' is invalid).
46        If `ignore_trailing_sep` is True, a separator at the end of the
47        string `s` is ignored.
48
49    Returns
50    -------
51    values : list
52        List of converted values from the string.
53    """
54    if item_eval is None:
55        item_eval = lambda x: x
56    s = s.strip()
57    if sep is not None and ignore_trailing_sep and s.endswith(sep):
58        s = s[: -len(sep)]
59        s = s.rstrip()
60    if s == "":
61        values = []
62    else:
63        values = [item_eval(x.strip()) for x in s.split(sep)]
64    return values
65
66
67def _format_list_str(values, sep=",", item_format=str):
68    """Convert a list to a string.
69
70    Each item in the list `values` is converted to a string with the
71    function `item_format`, and these are joined with `sep` plus a space.
72    If `sep` is None, a single space is used to join the items.
73
74    Parameters
75    ----------
76    values : list
77        The list of values to be represented as a string.
78    sep : str
79        String used to join the items.  A space is also added after
80        `sep`.
81    item_format : callable
82        Converts its single argument to a string.
83
84    Returns
85    -------
86    s : str
87        The result of converting the list to a string.
88    """
89    if sep is None:
90        joiner = " "
91    else:
92        joiner = sep + " "
93    s = joiner.join(item_format(x) for x in values)
94    return s
95
96
97def _validate_range_value(range_object, object, name, value):
98    """Validate a Range value.
99
100    This function is used by the CSVListEditor to validate a value
101    when editing a list of ranges where the Range is dynamic (that
102    is, one or both of the 'low' and 'high' values are strings that
103    refer to other traits in `object`.
104
105    The function implements the same validation logic as in the method
106    traits.trait_types.BaseRange._set(), but does not call the
107    set_value() method; instead it simply returns the valid value.
108    If the value is not valid, range_object.error(...) is called.
109
110    Parameters
111    ----------
112    range_object : instance of traits.trait_types.Range
113
114    object : instance of HasTraits
115        This is the HasTraits object that holds the traits
116        to which the one or both of range_object.low and
117        range_object.high refer.
118
119    name : str
120        The name of the List trait in `object`.
121
122    value : object (e.g. int, float, str)
123        The value to be validated.
124
125    Returns
126    -------
127    value : object
128        The validated value.  It might not be the same
129        type as the input argument (e.g. if the range type
130        is float and the input value is an int, the return
131        value will be a float).
132    """
133    low = eval(range_object._low)
134    high = eval(range_object._high)
135    if low is None and high is None:
136        if isinstance(value, RangeTypes):
137            return value
138    else:
139        new_value = range_object._typed_value(value, low, high)
140
141        satisfies_low = (
142            low is None
143            or low < new_value
144            or ((not range_object._exclude_low) and (low == new_value))
145        )
146
147        satisfies_high = (
148            high is None
149            or high > new_value
150            or ((not range_object._exclude_high) and (high == new_value))
151        )
152
153        if satisfies_low and satisfies_high:
154            return value
155
156    # Note: this is the only explicit use of 'object' and 'name'.
157    range_object.error(object, name, value)
158
159
160def _prepare_method(cls, parent):
161    """ Unbound implementation of the prepare editor method to add a
162    change notification hook in the items of the list before calling
163    the parent prepare method of the parent class.
164
165    """
166    name = cls.extended_name
167    if name != "None":
168        cls.context_object.on_trait_change(
169            cls._update_editor, name + "[]", dispatch="ui"
170        )
171    super(cls.__class__, cls).prepare(parent)
172
173
174def _dispose_method(cls):
175    """ Unbound implementation of the dispose editor method to remove
176    the change notification hook in the items of the list before calling
177    the parent dispose method of the parent class.
178
179    """
180    if cls.ui is None:
181        return
182
183    name = cls.extended_name
184    if name != "None":
185        cls.context_object.on_trait_change(
186            cls._update_editor, name + "[]", remove=True
187        )
188    super(cls.__class__, cls).dispose()
189
190
191class CSVListEditor(TextEditor):
192    """A text editor for a List.
193
194    This editor provides a single line of input text of comma separated
195    values.  (Actually, the default separator is a comma, but this can
196    changed.)  The editor can only be used with List traits whose inner
197    trait is one of Int, Float, Str, Enum, or Range.
198
199    The 'simple', 'text', 'custom' and readonly styles are based on
200    TextEditor. The 'readonly' style provides the same formatting in the
201    text field as the other editors, but the user cannot change the value.
202
203    Like other Traits editors, the background of the text field will turn
204    red if the user enters an incorrectly formatted list or if the values
205    do not match the type of the inner trait.  This validation only occurs
206    while editing the text field.  If, for example, the inner trait is
207    Range(low='lower', high='upper'), a change in 'upper' will not trigger
208    the validation code of the editor.
209
210    The editor removes whitespace of entered items with strip(), so for
211    Str types, the editor should not be used if whitespace at the beginning
212    or end of the string must be preserved.
213
214    Parameters
215    ----------
216    sep : str or None, optional
217        The separator of the values in the list.  If None, each contiguous
218        sequence of whitespace is a separator.
219        Default is ','.
220
221    ignore_trailing_sep : bool, optional
222        If this is False, a line containing a trailing separator is invalid.
223        Default is True.
224
225    auto_set : bool
226        If True, then every keystroke sets the value of the trait.
227
228    enter_set : bool
229        If True, the user input sets the value when the Enter key is pressed.
230
231    Example
232    -------
233    The following will display a window containing a single input field.
234    Entering, say, '0, .5, 1' in this field will result in the list
235    x = [0.0, 0.5, 1.0].
236    """
237
238    #: The separator of the element in the list.
239    sep = Either(None, Str, default=",")
240
241    #: If False, it is an error to have a trailing separator.
242    ignore_trailing_sep = Bool(True)
243
244    #: Include some of the TextEditor API:
245
246    #: Is user input set on every keystroke?
247    auto_set = Bool(True)
248
249    #: Is user input set when the Enter key is pressed?
250    enter_set = Bool(False)
251
252    def _funcs(self, object, name):
253        """Create the evalution and formatting functions for the editor.
254
255        Parameters
256        ----------
257        object : instance of HasTraits
258            This is the object that has the List trait for which we are
259            creating an editor.
260
261        name : str
262            Name of the List trait on `object`.
263
264        Returns
265        -------
266        evaluate, fmt_func : callable, callable
267            The functions for converting a string to a list (`evaluate`)
268            and a list to a string (`fmt_func`).  These are the functions
269            that are ultimately given as the keyword arguments 'evaluate'
270            and 'format_func' of the TextEditor that will be generated by
271            the CSVListEditor editor factory functions.
272        """
273        t = getattr(object, name)
274        # Get the list of inner traits.  Only a single inner trait is allowed.
275        it_list = t.trait.inner_traits()
276        if len(it_list) > 1:
277            raise TraitError(
278                "Only one inner trait may be specified when "
279                "using a CSVListEditor."
280            )
281
282        # `it` is the single inner trait.  This will be an instance of
283        # traits.traits.CTrait.
284        it = it_list[0]
285        # The following 'if' statement figures out the appropriate evaluation
286        # function (evaluate) and formatting function (fmt_func) for the
287        # given inner trait.
288        if (
289            it.is_trait_type(Int)
290            or it.is_trait_type(Float)
291            or it.is_trait_type(Str)
292        ):
293            evaluate = lambda s: _eval_list_str(
294                s,
295                sep=self.sep,
296                item_eval=it.trait_type.evaluate,
297                ignore_trailing_sep=self.ignore_trailing_sep,
298            )
299            fmt_func = lambda vals: _format_list_str(vals, sep=self.sep)
300        elif it.is_trait_type(Enum):
301            values, mapping, inverse_mapping = enum_values_changed(it)
302            evaluate = lambda s: _eval_list_str(
303                s,
304                sep=self.sep,
305                item_eval=mapping.__getitem__,
306                ignore_trailing_sep=self.ignore_trailing_sep,
307            )
308            fmt_func = lambda vals: _format_list_str(
309                vals, sep=self.sep, item_format=inverse_mapping.__getitem__
310            )
311        elif it.is_trait_type(Range):
312            # Get the type of the values from the default value.
313            # range_object will be an instance of traits.trait_types.Range.
314            range_object = it.handler
315            if range_object.default_value_type == 8:
316                # range_object.default_value is callable.
317                defval = range_object.default_value(object)
318            else:
319                # range_object.default_value *is* the default value.
320                defval = range_object.default_value
321            typ = type(defval)
322
323            if range_object.validate is None:
324                # This will be the case for dynamic ranges.
325                item_eval = lambda s: _validate_range_value(
326                    range_object, object, name, typ(s)
327                )
328            else:
329                # Static ranges have a validate method.
330                item_eval = lambda s: range_object.validate(
331                    object, name, typ(s)
332                )
333
334            evaluate = lambda s: _eval_list_str(
335                s,
336                sep=self.sep,
337                item_eval=item_eval,
338                ignore_trailing_sep=self.ignore_trailing_sep,
339            )
340            fmt_func = lambda vals: _format_list_str(vals, sep=self.sep)
341        else:
342            raise TraitError(
343                "To use a CSVListEditor, the inner trait of the "
344                "List must be Int, Float, Range, Str or Enum."
345            )
346
347        return evaluate, fmt_func
348
349    def simple_editor(self, ui, object, name, description, parent):
350        """ Generates an editor using the "simple" style.
351        """
352        self.evaluate, self.format_func = self._funcs(object, name)
353        return self.simple_editor_class(
354            parent,
355            factory=self,
356            ui=ui,
357            object=object,
358            name=name,
359            description=description,
360        )
361
362    def custom_editor(self, ui, object, name, description, parent):
363        """ Generates an editor using the "custom" style.
364        """
365        self.evaluate, self.format_func = self._funcs(object, name)
366        return self.custom_editor_class(
367            parent,
368            factory=self,
369            ui=ui,
370            object=object,
371            name=name,
372            description=description,
373        )
374
375    def text_editor(self, ui, object, name, description, parent):
376        """ Generates an editor using the "text" style.
377        """
378        self.evaluate, self.format_func = self._funcs(object, name)
379        return self.text_editor_class(
380            parent,
381            factory=self,
382            ui=ui,
383            object=object,
384            name=name,
385            description=description,
386        )
387
388    def readonly_editor(self, ui, object, name, description, parent):
389        """ Generates an "editor" that is read-only.
390        """
391        self.evaluate, self.format_func = self._funcs(object, name)
392        return self.readonly_editor_class(
393            parent,
394            factory=self,
395            ui=ui,
396            object=object,
397            name=name,
398            description=description,
399        )
400