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