1
#------------------------------------------------------------------------------
2
# Copyright (c) 2007, Riverbank Computing Limited
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
9
# Author: Riverbank Computing Limited
10
#------------------------------------------------------------------------------
12
""" Defines the tree editor for the PyQt user interface toolkit.
15
#-------------------------------------------------------------------------------
17
#-------------------------------------------------------------------------------
21
from pyface.qt import QtCore, QtGui
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
31
from clipboard import clipboard, PyMimeData
32
from editor import Editor
33
from helper import open_fbi, pixmap_cache
35
#-------------------------------------------------------------------------------
36
# The core tree node menu actions:
37
#-------------------------------------------------------------------------------
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)' )
56
#-------------------------------------------------------------------------------
57
# 'SimpleEditor' class:
58
#-------------------------------------------------------------------------------
60
class SimpleEditor ( Editor ):
61
""" Simple style of tree editor.
63
#---------------------------------------------------------------------------
65
#---------------------------------------------------------------------------
67
# Is the tree editor is scrollable? This value overrides the default.
70
# Allows an external agent to set the tree selection
73
# The currently selected object
76
# The event fired when a tree node is clicked on:
79
# The event fired when a tree node is double-clicked on:
82
# The event fired when the application wants to veto an operation:
85
#---------------------------------------------------------------------------
86
# Finishes initializing the editor by creating the underlying toolkit
88
#---------------------------------------------------------------------------
90
def init ( self, parent ):
91
""" Finishes initializing the editor by creating the underlying toolkit
94
factory = self.factory
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
104
# If this is the editor that defines the trait editor panel:
105
if factory_editor is None:
107
# Remember which editor has the trait editor in the factory:
108
factory._editor = self
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
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:
121
# If the editor is part of this UI:
122
if editor.ui is self.ui:
124
# Then bind it to the trait editor panel:
125
editor._editor = self.control
127
# Indicate all pending editors have been processed:
128
factory._shared_editors = None
130
# We only needed to build the trait editor panel, so exit:
133
# Check to see if the matching trait editor panel has been
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 )
143
# Otherwise, bind our trait editor panel to the shared one:
144
self._editor = editor.control
146
# Finally, create only the tree control:
147
self.control = self._tree = _TreeWidget(self)
149
# If editable, create a tree control and an editor panel:
150
self._tree = _TreeWidget(self)
152
self._editor = sa = QtGui.QScrollArea()
153
sa.setFrameShape(QtGui.QFrame.NoFrame)
154
sa._node_ui = sa._editor_nid = None
156
if factory.orientation == 'horizontal':
157
orient = QtCore.Qt.Horizontal
159
orient = QtCore.Qt.Vertical
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)
167
# Otherwise, just create the tree control:
168
self.control = self._tree = _TreeWidget(self)
170
# Set up the mapping between objects and tree id's:
173
# Initialize the 'undo state' stack:
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' )
182
#---------------------------------------------------------------------------
183
# Handles the 'selection' trait being changed:
184
#---------------------------------------------------------------------------
186
def _selection_changed ( self, selection ):
187
""" Handles the **selection** event.
190
self._tree.setCurrentItem(self._object_info(selection)[2])
194
#---------------------------------------------------------------------------
195
# Handles the 'selected' trait being changed:
196
#---------------------------------------------------------------------------
198
def _selected_changed ( self, selected ):
199
""" Handles the **selected** trait being changed.
201
if not self._no_update_selected:
202
self._selection_changed( selected )
204
#---------------------------------------------------------------------------
205
# Handles the 'veto' event being fired:
206
#---------------------------------------------------------------------------
208
def _veto_changed ( self ):
209
""" Handles the 'veto' event being fired.
213
#---------------------------------------------------------------------------
214
# Disposes of the contents of an editor:
215
#---------------------------------------------------------------------------
217
def dispose ( self ):
218
""" Disposes of the contents of an editor.
220
if self._tree is not None:
221
# Stop the chatter (specifically about the changing selection).
222
self._tree.blockSignals(True)
224
self._delete_node(self._tree.invisibleRootItem())
228
super( SimpleEditor, self ).dispose()
230
#---------------------------------------------------------------------------
231
# Expands from the specified node the specified number of sub-levels:
232
#---------------------------------------------------------------------------
234
def expand_levels ( self, nid, levels, expand = True ):
235
""" Expands from the specified node the specified number of sub-levels.
238
expanded, node, object = self._get_node_data( nid )
239
if self._has_children( node, object ):
240
self._expand_node( nid )
242
nid.setExpanded(True)
243
for cnid in self._nodes_for( nid ):
244
self.expand_levels( cnid, levels - 1 )
246
#---------------------------------------------------------------------------
247
# Updates the editor when the object trait changes external to the editor:
248
#---------------------------------------------------------------------------
250
def update_editor ( self ):
251
""" Updates the editor when the object trait changes externally to the
259
object, node = self._node_for( self.value )
261
if self.factory.hide_root:
262
nid = tree.invisibleRootItem()
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))
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)
278
self.expand_levels( nid, self.factory.auto_open, False )
279
# FIXME: Clear the current editor (if any)...
281
#---------------------------------------------------------------------------
282
# Returns the editor's control for indicating error status:
283
#---------------------------------------------------------------------------
285
def get_error_control ( self ):
286
""" Returns the editor's control for indicating error status.
290
#---------------------------------------------------------------------------
291
# Appends a new node to the specified node:
292
#---------------------------------------------------------------------------
294
def _append_node ( self, nid, node, object ):
295
""" Appends a new node to the specified node.
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))
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 )
308
# Automatically expand the new node (if requested):
310
if node.can_auto_open( object ):
311
cnid.setExpanded(True)
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
317
cnid._dummy = QtGui.QTreeWidgetItem(cnid)
319
# Return the newly created node:
322
#---------------------------------------------------------------------------
323
# Deletes a specified tree node and all its children:
324
#---------------------------------------------------------------------------
326
def _delete_node ( self, nid ):
327
""" Deletes a specified tree node and all its children.
329
for cnid in self._nodes_for( nid ):
330
self._delete_node( cnid )
332
if nid is self._tree.invisibleRootItem():
335
# See if it is a dummy.
337
if pnid is not None and getattr(pnid, '_dummy', None) is nid:
338
pnid.removeChild(nid)
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]):
351
if len( object_info ) == 0:
352
self._remove_listeners( node, object )
353
del self._map[ id_object ]
356
self._tree.takeTopLevelItem(self._tree.indexOfTopLevelItem(nid))
358
pnid.removeChild(nid)
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):
364
#---------------------------------------------------------------------------
365
# Expands the contents of a specified node (if required):
366
#---------------------------------------------------------------------------
368
def _expand_node ( self, nid ):
369
""" Expands the contents of a specified node (if required).
371
expanded, node, object = self._get_node_data( nid )
373
# Lazily populate the item's children:
375
# Remove any dummy node.
376
dummy = getattr(nid, '_dummy', None)
377
if dummy is not None:
378
nid.removeChild(dummy)
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 )
386
# Indicate the item is now populated:
387
self._set_node_data( nid, ( True, node, object) )
389
#---------------------------------------------------------------------------
390
# Returns each of the child nodes of a specified node id:
391
#---------------------------------------------------------------------------
393
def _nodes_for ( self, nid ):
394
""" Returns all child node ids of a specified node id.
396
return [nid.child(i) for i in range(nid.childCount())]
398
#---------------------------------------------------------------------------
399
# Return the index of a specified node id within its parent:
400
#---------------------------------------------------------------------------
402
def _node_index ( self, nid ):
405
return ( None, None, None )
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 )
412
#---------------------------------------------------------------------------
413
# Returns whether a specified object has any children:
414
#---------------------------------------------------------------------------
416
def _has_children ( self, node, object ):
417
""" Returns whether a specified object has any children.
419
return (node.allows_children( object ) and node.has_children( object ))
421
#---------------------------------------------------------------------------
422
# Returns the icon index for the specified object:
423
#---------------------------------------------------------------------------
426
'<item>': QtGui.QStyle.SP_FileIcon,
427
'<group>': QtGui.QStyle.SP_DirClosedIcon,
428
'<open>': QtGui.QStyle.SP_DirOpenIcon
431
def _get_icon ( self, node, object, is_expanded = False ):
432
""" Returns the index of the specified object icon.
434
if not self.factory.show_icons:
437
icon_name = node.get_icon(object, is_expanded)
438
if isinstance(icon_name, basestring):
439
icon = self.STD_ICON_MAP.get(icon_name)
442
return self._tree.style().standardIcon(icon)
444
path = node.get_icon_path( object )
445
if isinstance( path, basestring ):
446
path = [ path, node ]
449
reference = resource_manager.locate_image( icon_name, path )
450
if reference is None:
452
file_name = reference.filename
454
# Assume it is an ImageResource, and get its file name directly:
455
file_name = icon_name.absolute_path
457
return QtGui.QIcon(pixmap_cache(file_name))
459
#---------------------------------------------------------------------------
460
# Adds the event listeners for a specified object:
461
#---------------------------------------------------------------------------
463
def _add_listeners ( self, node, object ):
464
""" Adds the event listeners for a specified object.
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)
470
node.when_label_changed( object, self._label_updated, False )
472
#---------------------------------------------------------------------------
473
# Removes any event listeners from a specified object:
474
#---------------------------------------------------------------------------
476
def _remove_listeners ( self, node, object ):
477
""" Removes any event listeners from a specified object.
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 )
483
node.when_label_changed( object, self._label_updated, True )
485
#---------------------------------------------------------------------------
486
# Returns the tree node data for a specified object in the form
487
# ( expanded, node, nid ):
488
#---------------------------------------------------------------------------
490
def _object_info ( self, object, name = '' ):
491
""" Returns the tree node data for a specified object in the form
492
( expanded, node, nid ).
494
info = self._map[ id( object ) ]
495
for name2, nid in info:
501
expanded, node, ignore = self._get_node_data( nid )
503
return ( expanded, node, nid )
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 ), ... ].
510
for name2, nid in self._map[ id( object ) ]:
512
expanded, node, ignore = self._get_node_data( nid )
513
result.append( ( expanded, node, nid ) )
517
#---------------------------------------------------------------------------
518
# Returns the TreeNode associated with a specified object:
519
#---------------------------------------------------------------------------
521
def _node_for ( self, object ):
522
""" Returns the TreeNode associated with a specified object.
524
if ((type( object ) is tuple) and (len( object ) == 2) and
525
isinstance( object[1], TreeNode )):
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 ) ]
533
# If only one found, we're done, return it:
534
if len( nodes ) == 1:
535
return ( object, nodes[0] )
537
# If none found, give up:
538
if len( nodes ) == 0:
539
return ( object, ITreeNodeAdapterBridge(adapter=object) )
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 ]
546
# If only one left, then return that node:
547
if len( nodes ) == 1:
548
return ( object, nodes[0] )
550
# Otherwise, return a MultiTreeNode based on all selected nodes...
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':
555
for i, node in enumerate( nodes ):
556
if node.children == '':
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 ] )
568
# Otherwise create one, cache it, and return it:
569
factory.multi_nodes[ key ] = multi_node = MultiTreeNode(
570
root_node = root_node,
573
return ( object, multi_node )
575
#---------------------------------------------------------------------------
576
# Returns the TreeNode associated with a specified class:
577
#---------------------------------------------------------------------------
579
def _node_for_class ( self, klass ):
580
""" Returns the TreeNode associated with a specified class.
582
for node in self.factory.nodes:
583
if issubclass( klass, tuple( node.node_for ) ):
587
#---------------------------------------------------------------------------
588
# Returns the node and class associated with a specified class name:
589
#---------------------------------------------------------------------------
591
def _node_for_class_name ( self, class_name ):
592
""" Returns the node and class associated with a specified class name.
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 )
600
#---------------------------------------------------------------------------
601
# Updates the icon for a specified node:
602
#---------------------------------------------------------------------------
604
def _update_icon(self, nid):
605
""" Updates the icon for a specified node.
607
expanded, node, object = self._get_node_data(nid)
608
nid.setIcon(0, self._get_icon(node, object, expanded))
610
#---------------------------------------------------------------------------
611
# Begins an 'undoable' transaction:
612
#---------------------------------------------------------------------------
614
def _begin_undo ( self ):
615
""" Begins an "undoable" transaction.
618
self._undoable.append( ui._undoable )
619
if (ui._undoable == -1) and (ui.history is not None):
620
ui._undoable = ui.history.now
622
#---------------------------------------------------------------------------
623
# Ends an 'undoable' transaction:
624
#---------------------------------------------------------------------------
626
def _end_undo ( self ):
627
if self._undoable.pop() == -1:
628
self.ui._undoable = -1
630
#---------------------------------------------------------------------------
631
# Gets an 'undo' item for a change made to a node's children:
632
#---------------------------------------------------------------------------
634
def _get_undo_item ( self, object, name, event ):
635
return ListUndoItem( object = object,
639
removed = event.removed )
641
#---------------------------------------------------------------------------
642
# Performs an undoable 'append' operation:
643
#---------------------------------------------------------------------------
645
def _undoable_append ( self, node, object, data, make_copy = True ):
646
""" Performs an undoable append operation.
651
data = copy.deepcopy( data )
652
node.append_child( object, data )
656
#---------------------------------------------------------------------------
657
# Performs an undoable 'insert' operation:
658
#---------------------------------------------------------------------------
660
def _undoable_insert ( self, node, object, index, data, make_copy = True ):
661
""" Performs an undoable insert operation.
666
data = copy.deepcopy( data )
667
node.insert_child( object, index, data )
671
#---------------------------------------------------------------------------
672
# Performs an undoable 'delete' operation:
673
#---------------------------------------------------------------------------
675
def _undoable_delete ( self, node, object, index ):
676
""" Performs an undoable delete operation.
680
node.delete_child( object, index )
684
#---------------------------------------------------------------------------
685
# Gets the id associated with a specified object (if any):
686
#---------------------------------------------------------------------------
688
def _get_object_nid ( self, object, name = '' ):
689
""" Gets the ID associated with a specified object (if any).
691
info = self._map.get( id( object ) )
694
for name2, nid in info:
700
#---------------------------------------------------------------------------
701
# Clears the current editor pane (if any):
702
#---------------------------------------------------------------------------
704
def _clear_editor ( self ):
705
""" Clears the current editor pane (if any).
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
713
#---------------------------------------------------------------------------
714
# Gets/Sets the node specific data:
715
#---------------------------------------------------------------------------
718
def _get_node_data(nid):
719
""" Gets the node specific data. """
723
def _set_node_data(nid, data):
724
""" Sets the node specific data. """
727
#----- User callable methods: --------------------------------------------------
729
#---------------------------------------------------------------------------
730
# Gets the object associated with a specified node:
731
#---------------------------------------------------------------------------
733
def get_object ( self, nid ):
734
""" Gets the object associated with a specified node.
736
return self._get_node_data( nid )[2]
738
#---------------------------------------------------------------------------
739
# Returns the object which is the immmediate parent of a specified object
741
#---------------------------------------------------------------------------
743
def get_parent ( self, object, name = '' ):
744
""" Returns the object that is the immmediate parent of a specified
747
nid = self._get_object_nid( object, name )
750
if pnid is not self._tree.invisibleRootItem():
751
return self.get_object( pnid )
754
#---------------------------------------------------------------------------
755
# Returns the node associated with a specified object:
756
#---------------------------------------------------------------------------
758
def get_node ( self, object, name = '' ):
759
""" Returns the node associated with a specified object.
761
nid = self._get_object_nid( object, name )
763
return self._get_node_data( nid )[1]
766
#----- Tree event handlers: ----------------------------------------------------
768
#---------------------------------------------------------------------------
769
# Handles a tree node being expanded:
770
#---------------------------------------------------------------------------
772
def _on_item_expanded(self, nid):
773
""" Handles a tree node being expanded.
775
expanded, node, object = self._get_node_data(nid)
777
# If 'auto_close' requested for this node type, close all of the node's
779
if node.can_auto_close(object):
780
parent = nid.parent()
782
if parent is not None:
783
for snid in self._nodes_for(parent):
785
snid.setExpanded(False)
787
# Expand the node (i.e. populate its children if they are not there
789
self._expand_node(nid)
791
self._update_icon(nid)
793
#---------------------------------------------------------------------------
794
# Handles a tree node being collapsed:
795
#---------------------------------------------------------------------------
797
def _on_item_collapsed(self, nid):
798
""" Handles a tree node being collapsed.
800
self._update_icon(nid)
802
#---------------------------------------------------------------------------
803
# Handles a tree item click:
804
#---------------------------------------------------------------------------
806
def _on_item_clicked(self, nid, col):
807
""" Handles a tree item being clicked.
809
_, node, object = self._get_node_data(nid)
811
if node.click(object) is True and self.factory.on_click is not None:
812
self.ui.evaluate(self.factory.on_click, object)
814
# Fire the 'click' event with the object as its value:
817
#---------------------------------------------------------------------------
818
# Handles a tree item double click:
819
#---------------------------------------------------------------------------
821
def _on_item_dclicked(self, nid, col):
822
""" Handles a tree item being double-clicked.
824
_, node, object = self._get_node_data(nid)
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)
833
# Fire the 'dclick' event with the clicked on object as value:
836
#---------------------------------------------------------------------------
837
# Handles a tree node being selected:
838
#---------------------------------------------------------------------------
840
def _on_tree_sel_changed(self):
841
""" Handles a tree node being selected.
843
# Get the new selection:
844
nids = self._tree.selectedItems()
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)
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?)
857
# QTreeWidgetItem does not have an equal operator, so use id()
858
if id(nid) == id(nids[0]):
860
not_handled = node.select(sel_object)
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
872
self._no_update_selected = True
873
self.selected = selected
874
self._no_update_selected = False
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)
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)
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 )
893
view = object.trait_view()
894
if (self.ui.history is not None) or (view.kind == 'subpanel'):
895
ui = object.edit_traits( parent = editor,
899
# Otherwise, just set up our own new one:
900
ui = object.edit_traits( parent = editor,
905
# Make our UI the parent of the new UI:
908
# Remember the new editor's UI and node info:
910
editor._editor_nid = nid
912
# Finish setting up the editor:
913
ui.control.layout().setContentsMargins(0, 0, 0, 0)
914
editor.setWidget(ui.control)
916
# Allow the editor view to show any changes that have occurred:
917
editor.setUpdatesEnabled(True)
919
#---------------------------------------------------------------------------
920
# Handles the user right clicking on a tree node:
921
#---------------------------------------------------------------------------
923
def _on_context_menu(self, pos):
924
""" Handles the user requesting a context menuright clicking on a tree node.
926
nid = self._tree.itemAt(pos)
931
_, node, object = self._get_node_data(nid)
933
self._data = (node, object, nid)
934
self._context = {'object': object,
937
'info': self.ui.info,
938
'handler': self.ui.handler}
940
# Try to get the parent node of the node clicked on:
942
if pnid is None or pnid is self._tree.invisibleRootItem():
943
parent_node = parent_object = None
945
_, parent_node, parent_object = self._get_node_data(pnid)
947
self._menu_node = node
948
self._menu_parent_node = parent_node
949
self._menu_parent_object = parent_object
951
menu = node.get_menu(object)
954
# Use the standard, default menu:
955
menu = self._standard_menu(node, object)
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:
963
actions = self._new_actions( node, object )
964
if len( actions ) > 0:
965
group.insert( 0, Menu( name = 'New', *actions ) )
968
# All other values mean no menu should be displayed:
971
# Only display the menu if a valid menu is defined:
973
qmenu = menu.create_menu( self._tree, self )
974
qmenu.exec_(self._tree.mapToGlobal(pos))
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
980
#---------------------------------------------------------------------------
981
# Returns the standard contextual pop-up menu:
982
#---------------------------------------------------------------------------
984
def _standard_menu ( self, node, object ):
985
""" Returns the standard contextual pop-up menu.
987
actions = [ CutAction, CopyAction, PasteAction, Separator(),
988
DeleteAction, Separator(), RenameAction ]
990
# See if the 'New' menu section should be added:
991
items = self._new_actions( node, object )
993
actions[0:0] = [ Menu( name = 'New', *items ), Separator() ]
995
return Menu( *actions )
997
#---------------------------------------------------------------------------
998
# Returns a list of Actions that will create 'new' objects:
999
#---------------------------------------------------------------------------
1001
def _new_actions ( self, node, object ):
1002
""" Returns a list of Actions that will create new objects.
1004
object = self._data[1]
1006
add = node.get_add( object )
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 )
1019
Action( name = name,
1020
action = "editor._menu_new_node('%s',%s)" %
1021
( class_name, prompt ) ) )
1024
#---------------------------------------------------------------------------
1025
# Menu action helper methods:
1026
#---------------------------------------------------------------------------
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 ))
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 ))
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 ))
1045
def _is_pasteable ( self, object ):
1046
return self._menu_node.can_add(object, clipboard.instance_type)
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 )
1053
can_delete = ((parent is not None) and parent.can_delete( object ))
1054
return (can_delete and self._menu_node.can_delete_me( object ))
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 )
1061
can_rename = ((parent is not None) and parent.can_rename( object ))
1063
can_rename = (can_rename and self._menu_node.can_rename_me( object ))
1065
# Set the widget item's editable flag appropriately.
1066
nid = self._get_object_nid(object)
1069
flags |= QtCore.Qt.ItemIsEditable
1071
flags &= ~QtCore.Qt.ItemIsEditable
1076
#----- pyface.action 'controller' interface implementation: --------------------
1078
#---------------------------------------------------------------------------
1079
# Adds a menu item to the menu being constructed:
1080
#---------------------------------------------------------------------------
1082
def add_to_menu ( self, menu_item ):
1083
""" Adds a menu item to the menu bar being constructed.
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' )
1089
#---------------------------------------------------------------------------
1090
# Adds a tool bar item to the tool bar being constructed:
1091
#---------------------------------------------------------------------------
1093
def add_to_toolbar ( self, toolbar_item ):
1094
""" Adds a toolbar item to the toolbar being constructed.
1096
self.add_to_menu( toolbar_item )
1098
#---------------------------------------------------------------------------
1099
# Returns whether the menu action should be defined in the user interface:
1100
#---------------------------------------------------------------------------
1102
def can_add_to_menu ( self, action ):
1103
""" Returns whether the action should be defined in the user interface.
1105
if action.defined_when != '':
1107
if not eval( action.defined_when, globals(), self._context ):
1112
if action.visible_when != '':
1114
if not eval( action.visible_when, globals(), self._context ):
1121
#---------------------------------------------------------------------------
1122
# Returns whether the toolbar action should be defined in the user
1124
#---------------------------------------------------------------------------
1126
def can_add_to_toolbar ( self, action ):
1127
""" Returns whether the toolbar action should be defined in the user
1130
return self.can_add_to_menu( action )
1132
#---------------------------------------------------------------------------
1133
# Performs the action described by a specified Action object:
1134
#---------------------------------------------------------------------------
1136
def perform ( self, action, action_event = None ):
1137
""" Performs the action described by a specified Action object.
1139
self.ui.do_undoable( self._perform, action )
1141
def _perform ( self, action ):
1142
node, object, nid = self._data
1143
method_name = action.action
1145
handler = self.ui.handler
1147
if method_name.find( '.' ) >= 0:
1148
if method_name.find( '(' ) < 0:
1151
eval( method_name, globals(),
1156
'handler': handler } )
1158
# fixme: Should the exception be logged somewhere?
1162
method = getattr( handler, method_name, None )
1163
if method is not None:
1164
method( info, object )
1167
if action.on_perform is not None:
1168
action.on_perform( object )
1170
#----- Menu support methods: ---------------------------------------------------
1172
#---------------------------------------------------------------------------
1173
# Evaluates a condition within a defined context and sets a specified
1174
# object trait based on the (assumed) boolean result:
1175
#---------------------------------------------------------------------------
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
1185
if not eval( condition, globals(), self._context ):
1189
setattr( object, trait, value )
1191
#----- Menu event handlers: ----------------------------------------------------
1193
#---------------------------------------------------------------------------
1194
# Copies the current tree node object to the paste buffer:
1195
#---------------------------------------------------------------------------
1197
def _menu_copy_node ( self ):
1198
""" Copies the current tree node object to the paste buffer.
1200
clipboard.instance = self._data[1]
1203
#---------------------------------------------------------------------------
1204
# Cuts the current tree node object into the paste buffer:
1205
#---------------------------------------------------------------------------
1207
def _menu_cut_node ( self ):
1208
""" Cuts the current tree node object into the paste buffer.
1210
node, object, nid = self._data
1211
clipboard.instance = object
1213
self._undoable_delete(*self._node_index(nid))
1215
#---------------------------------------------------------------------------
1216
# Pastes the current contents of the paste buffer into the current node:
1217
#---------------------------------------------------------------------------
1219
def _menu_paste_node ( self ):
1220
""" Pastes the current contents of the paste buffer into the current
1223
node, object, nid = self._data
1225
self._undoable_append(node, object, clipboard.instance, False)
1227
#---------------------------------------------------------------------------
1228
# Deletes the current node from the tree:
1229
#---------------------------------------------------------------------------
1231
def _menu_delete_node ( self ):
1232
""" Deletes the current node from the tree.
1234
node, object, nid = self._data
1236
rc = node.confirm_delete( object )
1239
if self.ui.history is None:
1240
# If no undo history, ask user to confirm the delete:
1241
butn = QtGui.QMessageBox.question(
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:
1249
self._undoable_delete( *self._node_index( nid ) )
1251
#---------------------------------------------------------------------------
1252
# Renames the current tree node:
1253
#---------------------------------------------------------------------------
1255
def _menu_rename_node ( self ):
1256
""" Rename the current node.
1258
_, _, nid = self._data
1260
self._tree.editItem(nid)
1262
def _on_nid_changed(self, nid, col):
1263
""" Handle changes to a widget item.
1265
# The node data may not have been set up for the nid yet. Ignore it if
1268
_, node, object = self._get_node_data(nid)
1272
new_label = unicode(nid.text(col))
1273
old_label = node.get_label(object)
1275
if new_label != old_label:
1277
node.set_label(object, new_label)
1279
nid.setText(col, old_label)
1281
#---------------------------------------------------------------------------
1282
# Adds a new object to the current node:
1283
#---------------------------------------------------------------------------
1285
def _menu_new_node ( self, class_name, prompt = False ):
1286
""" Adds a new object to the current node.
1288
node, object, nid = self._data
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 )
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))
1300
#----- Model event handlers: ---------------------------------------------------
1302
#---------------------------------------------------------------------------
1303
# Handles the children of a node being completely replaced:
1304
#---------------------------------------------------------------------------
1306
def _children_replaced ( self, object, name = '', new = None ):
1307
""" Handles the children of a node being completely replaced.
1310
for expanded, node, nid in self._object_info_for( object, name ):
1311
children = node.get_children( object )
1313
# Only add/remove the changes if the node has already been expanded:
1315
# Delete all current child nodes:
1316
for cnid in self._nodes_for( nid ):
1317
self._delete_node( cnid )
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 )
1325
# Try to expand the node (if requested):
1326
if node.can_auto_open( object ):
1327
nid.setExpanded(True)
1329
#---------------------------------------------------------------------------
1330
# Handles the children of a node being changed:
1331
#---------------------------------------------------------------------------
1333
def _children_updated ( self, object, name, event ):
1334
""" Handles the children of a node being changed.
1336
# Log the change that was made made (removing '_items' from the end of
1339
self.log_change( self._get_undo_item, object, name, event )
1341
# Get information about the node that was changed:
1343
n = len( event.added )
1344
end = start + len( event.removed )
1347
for expanded, node, nid in self._object_info_for( object, name ):
1348
children = node.get_children( object )
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 )
1355
# Only add/remove the changes if the node has already been 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 )
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 )
1367
# Try to expand the node (if requested):
1368
if node.can_auto_open( object ):
1369
nid.setExpanded(True)
1371
#---------------------------------------------------------------------------
1372
# Handles the label of an object being changed:
1373
#---------------------------------------------------------------------------
1375
def _label_updated ( self, object, name, label ):
1376
""" Handles the label of an object being changed.
1378
# Prevent the itemChanged() signal from being emitted.
1379
blk = self._tree.blockSignals(True)
1382
for name2, nid in self._map[ id( object ) ]:
1385
node = self._get_node_data( nid )[1]
1386
nid.setText(0, node.get_label(object))
1387
self._update_icon(nid)
1389
self._tree.blockSignals(blk)
1391
#-- UI preference save/restore interface ---------------------------------------
1393
#---------------------------------------------------------------------------
1394
# Restores any saved user preference information associated with the
1396
#---------------------------------------------------------------------------
1398
def restore_prefs ( self, prefs ):
1399
""" Restores any saved user preference information associated with the
1402
if isinstance(self.control, QtGui.QSplitter):
1403
if isinstance(prefs, dict):
1404
structure = prefs.get('structure')
1408
self.control.restoreState(structure)
1410
#---------------------------------------------------------------------------
1411
# Returns any user preference information associated with the editor:
1412
#---------------------------------------------------------------------------
1414
def save_prefs ( self ):
1415
""" Returns any user preference information associated with the editor.
1417
if isinstance(self.control, QtGui.QSplitter):
1418
return {'structure': str(self.control.saveState())}
1422
#-- End UI preference save/restore interface -----------------------------------
1424
#-------------------------------------------------------------------------------
1425
# '_TreeWidget' class:
1426
#-------------------------------------------------------------------------------
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
1433
def __init__(self, editor, parent=None):
1434
""" Initialise the tree widget.
1436
QtGui.QTreeWidget.__init__(self, parent)
1438
self.header().hide()
1439
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
1440
self.setDragEnabled(True)
1441
self.setAcceptDrops(True)
1443
if editor.factory.selection_mode == 'extended':
1444
self.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)
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)
1451
QtCore.SIGNAL('itemClicked(QTreeWidgetItem *, int)'),
1452
editor._on_item_clicked)
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)
1461
QtCore.SIGNAL('itemChanged(QTreeWidgetItem *, int)'),
1462
editor._on_nid_changed)
1464
self._editor = editor
1465
self._dragging = None
1467
def startDrag(self, actions):
1468
""" Reimplemented to start the drag of a tree widget item.
1470
nid = self.currentItem()
1474
self._dragging = nid
1476
_, node, object = self._editor._get_node_data(nid)
1478
# Convert the item being dragged to MIME data.
1479
md = PyMimeData(node.get_drag_object(object))
1481
# Render the item being dragged as a pixmap.
1482
nid_rect = self.visualItemRect(nid)
1483
rect = nid_rect.intersected(self.viewport().rect())
1485
pm = QtGui.QPixmap(rect.size())
1486
pm.fill(self.palette().base().color())
1488
painter = QtGui.QPainter(pm)
1490
option = self.viewOptions()
1491
option.state |= QtGui.QStyle.State_Selected
1492
option.rect = QtCore.QRect(nid_rect.topLeft() - rect.topLeft(), nid_rect.size())
1494
self.itemDelegate().paint(painter, option, self.indexFromItem(nid))
1498
# Calculate the hotspot so that the pixmap appears on top of the
1500
rect.adjust(self.horizontalOffset(), self.verticalOffset(), 0, 0)
1501
hspos = self.mapFromGlobal(QtGui.QCursor.pos()) - rect.topLeft()
1504
drag = QtGui.QDrag(self)
1505
drag.setMimeData(md)
1507
drag.setHotSpot(hspos)
1510
def dragEnterEvent(self, e):
1511
""" Reimplemented to see if the current drag can be handled by the
1514
# Assume the drag is invalid.
1517
# Check what is being dragged.
1518
md = PyMimeData.coerce(e.mimeData())
1522
# We might be able to handle it (but it depends on what the final
1524
e.acceptProposedAction()
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.
1530
# Assume the drag is invalid.
1533
# Get the tree widget item under the cursor.
1534
nid = self.itemAt(e.pos())
1538
# Check that the target is not the source of a child of the source.
1539
if self._dragging is not None:
1541
while pnid is not None:
1542
if pnid is self._dragging:
1545
pnid = pnid.parent()
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)
1554
_, node, object = self._editor._get_node_data(nid)
1557
# See if the model will accept a drop.
1558
data = PyMimeData.coerce(e.mimeData()).instance()
1560
if not node._is_droppable(object, data, insert):
1563
e.acceptProposedAction()
1565
def dropEvent(self, e):
1566
""" Reimplemented to update the model and tree.
1568
# Assume the drop is invalid.
1571
dragging = self._dragging
1572
self._dragging = None
1574
# Get the tree widget item under the cursor.
1575
nid = self.itemAt(e.pos())
1579
# Get the data being dropped.
1580
data = PyMimeData.coerce(e.mimeData()).instance()
1582
editor = self._editor
1583
_, node, object = editor._get_node_data(nid)
1585
if e.proposedAction() == QtCore.Qt.MoveAction:
1586
if not node._is_droppable( object, data, False ):
1589
if dragging is not None:
1590
data = node._drop_object( object, data, False )
1591
if data is not None:
1593
editor._begin_undo()
1594
editor._undoable_delete(
1595
*editor._node_index( dragging ) )
1596
editor._undoable_append( node, object, data, False )
1600
data = node._drop_object( object, data )
1601
if data is not None:
1602
editor._undoable_append( node, object, data, False )
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)):
1615
editor._begin_undo()
1616
editor._undoable_delete( from_node, from_object,
1618
editor._undoable_insert( to_node, to_object, to_index,
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,
1628
e.acceptProposedAction()