1
#-------------------------------------------------------------------------------
3
# Copyright (c) 2007, 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
""" A traits UI editor for editing tabular data (arrays, list of tuples, lists
22
#-------------------------------------------------------------------------------
24
#-------------------------------------------------------------------------------
27
import wx.lib.mixins.listctrl as listmix
30
import HasStrictTraits, Int, \
31
List, Bool, Instance, Any, Event, \
32
Property, TraitListEvent
34
# FIXME: TabularEditor (the editor factory for tabular editors) is a proxy class
35
# defined here just for backward compatibility. The class has been moved to the
36
# traitsui.editors.tabular_editor file.
37
from traitsui.editors.tabular_editor \
40
from traitsui.ui_traits \
43
from traitsui.tabular_adapter \
46
from traitsui.wx.editor \
49
from pyface.image_resource \
52
from pyface.timer.api \
56
import is_mac, scrollbar_dx
59
from pyface.wx.drag_and_drop \
60
import PythonDropSource, PythonDropTarget
62
PythonDropSource = PythonDropTarget = None
64
#-------------------------------------------------------------------------------
66
#-------------------------------------------------------------------------------
68
# Mapping for trait alignment values to wx alignment values:
70
'left': wx.LIST_FORMAT_LEFT,
71
'center': wx.LIST_FORMAT_CENTRE,
72
'right': wx.LIST_FORMAT_RIGHT
75
class TextEditMixin(listmix.TextEditMixin):
76
def __init__(self, edit_labels):
77
""" edit_labels controls whether the first column is editable
79
self.edit_labels = edit_labels
80
listmix.TextEditMixin.__init__(self)
82
def OpenEditor(self, col, row):
83
if col == 0 and not self.edit_labels:
86
return listmix.TextEditMixin.OpenEditor(self, col, row)
88
#-------------------------------------------------------------------------------
90
#-------------------------------------------------------------------------------
92
class wxListCtrl ( wx.ListCtrl, TextEditMixin ):
93
""" Subclass of wx.ListCtrl to provide correct virtual list behavior.
96
def __init__(self, parent, ID, pos=wx.DefaultPosition, size=wx.DefaultSize,
97
style=0, can_edit = False, edit_labels=False):
99
wx.ListCtrl.__init__(self, parent, ID, pos, size, style)
101
# if the selected is editable, then we have to init the mixin
103
TextEditMixin.__init__(self, edit_labels)
105
def SetVirtualData(self, row, col, text):
106
# this method is called but the job is already done by
107
# the _end_label_edit method. Commmented code is availabed
111
#return editor.adapter.set_text( editor.object, editor.name,
115
def OnGetItemAttr ( self, row ):
116
""" Returns the display attributes to use for the specified list item.
118
# fixme: There appears to be a bug in wx in that they do not correctly
119
# manage the reference count for the returned object, and it seems to be
120
# gc'ed before they finish using it. So we store an object reference to
121
# it to prevent it from going away too soon...
122
self._attr = attr = wx.ListItemAttr()
123
editor = self._editor
124
object, name = editor.object, editor.name
126
color = editor.adapter.get_bg_color( object, name, row )
127
if color is not None:
128
attr.SetBackgroundColour( color )
130
color = editor.adapter.get_text_color( object, name, row )
131
if color is not None:
132
attr.SetTextColour( color )
134
font = editor.adapter.get_font( object, name, row )
140
def OnGetItemImage ( self, row ):
141
""" Returns the image index to use for the specified list item.
143
editor = self._editor
144
image = editor._get_image( editor.adapter.get_image( editor.object,
145
editor.name, row, 0 ) )
146
if image is not None:
151
def OnGetItemColumnImage ( self, row, column ):
152
""" Returns the image index to use for the specified list item.
154
editor = self._editor
155
image = editor._get_image( editor.adapter.get_image( editor.object,
156
editor.name, row, column ) )
157
if image is not None:
162
def OnGetItemText ( self, row, column ):
163
""" Returns the text to use for the specified list item.
165
editor = self._editor
166
return editor.adapter.get_text( editor.object, editor.name,
169
#-------------------------------------------------------------------------------
170
# 'TabularEditor' class:
171
#-------------------------------------------------------------------------------
173
class TabularEditor ( Editor ):
174
""" A traits UI editor for editing tabular data (arrays, list of tuples,
175
lists of objects, etc).
178
#-- Trait Definitions ------------------------------------------------------
180
# The event fired when a table update is needed:
183
# The current set of selected items (which one is used depends upon the
184
# initial state of the editor factory 'multi_select' trait):
186
multi_selected = List
188
# The current set of selected item indices (which one is used depends upon
189
# the initial state of the editor factory 'multi_select' trait):
190
selected_row = Int( -1 )
191
multi_selected_rows = List( Int )
193
# The most recently actived item and its index:
197
# The most recent left click data:
198
clicked = Instance( 'TabularEditorEvent' )
200
# The most recent left double click data:
201
dclicked = Instance( 'TabularEditorEvent' )
203
# The most recent right click data:
204
right_clicked = Instance( 'TabularEditorEvent' )
206
# The most recent right double click data:
207
right_dclicked = Instance( 'TabularEditorEvent' )
209
# The most recent column click data:
210
column_clicked = Instance( 'TabularEditorEvent' )
212
# Is the tabular editor scrollable? This value overrides the default.
215
# Row index of item to select after rebuilding editor list:
218
# Should the selected item be edited after rebuilding the editor list:
221
# The adapter from trait values to editor values:
222
adapter = Instance( TabularAdapter )
224
# Dictionary mapping image names to wx.ImageList indices:
227
# Dictionary mapping ImageResource objects to wx.ImageList indices:
228
image_resources = Any( {} )
230
# An image being converted:
233
# Flag for marking whether the update was within the visible area
234
_update_visible = Bool(False)
236
#---------------------------------------------------------------------------
237
# Finishes initializing the editor by creating the underlying toolkit
239
#---------------------------------------------------------------------------
241
def init ( self, parent ):
242
""" Finishes initializing the editor by creating the underlying toolkit
245
factory = self.factory
247
# Set up the adapter to use:
248
self.adapter = factory.adapter
250
# Determine the style to use for the list control:
251
style = wx.LC_REPORT | wx.LC_VIRTUAL | wx.BORDER_NONE
253
if factory.editable_labels:
254
style |= wx.LC_EDIT_LABELS
256
if factory.horizontal_lines:
257
style |= wx.LC_HRULES
259
if factory.vertical_lines:
260
style |= wx.LC_VRULES
262
if not factory.multi_select:
263
style |= wx.LC_SINGLE_SEL
265
if not factory.show_titles:
266
style |= wx.LC_NO_HEADER
268
# Create the list control and link it back to us:
269
self.control = control = wxListCtrl( parent, -1, style = style,
270
can_edit = factory.editable,
271
edit_labels = factory.editable_labels)
272
control._editor = self
274
# Create the list control column:
275
#fixme: what do we do here?
276
#control.InsertColumn( 0, '' )
278
# Set up the list control's event handlers:
280
wx.EVT_LIST_BEGIN_DRAG( parent, id, self._begin_drag )
281
wx.EVT_LIST_BEGIN_LABEL_EDIT( parent, id, self._begin_label_edit )
282
wx.EVT_LIST_END_LABEL_EDIT( parent, id, self._end_label_edit )
283
wx.EVT_LIST_ITEM_SELECTED( parent, id, self._item_selected )
284
wx.EVT_LIST_ITEM_DESELECTED( parent, id, self._item_selected )
285
wx.EVT_LIST_KEY_DOWN( parent, id, self._key_down )
286
wx.EVT_LIST_ITEM_ACTIVATED( parent, id, self._item_activated )
287
wx.EVT_LIST_COL_END_DRAG( parent, id, self._size_modified )
288
wx.EVT_LIST_COL_RIGHT_CLICK( parent, id, self._column_right_clicked )
289
wx.EVT_LIST_COL_CLICK( parent, id, self._column_clicked )
290
wx.EVT_LEFT_DOWN( control, self._left_down )
291
wx.EVT_LEFT_DCLICK( control, self._left_dclick )
292
wx.EVT_RIGHT_DOWN( control, self._right_down )
293
wx.EVT_RIGHT_DCLICK( control, self._right_dclick )
294
wx.EVT_MOTION( control, self._motion )
295
wx.EVT_SIZE( control, self._size_modified )
297
# Set up the drag and drop target:
298
if PythonDropTarget is not None:
299
control.SetDropTarget( PythonDropTarget( self ) )
301
# Set up the selection listener (if necessary):
302
if factory.multi_select:
303
self.sync_value( factory.selected, 'multi_selected', 'both',
305
self.sync_value( factory.selected_row, 'multi_selected_rows',
306
'both', is_list = True )
308
self.sync_value( factory.selected, 'selected', 'both' )
309
self.sync_value( factory.selected_row, 'selected_row', 'both' )
311
# Synchronize other interesting traits as necessary:
312
self.sync_value( factory.update, 'update', 'from' )
314
self.sync_value( factory.activated, 'activated', 'to' )
315
self.sync_value( factory.activated_row, 'activated_row', 'to' )
317
self.sync_value( factory.clicked, 'clicked', 'to' )
318
self.sync_value( factory.dclicked, 'dclicked', 'to' )
320
self.sync_value( factory.right_clicked, 'right_clicked', 'to' )
321
self.sync_value( factory.right_dclicked, 'right_dclicked', 'to' )
323
self.sync_value( factory.column_clicked, 'column_clicked', 'to' )
325
# Make sure we listen for 'items' changes as well as complete list
328
self.context_object.on_trait_change( self.update_editor,
329
self.extended_name + '_items', dispatch = 'ui' )
333
# If the user has requested automatic update, attempt to set up the
334
# appropriate listeners:
335
if factory.auto_update:
336
self.context_object.on_trait_change( self.refresh_editor,
337
self.extended_name + '.-', dispatch = 'ui' )
339
# Create the mapping from user supplied images to wx.ImageList indices:
340
for image_resource in factory.images:
341
self._add_image( image_resource )
343
# Refresh the editor whenever the adapter changes:
344
self.on_trait_change( self._refresh, 'adapter.+update',
347
# Rebuild the editor columns and headers whenever the adapter's
349
self.on_trait_change( self._rebuild_all, 'adapter.columns',
352
# Make sure the tabular view gets initialized:
355
# Set the list control's tooltip:
358
def dispose ( self ):
359
""" Disposes of the contents of an editor.
361
# Remove all of the wx event handlers:
362
control = self.control
363
parent = control.GetParent()
365
wx.EVT_LIST_BEGIN_DRAG( parent, id, None )
366
wx.EVT_LIST_BEGIN_LABEL_EDIT( parent, id, None )
367
wx.EVT_LIST_END_LABEL_EDIT( parent, id, None )
368
wx.EVT_LIST_ITEM_SELECTED( parent, id, None )
369
wx.EVT_LIST_ITEM_DESELECTED( parent, id, None )
370
wx.EVT_LIST_KEY_DOWN( parent, id, None )
371
wx.EVT_LIST_ITEM_ACTIVATED( parent, id, None )
372
wx.EVT_LIST_COL_END_DRAG( parent, id, None )
373
wx.EVT_LIST_COL_RIGHT_CLICK( parent, id, None )
374
wx.EVT_LIST_COL_CLICK( parent, id, None )
375
wx.EVT_LEFT_DOWN( control, None )
376
wx.EVT_LEFT_DCLICK( control, None )
377
wx.EVT_RIGHT_DOWN( control, None )
378
wx.EVT_RIGHT_DCLICK( control, None )
379
wx.EVT_MOTION( control, None )
380
wx.EVT_SIZE( control, None )
382
self.context_object.on_trait_change( self.update_editor,
383
self.extended_name + '_items', remove = True )
385
if self.factory.auto_update:
386
self.context_object.on_trait_change( self.refresh_editor,
387
self.extended_name + '.-', remove = True )
389
self.on_trait_change( self._refresh, 'adapter.+update', remove = True )
390
self.on_trait_change( self._rebuild_all, 'adapter.columns',
393
super( TabularEditor, self ).dispose()
395
def _update_changed ( self, event ):
396
""" Handles the 'update' event being fired.
400
elif isinstance( event, int ):
401
self._refresh_row( event )
403
self._refresh_editor( event )
405
def refresh_editor ( self, item, name, old, new ):
406
""" Handles a table item attribute being changed.
408
self._refresh_editor( item )
410
def _refresh_editor ( self, item ):
411
""" Handles a table item being changed.
413
adapter = self.adapter
414
object, name = self.object, self.name
415
agi = adapter.get_item
416
for row in xrange( adapter.len( object, name ) ):
417
if item is agi( object, name, row ):
418
self._refresh_row( row )
423
def _refresh_row ( self, row ):
424
""" Updates the editor control when a specified table row changes.
426
self.control.RefreshRect(
427
self.control.GetItemRect( row, wx.LIST_RECT_BOUNDS ) )
429
def _update_editor ( self, object, name, old_value, new_value ):
430
""" Performs updates when the object trait changes.
431
Overloads traitsui.editor.UIEditor
433
self._update_visible = True
435
super(TabularEditor, self)._update_editor(object, name,
436
old_value, new_value)
439
def update_editor ( self ):
440
""" Updates the editor when the object trait changes externally to the
443
control = self.control
444
n = self.adapter.len( self.object, self.name )
445
top = control.GetTopItem()
446
pn = control.GetCountPerPage()
447
bottom = min(top + pn - 1, n)
449
control.SetItemCount( n )
451
if self._update_visible:
452
control.RefreshItems( 0, n-1 )
453
self._update_visible = False
455
if len( self.multi_selected_rows ) > 0:
456
self._multi_selected_rows_changed( self.multi_selected_rows )
457
if len( self.multi_selected ) > 0:
458
self._multi_selected_changed( self.multi_selected )
460
edit, self.edit = self.edit, False
461
row, self.row = self.row, None
471
if visible >= 0 and visible < control.GetItemCount():
472
control.EnsureVisible( visible )
476
if 0 <= (row - top) < pn:
477
control.EnsureVisible( top + pn - 2 )
479
control.EnsureVisible( row + pn - 1 )
481
control.EnsureVisible( row )
483
control.SetItemState( row,
484
wx.LIST_STATE_SELECTED | wx.LIST_STATE_FOCUSED,
485
wx.LIST_STATE_SELECTED | wx.LIST_STATE_FOCUSED )
488
control.EditLabel( row )
490
#-- Trait Event Handlers ---------------------------------------------------
492
def _selected_changed ( self, selected ):
493
""" Handles the editor's 'selected' trait being changed.
495
if not self._no_update:
497
for row in self._get_selected():
498
self.control.SetItemState( row, 0, wx.LIST_STATE_SELECTED )
501
self.control.SetItemState( self.value.index( selected ),
502
wx.LIST_STATE_SELECTED, wx.LIST_STATE_SELECTED )
506
def _selected_row_changed ( self, old, new ):
507
""" Handles the editor's 'selected_index' trait being changed.
509
if not self._no_update:
512
self.control.SetItemState( old, 0, wx.LIST_STATE_SELECTED )
514
self.control.SetItemState( new, wx.LIST_STATE_SELECTED,
515
wx.LIST_STATE_SELECTED )
517
def _multi_selected_changed ( self, selected ):
518
""" Handles the editor's 'multi_selected' trait being changed.
520
if not self._no_update:
523
self._multi_selected_rows_changed( [ values.index( item )
524
for item in selected ] )
528
def _multi_selected_items_changed ( self, event ):
529
""" Handles the editor's 'multi_selected' trait being modified.
533
self._multi_selected_rows_items_changed( TraitListEvent( 0,
534
[ values.index( item ) for item in event.removed ],
535
[ values.index( item ) for item in event.added ] ) )
539
def _multi_selected_rows_changed ( self, selected_rows ):
540
""" Handles the editor's 'multi_selected_rows' trait being changed.
542
if not self._no_update:
543
control = self.control
544
selected = self._get_selected()
546
# Select any new items that aren't already selected:
547
for row in selected_rows:
549
selected.remove( row )
551
control.SetItemState( row, wx.LIST_STATE_SELECTED,
552
wx.LIST_STATE_SELECTED )
554
# Unselect all remaining selected items that aren't selected now:
556
control.SetItemState( row, 0, wx.LIST_STATE_SELECTED )
558
def _multi_selected_rows_items_changed ( self, event ):
559
""" Handles the editor's 'multi_selected_rows' trait being modified.
561
control = self.control
563
# Remove all items that are no longer selected:
564
for row in event.removed:
565
control.SetItemState( row, 0, wx.LIST_STATE_SELECTED )
567
# Select all newly added items:
568
for row in event.added:
569
control.SetItemState( row, wx.LIST_STATE_SELECTED,
570
wx.LIST_STATE_SELECTED )
572
#-- List Control Event Handlers --------------------------------------------
574
def _left_down ( self, event ):
575
""" Handles the left mouse button being pressed.
577
self._mouse_click( event, 'clicked' )
579
def _left_dclick ( self, event ):
580
""" Handles the left mouse button being double clicked.
582
self._mouse_click( event, 'dclicked' )
584
def _right_down ( self, event ):
585
""" Handles the right mouse button being pressed.
587
self._mouse_click( event, 'right_clicked' )
589
def _right_dclick ( self, event ):
590
""" Handles the right mouse button being double clicked.
592
self._mouse_click( event, 'right_dclicked' )
594
def _begin_drag ( self, event ):
595
""" Handles the user beginning a drag operation with the left mouse
598
if PythonDropSource is not None:
599
adapter = self.adapter
600
object, name = self.object, self.name
601
selected = self._get_selected()
604
# Collect all of the selected items to drag:
606
drag = adapter.get_drag( object, name, row )
610
drag_items.append( drag )
612
# Save the drag item indices, so that we can later handle a
613
# completed 'move' operation:
614
self._drag_rows = selected
617
# If only one item is being dragged, drag it as an item, not a
619
if len( drag_items ) == 1:
620
drag_items = drag_items[0]
622
# Perform the drag and drop operation:
623
ds = PythonDropSource( self.control, drag_items )
625
# If moves are allowed and the result was a drag move:
626
if ((ds.result == wx.DragMove) and
627
(self._drag_local or self.factory.drag_move)):
628
# Then delete all of the original items (in reverse order
629
# from highest to lowest, so the indices don't need to be
631
rows = self._drag_rows
634
adapter.delete( object, name, row )
636
self._drag_rows = None
637
self._drag_local = False
639
def _begin_label_edit ( self, event ):
640
""" Handles the user starting to edit an item label.
642
if not self.adapter.get_can_edit( self.object, self.name,
646
def _end_label_edit ( self, event ):
647
""" Handles the user finishing editing an item label.
649
self.adapter.set_text( self.object, self.name, event.GetIndex(),
650
event.GetColumn(), event.GetText() )
651
self.row = event.GetIndex() + 1
653
def _item_selected ( self, event ):
654
""" Handles an item being selected.
656
self._no_update = True
658
get_item = self.adapter.get_item
659
object, name = self.object, self.name
660
selected_rows = self._get_selected()
661
if self.factory.multi_select:
662
self.multi_selected_rows = selected_rows
663
self.multi_selected = [ get_item( object, name, row )
664
for row in selected_rows ]
665
elif len( selected_rows ) == 0:
666
self.selected_row = -1
669
self.selected_row = selected_rows[0]
670
self.selected = get_item( object, name, selected_rows[0] )
672
self._no_update = False
674
def _item_activated ( self, event ):
675
""" Handles an item being activated (double-clicked or enter pressed).
677
self.activated_row = event.GetIndex()
678
self.activated = self.adapter.get_item( self.object, self.name,
681
def _key_down ( self, event ):
682
""" Handles the user pressing a key in the list control.
684
key = event.GetKeyCode()
685
if key == wx.WXK_NEXT:
687
elif key in ( wx.WXK_BACK, wx.WXK_DELETE ):
688
self._delete_current()
689
elif key == wx.WXK_INSERT:
690
self._insert_current()
691
elif key == wx.WXK_LEFT:
692
self._move_up_current()
693
elif key == wx.WXK_RIGHT:
694
self._move_down_current()
695
elif key in ( wx.WXK_RETURN, wx.WXK_ESCAPE ):
700
def _column_right_clicked ( self, event ):
701
""" Handles the user right-clicking a column header.
703
column = event.GetColumn()
704
if ((self._cached_widths is not None) and
705
(0 <= column < len( self._cached_widths ))):
706
self._cached_widths[ column ] = None
707
self._size_modified( event )
709
def _column_clicked ( self, event ):
710
""" Handles the right mouse button being double clicked.
712
editor_event = TabularEditorEvent(
715
column = event.GetColumn()
718
setattr( self, 'column_clicked', editor_event)
721
def _size_modified ( self, event ):
722
""" Handles the size of the list control being changed.
724
control = self.control
725
n = control.GetColumnCount()
727
dx, dy = control.GetClientSizeTuple()
728
control.SetColumnWidth( 0, dx - 1 )
730
do_later( self._set_column_widths )
734
def _motion ( self, event ):
735
""" Handles the user moving the mouse.
738
column = self._get_column( x )
739
row, flags = self.control.HitTest( wx.Point( x, event.GetY() ) )
740
if (row != self._last_row) or (column != self._last_column):
741
self._last_row, self._last_column = row, column
742
if (row == -1) or (column is None):
745
tooltip = self.adapter.get_tooltip( self.object, self.name,
747
if tooltip != self._last_tooltip:
748
self._last_tooltip = tooltip
749
wx.ToolTip.Enable( False )
750
wx.ToolTip.Enable( True )
751
self.control.SetToolTip( wx.ToolTip( tooltip ) )
753
#-- Drag and Drop Event Handlers -------------------------------------------
755
def wx_dropped_on ( self, x, y, data, drag_result ):
756
""" Handles a Python object being dropped on the list control.
758
row, flags = self.control.HitTest( wx.Point( x, y ) )
760
# If the user dropped it on an empty list, set the target as past the
763
((flags & wx.LIST_HITTEST_NOWHERE) != 0) and
764
(self.control.GetItemCount() == 0)):
767
# If we have a valid drop target row, proceed:
769
if not isinstance( data, list ):
770
# Handle the case of just a single item being dropped:
771
self._wx_dropped_on( row, data )
773
# Handles the case of a list of items being dropped, being
774
# careful to preserve the original order of the source items if
778
self._wx_dropped_on( row, item )
780
# If this was an inter-list drag, mark it as 'local':
781
if self._drag_indices is not None:
782
self._drag_local = True
784
# Return a successful drop result:
787
# Indicate we could not process the drop:
790
def _wx_dropped_on ( self, row, item ):
791
""" Helper method for handling a single item dropped on the list
794
adapter = self.adapter
795
object, name = self.object, self.name
797
# Obtain the destination of the dropped item relative to the target:
798
destination = adapter.get_dropped( object, name, row, item )
800
# Adjust the target index accordingly:
801
if destination == 'after':
804
# Insert the dropped item at the requested position:
805
adapter.insert( object, name, row, item )
807
# If the source for the drag was also this list control, we need to
808
# adjust the original source indices to account for their new position
809
# after the drag operation:
810
rows = self._drag_rows
812
for i in range( len( rows ) - 1, -1, -1 ):
818
def wx_drag_over ( self, x, y, data, drag_result ):
819
""" Handles a Python object being dragged over the tree.
821
if isinstance( data, list ):
824
rc = self.wx_drag_over( x, y, item, drag_result )
825
if rc == wx.DragNone:
830
row, flags = self.control.HitTest( wx.Point( x, y ) )
832
# If the user is dragging over an empty list, set the target to the end
835
((flags & wx.LIST_HITTEST_NOWHERE) != 0) and
836
(self.control.GetItemCount() == 0)):
839
# If the drag target index is valid and the adapter says it is OK to
840
# drop the data here, then indicate the data can be dropped:
842
self.adapter.get_can_drop( self.object, self.name, row, data )):
845
# Else indicate that we will not accept the data:
848
#-- UI preference save/restore interface -----------------------------------
850
def restore_prefs ( self, prefs ):
851
""" Restores any saved user preference information associated with the
854
self._cached_widths = cws = prefs.get( 'cached_widths' )
856
set_column_width = self.control.SetColumnWidth
857
for i, width in enumerate( cws ):
858
if width is not None:
859
set_column_width( i, width )
861
def save_prefs ( self ):
862
""" Returns any user preference information associated with the editor.
864
cws = self._cached_widths
866
cws = [ ( None, cw )[ cw >= 0 ] for cw in cws ]
868
return { 'cached_widths': cws }
870
#-- Private Methods --------------------------------------------------------
872
def _refresh ( self ):
873
""" Refreshes the contents of the editor's list control.
875
n = self.adapter.len( self.object, self.name )
877
self.control.RefreshItems( 0, n - 1)
879
def _rebuild ( self ):
880
""" Rebuilds the contents of the editor's list control.
882
control = self.control
884
adapter, object, name = self.adapter, self.object, self.name
885
adapter.object, adapter.name = object, name
886
get_alignment = adapter.get_alignment
887
get_width = adapter.get_width
888
for i, label in enumerate( adapter.label_map ):
889
control.InsertColumn( i, label,
890
alignment_map.get( get_alignment( object, name, i ),
891
wx.LIST_FORMAT_LEFT ) )
892
self._set_column_widths()
894
def _rebuild_all ( self ):
895
""" Rebuilds the structure of the list control, then refreshes its
901
def _set_column_widths ( self ):
902
""" Set the column widths for the current set of columns.
904
control = self.control
908
object, name = self.object, self.name
909
dx, dy = control.GetClientSize()
912
n = control.GetColumnCount()
913
get_width = self.adapter.get_width
917
cached = self._cached_widths
918
current = [ control.GetColumnWidth( i ) for i in xrange( n ) ]
919
if (cached is None) or (len( cached ) != n):
920
self._cached_widths = cached = [ None ] * n
922
for i in xrange( n ):
924
if (cw is None) or (-cw == current[i]):
925
width = float( get_width( object, name, i ) )
937
cached[i] = width = current[i]
940
widths.append( width )
942
adx = max( 0, dx - pdx )
950
widths[i] = w = max( 30, int( round( (adx * width)/wdx ) ) )
956
control.SetColumnWidth( i, width )
960
def _add_image ( self, image_resource ):
961
""" Adds a new image to the wx.ImageList and its associated mapping.
963
bitmap = image_resource.create_image().ConvertToBitmap()
965
image_list = self._image_list
966
if image_list is None:
967
self._image_list = image_list = wx.ImageList( bitmap.GetWidth(),
969
self.control.AssignImageList( image_list, wx.IMAGE_LIST_SMALL )
971
self.image_resources[image_resource] = \
972
self.images[ image_resource.name ] = row = image_list.Add( bitmap )
976
def _get_image ( self, image ):
977
""" Converts a user specified image to a wx.ListCtrl image index.
979
if isinstance( image, basestring ):
983
if isinstance( image, ImageResource ):
984
result = self.image_resources.get( image )
985
if result is not None:
988
return self._add_image( image )
990
return self.images.get( image )
992
def _get_selected ( self ):
993
""" Returns a list of the rows of all currently selected list items.
997
control = self.control
999
# Handle case where the list is cleared
1000
if len( self.value ) == 0:
1004
item = control.GetNextItem( item, wx.LIST_NEXT_ALL,
1005
wx.LIST_STATE_SELECTED )
1009
selected.append( item )
1013
def _append_new ( self ):
1014
""" Append a new item to the end of the list control.
1016
if 'append' in self.factory.operations:
1017
adapter = self.adapter
1018
self.row = self.control.GetItemCount()
1020
adapter.insert( self.object, self.name, self.row,
1021
adapter.get_default_value( self.object, self.name ) )
1023
def _insert_current ( self ):
1024
""" Inserts a new item after the currently selected list control item.
1026
if 'insert' in self.factory.operations:
1027
selected = self._get_selected()
1028
if len( selected ) == 1:
1029
adapter = self.adapter
1030
adapter.insert( self.object, self.name, selected[0],
1031
adapter.get_default_value( self.object, self.name ) )
1032
self.row = selected[0]
1035
def _delete_current ( self ):
1036
""" Deletes the currently selected items from the list control.
1038
if 'delete' in self.factory.operations:
1039
selected = self._get_selected()
1040
if len( selected ) == 0:
1043
delete = self.adapter.delete
1045
for row in selected:
1046
delete( self.object, self.name, row )
1050
def _move_up_current ( self ):
1051
""" Moves the currently selected item up one line in the list control.
1053
if 'move' in self.factory.operations:
1054
selected = self._get_selected()
1055
if len( selected ) == 1:
1058
adapter = self.adapter
1059
object, name = self.object, self.name
1060
item = adapter.get_item( object, name, row )
1061
adapter.delete( object, name, row )
1062
adapter.insert( object, name, row - 1, item )
1065
def _move_down_current ( self ):
1066
""" Moves the currently selected item down one line in the list control.
1068
if 'move' in self.factory.operations:
1069
selected = self._get_selected()
1070
if len( selected ) == 1:
1072
if row < (self.control.GetItemCount() - 1):
1073
adapter = self.adapter
1074
object, name = self.object, self.name
1075
item = adapter.get_item( object, name, row )
1076
adapter.delete( object, name, row )
1077
adapter.insert( object, name, row + 1, item )
1080
def _edit_current ( self ):
1081
""" Allows the user to edit the current item in the list control.
1083
if 'edit' in self.factory.operations and self.factory.editable_labels:
1084
selected = self._get_selected()
1085
if len( selected ) == 1:
1086
self.control.EditLabel( selected[0] )
1088
def _get_column ( self, x, translate = False ):
1089
""" Returns the column index corresponding to a specified x position.
1092
control = self.control
1093
for i in range( control.GetColumnCount() ):
1094
x -= control.GetColumnWidth( i )
1097
return self.adapter.get_column(
1098
self.object, self.name, i )
1104
def _mouse_click ( self, event, trait ):
1105
""" Generate a TabularEditorEvent event for a specified mouse event and
1109
row, flags = self.control.HitTest( wx.Point( x, event.GetY() ) )
1110
if row == wx.NOT_FOUND:
1111
if self.factory.multi_select:
1112
self.multi_selected = []
1113
self.multi_selected_rows = []
1115
self.selected = None
1116
self.selected_row = -1
1118
if self.factory.multi_select and event.ShiftDown():
1119
# Handle shift-click multi-selections because the wx.ListCtrl
1120
# does not (by design, apparently).
1121
# We must append this to the event queue because the
1122
# multi-selection will not be recorded until this event handler
1123
# finishes and lets the widget actually handle the event.
1124
do_later( self._item_selected, None )
1126
setattr( self, trait, TabularEditorEvent(
1129
column = self._get_column( x, translate = True )
1132
# wx should continue with additional event handlers. Skip(False)
1133
# actually means to skip looking, skip(True) means to keep looking.
1134
# This seems backwards to me...
1137
#-------------------------------------------------------------------------------
1138
# 'TabularEditorEvent' class:
1139
#-------------------------------------------------------------------------------
1141
class TabularEditorEvent ( HasStrictTraits ):
1143
# The index of the row:
1146
# The id of the column (either a string or an integer):
1152
#-- Private Traits ---------------------------------------------------------
1154
# The editor the event is associated with:
1155
editor = Instance( TabularEditor )
1157
#-- Property Implementations -----------------------------------------------
1159
def _get_item ( self ):
1160
editor = self.editor
1161
return editor.adapter.get_item( editor.object, editor.name, self.row )