def __init__(self, parent, image_size, viewer): """ Creates a new table viewer. 'parent' is the toolkit-specific control that is the table's parent. 'image_size' is a tuple in the form (int width, int height) that specifies the size of the icons (if any) displayed in the table. """ # The vierer that we are providing the control for. self._viewer = viewer # Base-class constructor. wx.ListCtrl.__init__(self, parent, -1, style=self.STYLE) # Table item images. self._image_list = ImageList(image_size[0], image_size[1]) self.AssignImageList(self._image_list, wx.IMAGE_LIST_SMALL) # Set up attributes to show alternate rows with a different background # colour. self._even_row_attribute = wx.ListItemAttr() self._even_row_attribute.SetBackgroundColour( self._viewer.even_row_background) self._odd_row_attribute = wx.ListItemAttr() self._odd_row_attribute.SetBackgroundColour( self._viewer.odd_row_background) return
def __init__(self, parent, image_size=(16, 16), **traits): """ Creates a new tree viewer. 'parent' is the toolkit-specific control that is the tree's parent. 'image_size' is a tuple in the form (int width, int height) that specifies the size of the label images (if any) displayed in the tree. """ # Base class constructor. super(TreeViewer, self).__init__(**traits) # Create the toolkit-specific control. self.control = tree = wx.TreeCtrl(parent, -1, style=self._get_style()) # Get our actual Id. wxid = tree.GetId() # Wire up the wx tree events. wx.EVT_CHAR(tree, self._on_char) wx.EVT_LEFT_DOWN(tree, self._on_left_down) wx.EVT_RIGHT_DOWN(tree, self._on_right_down) wx.EVT_TREE_ITEM_ACTIVATED(tree, wxid, self._on_tree_item_activated) wx.EVT_TREE_ITEM_COLLAPSED(tree, wxid, self._on_tree_item_collapsed) wx.EVT_TREE_ITEM_COLLAPSING(tree, wxid, self._on_tree_item_collapsing) wx.EVT_TREE_ITEM_EXPANDED(tree, wxid, self._on_tree_item_expanded) wx.EVT_TREE_ITEM_EXPANDING(tree, wxid, self._on_tree_item_expanding) wx.EVT_TREE_BEGIN_LABEL_EDIT(tree, wxid, self._on_tree_begin_label_edit) wx.EVT_TREE_END_LABEL_EDIT(tree, wxid, self._on_tree_end_label_edit) wx.EVT_TREE_BEGIN_DRAG(tree, wxid, self._on_tree_begin_drag) wx.EVT_TREE_SEL_CHANGED(tree, wxid, self._on_tree_sel_changed) # The image list is a wxPython-ism that caches all images used in the # control. self._image_list = ImageList(image_size[0], image_size[1]) if self.show_images: tree.AssignImageList(self._image_list) # Mapping from element to wx tree item Ids. self._element_to_id_map = {} # Add the root item. if self.input is not None: self._add_element(None, self.input) return
class TreeViewer(ContentViewer): """ A viewer based on a tree control. """ # The default tree style. STYLE = wx.TR_EDIT_LABELS | wx.TR_HAS_BUTTONS | wx.CLIP_CHILDREN #### 'TreeViewer' interface ############################################### # The content provider provides the actual tree data. content_provider = Instance(TreeContentProvider) # The label provider provides, err, the labels for the items in the tree # (a label can have text and/or an image). label_provider = Instance(TreeLabelProvider, ()) # Selection mode (must be either of 'single' or 'extended'). selection_mode = Enum('single', 'extended') # The currently selected elements. selection = List # Should an image be shown for each element? show_images = Bool(True) # Should the root of the tree be shown? show_root = Bool(True) #### Events #### # An element has been activated (ie. double-clicked). element_activated = Event # A drag operation was started on an element. element_begin_drag = Event # An element that has children has been collapsed. element_collapsed = Event # An element that has children has been expanded. element_expanded = Event # A left-click occurred on an element. element_left_clicked = Event # A right-click occurred on an element. element_right_clicked = Event # A key was pressed while the tree is in focus. key_pressed = Event ########################################################################### # 'object' interface. ########################################################################### def __init__(self, parent, image_size=(16, 16), **traits): """ Creates a new tree viewer. 'parent' is the toolkit-specific control that is the tree's parent. 'image_size' is a tuple in the form (int width, int height) that specifies the size of the label images (if any) displayed in the tree. """ # Base class constructor. super(TreeViewer, self).__init__(**traits) # Create the toolkit-specific control. self.control = tree = wx.TreeCtrl(parent, -1, style=self._get_style()) # Get our actual Id. wxid = tree.GetId() # Wire up the wx tree events. wx.EVT_CHAR(tree, self._on_char) wx.EVT_LEFT_DOWN(tree, self._on_left_down) wx.EVT_RIGHT_DOWN(tree, self._on_right_down) wx.EVT_TREE_ITEM_ACTIVATED(tree, wxid, self._on_tree_item_activated) wx.EVT_TREE_ITEM_COLLAPSED(tree, wxid, self._on_tree_item_collapsed) wx.EVT_TREE_ITEM_COLLAPSING(tree, wxid, self._on_tree_item_collapsing) wx.EVT_TREE_ITEM_EXPANDED(tree, wxid, self._on_tree_item_expanded) wx.EVT_TREE_ITEM_EXPANDING(tree, wxid, self._on_tree_item_expanding) wx.EVT_TREE_BEGIN_LABEL_EDIT(tree, wxid, self._on_tree_begin_label_edit) wx.EVT_TREE_END_LABEL_EDIT(tree, wxid, self._on_tree_end_label_edit) wx.EVT_TREE_BEGIN_DRAG(tree, wxid, self._on_tree_begin_drag) wx.EVT_TREE_SEL_CHANGED(tree, wxid, self._on_tree_sel_changed) # The image list is a wxPython-ism that caches all images used in the # control. self._image_list = ImageList(image_size[0], image_size[1]) if self.show_images: tree.AssignImageList(self._image_list) # Mapping from element to wx tree item Ids. self._element_to_id_map = {} # Add the root item. if self.input is not None: self._add_element(None, self.input) return ########################################################################### # 'TreeViewer' interface. ########################################################################### def is_expanded(self, element): """ Returns True if the element is expanded, otherwise False. """ key = self._get_key(element) if key in self._element_to_id_map: is_expanded = self.control.IsExpanded(self._element_to_id_map[key]) else: is_expanded = False return is_expanded def is_selected(self, element): """ Returns True if the element is selected, otherwise False. """ key = self._get_key(element) if key in self._element_to_id_map: is_selected = self.control.IsSelected(self._element_to_id_map[key]) else: is_selected = False return is_selected def refresh(self, element): """ Refresh the tree starting from the specified element. Call this when the STRUCTURE of the content has changed. """ # Has the element actually appeared in the tree yet? pid = self._element_to_id_map.get(self._get_key(element), None) if pid is not None: # The item data is a tuple. The first element indicates whether or # not we have already populated the item with its children. The # second element is the actual item data. populated, element = self.control.GetPyData(pid) # fixme: We should find a cleaner way other than deleting all of # the element's children and re-adding them! self._delete_children(pid) self.control.SetPyData(pid, (False, element)) # Does the element have any children? has_children = self.content_provider.has_children(element) self.control.SetItemHasChildren(pid, has_children) # Expand it. self.control.Expand(pid) else: print '**** pid is None!!! ****' return def update(self, element): """ Update the tree starting from the specified element. Call this when the APPEARANCE of the content has changed. """ pid = self._element_to_id_map.get(self._get_key(element), None) if pid is not None: self._refresh_element(pid, element) for child in self.content_provider.get_children(element): cid = self._element_to_id_map.get(self._get_key(child), None) if cid is not None: self._refresh_element(cid, child) return ########################################################################### # Private interface. ########################################################################### def _get_style(self): """ Returns the wx style flags for creating the tree control. """ # Start with the default flags. style = self.STYLE if not self.show_root: style = style | wx.TR_HIDE_ROOT | wx.TR_LINES_AT_ROOT if self.selection_mode != 'single': style = style | wx.TR_MULTIPLE | wx.TR_EXTENDED return style def _add_element(self, pid, element): """ Adds 'element' as a child of the element identified by 'pid'. If 'pid' is None then we are adding the root element. """ # Get the tree item image index and text. image_index = self._get_image_index(element) text = self._get_text(element) # Add the element. if pid is None: wxid = self.control.AddRoot(text, image_index, image_index) else: wxid = self.control.AppendItem(pid, text, image_index, image_index) # If we are adding the root element but the root is hidden, get its # children. if pid is None and not self.show_root: children = self.content_provider.get_children(element) for child in children: self._add_element(wxid, child) # Does the element have any children? has_children = self.content_provider.has_children(element) self.control.SetItemHasChildren(wxid, has_children) # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data. if pid is None: if self.show_root: self.control.SetPyData(wxid, (False, element)) else: self.control.SetPyData(wxid, (False, element)) # Make sure that we can find the element's Id later. self._element_to_id_map[self._get_key(element)] = wxid # If we are adding the root item then automatically expand it. if pid is None and self.show_root: self.control.Expand(wxid) return def _get_image_index(self, element): """ Returns the tree item image index for an element. """ # Get the image used to represent the element. image = self.label_provider.get_image(self, element) if image is not None: image_index = self._image_list.GetIndex(image.absolute_path) else: image_index = -1 return image_index def _get_key(self, element): """ Generate the key for the element to id map. """ try: key = hash(element) except: key = id(element) return key def _get_text(self, element): """ Returns the tree item text for an element. """ text = self.label_provider.get_text(self, element) if text is None: text = '' return text def _refresh_element(self, wxid, element): """ Refreshes the image and text of the specified element. """ # Get the tree item image index. image_index = self._get_image_index(element) self.control.SetItemImage(wxid, image_index, wx.TreeItemIcon_Normal) self.control.SetItemImage(wxid, image_index, wx.TreeItemIcon_Selected) # Get the tree item text. text = self._get_text(element) self.control.SetItemText(wxid, text) # Does the item have any children? has_children = self.content_provider.has_children(element) self.control.SetItemHasChildren(wxid, has_children) return def _unpack_event(self, event): """ Unpacks the event to see whether a tree element was involved. """ try: point = event.GetPosition() except: point = event.GetPoint() wxid, flags = self.control.HitTest(point) # Warning: On GTK we have to check the flags before we call 'GetPyData' # because if we call it when the hit test returns 'nowhere' it will # barf (on Windows it simply returns 'None' 8^() if flags & wx.TREE_HITTEST_NOWHERE: data = None else: data = self.control.GetPyData(wxid) return data, wxid, flags, point def _get_selection(self): """ Returns a list of the selected elements. """ elements = [] for wxid in self.control.GetSelections(): data = self.control.GetPyData(wxid) if data is not None: populated, element = data elements.append(element) # 'data' can be None here if (for example) the element has been # deleted. # # fixme: Can we stop this happening?!?!? else: pass return elements def _delete_children(self, pid): """ Recursively deletes the children of the specified element. """ cookie = 0 (cid, cookie) = self.control.GetFirstChild(pid, cookie) while cid.IsOk(): # Recursively delete the child's children. self._delete_children(cid) # Remove the reference to the item's data. populated, element = self.control.GetPyData(cid) del self._element_to_id_map[self._get_key(element)] self.control.SetPyData(cid, None) # Next! (cid, cookie) = self.control.GetNextChild(pid, cookie) self.control.DeleteChildren(pid) return #### Trait event handlers ################################################# def _input_changed(self): """ Called when the tree's input has been changed. """ # Delete everything... if self.control is not None: self.control.DeleteAllItems() self._element_to_id_map = {} # ... and then add the root item back in. if self.input is not None: self._add_element(None, self.input) return def _element_begin_drag_changed(self, element): """ Called when a drag is started on a element. """ # We ask the label provider for the actual value to drag. drag_value = self.label_provider.get_drag_value(self, element) # Start the drag. PythonDropSource(self.control, drag_value) return #### wx event handlers #################################################### def _on_right_down(self, event): """ Called when the right mouse button is clicked on the tree. """ data, id, flags, point = self._unpack_event(event) # Did the right click occur on a tree item? if data is not None: populated, element = data # Trait notification. self.element_right_clicked = (element, point) # Give other event handlers a chance. event.Skip() return def _on_left_down(self, event): """ Called when the left mouse button is clicked on the tree. """ data, wxid, flags, point = self._unpack_event(event) # Save point for tree_begin_drag method to workaround a bug in ?? when # wx.TreeEvent.GetPoint returns only (0,0). This happens under linux # when using wx-2.4.2.4, for instance. self._point_left_clicked = point # Did the left click occur on a tree item? if data is not None: populated, element = data # Trait notification. self.element_left_clicked = (element, point) # Give other event handlers a chance. event.Skip() return def _on_tree_item_expanding(self, event): """ Called when a tree item is about to expand. """ # Which item is expanding? wxid = event.GetItem() # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data. populated, element = self.control.GetPyData(wxid) # Give the label provider a chance to veto the expansion. if self.label_provider.is_expandable(self, element): # Lazily populate the item's children. if not populated: children = self.content_provider.get_children(element) # Sorting... if self.sorter is not None: self.sorter.sort(self, element, children) # Filtering.... for child in children: for filter in self.filters: if not filter.select(self, element, child): break else: self._add_element(wxid, child) # The element is now populated! self.control.SetPyData(wxid, (True, element)) else: event.Veto() return def _on_tree_item_expanded(self, event): """ Called when a tree item has been expanded. """ # Which item was expanded? wxid = event.GetItem() # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data. populated, element = self.control.GetPyData(wxid) # Make sure that the element's 'open' icon is displayed etc. self._refresh_element(wxid, element) # Trait notification. self.element_expanded = element return def _on_tree_item_collapsing(self, event): """ Called when a tree item is about to collapse. """ # Which item is collapsing? wxid = event.GetItem() # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data. populated, element = self.control.GetPyData(wxid) # Give the label provider a chance to veto the collapse. if not self.label_provider.is_collapsible(self, element): event.Veto() return def _on_tree_item_collapsed(self, event): """ Called when a tree item has been collapsed. """ # Which item was collapsed? wxid = event.GetItem() # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data. populated, element = self.control.GetPyData(wxid) # Make sure that the element's 'closed' icon is displayed etc. self._refresh_element(wxid, element) # Trait notification. self.element_collapsed = element return def _on_tree_item_activated(self, event): """ Called when a tree item is activated (i.e., double clicked). """ # Which item was activated? wxid = event.GetItem() # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data. populated, element = self.control.GetPyData(wxid) # Trait notification. self.element_activated = element return def _on_tree_sel_changed(self, event): """ Called when the selection is changed. """ # Trait notification. self.selection = self._get_selection() return def _on_tree_begin_drag(self, event): """ Called when a drag operation is starting on a tree item. """ # Get the element, its id and the point where the event occurred. data, wxid, flags, point = self._unpack_event(event) if point == (0, 0): # Apply workaround. point = self._point_left_clicked wxid, flags = self.control.HitTest(point) data = self.control.GetPyData(wxid) if data is not None: populated, element = data # Trait notification. self.element_begin_drag = element return def _on_tree_begin_label_edit(self, event): """ Called when the user has started editing an item's label. """ wxid = event.GetItem() # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data. populated, element = self.control.GetPyData(wxid) # Give the label provider a chance to veto the edit. if not self.label_provider.is_editable(self, element): event.Veto() return def _on_tree_end_label_edit(self, event): """ Called when the user has finished editing an item's label. """ wxid = event.GetItem() # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data. populated, element = self.control.GetPyData(wxid) # Give the label provider a chance to veto the edit. label = event.GetLabel() if not self.label_provider.set_text(self, element, label): event.Veto() return def _on_char(self, event): """ Called when a key is pressed when the tree has focus. """ # Trait notification. self.key_pressed = event.GetKeyCode() return
class Tree(Widget): """ A tree control with a model/ui architecture. """ # The default tree style. STYLE = wx.TR_EDIT_LABELS | wx.TR_HAS_BUTTONS | wx.CLIP_CHILDREN #### 'Tree' interface ##################################################### # The tree's filters (empty if no filtering is required). filters = List(Filter) # Mode for lines connecting tree nodes which emphasize hierarchy: # 'appearance' - only on when lines look good, # 'on' - always on, 'off' - always off # NOTE: on and off are ignored in favor of show_lines for now lines_mode = Enum('appearance', 'on', 'off') # The model that provides the data for the tree. model = Instance(TreeModel, ()) # The root of the tree (this is for convenience, it just delegates to # the tree's model). root = Property(Any) # The objects currently selected in the tree. selection = List # Selection mode. selection_mode = Enum('single', 'extended') # Should an image be shown for each node? show_images = Bool(True) # Should lines be drawn between levels in the tree. show_lines = Bool(True) # Should the root of the tree be shown? show_root = Bool(True) # The tree's sorter (None if no sorting is required). sorter = Instance(Sorter) #### Events #### # A right-click occurred on the control (not a node!). control_right_clicked = Event #(Point) # A key was pressed while the tree has focus. key_pressed = Event(KeyPressedEvent) # A node has been activated (ie. double-clicked). node_activated = Event #(Any) # A drag operation was started on a node. node_begin_drag = Event #(Any) # A (non-leaf) node has been collapsed. node_collapsed = Event #(Any) # A (non-leaf) node has been expanded. node_expanded = Event #(Any) # A left-click occurred on a node. # # Tuple(node, point). node_left_clicked = Event #(Tuple) # A right-click occurred on a node. # # Tuple(node, point) node_right_clicked = Event #(Tuple) #### Private interface #################################################### # A name to distinguish the tree for debugging! # # fixme: This turns out to be kinda useful... Should 'Widget' have a name # trait? _name = Str('Anonymous tree') # An optional callback to detect the end of a label edit. This is # useful because the callback will be invoked even if the node label was # not actually changed. _label_edit_callback = Trait(None, Callable, None) # Flag for allowing selection events to be ignored _ignore_selection_events = Bool(False) ########################################################################### # 'object' interface. ########################################################################### def __init__(self, parent, image_size=(16, 16), **traits): """ Creates a new tree. 'parent' is the toolkit-specific control that is the tree's parent. 'image_size' is a tuple in the form (int width, int height) that specifies the size of the images (if required) displayed in the tree. """ # Base class constructors. super(Tree, self).__init__(**traits) # Get our wx Id. wxid = wx.NewId() # Create the toolkit-specific control. self.control = tree = _Tree(self, parent, wxid, style=self._get_style()) # Wire up the wx tree events. wx.EVT_CHAR(tree, self._on_char) wx.EVT_LEFT_DOWN(tree, self._on_left_down) # fixme: This is not technically correct as context menus etc should # appear on a right up (or right click). Unfortunately, if we # change this to 'EVT_RIGHT_UP' wx does not fire the event unless the # right mouse button is double clicked 8^() Sad, but true! wx.EVT_RIGHT_DOWN(tree, self._on_right_down) # fixme: This is not technically correct as we would really like to use # 'EVT_TREE_ITEM_ACTIVATED'. Unfortunately, (in 2.6 at least), it # throws an exception when the 'Enter' key is pressed as the wx tree # item Id in the event seems to be invalid. It also seems to cause # any child frames that my be created in response to the event to # appear *behind* the parent window, which is, errrr, not great ;^) wx.EVT_LEFT_DCLICK(tree, self._on_tree_item_activated) #wx.EVT_TREE_ITEM_ACTIVATED(tree, wxid, self._on_tree_item_activated) wx.EVT_TREE_ITEM_COLLAPSING(tree, wxid, self._on_tree_item_collapsing) wx.EVT_TREE_ITEM_COLLAPSED(tree, wxid, self._on_tree_item_collapsed) wx.EVT_TREE_ITEM_EXPANDING(tree, wxid, self._on_tree_item_expanding) wx.EVT_TREE_ITEM_EXPANDED(tree, wxid, self._on_tree_item_expanded) wx.EVT_TREE_BEGIN_LABEL_EDIT(tree, wxid, self._on_tree_begin_label_edit) wx.EVT_TREE_END_LABEL_EDIT(tree, wxid, self._on_tree_end_label_edit) wx.EVT_TREE_BEGIN_DRAG(tree, wxid, self._on_tree_begin_drag) wx.EVT_TREE_SEL_CHANGED(tree, wxid, self._on_tree_sel_changed) wx.EVT_TREE_DELETE_ITEM(tree, wxid, self._on_tree_delete_item) # Enable the tree as a drag and drop target. self.control.SetDropTarget(PythonDropTarget(self)) # The image list is a wxPython-ism that caches all images used in the # control. self._image_list = ImageList(image_size[0], image_size[1]) if self.show_images: tree.AssignImageList(self._image_list) # Mapping from node to wx tree item Ids. self._node_to_id_map = {} # Add the root node. if self.root is not None: self._add_root_node(self.root) # Listen for changes to the model. self._add_model_listeners(self.model) return ########################################################################### # 'Tree' interface. ########################################################################### #### Properties ########################################################### def _get_root(self): """ Returns the root node of the tree. """ return self.model.root def _set_root(self, root): """ Sets the root node of the tree. """ self.model.root = root return #### Methods ############################################################## def collapse(self, node): """ Collapses the specified node. """ wxid = self._get_wxid(node) if wxid is not None: self.control.Collapse(wxid) return def edit_label(self, node, callback=None): """ Edits the label of the specified node. If a callback is specified it will be called when the label edit completes WHETHER OR NOT the label was actually changed. The callback must take exactly 3 arguments:- (tree, node, label) """ wxid = self._get_wxid(node) if wxid is not None: self._label_edit_callback = callback self.control.EditLabel(wxid) return def expand(self, node): """ Expands the specified node. """ wxid = self._get_wxid(node) if wxid is not None: self.control.Expand(wxid) return def expand_all(self): """ Expands every node in the tree. """ if self.show_root: self._expand_item(self._get_wxid(self.root)) else: for child in self._get_children(self.root): self._expand_item(self._get_wxid(child)) return def get_parent(self, node): """ Returns the parent of a node. This will only work iff the node has been displayed in the tree. If it hasn't then None is returned. """ # Has the node actually appeared in the tree yet? wxid = self._get_wxid(node) if wxid is not None: pid = self.control.GetItemParent(wxid) # The item data is a tuple. The first element indicates whether or # not we have already populated the item with its children. The # second element is the actual item data. populated, parent = self.control.GetPyData(pid) else: parent = None return parent def is_expanded(self, node): """ Returns True if the node is expanded, otherwise False. """ wxid = self._get_wxid(node) if wxid is not None: # If the root node is hidden then it is always expanded! if node is self.root and not self.show_root: is_expanded = True else: is_expanded = self.control.IsExpanded(wxid) else: is_expanded = False return is_expanded def is_selected(self, node): """ Returns True if the node is selected, otherwise False. """ wxid = self._get_wxid(node) if wxid is not None: is_selected = self.control.IsSelected(wxid) else: is_selected = False return is_selected def refresh(self, node): """ Refresh the tree starting from the specified node. Call this when the structure of the content has changed DRAMATICALLY. """ # Has the node actually appeared in the tree yet? pid = self._get_wxid(node) if pid is not None: # Delete all of the node's children and re-add them. self.control.DeleteChildren(pid) self.control.SetPyData(pid, (False, node)) # Does the node have any children? has_children = self._has_children(node) self.control.SetItemHasChildren(pid, has_children) # fixme: At least on Windows, wx does not fire an expanding # event for a hidden root node, so we have to populate the node # manually. if node is self.root and not self.show_root: # Add the child nodes. for child in self._get_children(node): self._add_node(pid, child) else: # Expand it. if self.control.IsExpanded(pid): self.control.Collapse(pid) self.control.Expand(pid) return def select(self, node): """ Selects the specified node. """ wxid = self._get_wxid(node) if wxid is not None: self.control.SelectItem(wxid) return def set_selection(self, list): """ Selects the specified list of nodes. """ logger.debug('Setting selection to [%s] within Tree [%s]', list, self) # Update the control to reflect the target list by unselecting # everything and then selecting each item in the list. During this # process, we want to avoid changing our own selection. self._ignore_selection_events = True self.control.UnselectAll() for node in list: try: self.select(node) except: logger.exception('Unable to select node [%s]', node) self._ignore_selection_events = False # Update our selection to reflect the final selection state. self.selection = self._get_selection() ########################################################################### # 'PythonDropTarget' interface. ########################################################################### def on_drag_over(self, x, y, obj, default_drag_result): """ Called when a node is dragged over the tree. """ result = wx.DragNone # Find the node that we are dragging over... node = self._get_drag_drop_node(x, y) if node is not None: # Ask the model if the node allows the object to be dropped onto # it. if self.model.can_drop(node, obj): result = default_drag_result return result def on_drop(self, x, y, obj, default_drag_result): """ Called when a node is dropped on the tree. """ # Find the node that we are dragging over... node = self._get_drag_drop_node(x, y) if node is not None: self.model.drop(node, obj) return default_drag_result ########################################################################### # Private interface. ########################################################################### def _get_wxid(self, node): """ Returns the wxid for the specified node. Returns None if the node has not yet appeared in the tree. """ # The model must generate a unique key for each node (unique within the # model). key = self.model.get_key(node) return self._node_to_id_map.get(key, None) def _set_wxid(self, node, wxid): """ Sets the wxid for the specified node. """ # The model must generate a unique key for each node (unique within the # model). key = self.model.get_key(node) self._node_to_id_map[key] = wxid return def _remove_wxid(self, node): """ Removes the wxid for the specified node. """ # The model must generate a unique key for each node (unique within the # model). key = self.model.get_key(node) try: del self._node_to_id_map[key] except KeyError: # fixme: No, really, this is a serious one... How do we get in this # situation. It came up when using the canvas stuff... logger.warn('removing node: %s' % str(node)) return def _get_style(self): """ Returns the wx style flags for creating the tree control. """ # Start with the default flags. style = self.STYLE # Turn lines off for appearance on *nix. # ...for now, show_lines determines if lines are on or off, but # eventually lines_mode may eliminate the need for show_lines if self.lines_mode == 'appearance' and os.name == 'posix': self.show_lines = False if not self.show_lines: style = style | wx.TR_NO_LINES if not self.show_root: # fixme: It looks a little weird, but it we don't have the # 'lines at root' style then wx won't draw the expand/collapse # image on non-leaf nodes at the root level 8^() style = style | wx.TR_HIDE_ROOT | wx.TR_LINES_AT_ROOT if self.selection_mode != 'single': style = style | wx.TR_MULTIPLE | wx.TR_EXTENDED return style def _add_model_listeners(self, model): """ Adds listeners for model changes. """ # Listen for changes to the model. model.on_trait_change(self._on_root_changed, 'root') model.on_trait_change(self._on_nodes_changed, 'nodes_changed') model.on_trait_change(self._on_nodes_inserted, 'nodes_inserted') model.on_trait_change(self._on_nodes_removed, 'nodes_removed') model.on_trait_change(self._on_nodes_replaced, 'nodes_replaced') model.on_trait_change(self._on_structure_changed, 'structure_changed') return def _remove_model_listeners(self, model): """ Removes listeners for model changes. """ # Unhook the model event listeners. model.on_trait_change(self._on_root_changed, 'root', remove=True) model.on_trait_change(self._on_nodes_changed, 'nodes_changed', remove=True) model.on_trait_change(self._on_nodes_inserted, 'nodes_inserted', remove=True) model.on_trait_change(self._on_nodes_removed, 'nodes_removed', remove=True) model.on_trait_change(self._on_nodes_replaced, 'nodes_replaced', remove=True) model.on_trait_change(self._on_structure_changed, 'structure_changed', remove=True) return def _add_root_node(self, node): """ Adds the root node. """ # Get the tree item image index and the label text. image_index = self._get_image_index(node) text = self._get_text(node) # Add the node. wxid = self.control.AddRoot(text, image_index, image_index) # This gives the model a chance to wire up trait handlers etc. self.model.add_listener(node) # If the root node is hidden, get its children. if not self.show_root: # Add the child nodes. for child in self._get_children(node): self._add_node(wxid, child) # Does the node have any children? has_children = self._has_children(node) self.control.SetItemHasChildren(wxid, has_children) # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data (which in our case is an arbitrary # Python object provided by the tree model). if self.show_root: self.control.SetPyData(wxid, (not self.show_root, node)) # Make sure that we can find the node's Id. self._set_wxid(node, wxid) # Automatically expand the root. if self.show_root: self.control.Expand(wxid) return def _add_node(self, pid, node): """ Adds 'node' as a child of the node identified by 'pid'. If 'pid' is None then we are adding the root node. """ # Get the tree item image index and the label text. image_index = self._get_image_index(node) text = self._get_text(node) # Add the node. wxid = self.control.AppendItem(pid, text, image_index, image_index) # This gives the model a chance to wire up trait handlers etc. self.model.add_listener(node) # Does the node have any children? has_children = self._has_children(node) self.control.SetItemHasChildren(wxid, has_children) # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data (which in our case is an arbitrary # Python object provided by the tree model). self.control.SetPyData(wxid, (False, node)) # Make sure that we can find the node's Id. self._set_wxid(node, wxid) return def _insert_node(self, pid, node, index): """ Inserts 'node' as a child of the node identified by 'pid'. If 'pid' is None then we are adding the root node. """ # Get the tree item image index and the label text. image_index = self._get_image_index(node) text = self._get_text(node) # Add the node. wxid = self.control.InsertItemBefore(pid, index, text, image_index, image_index) # This gives the model a chance to wire up trait handlers etc. self.model.add_listener(node) # Does the node have any children? has_children = self._has_children(node) self.control.SetItemHasChildren(wxid, has_children) # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data (which in our case is an arbitrary # Python object provided by the tree model). self.control.SetPyData(wxid, (False, node)) # Make sure that we can find the node's Id. self._set_wxid(node, wxid) return def _remove_node(self, wxid, node): """ Removes a node from the tree. """ # This gives the model a chance to remove trait handlers etc. self.model.remove_listener(node) # Remove the reference to the item's data. self._remove_wxid(node) self.control.SetPyData(wxid, None) return def _update_node(self, wxid, node): """ Updates the image and text of the specified node. """ # Get the tree item image index. image_index = self._get_image_index(node) self.control.SetItemImage(wxid, image_index, wx.TreeItemIcon_Normal) self.control.SetItemImage(wxid, image_index, wx.TreeItemIcon_Selected) # Get the tree item text. text = self._get_text(node) self.control.SetItemText(wxid, text) return def _has_children(self, node): """ Returns True if a node has children. """ # fixme: To be correct we *should* apply filtering here, but that # seems to blow a hole throught models that have some efficient # mechanism for determining whether or not they have children. There # is also a precedent for doing it this way in Windoze, where a node # gets marked as though it can be expanded, even thought when the # expansion occurs, no children are present! return self.model.has_children(node) def _get_children(self, node): """ Get the children of a node. """ children = self.model.get_children(node) # Filtering.... filtered_children = [] for child in children: for filter in self.filters: if not filter.select(self, node, child): break else: filtered_children.append(child) # Sorting... if self.sorter is not None: self.sorter.sort(self, node, filtered_children) return filtered_children def _get_image_index(self, node): """ Returns the tree item image index for a node. """ expanded = self.is_expanded(node) selected = self.is_selected(node) # Get the image used to represent the node. image = self.model.get_image(node, selected, expanded) if image is not None: image_index = self._image_list.GetIndex(image) else: image_index = -1 return image_index def _get_drag_drop_node(self, x, y): """ Returns the node that is being dragged/dropped on. Returns None if the cursor is not over the icon or label of a node. """ data, wxid, flags, point = self._hit_test((x, y)) if data is not None: populated, node = data else: node = None return node def _get_text(self, node): """ Returns the tree item text for a node. """ text = self.model.get_text(node) if text is None: text = '' return text def _unpack_event(self, event, wxid=None): """ Unpacks the event to see whether a tree item was involved. """ try: point = event.GetPosition() except: point = event.GetPoint() return self._hit_test(point, wxid) def _hit_test(self, point, wxid=None): """ Determines whether a point is within a node's label or icon. """ flags = wx.TREE_HITTEST_ONITEMLABEL if (wxid is None) or (not wxid.IsOk()): wxid, flags = self.control.HitTest(point) # Warning: On GTK we have to check the flags before we call 'GetPyData' # because if we call it when the hit test returns 'nowhere' it will # barf (on Windows it simply returns 'None' 8^() if flags & wx.TREE_HITTEST_NOWHERE: data = None elif flags & wx.TREE_HITTEST_ONITEMICON \ or flags & wx.TREE_HITTEST_ONITEMLABEL: data = self.control.GetPyData(wxid) # fixme: Not sure why 'TREE_HITTEST_NOWHERE' doesn't catch everything! else: data = None return data, wxid, flags, point def _get_selection(self): """ Returns a list of the selected nodes """ selection = [] for wxid in self.control.GetSelections(): data = self.control.GetPyData(wxid) if data is not None: populated, node = data selection.append(self.model.get_selection_value(node)) return selection def _expand_item(self, wxid): """ Recursively expand a tree item. """ self.control.Expand(wxid) cid, cookie = self.control.GetFirstChild(wxid) while cid.IsOk(): self._expand_item(cid) cid, cookie = self.control.GetNextChild(wxid, cookie) return #### Trait event handlers ################################################# def _on_root_changed(self, root): """ Called when the root of the model has changed. """ # Delete everything... if self.control is not None: self.control.DeleteAllItems() self._node_to_id_map = {} # ... and then add the root item back in. if root is not None: self._add_root_node(root) return def _on_nodes_changed(self, event): """ Called when nodes have been changed. """ self._update_node(self._get_wxid(event.node), event.node) for child in event.children: cid = self._get_wxid(child) if cid is not None: self._update_node(cid, child) return def _on_nodes_inserted(self, event): """ Called when nodes have been inserted. """ parent = event.node children = event.children index = event.index # Has the node actually appeared in the tree yet? pid = self._get_wxid(parent) if pid is not None: # The item data is a tuple. The first element indicates whether or # not we have already populated the item with its children. The # second element is the actual item data. if self.show_root or parent is not self.root: populated, node = self.control.GetPyData(pid) else: populated = True # If the node is not yet populated then just get the children and # add them. if not populated: for child in self._get_children(parent): self._add_node(pid, child) # Otherwise, insert them. else: # An index of -1 means append! if index == -1: index = self.control.GetChildrenCount(pid, False) for child in children: self._insert_node(pid, child, index) index += 1 # The element is now populated! if self.show_root or parent is not self.root: self.control.SetPyData(pid, (True, parent)) # Does the node have any children now? has_children = self.control.GetChildrenCount(pid) > 0 self.control.SetItemHasChildren(pid, has_children) # If the node is not expanded then expand it. if not self.is_expanded(parent): self.expand(parent) return def _on_nodes_removed(self, event): """ Called when nodes have been removed. """ parent = event.node children = event.children # Has the node actually appeared in the tree yet? pid = self._get_wxid(parent) if pid is not None: for child in event.children: cid = self._get_wxid(child) if cid is not None: self.control.Delete(cid) # Does the node have any children left? has_children = self.control.GetChildrenCount(pid) > 0 self.control.SetItemHasChildren(pid, has_children) return def _on_nodes_replaced(self, event): """ Called when nodes have been replaced. """ for old_child, new_child in zip(event.old_children, event.children): cid = self._get_wxid(old_child) if cid is not None: # Remove listeners from the old node. self.model.remove_listener(old_child) # Delete all of the node's children. self.control.DeleteChildren(cid) # Update the visual appearance of the node. self._update_node(cid, new_child) # Update the node data. # # The item data is a tuple. The first element indicates # whether or not we have already populated the item with its # children. The second element is the actual item data (which # in our case is an arbitrary Python object provided by the # tree model). self.control.SetPyData(cid, (False, new_child)) # Remove the old node from the node to Id map. self._remove_wxid(old_child) # Add the new node to the node to Id map. self._set_wxid(new_child, cid) # Add listeners to the new node. self.model.add_listener(new_child) # Does the new node have any children? has_children = self._has_children(new_child) self.control.SetItemHasChildren(cid, has_children) # Update the tree's selection (in case the old node that was replaced # was selected, the selection should now include the new node). self.selection = self._get_selection() return def _on_structure_changed(self, event): """ Called when the structure of a node has changed drastically. """ self.refresh(event.node) return #### wx event handlers #################################################### def _on_char(self, event): """ Called when a key is pressed when the tree has focus. """ self.key_pressed = KeyPressedEvent( alt_down=event.m_altDown == 1, control_down=event.m_controlDown == 1, shift_down=event.m_shiftDown == 1, key_code=event.m_keyCode) event.Skip() return def _on_left_down(self, event): """ Called when the left mouse button is clicked on the tree. """ data, id, flags, point = self._unpack_event(event) # Save point for tree_begin_drag method to workaround a bug in ?? when # wx.TreeEvent.GetPoint returns only (0,0). This happens under linux # when using wx-2.4.2.4, for instance. self._point_left_clicked = point # Did the left click occur on a tree item? if data is not None: populated, node = data # Trait event notification. self.node_left_clicked = node, point # Give other event handlers a chance. event.Skip() return def _on_right_down(self, event): """ Called when the right mouse button is clicked on the tree. """ data, id, flags, point = self._unpack_event(event) # Did the right click occur on a tree item? if data is not None: populated, node = data # Trait event notification. self.node_right_clicked = node, point # Otherwise notify that the control itself was clicked else: self.control_right_clicked = point # Give other event handlers a chance. event.Skip() return def _on_tree_item_activated(self, event): """ Called when a tree item is activated (i.e., double clicked). """ # fixme: See the comment where the events are wired up for more # information. ## # Which item was activated? ## wxid = event.GetItem() # Which item was activated. point = event.GetPosition() wxid, flags = self.control.HitTest(point) # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data. populated, node = self.control.GetPyData(wxid) # Trait event notiification. self.node_activated = node return def _on_tree_item_collapsing(self, event): """ Called when a tree item is about to collapse. """ # Which item is collapsing? wxid = event.GetItem() # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data. populated, node = self.control.GetPyData(wxid) # Give the model a chance to veto the collapse. if not self.model.is_collapsible(node): event.Veto() return def _on_tree_item_collapsed(self, event): """ Called when a tree item has been collapsed. """ # Which item was collapsed? wxid = event.GetItem() # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data. populated, node = self.control.GetPyData(wxid) # Make sure that the item's 'closed' icon is displayed etc. self._update_node(wxid, node) # Trait event notification. self.node_collapsed = node return def _on_tree_item_expanding(self, event): """ Called when a tree item is about to expand. """ # Which item is expanding? wxid = event.GetItem() # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data. populated, node = self.control.GetPyData(wxid) # Give the model a chance to veto the expansion. if self.model.is_expandable(node): # Lazily populate the item's children. if not populated: # Add the child nodes. for child in self._get_children(node): self._add_node(wxid, child) # The element is now populated! self.control.SetPyData(wxid, (True, node)) else: event.Veto() return def _on_tree_item_expanded(self, event): """ Called when a tree item has been expanded. """ # Which item was expanded? wxid = event.GetItem() # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data. populated, node = self.control.GetPyData(wxid) # Make sure that the node's 'open' icon is displayed etc. self._update_node(wxid, node) # Trait event notification. self.node_expanded = node return def _on_tree_begin_label_edit(self, event): """ Called when the user has started editing an item's label. """ wxid = event.GetItem() # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data. populated, node = self.control.GetPyData(wxid) # Give the model a chance to veto the edit. if not self.model.is_editable(node): event.Veto() return def _on_tree_end_label_edit(self, event): """ Called when the user has finished editing am item's label. """ wxid = event.GetItem() # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data. populated, node = self.control.GetPyData(wxid) # Give the model a chance to veto the edit. label = event.GetLabel() # Making sure the new label is not an empty string if label is not None and len(label) > 0 and \ self.model.can_set_text(node, label): def end_label_edit(): """ Called to complete the label edit. """ # Set the node's text. self.model.set_text(node, label) # If a label edit callback was specified (in the call to # 'edit_label'), then call it). if self._label_edit_callback is not None: self._label_edit_callback(self, node, label) return # We use a deffered call here, because a name change can trigger # the structure of a node to change, and hence the actual tree # nodes might get moved/deleted before the label edit operation has # completed. When this happens wx gets very confused! By using # 'invoke_later' we allow the label edit to complete. GUI.invoke_later(end_label_edit) else: event.Veto() # If a label edit callback was specified (in the call to # 'edit_label'), then call it). if self._label_edit_callback is not None: self._label_edit_callback(self, node, label) return def _on_tree_begin_drag(self, event): """ Called when a drag operation is starting on a tree item. """ # Get the node, its id and the point where the event occurred. data, wxid, flags, point = self._unpack_event(event, event.GetItem()) if point == (0, 0): # Apply workaround for GTK. point = self.point_left_clicked wxid, flags = self.HitTest(point) data = self.control.GetPyData(wxid) if data is not None: populated, node = data # Give the model a chance to veto the drag. if self.model.is_draggable(node): # We ask the model for the actual value to drag. drag_value = self.model.get_drag_value(node) # fixme: This is a terrible hack to get the binding x passed # during a drag operation. Bindings should probably *always* # be dragged and our drag and drop mechanism should allow # extendable ways to extract the actual data. from pyface.wx.drag_and_drop import clipboard clipboard.node = [node] # Make sure that the tree selection is updated before we start # the drag. If we don't do this then if the first thing a # user does is drag a tree item (i.e., without a separate click # to select it first) then the selection appears empty. self.selection = self._get_selection() # Start the drag. PythonDropSource(self.control, drag_value, self) # Trait event notification. self.node_begin_drag = node else: event.Veto() return # fixme: This is part of the drag and drop hack... def on_dropped(self): """ Callback invoked when a drag/drop operation has completed. """ from pyface.wx.drag_and_drop import clipboard clipboard.node = None return def _on_tree_sel_changed(self, event): """ Called when the selection is changed. """ # Update our record of the selection to whatever was selected in the # tree UNLESS we are ignoring selection events. if not self._ignore_selection_events: # Trait notification. self.selection = self._get_selection() return def _on_tree_delete_item(self, event): """ Called when a tree item is being been deleted. """ # Which item is being deleted? wxid = event.GetItem() # Check if GetPyData() returned a valid to tuple to unpack # ...if so, remove the node from the tree, otherwise just return # # fixme: Whoever addeed this code (and the comment above) didn't say # when this was occurring. This is method is called in response to a wx # event to delete an item and hence the item data should never be None # surely?!? Was it happening just on one platform?!? data = self.control.GetPyData(wxid) if data is not None: # The item data is a tuple. The first element indicates whether or # not we have already populated the item with its children. The # second element is the actual item data. populated, node = data # Remove the node. self._remove_node(wxid, node) return
def __init__(self, parent, image_size=(16, 16), **traits): """ Creates a new tree. 'parent' is the toolkit-specific control that is the tree's parent. 'image_size' is a tuple in the form (int width, int height) that specifies the size of the images (if required) displayed in the tree. """ # Base class constructors. super(Tree, self).__init__(**traits) # Get our wx Id. wxid = wx.NewId() # Create the toolkit-specific control. self.control = tree = _Tree(self, parent, wxid, style=self._get_style()) # Wire up the wx tree events. wx.EVT_CHAR(tree, self._on_char) wx.EVT_LEFT_DOWN(tree, self._on_left_down) # fixme: This is not technically correct as context menus etc should # appear on a right up (or right click). Unfortunately, if we # change this to 'EVT_RIGHT_UP' wx does not fire the event unless the # right mouse button is double clicked 8^() Sad, but true! wx.EVT_RIGHT_DOWN(tree, self._on_right_down) # fixme: This is not technically correct as we would really like to use # 'EVT_TREE_ITEM_ACTIVATED'. Unfortunately, (in 2.6 at least), it # throws an exception when the 'Enter' key is pressed as the wx tree # item Id in the event seems to be invalid. It also seems to cause # any child frames that my be created in response to the event to # appear *behind* the parent window, which is, errrr, not great ;^) wx.EVT_LEFT_DCLICK(tree, self._on_tree_item_activated) #wx.EVT_TREE_ITEM_ACTIVATED(tree, wxid, self._on_tree_item_activated) wx.EVT_TREE_ITEM_COLLAPSING(tree, wxid, self._on_tree_item_collapsing) wx.EVT_TREE_ITEM_COLLAPSED(tree, wxid, self._on_tree_item_collapsed) wx.EVT_TREE_ITEM_EXPANDING(tree, wxid, self._on_tree_item_expanding) wx.EVT_TREE_ITEM_EXPANDED(tree, wxid, self._on_tree_item_expanded) wx.EVT_TREE_BEGIN_LABEL_EDIT(tree, wxid, self._on_tree_begin_label_edit) wx.EVT_TREE_END_LABEL_EDIT(tree, wxid, self._on_tree_end_label_edit) wx.EVT_TREE_BEGIN_DRAG(tree, wxid, self._on_tree_begin_drag) wx.EVT_TREE_SEL_CHANGED(tree, wxid, self._on_tree_sel_changed) wx.EVT_TREE_DELETE_ITEM(tree, wxid, self._on_tree_delete_item) # Enable the tree as a drag and drop target. self.control.SetDropTarget(PythonDropTarget(self)) # The image list is a wxPython-ism that caches all images used in the # control. self._image_list = ImageList(image_size[0], image_size[1]) if self.show_images: tree.AssignImageList(self._image_list) # Mapping from node to wx tree item Ids. self._node_to_id_map = {} # Add the root node. if self.root is not None: self._add_root_node(self.root) # Listen for changes to the model. self._add_model_listeners(self.model) return
class _Table(wx.ListCtrl): """ The wx control that we use to implement the table viewer. """ # Default style. STYLE = wx.LC_REPORT | wx.LC_HRULES | wx.LC_VRULES | wx.STATIC_BORDER \ | wx.LC_SINGLE_SEL | wx.LC_VIRTUAL | wx.LC_EDIT_LABELS \ | wx.CLIP_CHILDREN def __init__(self, parent, image_size, viewer): """ Creates a new table viewer. 'parent' is the toolkit-specific control that is the table's parent. 'image_size' is a tuple in the form (int width, int height) that specifies the size of the icons (if any) displayed in the table. """ # The vierer that we are providing the control for. self._viewer = viewer # Base-class constructor. wx.ListCtrl.__init__(self, parent, -1, style=self.STYLE) # Table item images. self._image_list = ImageList(image_size[0], image_size[1]) self.AssignImageList(self._image_list, wx.IMAGE_LIST_SMALL) # Set up attributes to show alternate rows with a different background # colour. self._even_row_attribute = wx.ListItemAttr() self._even_row_attribute.SetBackgroundColour( self._viewer.even_row_background) self._odd_row_attribute = wx.ListItemAttr() self._odd_row_attribute.SetBackgroundColour( self._viewer.odd_row_background) return ########################################################################### # Virtual 'ListCtrl' interface. ########################################################################### def OnGetItemText(self, row, column_index): """ Returns the text for the specified CELL. """ viewer = self._viewer element = viewer._elements[row] return viewer.label_provider.get_text(viewer, element, column_index) def OnGetItemImage(self, row): """ Returns the image for the specified ROW. """ viewer = self._viewer element = viewer._elements[row] # Get the icon used to represent the node. image = viewer.label_provider.get_image(viewer, element) if image is not None: image_index = self._image_list.GetIndex(image.absolute_path) else: image_index = -1 return image_index def OnGetItemAttr(self, row): """ Returns the attribute for the specified row. """ if row % 2 == 0: attribute = self._even_row_attribute else: attribute = self._odd_row_attribute return attribute