~ubuntu-branches/ubuntu/trusty/python-traitsui/trusty

« back to all changes in this revision

Viewing changes to traitsui/wx/tabular_editor.py

  • Committer: Bazaar Package Importer
  • Author(s): Varun Hiremath
  • Date: 2011-07-09 13:57:39 UTC
  • Revision ID: james.westby@ubuntu.com-20110709135739-x5u20q86huissmn1
Tags: upstream-4.0.0
ImportĀ upstreamĀ versionĀ 4.0.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#-------------------------------------------------------------------------------
 
2
#
 
3
#  Copyright (c) 2007, Enthought, Inc.
 
4
#  All rights reserved.
 
5
#
 
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
 
10
#
 
11
#  Thanks for using Enthought open source!
 
12
#
 
13
#  Author: David C. Morrill
 
14
#  Date:   05/20/2007
 
15
#
 
16
#-------------------------------------------------------------------------------
 
17
 
 
18
""" A traits UI editor for editing tabular data (arrays, list of tuples, lists
 
19
    of objects, etc).
 
20
"""
 
21
 
 
22
#-------------------------------------------------------------------------------
 
23
#  Imports:
 
24
#-------------------------------------------------------------------------------
 
25
 
 
26
import wx
 
27
import wx.lib.mixins.listctrl as listmix
 
28
 
 
29
from traits.api \
 
30
    import HasStrictTraits, Int, \
 
31
           List, Bool, Instance, Any, Event, \
 
32
           Property, TraitListEvent
 
33
 
 
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 \
 
38
    import TabularEditor
 
39
 
 
40
from traitsui.ui_traits \
 
41
    import Image
 
42
 
 
43
from traitsui.tabular_adapter \
 
44
    import TabularAdapter
 
45
 
 
46
from traitsui.wx.editor \
 
47
    import Editor
 
48
 
 
49
from pyface.image_resource \
 
50
    import ImageResource
 
51
 
 
52
from pyface.timer.api \
 
53
    import do_later
 
54
 
 
55
from constants \
 
56
    import is_mac, scrollbar_dx
 
57
 
 
58
try:
 
59
    from pyface.wx.drag_and_drop \
 
60
        import PythonDropSource, PythonDropTarget
 
61
except:
 
62
    PythonDropSource = PythonDropTarget = None
 
63
 
 
64
#-------------------------------------------------------------------------------
 
65
#  Constants:
 
66
#-------------------------------------------------------------------------------
 
67
 
 
68
# Mapping for trait alignment values to wx alignment values:
 
69
alignment_map = {
 
70
    'left':   wx.LIST_FORMAT_LEFT,
 
71
    'center': wx.LIST_FORMAT_CENTRE,
 
72
    'right':  wx.LIST_FORMAT_RIGHT
 
73
}
 
74
 
 
75
class TextEditMixin(listmix.TextEditMixin):
 
76
    def __init__(self, edit_labels):
 
77
        """ edit_labels controls whether the first column is editable
 
78
        """
 
79
        self.edit_labels = edit_labels
 
80
        listmix.TextEditMixin.__init__(self)
 
81
 
 
82
    def OpenEditor(self, col, row):
 
83
        if col == 0 and not self.edit_labels:
 
84
            return
 
85
        else:
 
86
            return listmix.TextEditMixin.OpenEditor(self, col, row)
 
87
 
 
88
#-------------------------------------------------------------------------------
 
89
#  'wxListCtrl' class:
 
90
#-------------------------------------------------------------------------------
 
91
 
 
92
class wxListCtrl ( wx.ListCtrl, TextEditMixin ):
 
93
    """ Subclass of wx.ListCtrl to provide correct virtual list behavior.
 
94
    """
 
95
 
 
96
    def __init__(self, parent, ID, pos=wx.DefaultPosition, size=wx.DefaultSize,
 
97
                 style=0, can_edit = False, edit_labels=False):
 
98
 
 
99
        wx.ListCtrl.__init__(self, parent, ID, pos, size, style)
 
100
 
 
101
        # if the selected is editable, then we have to init the mixin
 
102
        if can_edit:
 
103
            TextEditMixin.__init__(self, edit_labels)
 
104
 
 
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
 
108
        # if needed
 
109
        pass
 
110
        #edit = self._editor
 
111
        #return editor.adapter.set_text( editor.object, editor.name,
 
112
        #                                row, col, text )
 
113
 
 
114
 
 
115
    def OnGetItemAttr ( self, row ):
 
116
        """ Returns the display attributes to use for the specified list item.
 
117
        """
 
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
 
125
 
 
126
        color = editor.adapter.get_bg_color( object, name, row )
 
127
        if color is not None:
 
128
            attr.SetBackgroundColour( color )
 
129
 
 
130
        color = editor.adapter.get_text_color( object, name, row )
 
131
        if color is not None:
 
132
            attr.SetTextColour( color )
 
133
 
 
134
        font = editor.adapter.get_font( object, name, row )
 
135
        if font is not None:
 
136
            attr.SetFont( font )
 
137
 
 
138
        return attr
 
139
 
 
140
    def OnGetItemImage ( self, row ):
 
141
        """ Returns the image index to use for the specified list item.
 
142
        """
 
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:
 
147
            return image
 
148
 
 
149
        return -1
 
150
 
 
151
    def OnGetItemColumnImage ( self, row, column ):
 
152
        """ Returns the image index to use for the specified list item.
 
153
        """
 
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:
 
158
            return image
 
159
 
 
160
        return -1
 
161
 
 
162
    def OnGetItemText ( self, row, column ):
 
163
        """ Returns the text to use for the specified list item.
 
164
        """
 
165
        editor = self._editor
 
166
        return editor.adapter.get_text( editor.object, editor.name,
 
167
                                        row, column )
 
168
 
 
169
#-------------------------------------------------------------------------------
 
170
#  'TabularEditor' class:
 
171
#-------------------------------------------------------------------------------
 
172
 
 
173
class TabularEditor ( Editor ):
 
174
    """ A traits UI editor for editing tabular data (arrays, list of tuples,
 
175
        lists of objects, etc).
 
176
    """
 
177
 
 
178
    #-- Trait Definitions ------------------------------------------------------
 
179
 
 
180
    # The event fired when a table update is needed:
 
181
    update = Event
 
182
 
 
183
    # The current set of selected items (which one is used depends upon the
 
184
    # initial state of the editor factory 'multi_select' trait):
 
185
    selected       = Any
 
186
    multi_selected = List
 
187
 
 
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 )
 
192
 
 
193
    # The most recently actived item and its index:
 
194
    activated     = Any
 
195
    activated_row = Int
 
196
 
 
197
    # The most recent left click data:
 
198
    clicked = Instance( 'TabularEditorEvent' )
 
199
 
 
200
    # The most recent left double click data:
 
201
    dclicked = Instance( 'TabularEditorEvent' )
 
202
 
 
203
    # The most recent right click data:
 
204
    right_clicked = Instance( 'TabularEditorEvent' )
 
205
 
 
206
    # The most recent right double click data:
 
207
    right_dclicked = Instance( 'TabularEditorEvent' )
 
208
 
 
209
    # The most recent column click data:
 
210
    column_clicked = Instance( 'TabularEditorEvent' )
 
211
 
 
212
    # Is the tabular editor scrollable? This value overrides the default.
 
213
    scrollable = True
 
214
 
 
215
    # Row index of item to select after rebuilding editor list:
 
216
    row = Any
 
217
 
 
218
    # Should the selected item be edited after rebuilding the editor list:
 
219
    edit = Bool( False )
 
220
 
 
221
    # The adapter from trait values to editor values:
 
222
    adapter = Instance( TabularAdapter )
 
223
 
 
224
    # Dictionary mapping image names to wx.ImageList indices:
 
225
    images = Any( {} )
 
226
 
 
227
    # Dictionary mapping ImageResource objects to wx.ImageList indices:
 
228
    image_resources = Any( {} )
 
229
 
 
230
    # An image being converted:
 
231
    image = Image
 
232
 
 
233
    # Flag for marking whether the update was within the visible area
 
234
    _update_visible = Bool(False)
 
235
 
 
236
    #---------------------------------------------------------------------------
 
237
    #  Finishes initializing the editor by creating the underlying toolkit
 
238
    #  widget:
 
239
    #---------------------------------------------------------------------------
 
240
 
 
241
    def init ( self, parent ):
 
242
        """ Finishes initializing the editor by creating the underlying toolkit
 
243
            widget.
 
244
        """
 
245
        factory = self.factory
 
246
 
 
247
        # Set up the adapter to use:
 
248
        self.adapter = factory.adapter
 
249
 
 
250
        # Determine the style to use for the list control:
 
251
        style = wx.LC_REPORT | wx.LC_VIRTUAL | wx.BORDER_NONE
 
252
 
 
253
        if factory.editable_labels:
 
254
            style |= wx.LC_EDIT_LABELS
 
255
 
 
256
        if factory.horizontal_lines:
 
257
            style |= wx.LC_HRULES
 
258
 
 
259
        if factory.vertical_lines:
 
260
            style |= wx.LC_VRULES
 
261
 
 
262
        if not factory.multi_select:
 
263
            style |= wx.LC_SINGLE_SEL
 
264
 
 
265
        if not factory.show_titles:
 
266
            style |= wx.LC_NO_HEADER
 
267
 
 
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
 
273
 
 
274
        # Create the list control column:
 
275
        #fixme: what do we do here?
 
276
        #control.InsertColumn( 0, '' )
 
277
 
 
278
        # Set up the list control's event handlers:
 
279
        id = control.GetId()
 
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 )
 
296
 
 
297
        # Set up the drag and drop target:
 
298
        if PythonDropTarget is not None:
 
299
            control.SetDropTarget( PythonDropTarget( self ) )
 
300
 
 
301
        # Set up the selection listener (if necessary):
 
302
        if factory.multi_select:
 
303
            self.sync_value( factory.selected, 'multi_selected', 'both',
 
304
                             is_list = True )
 
305
            self.sync_value( factory.selected_row, 'multi_selected_rows',
 
306
                             'both', is_list = True )
 
307
        else:
 
308
            self.sync_value( factory.selected, 'selected', 'both' )
 
309
            self.sync_value( factory.selected_row, 'selected_row', 'both' )
 
310
 
 
311
        # Synchronize other interesting traits as necessary:
 
312
        self.sync_value( factory.update, 'update', 'from' )
 
313
 
 
314
        self.sync_value( factory.activated,     'activated',     'to' )
 
315
        self.sync_value( factory.activated_row, 'activated_row', 'to' )
 
316
 
 
317
        self.sync_value( factory.clicked,  'clicked',  'to' )
 
318
        self.sync_value( factory.dclicked, 'dclicked', 'to' )
 
319
 
 
320
        self.sync_value( factory.right_clicked,  'right_clicked',  'to' )
 
321
        self.sync_value( factory.right_dclicked, 'right_dclicked', 'to' )
 
322
 
 
323
        self.sync_value( factory.column_clicked, 'column_clicked', 'to' )
 
324
 
 
325
        # Make sure we listen for 'items' changes as well as complete list
 
326
        # replacements:
 
327
        try:
 
328
            self.context_object.on_trait_change( self.update_editor,
 
329
                                self.extended_name + '_items', dispatch = 'ui' )
 
330
        except:
 
331
            pass
 
332
 
 
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' )
 
338
 
 
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 )
 
342
 
 
343
        # Refresh the editor whenever the adapter changes:
 
344
        self.on_trait_change( self._refresh, 'adapter.+update',
 
345
                              dispatch = 'ui' )
 
346
 
 
347
        # Rebuild the editor columns and headers whenever the adapter's
 
348
        # 'columns' changes:
 
349
        self.on_trait_change( self._rebuild_all, 'adapter.columns',
 
350
                              dispatch = 'ui' )
 
351
 
 
352
        # Make sure the tabular view gets initialized:
 
353
        self._rebuild()
 
354
 
 
355
        # Set the list control's tooltip:
 
356
        self.set_tooltip()
 
357
 
 
358
    def dispose ( self ):
 
359
        """ Disposes of the contents of an editor.
 
360
        """
 
361
        # Remove all of the wx event handlers:
 
362
        control = self.control
 
363
        parent  = control.GetParent()
 
364
        id      = control.GetId()
 
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 )
 
381
 
 
382
        self.context_object.on_trait_change( self.update_editor,
 
383
                                  self.extended_name + '_items', remove = True )
 
384
 
 
385
        if self.factory.auto_update:
 
386
            self.context_object.on_trait_change( self.refresh_editor,
 
387
                                self.extended_name + '.-', remove = True )
 
388
 
 
389
        self.on_trait_change( self._refresh, 'adapter.+update',  remove = True )
 
390
        self.on_trait_change( self._rebuild_all, 'adapter.columns',
 
391
                              remove = True )
 
392
 
 
393
        super( TabularEditor, self ).dispose()
 
394
 
 
395
    def _update_changed ( self, event ):
 
396
        """ Handles the 'update' event being fired.
 
397
        """
 
398
        if event is True:
 
399
            self.update_editor()
 
400
        elif isinstance( event, int ):
 
401
            self._refresh_row( event )
 
402
        else:
 
403
            self._refresh_editor( event )
 
404
 
 
405
    def refresh_editor ( self, item, name, old, new ):
 
406
        """ Handles a table item attribute being changed.
 
407
        """
 
408
        self._refresh_editor( item )
 
409
 
 
410
    def _refresh_editor ( self, item ):
 
411
        """ Handles a table item being changed.
 
412
        """
 
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 )
 
419
                return
 
420
 
 
421
        self.update_editor()
 
422
 
 
423
    def _refresh_row ( self, row ):
 
424
        """ Updates the editor control when a specified table row changes.
 
425
        """
 
426
        self.control.RefreshRect(
 
427
             self.control.GetItemRect( row, wx.LIST_RECT_BOUNDS ) )
 
428
 
 
429
    def _update_editor ( self, object, name, old_value, new_value ):
 
430
        """ Performs updates when the object trait changes.
 
431
            Overloads traitsui.editor.UIEditor
 
432
        """
 
433
        self._update_visible = True
 
434
 
 
435
        super(TabularEditor, self)._update_editor(object, name,
 
436
                                                  old_value, new_value)
 
437
 
 
438
 
 
439
    def update_editor ( self ):
 
440
        """ Updates the editor when the object trait changes externally to the
 
441
            editor.
 
442
        """
 
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)
 
448
 
 
449
        control.SetItemCount( n )
 
450
 
 
451
        if self._update_visible:
 
452
            control.RefreshItems( 0, n-1 )
 
453
            self._update_visible = False
 
454
 
 
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 )
 
459
 
 
460
        edit, self.edit = self.edit, False
 
461
        row,  self.row  = self.row,  None
 
462
 
 
463
        if row is not None:
 
464
            if row >= n:
 
465
                row -= 1
 
466
                if row < 0:
 
467
                    row = None
 
468
 
 
469
        if row is None:
 
470
            visible = bottom
 
471
            if visible >= 0 and visible < control.GetItemCount():
 
472
                control.EnsureVisible( visible )
 
473
            return
 
474
 
 
475
 
 
476
        if 0 <= (row - top) < pn:
 
477
            control.EnsureVisible( top + pn - 2 )
 
478
        elif row < top:
 
479
            control.EnsureVisible( row + pn - 1 )
 
480
        else:
 
481
            control.EnsureVisible( row )
 
482
 
 
483
        control.SetItemState( row,
 
484
            wx.LIST_STATE_SELECTED | wx.LIST_STATE_FOCUSED,
 
485
            wx.LIST_STATE_SELECTED | wx.LIST_STATE_FOCUSED  )
 
486
 
 
487
        if edit:
 
488
            control.EditLabel( row )
 
489
 
 
490
    #-- Trait Event Handlers ---------------------------------------------------
 
491
 
 
492
    def _selected_changed ( self, selected ):
 
493
        """ Handles the editor's 'selected' trait being changed.
 
494
        """
 
495
        if not self._no_update:
 
496
            if selected is None:
 
497
                for row in self._get_selected():
 
498
                    self.control.SetItemState( row, 0, wx.LIST_STATE_SELECTED )
 
499
            else:
 
500
                try:
 
501
                    self.control.SetItemState( self.value.index( selected ),
 
502
                                wx.LIST_STATE_SELECTED, wx.LIST_STATE_SELECTED )
 
503
                except:
 
504
                    pass
 
505
 
 
506
    def _selected_row_changed ( self, old, new ):
 
507
        """ Handles the editor's 'selected_index' trait being changed.
 
508
        """
 
509
        if not self._no_update:
 
510
            if new < 0:
 
511
                if old >= 0:
 
512
                    self.control.SetItemState( old, 0, wx.LIST_STATE_SELECTED )
 
513
            else:
 
514
                self.control.SetItemState( new, wx.LIST_STATE_SELECTED,
 
515
                                                wx.LIST_STATE_SELECTED )
 
516
 
 
517
    def _multi_selected_changed ( self, selected ):
 
518
        """ Handles the editor's 'multi_selected' trait being changed.
 
519
        """
 
520
        if not self._no_update:
 
521
            values = self.value
 
522
            try:
 
523
                self._multi_selected_rows_changed( [ values.index( item )
 
524
                                                     for item in selected ] )
 
525
            except:
 
526
                pass
 
527
 
 
528
    def _multi_selected_items_changed ( self, event ):
 
529
        """ Handles the editor's 'multi_selected' trait being modified.
 
530
        """
 
531
        values = self.values
 
532
        try:
 
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   ] ) )
 
536
        except:
 
537
            pass
 
538
 
 
539
    def _multi_selected_rows_changed ( self, selected_rows ):
 
540
        """ Handles the editor's 'multi_selected_rows' trait being changed.
 
541
        """
 
542
        if not self._no_update:
 
543
            control  = self.control
 
544
            selected = self._get_selected()
 
545
 
 
546
            # Select any new items that aren't already selected:
 
547
            for row in selected_rows:
 
548
                if row in selected:
 
549
                    selected.remove( row )
 
550
                else:
 
551
                    control.SetItemState( row, wx.LIST_STATE_SELECTED,
 
552
                                               wx.LIST_STATE_SELECTED )
 
553
 
 
554
            # Unselect all remaining selected items that aren't selected now:
 
555
            for row in selected:
 
556
                control.SetItemState( row, 0, wx.LIST_STATE_SELECTED )
 
557
 
 
558
    def _multi_selected_rows_items_changed ( self, event ):
 
559
        """ Handles the editor's 'multi_selected_rows' trait being modified.
 
560
        """
 
561
        control = self.control
 
562
 
 
563
        # Remove all items that are no longer selected:
 
564
        for row in event.removed:
 
565
            control.SetItemState( row, 0, wx.LIST_STATE_SELECTED )
 
566
 
 
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 )
 
571
 
 
572
    #-- List Control Event Handlers --------------------------------------------
 
573
 
 
574
    def _left_down ( self, event ):
 
575
        """ Handles the left mouse button being pressed.
 
576
        """
 
577
        self._mouse_click( event, 'clicked' )
 
578
 
 
579
    def _left_dclick ( self, event ):
 
580
        """ Handles the left mouse button being double clicked.
 
581
        """
 
582
        self._mouse_click( event, 'dclicked' )
 
583
 
 
584
    def _right_down ( self, event ):
 
585
        """ Handles the right mouse button being pressed.
 
586
        """
 
587
        self._mouse_click( event, 'right_clicked' )
 
588
 
 
589
    def _right_dclick ( self, event ):
 
590
        """ Handles the right mouse button being double clicked.
 
591
        """
 
592
        self._mouse_click( event, 'right_dclicked' )
 
593
 
 
594
    def _begin_drag ( self, event ):
 
595
        """ Handles the user beginning a drag operation with the left mouse
 
596
            button.
 
597
        """
 
598
        if PythonDropSource is not None:
 
599
            adapter      = self.adapter
 
600
            object, name = self.object, self.name
 
601
            selected     = self._get_selected()
 
602
            drag_items   = []
 
603
 
 
604
            # Collect all of the selected items to drag:
 
605
            for row in selected:
 
606
                drag = adapter.get_drag( object, name, row )
 
607
                if drag is None:
 
608
                    return
 
609
 
 
610
                drag_items.append( drag )
 
611
 
 
612
            # Save the drag item indices, so that we can later handle a
 
613
            # completed 'move' operation:
 
614
            self._drag_rows = selected
 
615
 
 
616
            try:
 
617
                # If only one item is being dragged, drag it as an item, not a
 
618
                # list:
 
619
                if len( drag_items ) == 1:
 
620
                    drag_items = drag_items[0]
 
621
 
 
622
                # Perform the drag and drop operation:
 
623
                ds = PythonDropSource( self.control, drag_items )
 
624
 
 
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
 
630
                    # adjusted):
 
631
                    rows = self._drag_rows
 
632
                    rows.reverse()
 
633
                    for row in rows:
 
634
                        adapter.delete( object, name, row )
 
635
            finally:
 
636
                self._drag_rows  = None
 
637
                self._drag_local = False
 
638
 
 
639
    def _begin_label_edit ( self, event ):
 
640
        """ Handles the user starting to edit an item label.
 
641
        """
 
642
        if not self.adapter.get_can_edit( self.object, self.name,
 
643
                                          event.GetIndex() ):
 
644
            event.Veto()
 
645
 
 
646
    def _end_label_edit ( self, event ):
 
647
        """ Handles the user finishing editing an item label.
 
648
        """
 
649
        self.adapter.set_text( self.object, self.name, event.GetIndex(),
 
650
                               event.GetColumn(), event.GetText() )
 
651
        self.row = event.GetIndex() + 1
 
652
 
 
653
    def _item_selected ( self, event ):
 
654
        """ Handles an item being selected.
 
655
        """
 
656
        self._no_update = True
 
657
        try:
 
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
 
667
                self.selected     = None
 
668
            else:
 
669
                self.selected_row = selected_rows[0]
 
670
                self.selected     = get_item( object, name, selected_rows[0] )
 
671
        finally:
 
672
            self._no_update = False
 
673
 
 
674
    def _item_activated ( self, event ):
 
675
        """ Handles an item being activated (double-clicked or enter pressed).
 
676
        """
 
677
        self.activated_row = event.GetIndex()
 
678
        self.activated     = self.adapter.get_item( self.object, self.name,
 
679
                                                    self.activated_row )
 
680
 
 
681
    def _key_down ( self, event ):
 
682
        """ Handles the user pressing a key in the list control.
 
683
        """
 
684
        key = event.GetKeyCode()
 
685
        if key == wx.WXK_NEXT:
 
686
            self._append_new()
 
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 ):
 
696
            self._edit_current()
 
697
        else:
 
698
            event.Skip()
 
699
 
 
700
    def _column_right_clicked ( self, event ):
 
701
        """ Handles the user right-clicking a column header.
 
702
        """
 
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 )
 
708
 
 
709
    def _column_clicked ( self, event ):
 
710
        """ Handles the right mouse button being double clicked.
 
711
        """
 
712
        editor_event = TabularEditorEvent(
 
713
            editor = self,
 
714
            row    = 0,
 
715
            column = event.GetColumn()
 
716
        )
 
717
 
 
718
        setattr( self, 'column_clicked', editor_event)
 
719
        event.Skip()
 
720
 
 
721
    def _size_modified ( self, event ):
 
722
        """ Handles the size of the list control being changed.
 
723
        """
 
724
        control = self.control
 
725
        n       = control.GetColumnCount()
 
726
        if n == 1:
 
727
            dx, dy = control.GetClientSizeTuple()
 
728
            control.SetColumnWidth( 0, dx - 1 )
 
729
        elif n > 1:
 
730
            do_later( self._set_column_widths )
 
731
 
 
732
        event.Skip()
 
733
 
 
734
    def _motion ( self, event ):
 
735
        """ Handles the user moving the mouse.
 
736
        """
 
737
        x          = event.GetX()
 
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):
 
743
                tooltip = ''
 
744
            else:
 
745
                tooltip = self.adapter.get_tooltip( self.object, self.name,
 
746
                                                    row, column )
 
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 ) )
 
752
 
 
753
    #-- Drag and Drop Event Handlers -------------------------------------------
 
754
 
 
755
    def wx_dropped_on ( self, x, y, data, drag_result ):
 
756
        """ Handles a Python object being dropped on the list control.
 
757
        """
 
758
        row, flags = self.control.HitTest( wx.Point( x, y ) )
 
759
 
 
760
        # If the user dropped it on an empty list, set the target as past the
 
761
        # end of the list:
 
762
        if ((row == -1) and
 
763
            ((flags & wx.LIST_HITTEST_NOWHERE) != 0) and
 
764
            (self.control.GetItemCount() == 0)):
 
765
            row = 0
 
766
 
 
767
        # If we have a valid drop target row, proceed:
 
768
        if row != -1:
 
769
            if not isinstance( data, list ):
 
770
                # Handle the case of just a single item being dropped:
 
771
                self._wx_dropped_on( row, data )
 
772
            else:
 
773
                # Handles the case of a list of items being dropped, being
 
774
                # careful to preserve the original order of the source items if
 
775
                # possible:
 
776
                data.reverse()
 
777
                for item in data:
 
778
                    self._wx_dropped_on( row, item )
 
779
 
 
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
 
783
 
 
784
            # Return a successful drop result:
 
785
            return drag_result
 
786
 
 
787
        # Indicate we could not process the drop:
 
788
        return wx.DragNone
 
789
 
 
790
    def _wx_dropped_on ( self, row, item ):
 
791
        """ Helper method for handling a single item dropped on the list
 
792
            control.
 
793
        """
 
794
        adapter      = self.adapter
 
795
        object, name = self.object, self.name
 
796
 
 
797
        # Obtain the destination of the dropped item relative to the target:
 
798
        destination = adapter.get_dropped( object, name, row, item )
 
799
 
 
800
        # Adjust the target index accordingly:
 
801
        if destination == 'after':
 
802
            row += 1
 
803
 
 
804
        # Insert the dropped item at the requested position:
 
805
        adapter.insert( object, name, row, item )
 
806
 
 
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
 
811
        if rows is not None:
 
812
            for i in range( len( rows ) - 1, -1, -1 ):
 
813
                if rows[i] < row:
 
814
                    break
 
815
 
 
816
                rows[i] += 1
 
817
 
 
818
    def wx_drag_over ( self, x, y, data, drag_result ):
 
819
        """ Handles a Python object being dragged over the tree.
 
820
        """
 
821
        if isinstance( data, list ):
 
822
            rc = wx.DragNone
 
823
            for item in data:
 
824
                rc = self.wx_drag_over( x, y, item, drag_result )
 
825
                if rc == wx.DragNone:
 
826
                    break
 
827
 
 
828
            return rc
 
829
 
 
830
        row, flags = self.control.HitTest( wx.Point( x, y ) )
 
831
 
 
832
        # If the user is dragging over an empty list, set the target to the end
 
833
        # of the list:
 
834
        if ((row == -1) and
 
835
            ((flags & wx.LIST_HITTEST_NOWHERE) != 0) and
 
836
            (self.control.GetItemCount() == 0)):
 
837
            row = 0
 
838
 
 
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:
 
841
        if ((row != -1) and
 
842
            self.adapter.get_can_drop( self.object, self.name, row, data )):
 
843
            return drag_result
 
844
 
 
845
        # Else indicate that we will not accept the data:
 
846
        return wx.DragNone
 
847
 
 
848
    #-- UI preference save/restore interface -----------------------------------
 
849
 
 
850
    def restore_prefs ( self, prefs ):
 
851
        """ Restores any saved user preference information associated with the
 
852
            editor.
 
853
        """
 
854
        self._cached_widths = cws = prefs.get( 'cached_widths' )
 
855
        if cws is not None:
 
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 )
 
860
 
 
861
    def save_prefs ( self ):
 
862
        """ Returns any user preference information associated with the editor.
 
863
        """
 
864
        cws = self._cached_widths
 
865
        if cws is not None:
 
866
            cws = [ ( None, cw )[ cw >= 0 ] for cw in cws ]
 
867
 
 
868
        return { 'cached_widths': cws }
 
869
 
 
870
    #-- Private Methods --------------------------------------------------------
 
871
 
 
872
    def _refresh ( self ):
 
873
        """ Refreshes the contents of the editor's list control.
 
874
        """
 
875
        n = self.adapter.len( self.object, self.name )
 
876
        if n > 0:
 
877
            self.control.RefreshItems( 0, n - 1)
 
878
 
 
879
    def _rebuild ( self ):
 
880
        """ Rebuilds the contents of the editor's list control.
 
881
        """
 
882
        control = self.control
 
883
        control.ClearAll()
 
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()
 
893
 
 
894
    def _rebuild_all ( self ):
 
895
        """ Rebuilds the structure of the list control, then refreshes its
 
896
            contents.
 
897
        """
 
898
        self._rebuild()
 
899
        self.update_editor()
 
900
 
 
901
    def _set_column_widths ( self ):
 
902
        """ Set the column widths for the current set of columns.
 
903
        """
 
904
        control = self.control
 
905
        if control is None:
 
906
            return
 
907
 
 
908
        object, name = self.object, self.name
 
909
        dx, dy       = control.GetClientSize()
 
910
        if is_mac:
 
911
            dx -= scrollbar_dx
 
912
        n            = control.GetColumnCount()
 
913
        get_width    = self.adapter.get_width
 
914
        pdx          = 0
 
915
        wdx          = 0.0
 
916
        widths       = []
 
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
 
921
 
 
922
        for i in xrange( n ):
 
923
            cw = cached[i]
 
924
            if (cw is None) or (-cw == current[i]):
 
925
                width = float( get_width( object, name, i ) )
 
926
                if width <= 0.0:
 
927
                    width = 0.1
 
928
                if width <= 1.0:
 
929
                    wdx += width
 
930
                    cached[i] = -1
 
931
                else:
 
932
                    width = int( width )
 
933
                    pdx  += width
 
934
                    if cw is None:
 
935
                        cached[i] = width
 
936
            else:
 
937
                cached[i] = width = current[i]
 
938
                pdx += width
 
939
 
 
940
            widths.append( width )
 
941
 
 
942
        adx = max( 0, dx - pdx )
 
943
 
 
944
        control.Freeze()
 
945
        for i in range( n ):
 
946
            width = cached[i]
 
947
            if width < 0:
 
948
                width = widths[i]
 
949
                if width <= 1.0:
 
950
                    widths[i] = w = max( 30, int( round( (adx * width)/wdx ) ) )
 
951
                    wdx      -= width
 
952
                    width     = w
 
953
                    adx      -= width
 
954
                    cached[i] = -w
 
955
 
 
956
            control.SetColumnWidth( i, width )
 
957
 
 
958
        control.Thaw()
 
959
 
 
960
    def _add_image ( self, image_resource ):
 
961
        """ Adds a new image to the wx.ImageList and its associated mapping.
 
962
        """
 
963
        bitmap = image_resource.create_image().ConvertToBitmap()
 
964
 
 
965
        image_list = self._image_list
 
966
        if image_list is None:
 
967
            self._image_list = image_list = wx.ImageList( bitmap.GetWidth(),
 
968
                                                          bitmap.GetHeight() )
 
969
            self.control.AssignImageList( image_list, wx.IMAGE_LIST_SMALL )
 
970
 
 
971
        self.image_resources[image_resource] = \
 
972
        self.images[ image_resource.name ]   = row = image_list.Add( bitmap )
 
973
 
 
974
        return row
 
975
 
 
976
    def _get_image ( self, image ):
 
977
        """ Converts a user specified image to a wx.ListCtrl image index.
 
978
        """
 
979
        if isinstance( image, basestring ):
 
980
            self.image = image
 
981
            image      = self.image
 
982
 
 
983
        if isinstance( image, ImageResource ):
 
984
            result = self.image_resources.get( image )
 
985
            if result is not None:
 
986
                return result
 
987
 
 
988
            return self._add_image( image )
 
989
 
 
990
        return self.images.get( image )
 
991
 
 
992
    def _get_selected ( self ):
 
993
        """ Returns a list of the rows of all currently selected list items.
 
994
        """
 
995
        selected = []
 
996
        item     = -1
 
997
        control  = self.control
 
998
 
 
999
        # Handle case where the list is cleared
 
1000
        if len( self.value ) == 0:
 
1001
            return selected
 
1002
 
 
1003
        while True:
 
1004
            item = control.GetNextItem( item, wx.LIST_NEXT_ALL,
 
1005
                                              wx.LIST_STATE_SELECTED )
 
1006
            if item == -1:
 
1007
                break;
 
1008
 
 
1009
            selected.append( item )
 
1010
 
 
1011
        return selected
 
1012
 
 
1013
    def _append_new ( self ):
 
1014
        """ Append a new item to the end of the list control.
 
1015
        """
 
1016
        if 'append' in self.factory.operations:
 
1017
            adapter   = self.adapter
 
1018
            self.row  = self.control.GetItemCount()
 
1019
            self.edit = True
 
1020
            adapter.insert( self.object, self.name, self.row,
 
1021
                           adapter.get_default_value( self.object, self.name ) )
 
1022
 
 
1023
    def _insert_current ( self ):
 
1024
        """ Inserts a new item after the currently selected list control item.
 
1025
        """
 
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]
 
1033
                self.edit = True
 
1034
 
 
1035
    def _delete_current ( self ):
 
1036
        """ Deletes the currently selected items from the list control.
 
1037
        """
 
1038
        if 'delete' in self.factory.operations:
 
1039
            selected = self._get_selected()
 
1040
            if len( selected ) == 0:
 
1041
                return
 
1042
 
 
1043
            delete = self.adapter.delete
 
1044
            selected.reverse()
 
1045
            for row in selected:
 
1046
                delete( self.object, self.name, row )
 
1047
 
 
1048
            self.row = row
 
1049
 
 
1050
    def _move_up_current ( self ):
 
1051
        """ Moves the currently selected item up one line in the list control.
 
1052
        """
 
1053
        if 'move' in self.factory.operations:
 
1054
            selected = self._get_selected()
 
1055
            if len( selected ) == 1:
 
1056
                row = selected[0]
 
1057
                if row > 0:
 
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 )
 
1063
                    self.row = row - 1
 
1064
 
 
1065
    def _move_down_current ( self ):
 
1066
        """ Moves the currently selected item down one line in the list control.
 
1067
        """
 
1068
        if 'move' in self.factory.operations:
 
1069
            selected = self._get_selected()
 
1070
            if len( selected ) == 1:
 
1071
                row = selected[0]
 
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 )
 
1078
                    self.row = row + 1
 
1079
 
 
1080
    def _edit_current ( self ):
 
1081
        """ Allows the user to edit the current item in the list control.
 
1082
        """
 
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] )
 
1087
 
 
1088
    def _get_column ( self, x, translate = False ):
 
1089
        """ Returns the column index corresponding to a specified x position.
 
1090
        """
 
1091
        if x >= 0:
 
1092
            control = self.control
 
1093
            for i in range( control.GetColumnCount() ):
 
1094
                x -= control.GetColumnWidth( i )
 
1095
                if x < 0:
 
1096
                    if translate:
 
1097
                        return self.adapter.get_column(
 
1098
                                   self.object, self.name, i )
 
1099
 
 
1100
                    return i
 
1101
 
 
1102
        return None
 
1103
 
 
1104
    def _mouse_click ( self, event, trait ):
 
1105
        """ Generate a TabularEditorEvent event for a specified mouse event and
 
1106
            editor trait name.
 
1107
        """
 
1108
        x          = event.GetX()
 
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 = []
 
1114
            else:
 
1115
                self.selected = None
 
1116
                self.selected_row = -1
 
1117
        else:
 
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 )
 
1125
 
 
1126
            setattr( self, trait, TabularEditorEvent(
 
1127
                editor = self,
 
1128
                row    = row,
 
1129
                column = self._get_column( x, translate = True )
 
1130
            ) )
 
1131
 
 
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...
 
1135
        event.Skip(True)
 
1136
 
 
1137
#-------------------------------------------------------------------------------
 
1138
#  'TabularEditorEvent' class:
 
1139
#-------------------------------------------------------------------------------
 
1140
 
 
1141
class TabularEditorEvent ( HasStrictTraits ):
 
1142
 
 
1143
    # The index of the row:
 
1144
    row = Int
 
1145
 
 
1146
    # The id of the column (either a string or an integer):
 
1147
    column = Any
 
1148
 
 
1149
    # The row item:
 
1150
    item = Property
 
1151
 
 
1152
    #-- Private Traits ---------------------------------------------------------
 
1153
 
 
1154
    # The editor the event is associated with:
 
1155
    editor = Instance( TabularEditor )
 
1156
 
 
1157
    #-- Property Implementations -----------------------------------------------
 
1158
 
 
1159
    def _get_item ( self ):
 
1160
        editor = self.editor
 
1161
        return editor.adapter.get_item( editor.object, editor.name, self.row )
 
1162