1
#------------------------------------------------------------------------------
3
# Copyright (c) 2005, Enthought, Inc.
6
# This software is provided without warranty under the terms of the BSD
7
# license included in enthought/LICENSE.txt and may be redistributed only
8
# under the conditions described in the aforementioned license. The license
9
# is also available online at http://www.enthought.com/licenses/BSD.txt
11
# Thanks for using Enthought open source!
13
# Author: David C. Morrill
16
#------------------------------------------------------------------------------
18
""" Defines the various instance editors for the wxPython user interface
22
#-------------------------------------------------------------------------------
24
#-------------------------------------------------------------------------------
29
import HasTraits, Property
31
# FIXME: ToolkitEditorFactory is a proxy class defined here just for backward
32
# compatibility. The class has been moved to the
33
# traitsui.editors.instance_editor file.
34
from traitsui.editors.instance_editor \
35
import ToolkitEditorFactory
37
from traitsui.ui_traits \
40
from traitsui.helper \
43
from traitsui.handler \
46
from traitsui.instance_choice \
47
import InstanceChoiceItem
53
import DropColor, is_wx26
56
import TraitsUIPanel, position_window
58
from pyface.wx.drag_and_drop \
59
import PythonDropTarget
61
#-------------------------------------------------------------------------------
63
#-------------------------------------------------------------------------------
67
'horizontal': wx.HORIZONTAL,
68
'vertical': wx.VERTICAL
71
#-------------------------------------------------------------------------------
72
# 'CustomEditor' class:
73
#-------------------------------------------------------------------------------
75
class CustomEditor ( Editor ):
76
""" Custom style of editor for instances. If selection among instances is
77
allowed, the editor displays a combo box listing instances that can be
78
selected. If the current instance is editable, the editor displays a panel
79
containing trait editors for all the instance's traits.
82
# Background color when an item can be dropped on the editor:
85
# The orientation of the instance editor relative to the instance selector:
86
orientation = wx.VERTICAL
91
#---------------------------------------------------------------------------
93
#---------------------------------------------------------------------------
95
# List of InstanceChoiceItem objects used by the editor
98
# The maximum extra padding that should be allowed around the editor:
99
# (Override of the Editor base class trait)
102
# The view to use for displaying the instance:
105
#---------------------------------------------------------------------------
106
# Finishes initializing the editor by creating the underlying toolkit
108
#---------------------------------------------------------------------------
110
def init ( self, parent ):
111
""" Finishes initializing the editor by creating the underlying toolkit
114
factory = self.factory
115
if factory.name != '':
116
self._object, self._name, self._value = \
117
self.parse_extended_name( factory.name )
119
# Create a panel to hold the object trait's view:
121
self.control = self._panel = parent = TraitsUIPanel( parent, -1 )
123
# Build the instance selector if needed:
124
selectable = factory.selectable
125
droppable = factory.droppable
128
droppable |= item.is_droppable()
129
selectable |= item.is_selectable()
132
self._object_cache = {}
133
item = self.item_for( self.value )
135
self._object_cache[ id( item ) ] = self.value
137
self._choice = choice = wx.Choice( parent, -1, wx.Point( 0, 0 ),
138
wx.Size( -1, -1 ), [] )
139
wx.EVT_CHOICE( choice, choice.GetId(), self.update_object )
141
self._choice.SetBackgroundColour( self.ok_color )
143
self.set_tooltip( self._choice )
145
if factory.name != '':
146
self._object.on_trait_change( self.rebuild_items,
147
self._name, dispatch = 'ui' )
148
self._object.on_trait_change( self.rebuild_items,
149
self._name + '_items', dispatch = 'ui' )
151
factory.on_trait_change( self.rebuild_items, 'values',
153
factory.on_trait_change( self.rebuild_items, 'values_items',
159
self._choice = wx.TextCtrl( parent, -1, '',
160
style = wx.TE_READONLY )
161
self._choice.SetBackgroundColour( self.ok_color )
162
self.set_tooltip( self._choice )
165
self._choice.SetDropTarget( PythonDropTarget( self ) )
167
orientation = OrientationMap[ factory.orientation ]
168
if orientation is None:
169
orientation = self.orientation
171
if (selectable or droppable) and factory.editable:
172
sizer = wx.BoxSizer( orientation )
173
sizer.Add( self._choice, self.extra, wx.EXPAND )
174
if orientation == wx.VERTICAL:
176
wx.StaticLine( parent, -1, style = wx.LI_HORIZONTAL ), 0,
177
wx.EXPAND | wx.TOP | wx.BOTTOM, 5 )
178
self.create_editor( parent, sizer )
179
parent.SetSizer( sizer )
180
elif self.control is None:
181
if self._choice is None:
182
self._choice = choice = wx.Choice( parent, -1, wx.Point( 0, 0 ),
183
wx.Size( -1, -1 ), [] )
184
wx.EVT_CHOICE( choice, choice.GetId(), self.update_object )
185
self.control = self._choice
187
sizer = wx.BoxSizer( orientation )
188
self.create_editor( parent, sizer )
189
parent.SetSizer( sizer )
191
# Synchronize the 'view' to use:
192
# fixme: A normal assignment can cause a crash (for unknown reasons) in
193
# some cases, so we make sure that no notifications are generated:
194
self.trait_setq( view = factory.view )
195
self.sync_value( factory.view_name, 'view', 'from' )
197
#---------------------------------------------------------------------------
198
# Disposes of the contents of an editor:
199
#---------------------------------------------------------------------------
201
def dispose ( self ):
202
""" Disposes of the contents of an editor.
204
# Make sure we aren't hanging on to any object refs:
205
self._object_cache = None
207
if self._ui is not None:
210
choice = self._choice
211
if choice is not None:
212
if isinstance( choice, wx.Choice ):
213
wx.EVT_CHOICE( choice, choice.GetId(), None )
215
if self._object is not None:
216
self._object.on_trait_change( self.rebuild_items,
217
self._name, remove = True )
218
self._object.on_trait_change( self.rebuild_items,
219
self._name + '_items', remove = True )
221
self.factory.on_trait_change( self.rebuild_items, 'values',
223
self.factory.on_trait_change( self.rebuild_items,
224
'values_items', remove = True )
226
super( CustomEditor, self ).dispose()
228
#---------------------------------------------------------------------------
229
# Creates the editor control:
230
#---------------------------------------------------------------------------
232
def create_editor ( self, parent, sizer ):
233
""" Creates the editor control.
235
self._panel = TraitsUIPanel( parent, -1 )
236
sizer.Add( self._panel, 1, wx.EXPAND )
238
#---------------------------------------------------------------------------
239
# Gets the current list of InstanceChoiceItem items:
240
#---------------------------------------------------------------------------
242
def _get_items ( self ):
243
""" Gets the current list of InstanceChoiceItem items.
245
if self._items is not None:
248
factory = self.factory
249
if self._value is not None:
250
values = self._value() + factory.values
252
values = factory.values
255
adapter = factory.adapter
257
if not isinstance( value, InstanceChoiceItem ):
258
value = adapter( object = value )
259
items.append( value )
265
#---------------------------------------------------------------------------
266
# Rebuilds the object selector list:
267
#---------------------------------------------------------------------------
269
def rebuild_items ( self ):
270
""" Rebuilds the object selector list.
272
# Clear the current cached values:
275
# Rebuild the contents of the selector list:
278
choice = self._choice
280
for item in self.items:
281
if item.is_selectable():
282
item_name = item.get_name()
283
choice.Append( item_name )
284
if item.is_compatible( value ):
287
# Reselect the current item if possible:
289
choice.SetStringSelection( name )
291
# Otherwise, current value is no longer valid, try to discard it:
297
#---------------------------------------------------------------------------
298
# Returns the InstanceChoiceItem for a specified object:
299
#---------------------------------------------------------------------------
301
def item_for ( self, object ):
302
""" Returns the InstanceChoiceItem for a specified object.
304
for item in self.items:
305
if item.is_compatible( object ):
310
#---------------------------------------------------------------------------
311
# Returns the view to use for a specified object:
312
#---------------------------------------------------------------------------
314
def view_for ( self, object, item ):
315
""" Returns the view to use for a specified object.
319
view = item.get_view()
324
return self.ui.handler.trait_view_for( self.ui.info, view, object,
325
self.object_name, self.name )
327
#---------------------------------------------------------------------------
328
# Handles the user selecting a new value from the combo box:
329
#---------------------------------------------------------------------------
331
def update_object ( self, event ):
332
""" Handles the user selecting a new value from the combo box.
334
name = event.GetString()
335
for item in self.items:
336
if name == item.get_name():
338
object = self._object_cache.get( id_item )
340
object = item.get_object()
341
if (not self.factory.editable) and item.is_factory:
342
view = self.view_for( object, self.item_for( object ) )
343
view.ui( object, self.control, 'modal' )
345
if self.factory.cachable:
346
self._object_cache[ id_item ] = object
349
self.resynch_editor()
352
#---------------------------------------------------------------------------
353
# Updates the editor when the object trait changes external to the editor:
354
#---------------------------------------------------------------------------
356
def update_editor ( self ):
357
""" Updates the editor when the object trait changes externally to the
360
# Attach the current object value to the control (for use by
361
# DockWindowFeature):
363
# fixme: This code is somewhat fragile since it assumes that if a
364
# DockControl is involved, the parent of this editor will be the
365
# control being managed by the DockControl.
366
parent = self.control.GetParent()
367
parent._object = self.value
368
dock_control = getattr( parent, '_dock_control', None )
369
if dock_control is not None:
370
dock_control.reset_tab()
372
# Synchronize the editor contents:
373
self.resynch_editor()
375
# Update the selector (if any):
376
choice = self._choice
377
item = self.item_for( self.value )
378
if (choice is not None) and (item is not None):
379
name = item.get_name( self.value )
380
if self._object_cache is not None:
381
if choice.FindString( name ) < 0:
382
choice.Append( name )
383
choice.SetStringSelection( name )
385
choice.SetValue( name )
387
#---------------------------------------------------------------------------
388
# Resynchronizes the contents of the editor when the object trait changes
389
# external to the editor:
390
#---------------------------------------------------------------------------
392
def resynch_editor ( self ):
393
""" Resynchronizes the contents of the editor when the object trait
394
changes externally to the editor.
397
if panel is not None:
398
# Compute/update the maximum size the panel has ever been:
399
dx, dy = panel.GetSizeTuple()
401
if self._panel_size is not None:
402
mdx, mdy = self._panel_size
403
self._panel_size = size = wx.Size( max( mdx, dx ), max( mdy, dy ) )
405
# Dispose of the previous contents of the panel:
406
panel.SetSizer( None )
407
if self._ui is not None:
411
panel.DestroyChildren()
413
# Create the new content for the panel:
414
sizer = wx.BoxSizer( wx.VERTICAL )
417
if not isinstance( value, HasTraits ):
419
if value is not None:
420
str_value = self.str_value
421
control = wx.StaticText( panel, -1, str_value )
423
view = self.view_for( value, self.item_for( value ) )
424
context = value.trait_context()
426
if isinstance( value, Handler ):
428
context.setdefault( 'context', self.object )
429
context.setdefault( 'context_handler', self.ui.handler )
430
self._ui = ui = view.ui( context, panel, 'subpanel',
431
value.trait_view_elements(), handler,
434
self.scrollable = ui._scrollable
437
if view.resizable or view.scrollable or ui._scrollable:
440
# Make sure the panel and its contents are correctly sized (This
441
# code is complicated by the various layout bugs present in wx.
442
# Tamper with it at your own risk!):
444
if stretch and (size != ( 20, 20 )):
445
control.SetSize( size )
446
panel.SetSize( size )
448
panel.SetSize( control.GetSize() )
449
sizer.Add( control, stretch, wx.EXPAND )
450
panel.SetSizer( sizer )
452
self.control.Layout()
453
parent = self.control.GetParent()
456
# It is possible that this instance editor is embedded at some level
457
# in a ScrolledWindow. If so, we need to inform the window that the
458
# size of the editor's contents have (potentially) changed:
459
# NB: There is a typo in the wxPython 2.6 code that prevents the
460
# 'SendSizeEvent' from working correctly, so we just skip it.
462
while ((parent is not None) and
463
(not isinstance( parent, wx.ScrolledWindow ))):
464
parent = parent.GetParent()
466
if parent is not None:
467
parent.SendSizeEvent()
469
#---------------------------------------------------------------------------
470
# Handles an error that occurs while setting the object's trait value:
471
#---------------------------------------------------------------------------
473
def error ( self, excp ):
474
""" Handles an error that occurs while setting the object's trait value.
478
#---------------------------------------------------------------------------
479
# Returns the editor's control for indicating error status:
480
#---------------------------------------------------------------------------
482
def get_error_control ( self ):
483
""" Returns the editor's control for indicating error status.
485
return (self._choice or self.control)
487
#-- UI preference save/restore interface -----------------------------------
489
#---------------------------------------------------------------------------
490
# Restores any saved user preference information associated with the
492
#---------------------------------------------------------------------------
494
def restore_prefs ( self, prefs ):
495
""" Restores any saved user preference information associated with the
499
if (ui is not None) and (prefs.get( 'id' ) == ui.id):
500
ui.set_prefs( prefs.get( 'prefs' ) )
502
#---------------------------------------------------------------------------
503
# Returns any user preference information associated with the editor:
504
#---------------------------------------------------------------------------
506
def save_prefs ( self ):
507
""" Returns any user preference information associated with the editor.
510
if (ui is not None) and (ui.id != ''):
511
return { 'id': ui.id,
512
'prefs': ui.get_prefs() }
516
#-- Drag and drop event handlers -------------------------------------------
518
#---------------------------------------------------------------------------
519
# Handles a Python object being dropped on the control:
520
#---------------------------------------------------------------------------
522
def wx_dropped_on ( self, x, y, data, drag_result ):
523
""" Handles a Python object being dropped on the tree.
525
for item in self.items:
526
if item.is_droppable() and item.is_compatible( data ):
527
if self._object_cache is not None:
534
#---------------------------------------------------------------------------
535
# Handles a Python object being dragged over the control:
536
#---------------------------------------------------------------------------
538
def wx_drag_over ( self, x, y, data, drag_result ):
539
""" Handles a Python object being dragged over the tree.
541
for item in self.items:
542
if item.is_droppable() and item.is_compatible( data ):
547
#-- Traits event handlers --------------------------------------------------
549
def _view_changed ( self, view ):
550
self.resynch_editor()
552
#-------------------------------------------------------------------------------
553
# 'SimpleEditor' class:
554
#-------------------------------------------------------------------------------
556
class SimpleEditor ( CustomEditor ):
557
""" Simple style of editor for instances, which displays a button. Clicking
558
the button displays a dialog box in which the instance can be edited.
562
orientation = wx.HORIZONTAL
565
#---------------------------------------------------------------------------
566
# Creates the editor control:
567
#---------------------------------------------------------------------------
569
def create_editor ( self, parent, sizer ):
570
""" Creates the editor control (a button).
572
self._button = button = wx.Button( parent, -1, '' )
573
sizer.Add( button, 1, wx.EXPAND | wx.LEFT, 5 )
574
wx.EVT_BUTTON( button, button.GetId(), self.edit_instance )
576
#---------------------------------------------------------------------------
577
# Disposes of the contents of an editor:
578
#---------------------------------------------------------------------------
580
def dispose ( self ):
581
""" Disposes of the contents of an editor.
583
button = self._button
584
if button is not None:
585
wx.EVT_BUTTON( button, button.GetId(), None )
587
super( SimpleEditor, self ).dispose()
589
#---------------------------------------------------------------------------
590
# Edit the contents of the object trait when the user clicks the button:
591
#---------------------------------------------------------------------------
593
def edit_instance ( self, event ):
594
""" Edit the contents of the object trait when the user clicks the
597
# Create the user interface:
598
factory = self.factory
599
view = self.ui.handler.trait_view_for( self.ui.info, factory.view,
600
self.value, self.object_name,
602
ui = self.value.edit_traits( view, self.control, factory.kind,
605
# Check to see if the view was 'modal', in which case it will already
606
# have been closed (i.e. is None) by the time we get control back:
607
if ui.control is not None:
608
# Position the window on the display:
609
position_window( ui.control )
611
# Chain our undo history to the new user interface if it does not
613
if ui.history is None:
614
ui.history = self.ui.history
616
#---------------------------------------------------------------------------
617
# Resynchronizes the contents of the editor when the object trait changes
618
# external to the editor:
619
#---------------------------------------------------------------------------
621
def resynch_editor ( self ):
622
""" Resynchronizes the contents of the editor when the object trait
623
changes externally to the editor.
625
button = self._button
626
if button is not None:
627
label = self.factory.label
629
label = user_name_for( self.name )
630
button.SetLabel( label )
631
button.Enable( isinstance( self.value, HasTraits ) )
633
### EOF #######################################################################