~ubuntu-branches/ubuntu/utopic/python-traitsui/utopic

« back to all changes in this revision

Viewing changes to traitsui/qt4/tree_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
# Copyright (c) 2007, Riverbank Computing Limited
 
3
# All rights reserved.
 
4
#
 
5
# This software is provided without warranty under the terms of the BSD license.
 
6
# However, when used with the GPL version of PyQt the additional terms described in the PyQt GPL exception also apply
 
7
 
 
8
#
 
9
# Author: Riverbank Computing Limited
 
10
#------------------------------------------------------------------------------
 
11
 
 
12
""" Defines the tree editor for the PyQt user interface toolkit.
 
13
"""
 
14
 
 
15
#-------------------------------------------------------------------------------
 
16
#  Imports:
 
17
#-------------------------------------------------------------------------------
 
18
 
 
19
import copy
 
20
 
 
21
from pyface.qt import QtCore, QtGui
 
22
 
 
23
from pyface.resource_manager import resource_manager
 
24
from traits.api import Any, Event
 
25
from traits.trait_base import enumerate
 
26
from traitsui.api import TreeNode, ObjectTreeNode, MultiTreeNode
 
27
from traitsui.undo import ListUndoItem
 
28
from traitsui.tree_node import ITreeNodeAdapterBridge
 
29
from traitsui.menu import Menu, Action, Separator
 
30
 
 
31
from clipboard import clipboard, PyMimeData
 
32
from editor import Editor
 
33
from helper import open_fbi, pixmap_cache
 
34
 
 
35
#-------------------------------------------------------------------------------
 
36
#  The core tree node menu actions:
 
37
#-------------------------------------------------------------------------------
 
38
 
 
39
NewAction    = 'NewAction'
 
40
CopyAction   = Action( name         = 'Copy',
 
41
                       action       = 'editor._menu_copy_node',
 
42
                       enabled_when = 'editor._is_copyable(object)' )
 
43
CutAction    = Action( name         = 'Cut',
 
44
                       action       = 'editor._menu_cut_node',
 
45
                       enabled_when = 'editor._is_cutable(object)' )
 
46
PasteAction  = Action( name         = 'Paste',
 
47
                       action       = 'editor._menu_paste_node',
 
48
                       enabled_when = 'editor._is_pasteable(object)' )
 
49
DeleteAction = Action( name         = 'Delete',
 
50
                       action       = 'editor._menu_delete_node',
 
51
                       enabled_when = 'editor._is_deletable(object)' )
 
52
RenameAction = Action( name         = 'Rename',
 
53
                       action       = 'editor._menu_rename_node',
 
54
                       enabled_when = 'editor._is_renameable(object)' )
 
55
 
 
56
#-------------------------------------------------------------------------------
 
57
#  'SimpleEditor' class:
 
58
#-------------------------------------------------------------------------------
 
59
 
 
60
class SimpleEditor ( Editor ):
 
61
    """ Simple style of tree editor.
 
62
    """
 
63
    #---------------------------------------------------------------------------
 
64
    #  Trait definitions:
 
65
    #---------------------------------------------------------------------------
 
66
 
 
67
    # Is the tree editor is scrollable? This value overrides the default.
 
68
    scrollable = True
 
69
 
 
70
    # Allows an external agent to set the tree selection
 
71
    selection = Event
 
72
 
 
73
    # The currently selected object
 
74
    selected = Any
 
75
 
 
76
    # The event fired when a tree node is clicked on:
 
77
    click = Event
 
78
 
 
79
    # The event fired when a tree node is double-clicked on:
 
80
    dclick = Event
 
81
 
 
82
    # The event fired when the application wants to veto an operation:
 
83
    veto = Event
 
84
 
 
85
    #---------------------------------------------------------------------------
 
86
    #  Finishes initializing the editor by creating the underlying toolkit
 
87
    #  widget:
 
88
    #---------------------------------------------------------------------------
 
89
 
 
90
    def init ( self, parent ):
 
91
        """ Finishes initializing the editor by creating the underlying toolkit
 
92
            widget.
 
93
        """
 
94
        factory = self.factory
 
95
 
 
96
        self._editor = None
 
97
 
 
98
        if factory.editable:
 
99
 
 
100
            # Check to see if the tree view is based on a shared trait editor:
 
101
            if factory.shared_editor:
 
102
                factory_editor = factory.editor
 
103
 
 
104
                # If this is the editor that defines the trait editor panel:
 
105
                if factory_editor is None:
 
106
 
 
107
                    # Remember which editor has the trait editor in the factory:
 
108
                    factory._editor = self
 
109
 
 
110
                    # Create the trait editor panel:
 
111
                    self.control = QtGui.QWidget()
 
112
                    parent.addWidget(self.control)
 
113
                    self.control._node_ui = self.control._editor_nid = None
 
114
 
 
115
                    # Check to see if there are any existing editors that are
 
116
                    # waiting to be bound to the trait editor panel:
 
117
                    editors = factory._shared_editors
 
118
                    if editors is not None:
 
119
                        for editor in factory._shared_editors:
 
120
 
 
121
                            # If the editor is part of this UI:
 
122
                            if editor.ui is self.ui:
 
123
 
 
124
                                # Then bind it to the trait editor panel:
 
125
                                editor._editor = self.control
 
126
 
 
127
                        # Indicate all pending editors have been processed:
 
128
                        factory._shared_editors = None
 
129
 
 
130
                    # We only needed to build the trait editor panel, so exit:
 
131
                    return
 
132
 
 
133
                # Check to see if the matching trait editor panel has been
 
134
                # created yet:
 
135
                editor = factory_editor._editor
 
136
                if (editor is None) or (editor.ui is not self.ui):
 
137
                    # If not, add ourselves to the list of pending editors:
 
138
                    shared_editors = factory_editor._shared_editors
 
139
                    if shared_editors is None:
 
140
                        factory_editor._shared_editors = shared_editors = []
 
141
                    shared_editors.append( self )
 
142
                else:
 
143
                    # Otherwise, bind our trait editor panel to the shared one:
 
144
                    self._editor = editor.control
 
145
 
 
146
                # Finally, create only the tree control:
 
147
                self.control = self._tree = _TreeWidget(self)
 
148
            else:
 
149
                # If editable, create a tree control and an editor panel:
 
150
                self._tree = _TreeWidget(self)
 
151
 
 
152
                self._editor = sa = QtGui.QScrollArea()
 
153
                sa.setFrameShape(QtGui.QFrame.NoFrame)
 
154
                sa._node_ui = sa._editor_nid = None
 
155
 
 
156
                if factory.orientation == 'horizontal':
 
157
                    orient = QtCore.Qt.Horizontal
 
158
                else:
 
159
                    orient = QtCore.Qt.Vertical
 
160
 
 
161
                self.control = splitter = QtGui.QSplitter(orient)
 
162
                splitter.setSizePolicy(QtGui.QSizePolicy.Expanding,
 
163
                                       QtGui.QSizePolicy.Expanding)
 
164
                splitter.addWidget(self._tree)
 
165
                splitter.addWidget(sa)
 
166
        else:
 
167
            # Otherwise, just create the tree control:
 
168
            self.control = self._tree = _TreeWidget(self)
 
169
 
 
170
        # Set up the mapping between objects and tree id's:
 
171
        self._map = {}
 
172
 
 
173
        # Initialize the 'undo state' stack:
 
174
        self._undoable = []
 
175
 
 
176
        # Synchronize external object traits with the editor:
 
177
        self.sync_value( factory.selected, 'selected' )
 
178
        self.sync_value( factory.click,    'click',  'to' )
 
179
        self.sync_value( factory.dclick,   'dclick', 'to' )
 
180
        self.sync_value( factory.veto,     'veto',   'from' )
 
181
 
 
182
    #---------------------------------------------------------------------------
 
183
    #  Handles the 'selection' trait being changed:
 
184
    #---------------------------------------------------------------------------
 
185
 
 
186
    def _selection_changed ( self, selection ):
 
187
        """ Handles the **selection** event.
 
188
        """
 
189
        try:
 
190
            self._tree.setCurrentItem(self._object_info(selection)[2])
 
191
        except:
 
192
            pass
 
193
 
 
194
    #---------------------------------------------------------------------------
 
195
    #  Handles the 'selected' trait being changed:
 
196
    #---------------------------------------------------------------------------
 
197
 
 
198
    def _selected_changed ( self, selected ):
 
199
        """ Handles the **selected** trait being changed.
 
200
        """
 
201
        if not self._no_update_selected:
 
202
            self._selection_changed( selected )
 
203
 
 
204
    #---------------------------------------------------------------------------
 
205
    #  Handles the 'veto' event being fired:
 
206
    #---------------------------------------------------------------------------
 
207
 
 
208
    def _veto_changed ( self ):
 
209
        """ Handles the 'veto' event being fired.
 
210
        """
 
211
        self._veto = True
 
212
 
 
213
    #---------------------------------------------------------------------------
 
214
    #  Disposes of the contents of an editor:
 
215
    #---------------------------------------------------------------------------
 
216
 
 
217
    def dispose ( self ):
 
218
        """ Disposes of the contents of an editor.
 
219
        """
 
220
        if self._tree is not None:
 
221
            # Stop the chatter (specifically about the changing selection).
 
222
            self._tree.blockSignals(True)
 
223
 
 
224
            self._delete_node(self._tree.invisibleRootItem())
 
225
 
 
226
            self._tree = None
 
227
 
 
228
        super( SimpleEditor, self ).dispose()
 
229
 
 
230
    #---------------------------------------------------------------------------
 
231
    #  Expands from the specified node the specified number of sub-levels:
 
232
    #---------------------------------------------------------------------------
 
233
 
 
234
    def expand_levels ( self, nid, levels, expand = True ):
 
235
        """ Expands from the specified node the specified number of sub-levels.
 
236
        """
 
237
        if levels > 0:
 
238
            expanded, node, object = self._get_node_data( nid )
 
239
            if self._has_children( node, object ):
 
240
                self._expand_node( nid )
 
241
                if expand:
 
242
                    nid.setExpanded(True)
 
243
                for cnid in self._nodes_for( nid ):
 
244
                    self.expand_levels( cnid, levels - 1 )
 
245
 
 
246
    #---------------------------------------------------------------------------
 
247
    #  Updates the editor when the object trait changes external to the editor:
 
248
    #---------------------------------------------------------------------------
 
249
 
 
250
    def update_editor ( self ):
 
251
        """ Updates the editor when the object trait changes externally to the
 
252
            editor.
 
253
        """
 
254
        tree = self._tree
 
255
        saved_state = {}
 
256
 
 
257
        tree.clear()
 
258
 
 
259
        object, node = self._node_for( self.value )
 
260
        if node is not None:
 
261
            if self.factory.hide_root:
 
262
                nid = tree.invisibleRootItem()
 
263
            else:
 
264
                nid = QtGui.QTreeWidgetItem(tree)
 
265
                nid.setText(0, node.get_label(object))
 
266
                nid.setIcon(0, self._get_icon(node, object))
 
267
                nid.setToolTip(0, node.get_tooltip(object))
 
268
 
 
269
            self._map[ id( object ) ] = [ ( node.get_children_id(object), nid ) ]
 
270
            self._add_listeners( node, object )
 
271
            self._set_node_data( nid, ( False, node, object) )
 
272
            if self.factory.hide_root or self._has_children( node, object ):
 
273
                self._expand_node( nid )
 
274
                if not self.factory.hide_root:
 
275
                    nid.setExpanded(True)
 
276
                    tree.setCurrentItem(nid)
 
277
 
 
278
            self.expand_levels( nid, self.factory.auto_open, False )
 
279
        # FIXME: Clear the current editor (if any)...
 
280
 
 
281
    #---------------------------------------------------------------------------
 
282
    #  Returns the editor's control for indicating error status:
 
283
    #---------------------------------------------------------------------------
 
284
 
 
285
    def get_error_control ( self ):
 
286
        """ Returns the editor's control for indicating error status.
 
287
        """
 
288
        return self._tree
 
289
 
 
290
    #---------------------------------------------------------------------------
 
291
    #  Appends a new node to the specified node:
 
292
    #---------------------------------------------------------------------------
 
293
 
 
294
    def _append_node ( self, nid, node, object ):
 
295
        """ Appends a new node to the specified node.
 
296
        """
 
297
        cnid = QtGui.QTreeWidgetItem(nid)
 
298
        cnid.setText(0, node.get_label(object))
 
299
        cnid.setIcon(0, self._get_icon(node, object))
 
300
        cnid.setToolTip(0, node.get_tooltip(object))
 
301
 
 
302
        has_children = self._has_children(node, object)
 
303
        self._set_node_data( cnid, ( False, node, object ) )
 
304
        self._map.setdefault( id( object ), [] ).append(
 
305
            ( node.get_children_id(object), cnid ) )
 
306
        self._add_listeners( node, object )
 
307
 
 
308
        # Automatically expand the new node (if requested):
 
309
        if has_children:
 
310
            if node.can_auto_open( object ):
 
311
                cnid.setExpanded(True)
 
312
            else:
 
313
                # Qt only draws the control that expands the tree if there is a
 
314
                # child.  As the tree is being populated lazily we create a
 
315
                # dummy that will be removed when the node is expanded for the
 
316
                # first time.
 
317
                cnid._dummy = QtGui.QTreeWidgetItem(cnid)
 
318
 
 
319
        # Return the newly created node:
 
320
        return cnid
 
321
 
 
322
    #---------------------------------------------------------------------------
 
323
    #  Deletes a specified tree node and all its children:
 
324
    #---------------------------------------------------------------------------
 
325
 
 
326
    def _delete_node ( self, nid ):
 
327
        """ Deletes a specified tree node and all its children.
 
328
        """
 
329
        for cnid in self._nodes_for( nid ):
 
330
            self._delete_node( cnid )
 
331
 
 
332
        if nid is self._tree.invisibleRootItem():
 
333
            return
 
334
 
 
335
        # See if it is a dummy.
 
336
        pnid = nid.parent()
 
337
        if pnid is not None and getattr(pnid, '_dummy', None) is nid:
 
338
            pnid.removeChild(nid)
 
339
            del pnid._dummy
 
340
            return
 
341
 
 
342
        expanded, node, object = self._get_node_data(nid)
 
343
        id_object = id(object)
 
344
        object_info = self._map[id_object]
 
345
        for i, info in enumerate(object_info):
 
346
            # QTreeWidgetItem does not have an equal operator, so use id()
 
347
            if id(nid) == id(info[1]):
 
348
                del object_info[i]
 
349
                break
 
350
 
 
351
        if len( object_info ) == 0:
 
352
            self._remove_listeners( node, object )
 
353
            del self._map[ id_object ]
 
354
 
 
355
        if pnid is None:
 
356
            self._tree.takeTopLevelItem(self._tree.indexOfTopLevelItem(nid))
 
357
        else:
 
358
            pnid.removeChild(nid)
 
359
 
 
360
        # If the deleted node had an active editor panel showing, remove it:
 
361
        if (self._editor is not None) and (nid == self._editor._editor_nid):
 
362
            self._clear_editor()
 
363
 
 
364
    #---------------------------------------------------------------------------
 
365
    #  Expands the contents of a specified node (if required):
 
366
    #---------------------------------------------------------------------------
 
367
 
 
368
    def _expand_node ( self, nid ):
 
369
        """ Expands the contents of a specified node (if required).
 
370
        """
 
371
        expanded, node, object = self._get_node_data( nid )
 
372
 
 
373
        # Lazily populate the item's children:
 
374
        if not expanded:
 
375
            # Remove any dummy node.
 
376
            dummy = getattr(nid, '_dummy', None)
 
377
            if dummy is not None:
 
378
                nid.removeChild(dummy)
 
379
                del nid._dummy
 
380
 
 
381
            for child in node.get_children( object ):
 
382
                child, child_node = self._node_for( child )
 
383
                if child_node is not None:
 
384
                    self._append_node( nid, child_node, child )
 
385
 
 
386
            # Indicate the item is now populated:
 
387
            self._set_node_data( nid, ( True, node, object) )
 
388
 
 
389
    #---------------------------------------------------------------------------
 
390
    #  Returns each of the child nodes of a specified node id:
 
391
    #---------------------------------------------------------------------------
 
392
 
 
393
    def _nodes_for ( self, nid ):
 
394
        """ Returns all child node ids of a specified node id.
 
395
        """
 
396
        return [nid.child(i) for i in range(nid.childCount())]
 
397
 
 
398
    #---------------------------------------------------------------------------
 
399
    #  Return the index of a specified node id within its parent:
 
400
    #---------------------------------------------------------------------------
 
401
 
 
402
    def _node_index ( self, nid ):
 
403
        pnid = nid.parent()
 
404
        if pnid is None:
 
405
            return ( None, None, None )
 
406
 
 
407
        for i in range(pnid.childCount()):
 
408
            if pnid.child(i) is nid:
 
409
                _, pnode, pobject = self._get_node_data( pnid )
 
410
                return ( pnode, pobject, i )
 
411
 
 
412
    #---------------------------------------------------------------------------
 
413
    #  Returns whether a specified object has any children:
 
414
    #---------------------------------------------------------------------------
 
415
 
 
416
    def _has_children ( self, node, object ):
 
417
        """ Returns whether a specified object has any children.
 
418
        """
 
419
        return (node.allows_children( object ) and node.has_children( object ))
 
420
 
 
421
    #---------------------------------------------------------------------------
 
422
    #  Returns the icon index for the specified object:
 
423
    #---------------------------------------------------------------------------
 
424
 
 
425
    STD_ICON_MAP = {
 
426
        '<item>':   QtGui.QStyle.SP_FileIcon,
 
427
        '<group>':  QtGui.QStyle.SP_DirClosedIcon,
 
428
        '<open>':   QtGui.QStyle.SP_DirOpenIcon
 
429
    }
 
430
 
 
431
    def _get_icon ( self, node, object, is_expanded = False ):
 
432
        """ Returns the index of the specified object icon.
 
433
        """
 
434
        if not self.factory.show_icons:
 
435
            return QtGui.QIcon()
 
436
 
 
437
        icon_name = node.get_icon(object, is_expanded)
 
438
        if isinstance(icon_name, basestring):
 
439
            icon = self.STD_ICON_MAP.get(icon_name)
 
440
 
 
441
            if icon is not None:
 
442
                return self._tree.style().standardIcon(icon)
 
443
 
 
444
            path = node.get_icon_path( object )
 
445
            if isinstance( path, basestring ):
 
446
                path = [ path, node ]
 
447
            else:
 
448
                path.append( node )
 
449
            reference = resource_manager.locate_image( icon_name, path )
 
450
            if reference is None:
 
451
                return QtGui.QIcon()
 
452
            file_name = reference.filename
 
453
        else:
 
454
            # Assume it is an ImageResource, and get its file name directly:
 
455
            file_name = icon_name.absolute_path
 
456
 
 
457
        return QtGui.QIcon(pixmap_cache(file_name))
 
458
 
 
459
    #---------------------------------------------------------------------------
 
460
    #  Adds the event listeners for a specified object:
 
461
    #---------------------------------------------------------------------------
 
462
 
 
463
    def _add_listeners ( self, node, object ):
 
464
        """ Adds the event listeners for a specified object.
 
465
        """
 
466
        if node.allows_children( object ):
 
467
            node.when_children_replaced( object, self._children_replaced, False)
 
468
            node.when_children_changed(  object, self._children_updated,  False)
 
469
 
 
470
        node.when_label_changed( object, self._label_updated, False )
 
471
 
 
472
    #---------------------------------------------------------------------------
 
473
    #  Removes any event listeners from a specified object:
 
474
    #---------------------------------------------------------------------------
 
475
 
 
476
    def _remove_listeners ( self, node, object ):
 
477
        """ Removes any event listeners from a specified object.
 
478
        """
 
479
        if node.allows_children( object ):
 
480
            node.when_children_replaced( object, self._children_replaced, True )
 
481
            node.when_children_changed(  object, self._children_updated,  True )
 
482
 
 
483
        node.when_label_changed( object, self._label_updated, True )
 
484
 
 
485
    #---------------------------------------------------------------------------
 
486
    #  Returns the tree node data for a specified object in the form
 
487
    #  ( expanded, node, nid ):
 
488
    #---------------------------------------------------------------------------
 
489
 
 
490
    def _object_info ( self, object, name = '' ):
 
491
        """ Returns the tree node data for a specified object in the form
 
492
            ( expanded, node, nid ).
 
493
        """
 
494
        info = self._map[ id( object ) ]
 
495
        for name2, nid in info:
 
496
            if name == name2:
 
497
                break
 
498
        else:
 
499
            nid = info[0][1]
 
500
 
 
501
        expanded, node, ignore = self._get_node_data( nid )
 
502
 
 
503
        return ( expanded, node, nid )
 
504
 
 
505
    def _object_info_for ( self, object, name = '' ):
 
506
        """ Returns the tree node data for a specified object as a list of the
 
507
            form: [ ( expanded, node, nid ), ... ].
 
508
        """
 
509
        result = []
 
510
        for name2, nid in self._map[ id( object ) ]:
 
511
            if name == name2:
 
512
                expanded, node, ignore = self._get_node_data( nid )
 
513
                result.append( ( expanded, node, nid ) )
 
514
 
 
515
        return result
 
516
 
 
517
    #---------------------------------------------------------------------------
 
518
    #  Returns the TreeNode associated with a specified object:
 
519
    #---------------------------------------------------------------------------
 
520
 
 
521
    def _node_for ( self, object ):
 
522
        """ Returns the TreeNode associated with a specified object.
 
523
        """
 
524
        if ((type( object ) is tuple) and (len( object ) == 2) and
 
525
            isinstance( object[1], TreeNode )):
 
526
            return object
 
527
 
 
528
        # Select all nodes which understand this object:
 
529
        factory = self.factory
 
530
        nodes   = [ node for node in factory.nodes
 
531
                    if node.is_node_for( object ) ]
 
532
 
 
533
        # If only one found, we're done, return it:
 
534
        if len( nodes ) == 1:
 
535
            return ( object, nodes[0] )
 
536
 
 
537
        # If none found, give up:
 
538
        if len( nodes ) == 0:
 
539
            return ( object, ITreeNodeAdapterBridge(adapter=object) )
 
540
 
 
541
        # Use all selected nodes that have the same 'node_for' list as the
 
542
        # first selected node:
 
543
        base  = nodes[0].node_for
 
544
        nodes = [ node for node in nodes if base == node.node_for ]
 
545
 
 
546
        # If only one left, then return that node:
 
547
        if len( nodes ) == 1:
 
548
            return ( object, nodes[0] )
 
549
 
 
550
        # Otherwise, return a MultiTreeNode based on all selected nodes...
 
551
 
 
552
        # Use the node with no specified children as the root node. If not
 
553
        # found, just use the first selected node as the 'root node':
 
554
        root_node = None
 
555
        for i, node in enumerate( nodes ):
 
556
            if node.children == '':
 
557
                root_node = node
 
558
                del nodes[i]
 
559
                break
 
560
        else:
 
561
            root_node = nodes[0]
 
562
 
 
563
        # If we have a matching MultiTreeNode already cached, return it:
 
564
        key = ( root_node, ) + tuple( nodes )
 
565
        if key in factory.multi_nodes:
 
566
            return ( object, factory.multi_nodes[ key ] )
 
567
 
 
568
        # Otherwise create one, cache it, and return it:
 
569
        factory.multi_nodes[ key ] = multi_node = MultiTreeNode(
 
570
                                                       root_node = root_node,
 
571
                                                       nodes     = nodes )
 
572
 
 
573
        return ( object, multi_node )
 
574
 
 
575
    #---------------------------------------------------------------------------
 
576
    #  Returns the TreeNode associated with a specified class:
 
577
    #---------------------------------------------------------------------------
 
578
 
 
579
    def _node_for_class ( self, klass ):
 
580
        """ Returns the TreeNode associated with a specified class.
 
581
        """
 
582
        for node in self.factory.nodes:
 
583
            if issubclass( klass, tuple( node.node_for ) ):
 
584
                return node
 
585
        return None
 
586
 
 
587
    #---------------------------------------------------------------------------
 
588
    #  Returns the node and class associated with a specified class name:
 
589
    #---------------------------------------------------------------------------
 
590
 
 
591
    def _node_for_class_name ( self, class_name ):
 
592
        """ Returns the node and class associated with a specified class name.
 
593
        """
 
594
        for node in self.factory.nodes:
 
595
            for klass in node.node_for:
 
596
                if class_name == klass.__name__:
 
597
                    return ( node, klass )
 
598
        return ( None, None )
 
599
 
 
600
    #---------------------------------------------------------------------------
 
601
    #  Updates the icon for a specified node:
 
602
    #---------------------------------------------------------------------------
 
603
 
 
604
    def _update_icon(self, nid):
 
605
        """ Updates the icon for a specified node.
 
606
        """
 
607
        expanded, node, object = self._get_node_data(nid)
 
608
        nid.setIcon(0, self._get_icon(node, object, expanded))
 
609
 
 
610
    #---------------------------------------------------------------------------
 
611
    #  Begins an 'undoable' transaction:
 
612
    #---------------------------------------------------------------------------
 
613
 
 
614
    def _begin_undo ( self ):
 
615
        """ Begins an "undoable" transaction.
 
616
        """
 
617
        ui = self.ui
 
618
        self._undoable.append( ui._undoable )
 
619
        if (ui._undoable == -1) and (ui.history is not None):
 
620
            ui._undoable = ui.history.now
 
621
 
 
622
    #---------------------------------------------------------------------------
 
623
    #  Ends an 'undoable' transaction:
 
624
    #---------------------------------------------------------------------------
 
625
 
 
626
    def _end_undo ( self ):
 
627
        if self._undoable.pop() == -1:
 
628
            self.ui._undoable = -1
 
629
 
 
630
    #---------------------------------------------------------------------------
 
631
    #  Gets an 'undo' item for a change made to a node's children:
 
632
    #---------------------------------------------------------------------------
 
633
 
 
634
    def _get_undo_item ( self, object, name, event ):
 
635
        return ListUndoItem( object  = object,
 
636
                             name    = name,
 
637
                             index   = event.index,
 
638
                             added   = event.added,
 
639
                             removed = event.removed )
 
640
 
 
641
    #---------------------------------------------------------------------------
 
642
    #  Performs an undoable 'append' operation:
 
643
    #---------------------------------------------------------------------------
 
644
 
 
645
    def _undoable_append ( self, node, object, data, make_copy = True ):
 
646
        """ Performs an undoable append operation.
 
647
        """
 
648
        try:
 
649
            self._begin_undo()
 
650
            if make_copy:
 
651
                data = copy.deepcopy( data )
 
652
            node.append_child( object, data )
 
653
        finally:
 
654
            self._end_undo()
 
655
 
 
656
    #---------------------------------------------------------------------------
 
657
    #  Performs an undoable 'insert' operation:
 
658
    #---------------------------------------------------------------------------
 
659
 
 
660
    def _undoable_insert ( self, node, object, index, data, make_copy = True ):
 
661
        """ Performs an undoable insert operation.
 
662
        """
 
663
        try:
 
664
            self._begin_undo()
 
665
            if make_copy:
 
666
                data = copy.deepcopy( data )
 
667
            node.insert_child( object, index, data )
 
668
        finally:
 
669
            self._end_undo()
 
670
 
 
671
    #---------------------------------------------------------------------------
 
672
    #  Performs an undoable 'delete' operation:
 
673
    #---------------------------------------------------------------------------
 
674
 
 
675
    def _undoable_delete ( self, node, object, index ):
 
676
        """ Performs an undoable delete operation.
 
677
        """
 
678
        try:
 
679
            self._begin_undo()
 
680
            node.delete_child( object, index )
 
681
        finally:
 
682
            self._end_undo()
 
683
 
 
684
    #---------------------------------------------------------------------------
 
685
    #  Gets the id associated with a specified object (if any):
 
686
    #---------------------------------------------------------------------------
 
687
 
 
688
    def _get_object_nid ( self, object, name = '' ):
 
689
        """ Gets the ID associated with a specified object (if any).
 
690
        """
 
691
        info = self._map.get( id( object ) )
 
692
        if info is None:
 
693
            return None
 
694
        for name2, nid in info:
 
695
            if name == name2:
 
696
                return nid
 
697
        else:
 
698
            return info[0][1]
 
699
 
 
700
    #---------------------------------------------------------------------------
 
701
    #  Clears the current editor pane (if any):
 
702
    #---------------------------------------------------------------------------
 
703
 
 
704
    def _clear_editor ( self ):
 
705
        """ Clears the current editor pane (if any).
 
706
        """
 
707
        editor = self._editor
 
708
        if editor._node_ui is not None:
 
709
            editor.setWidget(None)
 
710
            editor._node_ui.dispose()
 
711
            editor._node_ui = editor._editor_nid = None
 
712
 
 
713
    #---------------------------------------------------------------------------
 
714
    #  Gets/Sets the node specific data:
 
715
    #---------------------------------------------------------------------------
 
716
 
 
717
    @staticmethod
 
718
    def _get_node_data(nid):
 
719
        """ Gets the node specific data. """
 
720
        return nid._py_data
 
721
 
 
722
    @staticmethod
 
723
    def _set_node_data(nid, data):
 
724
        """ Sets the node specific data. """
 
725
        nid._py_data = data
 
726
 
 
727
#----- User callable methods: --------------------------------------------------
 
728
 
 
729
    #---------------------------------------------------------------------------
 
730
    #  Gets the object associated with a specified node:
 
731
    #---------------------------------------------------------------------------
 
732
 
 
733
    def get_object ( self, nid ):
 
734
        """ Gets the object associated with a specified node.
 
735
        """
 
736
        return self._get_node_data( nid )[2]
 
737
 
 
738
    #---------------------------------------------------------------------------
 
739
    #  Returns the object which is the immmediate parent of a specified object
 
740
    #  in the tree:
 
741
    #---------------------------------------------------------------------------
 
742
 
 
743
    def get_parent ( self, object, name = '' ):
 
744
        """ Returns the object that is the immmediate parent of a specified
 
745
            object in the tree.
 
746
        """
 
747
        nid = self._get_object_nid( object, name )
 
748
        if nid is not None:
 
749
            pnid = nid.parent()
 
750
            if pnid is not self._tree.invisibleRootItem():
 
751
                return self.get_object( pnid )
 
752
        return None
 
753
 
 
754
    #---------------------------------------------------------------------------
 
755
    #  Returns the node associated with a specified object:
 
756
    #---------------------------------------------------------------------------
 
757
 
 
758
    def get_node ( self, object, name = '' ):
 
759
        """ Returns the node associated with a specified object.
 
760
        """
 
761
        nid = self._get_object_nid( object, name )
 
762
        if nid is not None:
 
763
            return self._get_node_data( nid )[1]
 
764
        return None
 
765
 
 
766
#----- Tree event handlers: ----------------------------------------------------
 
767
 
 
768
    #---------------------------------------------------------------------------
 
769
    #  Handles a tree node being expanded:
 
770
    #---------------------------------------------------------------------------
 
771
 
 
772
    def _on_item_expanded(self, nid):
 
773
        """ Handles a tree node being expanded.
 
774
        """
 
775
        expanded, node, object = self._get_node_data(nid)
 
776
 
 
777
        # If 'auto_close' requested for this node type, close all of the node's
 
778
        # siblings:
 
779
        if node.can_auto_close(object):
 
780
            parent = nid.parent()
 
781
 
 
782
            if parent is not None:
 
783
                for snid in self._nodes_for(parent):
 
784
                    if snid is not nid:
 
785
                        snid.setExpanded(False)
 
786
 
 
787
        # Expand the node (i.e. populate its children if they are not there
 
788
        # yet):
 
789
        self._expand_node(nid)
 
790
 
 
791
        self._update_icon(nid)
 
792
 
 
793
    #---------------------------------------------------------------------------
 
794
    #  Handles a tree node being collapsed:
 
795
    #---------------------------------------------------------------------------
 
796
 
 
797
    def _on_item_collapsed(self, nid):
 
798
        """ Handles a tree node being collapsed.
 
799
        """
 
800
        self._update_icon(nid)
 
801
 
 
802
    #---------------------------------------------------------------------------
 
803
    #  Handles a tree item click:
 
804
    #---------------------------------------------------------------------------
 
805
 
 
806
    def _on_item_clicked(self, nid, col):
 
807
        """ Handles a tree item being clicked.
 
808
        """
 
809
        _, node, object = self._get_node_data(nid)
 
810
 
 
811
        if node.click(object) is True and self.factory.on_click is not None:
 
812
            self.ui.evaluate(self.factory.on_click, object)
 
813
 
 
814
        # Fire the 'click' event with the object as its value:
 
815
        self.click = object
 
816
 
 
817
    #---------------------------------------------------------------------------
 
818
    #  Handles a tree item double click:
 
819
    #---------------------------------------------------------------------------
 
820
 
 
821
    def _on_item_dclicked(self, nid, col):
 
822
        """ Handles a tree item being double-clicked.
 
823
        """
 
824
        _, node, object = self._get_node_data(nid)
 
825
 
 
826
        if node.dclick(object) is True:
 
827
            if self.factory.on_dclick is not None:
 
828
                self.ui.evaluate(self.factory.on_dclick, object)
 
829
                self._veto = True
 
830
        else:
 
831
            self._veto = True
 
832
 
 
833
        # Fire the 'dclick' event with the clicked on object as value:
 
834
        self.dclick = object
 
835
 
 
836
    #---------------------------------------------------------------------------
 
837
    #  Handles a tree node being selected:
 
838
    #---------------------------------------------------------------------------
 
839
 
 
840
    def _on_tree_sel_changed(self):
 
841
        """ Handles a tree node being selected.
 
842
        """
 
843
        # Get the new selection:
 
844
        nids = self._tree.selectedItems()
 
845
 
 
846
        selected = []
 
847
        if len(nids) > 0:
 
848
            for nid in nids:
 
849
                # If there is a real selection, get the associated object:
 
850
                expanded, node, sel_object = self._get_node_data(nid)
 
851
                selected.append(sel_object)
 
852
 
 
853
                # Try to inform the node specific handler of the selection, if
 
854
                # there are multiple selections, we only care about the first
 
855
                # (or maybe the last makes more sense?)
 
856
 
 
857
                # QTreeWidgetItem does not have an equal operator, so use id()
 
858
                if id(nid) == id(nids[0]):
 
859
                    object = sel_object
 
860
                    not_handled = node.select(sel_object)
 
861
        else:
 
862
            nid = None
 
863
            object = None
 
864
            not_handled = True
 
865
 
 
866
        # Set the value of the new selection:
 
867
        if self.factory.selection_mode == 'single':
 
868
            self._no_update_selected = True
 
869
            self.selected = object
 
870
            self._no_update_selected = False
 
871
        else:
 
872
            self._no_update_selected = True
 
873
            self.selected = selected
 
874
            self._no_update_selected = False
 
875
 
 
876
        # If no one has been notified of the selection yet, inform the editor's
 
877
        # select handler (if any) of the new selection:
 
878
        if not_handled is True:
 
879
            self.ui.evaluate(self.factory.on_select, object)
 
880
 
 
881
        # Check to see if there is an associated node editor pane:
 
882
        editor = self._editor
 
883
        if editor is not None:
 
884
            # If we already had a node editor, destroy it:
 
885
            editor.setUpdatesEnabled(False)
 
886
            self._clear_editor()
 
887
 
 
888
            # If there is a selected object, create a new editor for it:
 
889
            if object is not None:
 
890
                # Try to chain the undo history to the main undo history:
 
891
                view = node.get_view( object )
 
892
                if view is None:
 
893
                    view = object.trait_view()
 
894
                if (self.ui.history is not None) or (view.kind == 'subpanel'):
 
895
                    ui = object.edit_traits( parent = editor,
 
896
                                             view   = view,
 
897
                                             kind   = 'subpanel' )
 
898
                else:
 
899
                    # Otherwise, just set up our own new one:
 
900
                    ui = object.edit_traits( parent = editor,
 
901
                                             view   = view,
 
902
                                             kind   = 'panel' )
 
903
 
 
904
 
 
905
                # Make our UI the parent of the new UI:
 
906
                ui.parent = self.ui
 
907
 
 
908
                # Remember the new editor's UI and node info:
 
909
                editor._node_ui    = ui
 
910
                editor._editor_nid = nid
 
911
 
 
912
                # Finish setting up the editor:
 
913
                ui.control.layout().setContentsMargins(0, 0, 0, 0)
 
914
                editor.setWidget(ui.control)
 
915
 
 
916
            # Allow the editor view to show any changes that have occurred:
 
917
            editor.setUpdatesEnabled(True)
 
918
 
 
919
    #---------------------------------------------------------------------------
 
920
    #  Handles the user right clicking on a tree node:
 
921
    #---------------------------------------------------------------------------
 
922
 
 
923
    def _on_context_menu(self, pos):
 
924
        """ Handles the user requesting a context menuright clicking on a tree node.
 
925
        """
 
926
        nid = self._tree.itemAt(pos)
 
927
 
 
928
        if nid is None:
 
929
            return
 
930
 
 
931
        _, node, object = self._get_node_data(nid)
 
932
 
 
933
        self._data = (node, object, nid)
 
934
        self._context = {'object':  object,
 
935
                         'editor':  self,
 
936
                         'node':    node,
 
937
                         'info':    self.ui.info,
 
938
                         'handler': self.ui.handler}
 
939
 
 
940
        # Try to get the parent node of the node clicked on:
 
941
        pnid = nid.parent()
 
942
        if pnid is None or pnid is self._tree.invisibleRootItem():
 
943
            parent_node = parent_object = None
 
944
        else:
 
945
            _, parent_node, parent_object = self._get_node_data(pnid)
 
946
 
 
947
        self._menu_node = node
 
948
        self._menu_parent_node = parent_node
 
949
        self._menu_parent_object = parent_object
 
950
 
 
951
        menu = node.get_menu(object)
 
952
 
 
953
        if menu is None:
 
954
            # Use the standard, default menu:
 
955
            menu = self._standard_menu(node, object)
 
956
 
 
957
        elif isinstance(menu, Menu):
 
958
            # Use the menu specified by the node:
 
959
            group = menu.find_group(NewAction)
 
960
            if group is not None:
 
961
                # Only set it the first time:
 
962
                group.id = ''
 
963
                actions  = self._new_actions( node, object )
 
964
                if len( actions ) > 0:
 
965
                    group.insert( 0, Menu( name = 'New', *actions ) )
 
966
 
 
967
        else:
 
968
            # All other values mean no menu should be displayed:
 
969
            menu = None
 
970
 
 
971
        # Only display the menu if a valid menu is defined:
 
972
        if menu is not None:
 
973
            qmenu = menu.create_menu( self._tree, self )
 
974
            qmenu.exec_(self._tree.mapToGlobal(pos))
 
975
 
 
976
        # Reset all menu related cached values:
 
977
        self._data = self._context = self._menu_node = \
 
978
        self._menu_parent_node = self._menu_parent_object = None
 
979
 
 
980
    #---------------------------------------------------------------------------
 
981
    #  Returns the standard contextual pop-up menu:
 
982
    #---------------------------------------------------------------------------
 
983
 
 
984
    def _standard_menu ( self, node, object ):
 
985
        """ Returns the standard contextual pop-up menu.
 
986
        """
 
987
        actions = [ CutAction, CopyAction, PasteAction, Separator(),
 
988
                    DeleteAction, Separator(), RenameAction ]
 
989
 
 
990
        # See if the 'New' menu section should be added:
 
991
        items = self._new_actions( node, object )
 
992
        if len( items ) > 0:
 
993
            actions[0:0] = [ Menu( name = 'New', *items ), Separator() ]
 
994
 
 
995
        return Menu( *actions )
 
996
 
 
997
    #---------------------------------------------------------------------------
 
998
    #  Returns a list of Actions that will create 'new' objects:
 
999
    #---------------------------------------------------------------------------
 
1000
 
 
1001
    def _new_actions ( self, node, object ):
 
1002
        """ Returns a list of Actions that will create new objects.
 
1003
        """
 
1004
        object = self._data[1]
 
1005
        items  = []
 
1006
        add    = node.get_add( object )
 
1007
        if len( add ) > 0:
 
1008
            for klass in add:
 
1009
                prompt = False
 
1010
                if isinstance( klass, tuple ):
 
1011
                    klass, prompt = klass
 
1012
                add_node = self._node_for_class( klass )
 
1013
                if add_node is not None:
 
1014
                    class_name = klass.__name__
 
1015
                    name       = add_node.get_name( object )
 
1016
                    if name == '':
 
1017
                        name = class_name
 
1018
                    items.append(
 
1019
                        Action( name   = name,
 
1020
                                action = "editor._menu_new_node('%s',%s)" %
 
1021
                                         ( class_name, prompt ) ) )
 
1022
        return items
 
1023
 
 
1024
    #---------------------------------------------------------------------------
 
1025
    #  Menu action helper methods:
 
1026
    #---------------------------------------------------------------------------
 
1027
 
 
1028
    def _is_copyable ( self, object ):
 
1029
        parent = self._menu_parent_node
 
1030
        if isinstance( parent, ObjectTreeNode ):
 
1031
            return parent.can_copy( self._menu_parent_object )
 
1032
        return ((parent is not None) and parent.can_copy( object ))
 
1033
 
 
1034
    def _is_cutable ( self, object ):
 
1035
        parent = self._menu_parent_node
 
1036
        if isinstance( parent, ObjectTreeNode ):
 
1037
            can_cut = (parent.can_copy( self._menu_parent_object ) and
 
1038
                       parent.can_delete( self._menu_parent_object ))
 
1039
        else:
 
1040
            can_cut = ((parent is not None) and
 
1041
                       parent.can_copy( object ) and
 
1042
                       parent.can_delete( object ))
 
1043
        return (can_cut and self._menu_node.can_delete_me( object ))
 
1044
 
 
1045
    def _is_pasteable ( self, object ):
 
1046
        return self._menu_node.can_add(object, clipboard.instance_type)
 
1047
 
 
1048
    def _is_deletable ( self, object ):
 
1049
        parent = self._menu_parent_node
 
1050
        if isinstance( parent, ObjectTreeNode ):
 
1051
            can_delete = parent.can_delete( self._menu_parent_object )
 
1052
        else:
 
1053
            can_delete = ((parent is not None) and parent.can_delete( object ))
 
1054
        return (can_delete and self._menu_node.can_delete_me( object ))
 
1055
 
 
1056
    def _is_renameable ( self, object ):
 
1057
        parent = self._menu_parent_node
 
1058
        if isinstance( parent, ObjectTreeNode ):
 
1059
            can_rename = parent.can_rename( self._menu_parent_object )
 
1060
        else:
 
1061
            can_rename = ((parent is not None) and parent.can_rename( object ))
 
1062
 
 
1063
        can_rename = (can_rename and self._menu_node.can_rename_me( object ))
 
1064
 
 
1065
        # Set the widget item's editable flag appropriately.
 
1066
        nid = self._get_object_nid(object)
 
1067
        flags = nid.flags()
 
1068
        if can_rename:
 
1069
            flags |= QtCore.Qt.ItemIsEditable
 
1070
        else:
 
1071
            flags &= ~QtCore.Qt.ItemIsEditable
 
1072
        nid.setFlags(flags)
 
1073
 
 
1074
        return can_rename
 
1075
 
 
1076
#----- pyface.action 'controller' interface implementation: --------------------
 
1077
 
 
1078
    #---------------------------------------------------------------------------
 
1079
    #  Adds a menu item to the menu being constructed:
 
1080
    #---------------------------------------------------------------------------
 
1081
 
 
1082
    def add_to_menu ( self, menu_item ):
 
1083
        """ Adds a menu item to the menu bar being constructed.
 
1084
        """
 
1085
        action = menu_item.item.action
 
1086
        self.eval_when( action.enabled_when, menu_item, 'enabled' )
 
1087
        self.eval_when( action.checked_when, menu_item, 'checked' )
 
1088
 
 
1089
    #---------------------------------------------------------------------------
 
1090
    #  Adds a tool bar item to the tool bar being constructed:
 
1091
    #---------------------------------------------------------------------------
 
1092
 
 
1093
    def add_to_toolbar ( self, toolbar_item ):
 
1094
        """ Adds a toolbar item to the toolbar being constructed.
 
1095
        """
 
1096
        self.add_to_menu( toolbar_item )
 
1097
 
 
1098
    #---------------------------------------------------------------------------
 
1099
    #  Returns whether the menu action should be defined in the user interface:
 
1100
    #---------------------------------------------------------------------------
 
1101
 
 
1102
    def can_add_to_menu ( self, action ):
 
1103
        """ Returns whether the action should be defined in the user interface.
 
1104
        """
 
1105
        if action.defined_when != '':
 
1106
            try:
 
1107
                if not eval( action.defined_when, globals(), self._context ):
 
1108
                    return False
 
1109
            except:
 
1110
                open_fbi()
 
1111
 
 
1112
        if action.visible_when != '':
 
1113
            try:
 
1114
                if not eval( action.visible_when, globals(), self._context ):
 
1115
                    return False
 
1116
            except:
 
1117
                open_fbi()
 
1118
 
 
1119
        return True
 
1120
 
 
1121
    #---------------------------------------------------------------------------
 
1122
    #  Returns whether the toolbar action should be defined in the user
 
1123
    #  interface:
 
1124
    #---------------------------------------------------------------------------
 
1125
 
 
1126
    def can_add_to_toolbar ( self, action ):
 
1127
        """ Returns whether the toolbar action should be defined in the user
 
1128
            interface.
 
1129
        """
 
1130
        return self.can_add_to_menu( action )
 
1131
 
 
1132
    #---------------------------------------------------------------------------
 
1133
    #  Performs the action described by a specified Action object:
 
1134
    #---------------------------------------------------------------------------
 
1135
 
 
1136
    def perform ( self, action, action_event = None ):
 
1137
        """ Performs the action described by a specified Action object.
 
1138
        """
 
1139
        self.ui.do_undoable( self._perform, action )
 
1140
 
 
1141
    def _perform ( self, action ):
 
1142
        node, object, nid = self._data
 
1143
        method_name       = action.action
 
1144
        info              = self.ui.info
 
1145
        handler           = self.ui.handler
 
1146
 
 
1147
        if method_name.find( '.' ) >= 0:
 
1148
            if method_name.find( '(' ) < 0:
 
1149
                method_name += '()'
 
1150
            try:
 
1151
                eval( method_name, globals(),
 
1152
                      { 'object':  object,
 
1153
                        'editor':  self,
 
1154
                        'node':    node,
 
1155
                        'info':    info,
 
1156
                        'handler': handler } )
 
1157
            except:
 
1158
                # fixme: Should the exception be logged somewhere?
 
1159
                pass
 
1160
            return
 
1161
 
 
1162
        method = getattr( handler, method_name, None )
 
1163
        if method is not None:
 
1164
            method( info, object )
 
1165
            return
 
1166
 
 
1167
        if action.on_perform is not None:
 
1168
            action.on_perform( object )
 
1169
 
 
1170
#----- Menu support methods: ---------------------------------------------------
 
1171
 
 
1172
    #---------------------------------------------------------------------------
 
1173
    #  Evaluates a condition within a defined context and sets a specified
 
1174
    #  object trait based on the (assumed) boolean result:
 
1175
    #---------------------------------------------------------------------------
 
1176
 
 
1177
    def eval_when ( self, condition, object, trait ):
 
1178
        """ Evaluates a condition within a defined context, and sets a
 
1179
        specified object trait based on the result, which is assumed to be a
 
1180
        Boolean.
 
1181
        """
 
1182
        if condition != '':
 
1183
            value = True
 
1184
            try:
 
1185
                if not eval( condition, globals(), self._context ):
 
1186
                    value = False
 
1187
            except:
 
1188
                open_fbi()
 
1189
            setattr( object, trait, value )
 
1190
 
 
1191
#----- Menu event handlers: ----------------------------------------------------
 
1192
 
 
1193
    #---------------------------------------------------------------------------
 
1194
    #  Copies the current tree node object to the paste buffer:
 
1195
    #---------------------------------------------------------------------------
 
1196
 
 
1197
    def _menu_copy_node ( self ):
 
1198
        """ Copies the current tree node object to the paste buffer.
 
1199
        """
 
1200
        clipboard.instance = self._data[1]
 
1201
        self._data = None
 
1202
 
 
1203
    #---------------------------------------------------------------------------
 
1204
    #   Cuts the current tree node object into the paste buffer:
 
1205
    #---------------------------------------------------------------------------
 
1206
 
 
1207
    def _menu_cut_node ( self ):
 
1208
        """ Cuts the current tree node object into the paste buffer.
 
1209
        """
 
1210
        node, object, nid = self._data
 
1211
        clipboard.instance = object
 
1212
        self._data = None
 
1213
        self._undoable_delete(*self._node_index(nid))
 
1214
 
 
1215
    #---------------------------------------------------------------------------
 
1216
    #  Pastes the current contents of the paste buffer into the current node:
 
1217
    #---------------------------------------------------------------------------
 
1218
 
 
1219
    def _menu_paste_node ( self ):
 
1220
        """ Pastes the current contents of the paste buffer into the current
 
1221
            node.
 
1222
        """
 
1223
        node, object, nid = self._data
 
1224
        self._data = None
 
1225
        self._undoable_append(node, object, clipboard.instance, False)
 
1226
 
 
1227
    #---------------------------------------------------------------------------
 
1228
    #  Deletes the current node from the tree:
 
1229
    #---------------------------------------------------------------------------
 
1230
 
 
1231
    def _menu_delete_node ( self ):
 
1232
        """ Deletes the current node from the tree.
 
1233
        """
 
1234
        node, object, nid = self._data
 
1235
        self._data        = None
 
1236
        rc = node.confirm_delete( object )
 
1237
        if rc is not False:
 
1238
            if rc is not True:
 
1239
                if self.ui.history is None:
 
1240
                    # If no undo history, ask user to confirm the delete:
 
1241
                    butn = QtGui.QMessageBox.question(
 
1242
                                self._tree,
 
1243
                                "Confirm Deletion",
 
1244
                                "Are you sure you want to delete %s?" % node.get_label( object ),
 
1245
                                QtGui.QMessageBox.Yes|QtGui.QMessageBox.No)
 
1246
                    if butn != QtGui.QMessageBox.Yes:
 
1247
                        return
 
1248
 
 
1249
            self._undoable_delete( *self._node_index( nid ) )
 
1250
 
 
1251
    #---------------------------------------------------------------------------
 
1252
    #  Renames the current tree node:
 
1253
    #---------------------------------------------------------------------------
 
1254
 
 
1255
    def _menu_rename_node ( self ):
 
1256
        """ Rename the current node.
 
1257
        """
 
1258
        _, _, nid = self._data
 
1259
        self._data = None
 
1260
        self._tree.editItem(nid)
 
1261
 
 
1262
    def _on_nid_changed(self, nid, col):
 
1263
        """ Handle changes to a widget item.
 
1264
        """
 
1265
        # The node data may not have been set up for the nid yet.  Ignore it if
 
1266
        # it hasn't.
 
1267
        try:
 
1268
            _, node, object = self._get_node_data(nid)
 
1269
        except:
 
1270
            return
 
1271
 
 
1272
        new_label = unicode(nid.text(col))
 
1273
        old_label = node.get_label(object)
 
1274
 
 
1275
        if new_label != old_label:
 
1276
            if new_label != '':
 
1277
                node.set_label(object, new_label)
 
1278
            else:
 
1279
                nid.setText(col, old_label)
 
1280
 
 
1281
    #---------------------------------------------------------------------------
 
1282
    #  Adds a new object to the current node:
 
1283
    #---------------------------------------------------------------------------
 
1284
 
 
1285
    def _menu_new_node ( self, class_name, prompt = False ):
 
1286
        """ Adds a new object to the current node.
 
1287
        """
 
1288
        node, object, nid   = self._data
 
1289
        self._data          = None
 
1290
        new_node, new_class = self._node_for_class_name( class_name )
 
1291
        new_object          = new_class()
 
1292
        if (not prompt) or new_object.edit_traits(
 
1293
                            parent = self.control, kind = 'livemodal' ).result:
 
1294
            self._undoable_append( node, object, new_object, False )
 
1295
 
 
1296
            # Automatically select the new object if editing is being performed:
 
1297
            if self.factory.editable:
 
1298
                self._tree.setCurrentItem(nid.child(nid.childCount() - 1))
 
1299
 
 
1300
#----- Model event handlers: ---------------------------------------------------
 
1301
 
 
1302
    #---------------------------------------------------------------------------
 
1303
    #  Handles the children of a node being completely replaced:
 
1304
    #---------------------------------------------------------------------------
 
1305
 
 
1306
    def _children_replaced ( self, object, name = '', new = None ):
 
1307
        """ Handles the children of a node being completely replaced.
 
1308
        """
 
1309
        tree = self._tree
 
1310
        for expanded, node, nid in self._object_info_for( object, name ):
 
1311
            children = node.get_children( object )
 
1312
 
 
1313
            # Only add/remove the changes if the node has already been expanded:
 
1314
            if expanded:
 
1315
                # Delete all current child nodes:
 
1316
                for cnid in self._nodes_for( nid ):
 
1317
                    self._delete_node( cnid )
 
1318
 
 
1319
                # Add all of the children back in as new nodes:
 
1320
                for child in children:
 
1321
                    child, child_node = self._node_for( child )
 
1322
                    if child_node is not None:
 
1323
                        self._append_node( nid, child_node, child )
 
1324
 
 
1325
            # Try to expand the node (if requested):
 
1326
            if node.can_auto_open( object ):
 
1327
                nid.setExpanded(True)
 
1328
 
 
1329
    #---------------------------------------------------------------------------
 
1330
    #  Handles the children of a node being changed:
 
1331
    #---------------------------------------------------------------------------
 
1332
 
 
1333
    def _children_updated ( self, object, name, event ):
 
1334
        """ Handles the children of a node being changed.
 
1335
        """
 
1336
        # Log the change that was made made (removing '_items' from the end of
 
1337
        # the name):
 
1338
        name = name[:-6]
 
1339
        self.log_change( self._get_undo_item, object, name, event )
 
1340
 
 
1341
        # Get information about the node that was changed:
 
1342
        start = event.index
 
1343
        n     = len( event.added )
 
1344
        end   = start + len( event.removed )
 
1345
        tree                = self._tree
 
1346
 
 
1347
        for expanded, node, nid in self._object_info_for( object, name ):
 
1348
            children = node.get_children( object )
 
1349
 
 
1350
            # If the new children aren't all at the end, remove/add them all:
 
1351
            if (n > 0) and ((start + n) != len( children )):
 
1352
                self._children_replaced( object, name, event )
 
1353
                return
 
1354
 
 
1355
            # Only add/remove the changes if the node has already been expanded:
 
1356
            if expanded:
 
1357
                # Remove all of the children that were deleted:
 
1358
                for cnid in self._nodes_for( nid )[ start: end ]:
 
1359
                    self._delete_node( cnid )
 
1360
 
 
1361
                # Add all of the children that were added:
 
1362
                for child in event.added:
 
1363
                    child, child_node = self._node_for( child )
 
1364
                    if child_node is not None:
 
1365
                        self._append_node( nid, child_node, child )
 
1366
 
 
1367
            # Try to expand the node (if requested):
 
1368
            if node.can_auto_open( object ):
 
1369
                nid.setExpanded(True)
 
1370
 
 
1371
    #---------------------------------------------------------------------------
 
1372
    #   Handles the label of an object being changed:
 
1373
    #---------------------------------------------------------------------------
 
1374
 
 
1375
    def _label_updated ( self, object, name, label ):
 
1376
        """  Handles the label of an object being changed.
 
1377
        """
 
1378
        # Prevent the itemChanged() signal from being emitted.
 
1379
        blk = self._tree.blockSignals(True)
 
1380
 
 
1381
        nids = {}
 
1382
        for name2, nid in self._map[ id( object ) ]:
 
1383
            if nid not in nids:
 
1384
                nids[ nid ] = None
 
1385
                node = self._get_node_data( nid )[1]
 
1386
                nid.setText(0, node.get_label(object))
 
1387
                self._update_icon(nid)
 
1388
 
 
1389
        self._tree.blockSignals(blk)
 
1390
 
 
1391
#-- UI preference save/restore interface ---------------------------------------
 
1392
 
 
1393
    #---------------------------------------------------------------------------
 
1394
    #  Restores any saved user preference information associated with the
 
1395
    #  editor:
 
1396
    #---------------------------------------------------------------------------
 
1397
 
 
1398
    def restore_prefs ( self, prefs ):
 
1399
        """ Restores any saved user preference information associated with the
 
1400
            editor.
 
1401
        """
 
1402
        if isinstance(self.control, QtGui.QSplitter):
 
1403
            if isinstance(prefs, dict):
 
1404
                structure = prefs.get('structure')
 
1405
            else:
 
1406
                structure = prefs
 
1407
 
 
1408
            self.control.restoreState(structure)
 
1409
 
 
1410
    #---------------------------------------------------------------------------
 
1411
    #  Returns any user preference information associated with the editor:
 
1412
    #---------------------------------------------------------------------------
 
1413
 
 
1414
    def save_prefs ( self ):
 
1415
        """ Returns any user preference information associated with the editor.
 
1416
        """
 
1417
        if isinstance(self.control, QtGui.QSplitter):
 
1418
            return {'structure': str(self.control.saveState())}
 
1419
 
 
1420
        return None
 
1421
 
 
1422
#-- End UI preference save/restore interface -----------------------------------
 
1423
 
 
1424
#-------------------------------------------------------------------------------
 
1425
#  '_TreeWidget' class:
 
1426
#-------------------------------------------------------------------------------
 
1427
 
 
1428
class _TreeWidget(QtGui.QTreeWidget):
 
1429
    """ The _TreeWidget class is a specialised QTreeWidget that reimplements
 
1430
        the drag'n'drop support so that it hooks into the provided Traits
 
1431
        support.
 
1432
    """
 
1433
    def __init__(self, editor, parent=None):
 
1434
        """ Initialise the tree widget.
 
1435
        """
 
1436
        QtGui.QTreeWidget.__init__(self, parent)
 
1437
 
 
1438
        self.header().hide()
 
1439
        self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
 
1440
        self.setDragEnabled(True)
 
1441
        self.setAcceptDrops(True)
 
1442
 
 
1443
        if editor.factory.selection_mode == 'extended':
 
1444
            self.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)
 
1445
 
 
1446
        self.connect(self, QtCore.SIGNAL('itemExpanded(QTreeWidgetItem *)'),
 
1447
                editor._on_item_expanded)
 
1448
        self.connect(self, QtCore.SIGNAL('itemCollapsed(QTreeWidgetItem *)'),
 
1449
                editor._on_item_collapsed)
 
1450
        self.connect(self,
 
1451
                QtCore.SIGNAL('itemClicked(QTreeWidgetItem *, int)'),
 
1452
                editor._on_item_clicked)
 
1453
        self.connect(self,
 
1454
                QtCore.SIGNAL('itemDoubleClicked(QTreeWidgetItem *, int)'),
 
1455
                editor._on_item_dclicked)
 
1456
        self.connect(self, QtCore.SIGNAL('itemSelectionChanged()'),
 
1457
                editor._on_tree_sel_changed)
 
1458
        self.connect(self, QtCore.SIGNAL('customContextMenuRequested(QPoint)'),
 
1459
                editor._on_context_menu)
 
1460
        self.connect(self,
 
1461
                QtCore.SIGNAL('itemChanged(QTreeWidgetItem *, int)'),
 
1462
                editor._on_nid_changed)
 
1463
 
 
1464
        self._editor = editor
 
1465
        self._dragging = None
 
1466
 
 
1467
    def startDrag(self, actions):
 
1468
        """ Reimplemented to start the drag of a tree widget item.
 
1469
        """
 
1470
        nid = self.currentItem()
 
1471
        if nid is None:
 
1472
            return
 
1473
 
 
1474
        self._dragging = nid
 
1475
 
 
1476
        _, node, object = self._editor._get_node_data(nid)
 
1477
 
 
1478
        # Convert the item being dragged to MIME data.
 
1479
        md = PyMimeData(node.get_drag_object(object))
 
1480
 
 
1481
        # Render the item being dragged as a pixmap.
 
1482
        nid_rect = self.visualItemRect(nid)
 
1483
        rect = nid_rect.intersected(self.viewport().rect())
 
1484
 
 
1485
        pm = QtGui.QPixmap(rect.size())
 
1486
        pm.fill(self.palette().base().color())
 
1487
 
 
1488
        painter = QtGui.QPainter(pm)
 
1489
 
 
1490
        option = self.viewOptions()
 
1491
        option.state |= QtGui.QStyle.State_Selected
 
1492
        option.rect = QtCore.QRect(nid_rect.topLeft() - rect.topLeft(), nid_rect.size())
 
1493
 
 
1494
        self.itemDelegate().paint(painter, option, self.indexFromItem(nid))
 
1495
 
 
1496
        painter.end()
 
1497
 
 
1498
        # Calculate the hotspot so that the pixmap appears on top of the
 
1499
        # original item.
 
1500
        rect.adjust(self.horizontalOffset(), self.verticalOffset(), 0, 0)
 
1501
        hspos = self.mapFromGlobal(QtGui.QCursor.pos()) - rect.topLeft()
 
1502
 
 
1503
        # Start the drag.
 
1504
        drag = QtGui.QDrag(self)
 
1505
        drag.setMimeData(md)
 
1506
        drag.setPixmap(pm)
 
1507
        drag.setHotSpot(hspos)
 
1508
        drag.exec_(actions)
 
1509
 
 
1510
    def dragEnterEvent(self, e):
 
1511
        """ Reimplemented to see if the current drag can be handled by the
 
1512
            tree.
 
1513
        """
 
1514
        # Assume the drag is invalid.
 
1515
        e.ignore()
 
1516
 
 
1517
        # Check what is being dragged.
 
1518
        md = PyMimeData.coerce(e.mimeData())
 
1519
        if md is None:
 
1520
            return
 
1521
 
 
1522
        # We might be able to handle it (but it depends on what the final
 
1523
        # target is).
 
1524
        e.acceptProposedAction()
 
1525
 
 
1526
    def dragMoveEvent(self, e):
 
1527
        """ Reimplemented to see if the current drag can be handled by the
 
1528
            particular tree widget item underneath the cursor.
 
1529
        """
 
1530
        # Assume the drag is invalid.
 
1531
        e.ignore()
 
1532
 
 
1533
        # Get the tree widget item under the cursor.
 
1534
        nid = self.itemAt(e.pos())
 
1535
        if nid is None:
 
1536
            return
 
1537
 
 
1538
        # Check that the target is not the source of a child of the source.
 
1539
        if self._dragging is not None:
 
1540
            pnid = nid
 
1541
            while pnid is not None:
 
1542
                if pnid is self._dragging:
 
1543
                    return
 
1544
 
 
1545
                pnid = pnid.parent()
 
1546
 
 
1547
        # A copy action is interpreted as moving the source to a particular
 
1548
        # place within the target's parent.  A move action is interpreted as
 
1549
        # moving the source to be a child of the target.
 
1550
        if e.proposedAction() == QtCore.Qt.CopyAction:
 
1551
            node, object, _ = self._editor._node_index(nid)
 
1552
            insert = True
 
1553
        else:
 
1554
            _, node, object = self._editor._get_node_data(nid)
 
1555
            insert = False
 
1556
 
 
1557
        # See if the model will accept a drop.
 
1558
        data = PyMimeData.coerce(e.mimeData()).instance()
 
1559
 
 
1560
        if not node._is_droppable(object, data, insert):
 
1561
            return
 
1562
 
 
1563
        e.acceptProposedAction()
 
1564
 
 
1565
    def dropEvent(self, e):
 
1566
        """ Reimplemented to update the model and tree.
 
1567
        """
 
1568
        # Assume the drop is invalid.
 
1569
        e.ignore()
 
1570
 
 
1571
        dragging = self._dragging
 
1572
        self._dragging = None
 
1573
 
 
1574
        # Get the tree widget item under the cursor.
 
1575
        nid = self.itemAt(e.pos())
 
1576
        if nid is None:
 
1577
            return
 
1578
 
 
1579
        # Get the data being dropped.
 
1580
        data = PyMimeData.coerce(e.mimeData()).instance()
 
1581
 
 
1582
        editor = self._editor
 
1583
        _, node, object = editor._get_node_data(nid)
 
1584
 
 
1585
        if e.proposedAction() == QtCore.Qt.MoveAction:
 
1586
            if not node._is_droppable( object, data, False ):
 
1587
                return
 
1588
 
 
1589
            if dragging is not None:
 
1590
                data = node._drop_object( object, data, False )
 
1591
                if data is not None:
 
1592
                    try:
 
1593
                        editor._begin_undo()
 
1594
                        editor._undoable_delete(
 
1595
                                 *editor._node_index( dragging ) )
 
1596
                        editor._undoable_append( node, object, data, False )
 
1597
                    finally:
 
1598
                        editor._end_undo()
 
1599
            else:
 
1600
                data = node._drop_object( object, data )
 
1601
                if data is not None:
 
1602
                    editor._undoable_append( node, object, data, False )
 
1603
        else:
 
1604
            to_node, to_object, to_index = editor._node_index( nid )
 
1605
            if to_node is not None:
 
1606
                if dragging is not None:
 
1607
                    data = node._drop_object( to_object, data, False )
 
1608
                    if data is not None:
 
1609
                        from_node, from_object, from_index = \
 
1610
                            editor._node_index( dragging )
 
1611
                        if ((to_object is from_object) and
 
1612
                            (to_index > from_index)):
 
1613
                            to_index -= 1
 
1614
                        try:
 
1615
                            editor._begin_undo()
 
1616
                            editor._undoable_delete( from_node, from_object,
 
1617
                                                   from_index )
 
1618
                            editor._undoable_insert( to_node, to_object, to_index,
 
1619
                                                   data, False )
 
1620
                        finally:
 
1621
                            editor._end_undo()
 
1622
                else:
 
1623
                    data = to_node._drop_object( to_object, data )
 
1624
                    if data is not None:
 
1625
                        editor._undoable_insert( to_node, to_object, to_index,
 
1626
                                               data, False )
 
1627
 
 
1628
        e.acceptProposedAction()