1#  Copyright (c) 2005-19, Enthought, Inc.
2#  All rights reserved.
3#
4#  This software is provided without warranty under the terms of the BSD
5#  license included in LICENSE.txt and may be redistributed only
6#  under the conditions described in the aforementioned license.  The license
7#  is also available online at http://www.enthought.com/licenses/BSD.txt
8#
9#  Thanks for using Enthought open source!
10#
11#  Author: David C. Morrill
12#  Date:   10/07/2004
13
14""" Defines the abstract Editor class, which represents an editing control for
15    an object trait in a Traits-based user interface.
16"""
17
18from contextlib import contextmanager
19from functools import partial
20
21from traits.api import (
22    Any,
23    Bool,
24    Callable,
25    HasPrivateTraits,
26    HasTraits,
27    Instance,
28    List,
29    Property,
30    ReadOnly,
31    Set,
32    Str,
33    TraitError,
34    TraitListEvent,
35    Tuple,
36    Undefined,
37    cached_property,
38)
39
40from traits.trait_base import not_none, xgetattr, xsetattr
41
42from .editor_factory import EditorFactory
43
44from .context_value import ContextValue
45
46from .undo import UndoItem
47
48from .item import Item
49
50# Reference to an EditorFactory object
51factory_trait = Instance(EditorFactory)
52
53
54class Editor(HasPrivateTraits):
55    """ Represents an editing control for an object trait in a Traits-based
56        user interface.
57    """
58
59    #: The UI (user interface) this editor is part of:
60    ui = Instance("traitsui.ui.UI", clean_up=True)
61
62    #: Full name of the object the editor is editing (e.g.
63    #: 'object.link1.link2'):
64    object_name = Str("object")
65
66    #: The object this editor is editing (e.g. object.link1.link2):
67    object = Instance(HasTraits, clean_up=True)
68
69    #: The name of the trait this editor is editing (e.g. 'value'):
70    name = ReadOnly()
71
72    #: The context object the editor is editing (e.g. object):
73    context_object = Property()
74
75    #: The extended name of the object trait being edited. That is,
76    #: 'object_name.name' minus the context object name at the beginning. For
77    #: example: 'link1.link2.value':
78    extended_name = Property()
79
80    #: Original value of object.name (e.g. object.link1.link2.value):
81    old_value = Any(clean_up=True)
82
83    #: Text description of the object trait being edited:
84    description = ReadOnly()
85
86    #: The Item object used to create this editor:
87    item = Instance(Item, (), clean_up=True)
88
89    #: The GUI widget defined by this editor:
90    control = Any(clean_up=True)
91
92    #: The GUI label (if any) defined by this editor:
93    label_control = Any(clean_up=True)
94
95    #: Is the underlying GUI widget enabled?
96    enabled = Bool(True)
97
98    #: Is the underlying GUI widget visible?
99    visible = Bool(True)
100
101    #: Is the underlying GUI widget scrollable?
102    scrollable = Bool(False)
103
104    #: The EditorFactory used to create this editor:
105    factory = Instance(EditorFactory, clean_up=True)
106
107    #: Is the editor updating the object.name value?
108    updating = Bool(False)
109
110    #: Current value for object.name:
111    value = Property()
112
113    #: Current value of object trait as a string:
114    str_value = Property()
115
116    #: The trait the editor is editing (not its value, but the trait itself):
117    value_trait = Property()
118
119    #: The current editor invalid state status:
120    invalid = Bool(False)
121
122    # -- private trait definitions ------------------------------------------
123
124    #: A set to track values being updated to prevent infinite recursion.
125    _no_trait_update = Set(Str)
126
127    #: A list of all values synchronized to.
128    _user_to = List(Tuple(Any, Str, Callable))
129
130    #: A list of all values synchronized from.
131    _user_from = List(Tuple(Str, Callable))
132
133    # ------------------------------------------------------------------------
134    # Editor interface
135    # ------------------------------------------------------------------------
136
137    # -- Abstract methods ---------------------------------------------------
138
139    def init(self, parent):
140        """ Create and initialize the underlying toolkit widget.
141
142        This method must be overriden by subclasses.  Implementations must
143        ensure that the :attr:`control` trait is set to an appropriate
144        toolkit object.
145
146        Parameters
147        ----------
148        parent : toolkit control
149            The parent toolkit object of the editor's toolkit objects.
150        """
151        raise NotImplementedError("This method must be overriden.")
152
153    def update_editor(self):
154        """ Updates the editor when the value changes externally to the editor.
155
156        This should normally be overridden in a subclass.
157        """
158        pass
159
160    def error(self, excp):
161        """ Handles an error that occurs while setting the object's trait value.
162
163        This should normally be overridden in a subclass.
164
165        Parameters
166        ----------
167        excp : Exception
168            The exception which occurred.
169        """
170        pass
171
172    def set_focus(self):
173        """ Assigns focus to the editor's underlying toolkit widget.
174
175        This method must be overriden by subclasses.
176        """
177        raise NotImplementedError("This method must be overriden.")
178
179    def string_value(self, value, format_func=None):
180        """ Returns the text representation of a specified object trait value.
181
182        This simply delegates to the factory's `string_value` method.
183        Sub-classes may choose to override the default implementation.
184
185        Parameters
186        ----------
187        value : any
188            The value being edited.
189        format_func : callable or None
190            A function that takes a value and returns a string.
191        """
192        return self.factory.string_value(value, format_func)
193
194    def restore_prefs(self, prefs):
195        """ Restores saved user preference information for the editor.
196
197        Editors with state may choose to override this. It will only be used
198        if the editor has an `id` value.
199
200        Parameters
201        ----------
202        prefs : dict
203            A dictionary of preference values.
204        """
205        pass
206
207    def save_prefs(self):
208        """ Returns any user preference information for the editor.
209
210        Editors with state may choose to override this. It will only be used
211        if the editor has an `id` value.
212
213        Returns
214        -------
215        prefs : dict or None
216            A dictionary of preference values, or None if no preferences to
217            be saved.
218        """
219        return None
220
221    # -- Editor life-cycle methods ------------------------------------------
222
223    def prepare(self, parent):
224        """ Finish setting up the editor.
225
226        Parameters
227        ----------
228        parent : toolkit control
229            The parent toolkit object of the editor's toolkit objects.
230        """
231        name = self.extended_name
232        if name != "None":
233            self.context_object.on_trait_change(
234                self._update_editor, name, dispatch="ui"
235            )
236        self.init(parent)
237        self._sync_values()
238        self.update_editor()
239
240    def dispose(self):
241        """ Disposes of the contents of an editor.
242
243        This disconnects any synchronised values and resets references
244        to other objects.
245
246        Subclasses may chose to override this method to perform additional
247        clean-up.
248        """
249        if self.ui is None:
250            return
251
252        name = self.extended_name
253        if name != "None":
254            self.context_object.on_trait_change(
255                self._update_editor, name, remove=True
256            )
257
258        for name, handler in self._user_from:
259            self.on_trait_change(handler, name, remove=True)
260
261        for object, name, handler in self._user_to:
262            object.on_trait_change(handler, name, remove=True)
263
264        # Break linkages to references we no longer need:
265        for name in self.trait_names(clean_up=True):
266            setattr(self, name, None)
267
268    # -- Undo/redo methods --------------------------------------------------
269
270    def log_change(self, undo_factory, *undo_args):
271        """ Logs a change made in the editor with undo/redo history.
272
273        Parameters
274        ----------
275        undo_factory : callable
276            Callable that creates an undo item.  Often self.get_undo_item.
277        *undo_args
278            Any arguments to pass to the undo factory.
279        """
280        ui = self.ui
281
282        # Create an undo history entry if we are maintaining a history:
283        undoable = ui._undoable
284        if undoable >= 0:
285            history = ui.history
286            if history is not None:
287                item = undo_factory(*undo_args)
288                if item is not None:
289                    if undoable == history.now:
290                        # Create a new undo transaction:
291                        history.add(item)
292                    else:
293                        # Extend the most recent undo transaction:
294                        history.extend(item)
295
296    def get_undo_item(self, object, name, old_value, new_value):
297        """ Creates an undo history entry.
298
299        Can be overridden in a subclass for special value types.
300
301        Parameters
302        ----------
303        object : HasTraits instance
304            The object being modified.
305        name : str
306            The name of the trait that is to be changed.
307        old_value : any
308            The original value of the trait.
309        new_value : any
310            The new value of the trait.
311        """
312        return UndoItem(
313            object=object, name=name, old_value=old_value, new_value=new_value
314        )
315
316    # -- Trait synchronization code -----------------------------------------
317
318    def sync_value(
319        self,
320        user_name,
321        editor_name,
322        mode="both",
323        is_list=False,
324        is_event=False,
325    ):
326        """ Synchronize an editor trait and a user object trait.
327
328        Also sets the initial value of the editor trait from the
329        user object trait (for modes 'from' and 'both'), and the initial
330        value of the user object trait from the editor trait (for mode
331        'to'), as long as the relevant traits are not events.
332
333        Parameters
334        ----------
335        user_name : str
336            The name of the trait to be used on the user object. If empty, no
337            synchronization will be set up.
338        editor_name : str
339            The name of the relevant editor trait.
340        mode : str, optional; one of 'to', 'from' or 'both'
341            The direction of synchronization. 'from' means that trait changes
342            in the user object should be propagated to the editor. 'to' means
343            that trait changes in the editor should be propagated to the user
344            object. 'both' means changes should be propagated in both
345            directions. The default is 'both'.
346        is_list : bool, optional
347            If true, synchronization for item events will be set up in
348            addition to the synchronization for the object itself.
349            The default is False.
350        is_event : bool, optional
351            If true, this method won't attempt to initialize the user
352            object or editor trait values. The default is False.
353        """
354        if user_name == "":
355            return
356
357        key = "%s:%s" % (user_name, editor_name)
358
359        parts = user_name.split(".")
360        if len(parts) == 1:
361            user_object = self.context_object
362            xuser_name = user_name
363        else:
364            user_object = self.ui.context[parts[0]]
365            xuser_name = ".".join(parts[1:])
366            user_name = parts[-1]
367
368        if mode in {"from", "both"}:
369            self._bind_from(key, user_object, xuser_name, editor_name, is_list)
370
371            if not is_event:
372                # initialize editor value from user value
373                with self.raise_to_debug():
374                    user_value = xgetattr(user_object, xuser_name)
375                    setattr(self, editor_name, user_value)
376
377        if mode in {"to", "both"}:
378            self._bind_to(key, user_object, xuser_name, editor_name, is_list)
379
380            if mode == "to" and not is_event:
381                # initialize user value from editor value
382                with self.raise_to_debug():
383                    editor_value = xgetattr(self, editor_name)
384                    xsetattr(user_object, xuser_name, editor_value)
385
386    # -- Utility methods -----------------------------------------------------
387
388    def parse_extended_name(self, name):
389        """ Extract the object, name and a getter from an extended name
390
391        Parameters
392        ----------
393        name : str
394            The extended name to parse.
395
396        Returns
397        -------
398        object, name, getter : any, str, callable
399            The object from the context, the (extended) name of the
400            attributes holding the value, and a callable which gets the
401            current value from the context.
402        """
403        base_name, __, name = name.partition(".")
404        if name:
405            object = self.ui.context[base_name]
406        else:
407            name = base_name
408            object = self.context_object
409
410        return (object, name, partial(xgetattr, object, name))
411
412    # -- Utility context managers --------------------------------------------
413
414    @contextmanager
415    def no_trait_update(self, name):
416        """ Context manager that blocks updates from the named trait. """
417        if name in self._no_trait_update:
418            yield
419            return
420
421        self._no_trait_update.add(name)
422        try:
423            yield
424        finally:
425            self._no_trait_update.remove(name)
426
427    @contextmanager
428    def raise_to_debug(self):
429        """ Context manager that uses raise to debug to raise exceptions. """
430        try:
431            yield
432        except Exception:
433            from traitsui.api import raise_to_debug
434
435            raise_to_debug()
436
437    @contextmanager
438    def updating_value(self):
439        """ Context manager to handle updating value. """
440        if self.updating:
441            yield
442            return
443
444        self.updating = True
445        try:
446            yield
447        finally:
448            self.updating = False
449
450    # ------------------------------------------------------------------------
451    # object interface
452    # ------------------------------------------------------------------------
453
454    def __init__(self, parent, **traits):
455        """ Initializes the editor object.
456        """
457        super(HasPrivateTraits, self).__init__(**traits)
458        try:
459            self.old_value = getattr(self.object, self.name)
460        except AttributeError:
461            ctrait = self.object.base_trait(self.name)
462            if ctrait.type == "event" or self.name == "spring":
463                # Getting the attribute will fail for 'Event' traits:
464                self.old_value = Undefined
465            else:
466                raise
467
468        # Synchronize the application invalid state status with the editor's:
469        self.sync_value(self.factory.invalid, "invalid", "from")
470
471    # ------------------------------------------------------------------------
472    # private methods
473    # ------------------------------------------------------------------------
474
475    def _update_editor(self, object, name, old_value, new_value):
476        """ Performs updates when the object trait changes.
477
478        This is designed to be used as a trait listener.
479        """
480        # If background threads have modified the trait the editor is bound to,
481        # their trait notifications are queued to the UI thread. It is possible
482        # that by the time the UI thread dispatches these events, the UI the
483        # editor is part of has already been closed. So we need to check if we
484        # are still bound to a live UI, and if not, exit immediately:
485        if self.ui is None:
486            return
487
488        # If the notification is for an object different than the one actually
489        # being edited, it is due to editing an item of the form:
490        # object.link1.link2.name, where one of the 'link' objects may have
491        # been modified. In this case, we need to rebind the current object
492        # being edited:
493        if object is not self.object:
494            self.object = self.ui.get_extended_value(self.object_name)
495
496        # If the editor has gone away for some reason, disconnect and exit:
497        if self.control is None:
498            self.context_object.on_trait_change(
499                self._update_editor, self.extended_name, remove=True
500            )
501            return
502
503        # Log the change that was made (as long as the Item is not readonly
504        # or it is not for an event):
505        if (
506            self.item.style != "readonly"
507            and object.base_trait(name).type != "event"
508        ):
509            # Indicate that the contents of the UI have been changed:
510            self.ui.modified = True
511
512            if self.updating:
513                self.log_change(
514                    self.get_undo_item, object, name, old_value, new_value
515                )
516
517        # If the change was not caused by the editor itself:
518        if not self.updating:
519            # Update the editor control to reflect the current object state:
520            self.update_editor()
521
522    def _sync_values(self):
523        """ Initialize and synchronize editor and factory traits
524
525        Initializes and synchronizes (as needed) editor traits with the
526        value of corresponding factory traits.  The name of the factory
527        trait and the editor trait must match and the factory trait needs
528        to have ``sync_value`` metadata set.  The strategy followed is:
529
530        - for each factory trait with ``sync_value`` metadata:
531
532          1.  if the value is a :class:`ContextValue` instance then
533              call :meth:`sync_value` with the ``name`` from the
534              context value.
535          2.  if the trait has ``sync_name`` metadata, look at the
536              referenced trait value and if it is a non-empty string
537              then use this value as the name of the value in the
538              context.
539          3.  otherwise initialize the current value of the factory
540              trait to the corresponding value of the editor.
541
542        - synchronization mode in cases 1 and 2 is taken from the
543          ``sync_value`` metadata of the editor trait first and then
544          the ``sync_value`` metadata of the factory trait if that is
545          empty.
546
547        - if the value is a container type, then the `is_list` metadata
548          is set to
549        """
550        factory = self.factory
551        for name, trait in factory.traits(sync_value=not_none).items():
552            value = getattr(factory, name)
553            self_trait = self.trait(name)
554            if self_trait.sync_value:
555                mode = self_trait.sync_value
556            else:
557                mode = trait.sync_value
558            if isinstance(value, ContextValue):
559                self.sync_value(
560                    value.name,
561                    name,
562                    mode,
563                    bool(self_trait.is_list),
564                    self_trait.type == "event",
565                )
566            elif (
567                trait.sync_name is not None
568                and getattr(factory, trait.sync_name, "") != ""
569            ):
570                # Note: this is implemented as a stepping stone from things
571                # like ``low_name`` and ``high_name`` to using context values.
572                sync_name = getattr(factory, trait.sync_name)
573                self.sync_value(
574                    sync_name,
575                    name,
576                    mode,
577                    bool(self_trait.is_list),
578                    self_trait.type == "event",
579                )
580            elif value is not Undefined:
581                setattr(self, name, value)
582
583    def _bind_from(self, key, user_object, xuser_name, editor_name, is_list):
584        """ Bind trait change handlers from a user object to the editor.
585
586        Parameters
587        ----------
588        key : str
589            The key to use to guard against recursive updates.
590        user_object : object
591            The object in the TraitsUI context that is being bound.
592        xuser_name: : str
593            The extended name of the trait to be used on the user object.
594        editor_name : str
595            The name of the relevant editor trait.
596        is_list : bool, optional
597            If true, synchronization for item events will be set up in
598            addition to the synchronization for the object itself.
599            The default is False.
600        """
601
602        def user_trait_modified(new):
603            if key not in self._no_trait_update:
604                with self.no_trait_update(key), self.raise_to_debug():
605                    xsetattr(self, editor_name, new)
606
607        user_object.on_trait_change(user_trait_modified, xuser_name)
608        self._user_to.append((user_object, xuser_name, user_trait_modified))
609
610        if is_list:
611
612            def user_list_modified(event):
613                if (
614                    isinstance(event, TraitListEvent)
615                    and key not in self._no_trait_update
616                ):
617                    with self.no_trait_update(key), self.raise_to_debug():
618                        n = event.index
619                        getattr(self, editor_name)[
620                            n:n + len(event.removed)
621                        ] = event.added
622
623            items = xuser_name + "_items"
624            user_object.on_trait_change(user_list_modified, items)
625            self._user_to.append((user_object, items, user_list_modified))
626
627    def _bind_to(self, key, user_object, xuser_name, editor_name, is_list):
628        """ Bind trait change handlers from a user object to the editor.
629
630        Parameters
631        ----------
632        key : str
633            The key to use to guard against recursive updates.
634        user_object : object
635            The object in the TraitsUI context that is being bound.
636        xuser_name: : str
637            The extended name of the trait to be used on the user object.
638        editor_name : str
639            The name of the relevant editor trait.
640        is_list : bool, optional
641            If true, synchronization for item events will be set up in
642            addition to the synchronization for the object itself.
643            The default is False.
644        """
645
646        def editor_trait_modified(new):
647            if key not in self._no_trait_update:
648                with self.no_trait_update(key), self.raise_to_debug():
649                    xsetattr(user_object, xuser_name, new)
650
651        self.on_trait_change(editor_trait_modified, editor_name)
652
653        self._user_from.append((editor_name, editor_trait_modified))
654
655        if is_list:
656
657            def editor_list_modified(event):
658                if key not in self._no_trait_update:
659                    with self.no_trait_update(key), self.raise_to_debug():
660                        n = event.index
661                        value = xgetattr(user_object, xuser_name)
662                        value[n:n + len(event.removed)] = event.added
663
664            self.on_trait_change(editor_list_modified, editor_name + "_items")
665            self._user_from.append(
666                (editor_name + "_items", editor_list_modified)
667            )
668
669    def __set_value(self, value):
670        """ Set the value of the trait the editor is editing.
671
672        This calls the appropriate setattr method on the handler to perform
673        the actual change.
674        """
675        with self.updating_value():
676            try:
677                handler = self.ui.handler
678                obj_name = self.object_name
679                name = self.name
680                method = (
681                    getattr(handler, "%s_%s_setattr" % (obj_name, name), None)
682                    or getattr(handler, "%s_setattr" % name, None)
683                    or getattr(handler, "setattr")
684                )
685                method(self.ui.info, self.object, name, value)
686            except TraitError as excp:
687                self.error(excp)
688                raise
689
690    # -- Traits property getters and setters --------------------------------
691
692    @cached_property
693    def _get_context_object(self):
694        """ Returns the context object the editor is using
695
696        In some cases a proxy object is edited rather than an object directly
697        in the context, in which case we return ``self.object``.
698        """
699        object_name = self.object_name
700        context_key = object_name.split(".", 1)[0]
701        if (object_name != "") and (context_key in self.ui.context):
702            return self.ui.context[context_key]
703
704        # This handles the case of a 'ListItemProxy', which is not in the
705        # ui.context, but is the editor 'object':
706        return self.object
707
708    @cached_property
709    def _get_extended_name(self):
710        """ Returns the extended trait name being edited.
711        """
712        return ("%s.%s" % (self.object_name, self.name)).split(".", 1)[1]
713
714    def _get_value_trait(self):
715        """ Returns the trait the editor is editing (Property implementation).
716        """
717        return self.object.trait(self.name)
718
719    def _get_value(self):
720        """ Returns the value of the trait the editor is editing.
721        """
722        return getattr(self.object, self.name, Undefined)
723
724    def _set_value(self, value):
725        """ Set the value of the trait the editor is editing.
726
727        Dispatches via the TraitsUI Undo/Redo mechanisms to make change
728        reversible, if desired.
729        """
730        if self.ui and self.name != "None":
731            self.ui.do_undoable(self.__set_value, value)
732
733    def _get_str_value(self):
734        """ Returns the text representation of the object trait.
735        """
736        return self.string_value(getattr(self.object, self.name, Undefined))
737