+# 12/09/2003 - Jeff Grimmett (grimmtooth@softhome.net)
+#
+# o 2.5 compatability update.
+# o I'm a little nervous about some of it though.
+#
+# 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net)
+#
+# o wxTreeModel -> TreeModel
+# o wxMVCTree -> MVCTree
+# o wxMVCTreeEvent -> MVCTreeEvent
+# o wxMVCTreeNotifyEvent -> MVCTreeNotifyEvent
+#
+
+"""
+MVCTree is a control which handles hierarchical data. It is constructed
+in model-view-controller architecture, so the display of that data, and
+the content of the data can be changed greatly without affecting the other parts.
+
+MVCTree actually is even more configurable than MVC normally implies, because
+almost every aspect of it is pluggable:
+ MVCTree - Overall controller, and the window that actually gets placed
+ in the GUI.
+ Painter - Paints the control. The 'view' part of MVC.
+ NodePainter - Paints just the nodes
+ LinePainter - Paints just the lines between the nodes
+ TextConverter - Figures out what text to print for each node
+ Editor - Edits the contents of a node, if the model is editable.
+ LayoutEngine - Determines initial placement of nodes
+ Transform - Adjusts positions of nodes for movement or special effects.
+ TreeModel - Contains the data which the rest of the control acts
+ on. The 'model' part of MVC.
+
+Author/Maintainer - Bryn Keller <xoltar@starship.python.net>
+
+
+NOTE: This module is *not* supported in any way. Use it however you
+ wish, but be warned that dealing with any consequences is
+ entirly up to you.
+ --Robin
+"""
+
+#------------------------------------------------------------------------
+import os
+import sys
+import traceback
+import warnings
+
+import wx
+#------------------------------------------------------------------------
+
+warningmsg = r"""\
+
+################################################\
+# This module is not supported in any way! |
+# |
+# See cource code for wx.lib.mvctree for more |
+# information. |
+################################################/
+
+"""
+
+warnings.warn(warningmsg, DeprecationWarning, stacklevel=2)
+#------------------------------------------------------------------------
+
+class MVCTreeNode:
+ """
+ Used internally by MVCTree to manage its data. Contains information about
+ screen placement, the actual data associated with it, and more. These are
+ the nodes passed to all the other helper parts to do their work with.
+ """
+ def __init__(self, data=None, parent = None, kids = None, x = 0, y = 0):
+ self.x = 0
+ self.y = 0
+ self.projx = 0
+ self.projy = 0
+ self.parent = parent
+ self.kids = kids
+ if self.kids is None:
+ self.kids = []
+ self.data = data
+ self.expanded = False
+ self.selected = False
+ self.built = False
+ self.scale = 0
+
+ def GetChildren(self):
+ return self.kids
+
+ def GetParent(self):
+ return self.parent
+
+ def Remove(self, node):
+ try:
+ self.kids.remove(node)
+ except:
+ pass
+ def Add(self, node):
+ self.kids.append(node)
+ node.SetParent(self)
+
+ def SetParent(self, parent):
+ if self.parent and not (self.parent is parent):
+ self.parent.Remove(self)
+ self.parent = parent
+ def __str__(self):
+ return "Node: " + str(self.data) + " (" + str(self.x) + ", " + str(self.y) + ")"
+ def __repr__(self):
+ return str(self.data)
+ def GetTreeString(self, tabs=0):
+ s = tabs * '\t' + str(self) + '\n'
+ for kid in self.kids:
+ s = s + kid.GetTreeString(tabs + 1)
+ return s
+
+
+class Editor:
+ def __init__(self, tree):
+ self.tree = tree
+ def Edit(self, node):
+ raise NotImplementedError
+ def EndEdit(self, node, commit):
+ raise NotImplementedError
+ def CanEdit(self, node):
+ raise NotImplementedError
+
+class LayoutEngine:
+ """
+ Interface for layout engines.
+ """
+ def __init__(self, tree):
+ self.tree = tree
+ def Layout(self, node):
+ raise NotImplementedError
+ def GetNodeList(self):
+ raise NotImplementedError
+
+class Transform:
+ """
+ Transform interface.
+ """
+ def __init__(self, tree):
+ self.tree = tree
+ def Transform(self, node, offset, rotation):
+ """
+ This method should only change the projx and projy attributes of
+ the node. These represent the position of the node as it should
+ be drawn on screen. Adjusting the x and y attributes can and
+ should cause havoc.
+ """
+ raise NotImplementedError
+
+ def GetSize(self):
+ """
+ Returns the size of the entire tree as laid out and transformed
+ as a tuple
+ """
+ raise NotImplementedError
+
+class Painter:
+ """
+ This is the interface that MVCTree expects from painters. All painters should
+ be Painter subclasses.
+ """
+ def __init__(self, tree):
+ self.tree = tree
+ self.textcolor = wx.NamedColour("BLACK")
+ self.bgcolor = wx.NamedColour("WHITE")
+ self.fgcolor = wx.NamedColour("BLUE")
+ self.linecolor = wx.NamedColour("GREY")
+ self.font = wx.Font(9, wx.DEFAULT, wx.NORMAL, wx.NORMAL, False)
+ self.bmp = None
+
+ def GetFont(self):
+ return self.font
+
+ def SetFont(self, font):
+ self.font = font
+ self.tree.Refresh()
+ def GetBuffer(self):
+ return self.bmp
+ def ClearBuffer(self):
+ self.bmp = None
+ def Paint(self, dc, node, doubleBuffered=1, paintBackground=1):
+ raise NotImplementedError
+ def GetTextColour(self):
+ return self.textcolor
+ def SetTextColour(self, color):
+ self.textcolor = color
+ self.textbrush = wx.Brush(color)
+ self.textpen = wx.Pen(color, 1, wx.SOLID)
+ def GetBackgroundColour(self):
+ return self.bgcolor
+ def SetBackgroundColour(self, color):
+ self.bgcolor = color
+ self.bgbrush = wx.Brush(color)
+ self.bgpen = wx.Pen(color, 1, wx.SOLID)
+ def GetForegroundColour(self):
+ return self.fgcolor
+ def SetForegroundColour(self, color):
+ self.fgcolor = color
+ self.fgbrush = wx.Brush(color)
+ self.fgpen = wx.Pen(color, 1, wx.SOLID)
+ def GetLineColour(self):
+ return self.linecolor
+ def SetLineColour(self, color):
+ self.linecolor = color
+ self.linebrush = wx.Brush(color)
+ self.linepen = wx.Pen( color, 1, wx.SOLID)
+ def GetForegroundPen(self):
+ return self.fgpen
+ def GetBackgroundPen(self):
+ return self.bgpen
+ def GetTextPen(self):
+ return self.textpen
+ def GetForegroundBrush(self):
+ return self.fgbrush
+ def GetBackgroundBrush(self):
+ return self.bgbrush
+ def GetTextBrush(self):
+ return self.textbrush
+ def GetLinePen(self):
+ return self.linepen
+ def GetLineBrush(self):
+ return self.linebrush
+ def OnMouse(self, evt):
+ if evt.LeftDClick():
+ x, y = self.tree.CalcUnscrolledPosition(evt.GetX(), evt.GetY())
+ for item in self.rectangles:
+ if item[1].Contains((x,y)):
+ self.tree.Edit(item[0].data)
+ self.tree.OnNodeClick(item[0], evt)
+ return
+ elif evt.ButtonDown():
+ x, y = self.tree.CalcUnscrolledPosition(evt.GetX(), evt.GetY())
+ for item in self.rectangles:
+ if item[1].Contains((x, y)):
+ self.tree.OnNodeClick(item[0], evt)
+ return
+ for item in self.knobs:
+ if item[1].Contains((x, y)):
+ self.tree.OnKnobClick(item[0])
+ return
+ evt.Skip()
+
+
+class TreeModel:
+ """
+ Interface for tree models
+ """
+ def GetRoot(self):
+ raise NotImplementedError
+ def SetRoot(self, root):
+ raise NotImplementedError
+ def GetChildCount(self, node):
+ raise NotImplementedError
+ def GetChildAt(self, node, index):
+ raise NotImplementedError
+ def GetParent(self, node):
+ raise NotImplementedError
+ def AddChild(self, parent, child):
+ if hasattr(self, 'tree') and self.tree:
+ self.tree.NodeAdded(parent, child)
+ def RemoveNode(self, child):
+ if hasattr(self, 'tree') and self.tree:
+ self.tree.NodeRemoved(child)
+ def InsertChild(self, parent, child, index):
+ if hasattr(self, 'tree') and self.tree:
+ self.tree.NodeInserted(parent, child, index)
+ def IsLeaf(self, node):
+ raise NotImplementedError
+
+ def IsEditable(self, node):
+ return False
+
+ def SetEditable(self, node):
+ return False
+
+class NodePainter:
+ """
+ This is the interface expected of a nodepainter.
+ """
+ def __init__(self, painter):
+ self.painter = painter
+ def Paint(self, node, dc, location = None):
+ """
+ location should be provided only to draw in an unusual position
+ (not the node's normal position), otherwise the node's projected x and y
+ coordinates will be used.
+ """
+ raise NotImplementedError
+
+class LinePainter:
+ """
+ The linepainter interface.
+ """
+ def __init__(self, painter):
+ self.painter = painter
+ def Paint(self, parent, child, dc):
+ raise NotImplementedError
+
+class TextConverter:
+ """
+ TextConverter interface.
+ """
+ def __init__(self, painter):
+ self.painter = painter
+ def Convert(node):
+ """
+ Should return a string. The node argument will be an
+ MVCTreeNode.
+ """
+ raise NotImplementedError
+
+
+class BasicTreeModel(TreeModel):
+ """
+ A very simple treemodel implementation, but flexible enough for many needs.
+ """
+ def __init__(self):
+ self.children = {}
+ self.parents = {}
+ self.root = None
+ def GetRoot(self):
+ return self.root
+ def SetRoot(self, root):
+ self.root = root
+ def GetChildCount(self, node):
+ if self.children.has_key(node):
+ return len(self.children[node])
+ else:
+ return 0
+ def GetChildAt(self, node, index):
+ return self.children[node][index]
+
+ def GetParent(self, node):
+ return self.parents[node]
+
+ def AddChild(self, parent, child):
+ self.parents[child]=parent
+ if not self.children.has_key(parent):
+ self.children[parent]=[]
+ self.children[parent].append(child)
+ TreeModel.AddChild(self, parent, child)
+ return child
+
+ def RemoveNode(self, node):
+ parent = self.parents[node]
+ del self.parents[node]
+ self.children[parent].remove(node)
+ TreeModel.RemoveNode(self, node)
+
+ def InsertChild(self, parent, child, index):
+ self.parents[child]=parent
+ if not self.children.has_key(parent):
+ self.children[parent]=[]
+ self.children[parent].insert(child, index)
+ TreeModel.InsertChild(self, parent, child, index)
+ return child
+
+ def IsLeaf(self, node):
+ return not self.children.has_key(node)
+
+ def IsEditable(self, node):
+ return False
+
+ def SetEditable(self, node, bool):
+ return False
+
+
+class FileEditor(Editor):
+ def Edit(self, node):
+ treenode = self.tree.nodemap[node]
+ self.editcomp = wxTextCtrl(self.tree, -1)
+ for rect in self.tree.painter.rectangles:
+ if rect[0] == treenode:
+ self.editcomp.SetPosition((rect[1][0], rect[1][1]))
+ break
+ self.editcomp.SetValue(node.fileName)
+ self.editcomp.SetSelection(0, len(node.fileName))
+ self.editcomp.SetFocus()
+ self.treenode = treenode
+# self.editcomp.Bind(wx.EVT_KEY_DOWN, self._key)
+ self.editcomp.Bind(wx.EVT_KEY_UP, self._key)
+ self.editcomp.Bind(wx.EVT_LEFT_DOWN, self._mdown)
+ self.editcomp.CaptureMouse()
+
+ def CanEdit(self, node):
+ return isinstance(node, FileWrapper)
+
+ def EndEdit(self, commit):
+ if not self.tree._EditEnding(self.treenode.data):
+ return
+ if commit:
+ node = self.treenode.data
+ try:
+ os.rename(node.path + os.sep + node.fileName, node.path + os.sep + self.editcomp.GetValue())
+ node.fileName = self.editcomp.GetValue()
+ except:
+ traceback.print_exc()
+ self.editcomp.ReleaseMouse()
+ self.editcomp.Destroy()
+ del self.editcomp
+ self.tree.Refresh()
+
+
+ def _key(self, evt):
+ if evt.KeyCode() == wx.WXK_RETURN:
+ self.EndEdit(True)
+ elif evt.KeyCode() == wx.WXK_ESCAPE:
+ self.EndEdit(False)
+ else:
+ evt.Skip()
+
+ def _mdown(self, evt):
+ if evt.IsButton():
+ x, y = evt.GetPosition()
+ w, h = self.editcomp.GetSize()
+ if x < 0 or y < 0 or x > w or y > h:
+ self.EndEdit(False)
+
+
+class FileWrapper:
+ """
+ Node class for FSTreeModel.
+ """
+ def __init__(self, path, fileName):
+ self.path = path
+ self.fileName = fileName
+
+ def __str__(self):
+ return self.fileName
+
+class FSTreeModel(BasicTreeModel):
+ """
+ This treemodel models the filesystem starting from a given path.
+ """
+ def __init__(self, path):
+ BasicTreeModel.__init__(self)
+ fw = FileWrapper(path, path.split(os.sep)[-1])
+ self._Build(path, fw)
+ self.SetRoot(fw)
+ self._editable = True
+ def _Build(self, path, fileWrapper):
+ for name in os.listdir(path):
+ fw = FileWrapper(path, name)
+ self.AddChild(fileWrapper, fw)
+ childName = path + os.sep + name
+ if os.path.isdir(childName):
+ self._Build(childName, fw)
+
+ def IsEditable(self, node):
+ return self._editable
+
+ def SetEditable(self, node, bool):
+ self._editable = bool
+
+class LateFSTreeModel(FSTreeModel):
+ """
+ This treemodel models the filesystem starting from a given path.
+ It retrieves the directory list as requested.
+ """
+ def __init__(self, path):
+ BasicTreeModel.__init__(self)
+ name = path.split(os.sep)[-1]
+ pathpart = path[:-len(name)]
+ fw = FileWrapper(pathpart, name)
+ self._Build(path, fw)
+ self.SetRoot(fw)
+ self._editable = True
+ self.children = {}
+ self.parents = {}
+ def _Build(self, path, parent):
+ ppath = parent.path + os.sep + parent.fileName
+ if not os.path.isdir(ppath):
+ return
+ for name in os.listdir(ppath):
+ fw = FileWrapper(ppath, name)
+ self.AddChild(parent, fw)
+ def GetChildCount(self, node):
+ if self.children.has_key(node):
+ return FSTreeModel.GetChildCount(self, node)
+ else:
+ self._Build(node.path, node)
+ return FSTreeModel.GetChildCount(self, node)
+
+ def IsLeaf(self, node):
+ return not os.path.isdir(node.path + os.sep + node.fileName)
+
+class StrTextConverter(TextConverter):
+ def Convert(self, node):
+ return str(node.data)
+
+class NullTransform(Transform):
+ def GetSize(self):
+ return tuple(self.size)
+
+ def Transform(self, node, offset, rotation):
+ self.size = [0,0]
+ list = self.tree.GetLayoutEngine().GetNodeList()
+ for node in list:
+ node.projx = node.x + offset[0]
+ node.projy = node.y + offset[1]
+ if node.projx > self.size[0]:
+ self.size[0] = node.projx
+ if node.projy > self.size[1]:
+ self.size[1] = node.projy
+
+class Rect:
+ def __init__(self, x, y, width, height):
+ self.x = x
+ self.y = y
+ self.width = width
+ self.height = height
+ def __getitem__(self, index):
+ return (self.x, self.y, self.width, self.height)[index]
+
+ def __setitem__(self, index, value):
+ name = ['x', 'y', 'width', 'height'][index]
+ setattr(self, name, value)
+
+ def Contains(self, other):
+ if type(other) == type(()):
+ other = Rect(other[0], other[1], 0, 0)
+ if other.x >= self.x:
+ if other.y >= self.y:
+ if other.width + other.x <= self.width + self.x:
+ if other.height + other.y <= self.height + self.y:
+ return True
+ return False
+
+ def __str__(self):
+ return "Rect: " + str([self.x, self.y, self.width, self.height])
+
+class TreeLayout(LayoutEngine):
+ def SetHeight(self, num):
+ self.NODE_HEIGHT = num
+
+ def __init__(self, tree):
+ LayoutEngine.__init__(self, tree)
+ self.NODE_STEP = 20
+ self.NODE_HEIGHT = 20
+ self.nodelist = []
+
+ def Layout(self, node):
+ self.nodelist = []
+ self.NODE_HEIGHT = self.tree.GetFont().GetPointSize() * 2
+ self.layoutwalk(node)
+
+ def GetNodeList(self):
+ return self.nodelist
+
+ def layoutwalk(self, node):
+ if node == self.tree.currentRoot:
+ node.level = 1
+ self.lastY = (-self.NODE_HEIGHT)
+ node.x = self.NODE_STEP * node.level
+ node.y = self.lastY + self.NODE_HEIGHT
+ self.lastY = node.y
+ self.nodelist.append(node)
+ if node.expanded:
+ for kid in node.kids:
+ kid.level = node.level + 1
+ self.layoutwalk(kid)
+
+class TreePainter(Painter):
+ """
+ The default painter class. Uses double-buffering, delegates the painting of nodes and
+ lines to helper classes deriving from NodePainter and LinePainter.
+ """
+ def __init__(self, tree, nodePainter = None, linePainter = None, textConverter = None):
+ Painter.__init__(self, tree)
+ if not nodePainter:
+ nodePainter = TreeNodePainter(self)
+ self.nodePainter = nodePainter
+ if not linePainter:
+ linePainter = TreeLinePainter(self)
+ self.linePainter = linePainter
+ if not textConverter:
+ textConverter = StrTextConverter(self)
+ self.textConverter = textConverter
+ self.charWidths = []
+
+ def Paint(self, dc, node, doubleBuffered=1, paintBackground=1):
+ if not self.charWidths:
+ self.charWidths = []
+ for i in range(25):
+ self.charWidths.append(dc.GetTextExtent("D")[0] * i)
+ self.charHeight = dc.GetTextExtent("D")[1]
+ self.textpen = wx.Pen(self.GetTextColour(), 1, wx.SOLID)
+ self.fgpen = wx.Pen(self.GetForegroundColour(), 1, wx.SOLID)
+ self.bgpen = wx.Pen(self.GetBackgroundColour(), 1, wx.SOLID)
+ self.linepen = wx.Pen(self.GetLineColour(), 1, wx.SOLID)
+ self.dashpen = wx.Pen(self.GetLineColour(), 1, wx.DOT)
+ self.textbrush = wx.Brush(self.GetTextColour(), wx.SOLID)
+ self.fgbrush = wx.Brush(self.GetForegroundColour(), wx.SOLID)
+ self.bgbrush = wx.Brush(self.GetBackgroundColour(), wx.SOLID)
+ self.linebrush = wx.Pen(self.GetLineColour(), 1, wx.SOLID)
+ treesize = self.tree.GetSize()
+ size = self.tree.transform.GetSize()
+ size = (max(treesize.width, size[0]+50), max(treesize.height, size[1]+50))
+ dc.BeginDrawing()
+ if doubleBuffered:
+ mem_dc = wx.MemoryDC()
+ if not self.GetBuffer():
+ self.knobs = []
+ self.rectangles = []
+ self.bmp = wx.EmptyBitmap(size[0], size[1])
+ mem_dc.SelectObject(self.GetBuffer())
+ mem_dc.SetPen(self.GetBackgroundPen())
+ mem_dc.SetBrush(self.GetBackgroundBrush())
+ mem_dc.DrawRectangle(0, 0, size[0], size[1])
+ mem_dc.SetFont(self.tree.GetFont())
+ self.paintWalk(node, mem_dc)
+ else:
+ mem_dc.SelectObject(self.GetBuffer())
+ xstart, ystart = self.tree.CalcUnscrolledPosition(0,0)
+ size = self.tree.GetClientSizeTuple()
+ dc.Blit(xstart, ystart, size[0], size[1], mem_dc, xstart, ystart)
+ else:
+ if node == self.tree.currentRoot:
+ self.knobs = []
+ self.rectangles = []
+ dc.SetPen(self.GetBackgroundPen())
+ dc.SetBrush(self.GetBackgroundBrush())
+ dc.SetFont(self.tree.GetFont())
+ if paintBackground:
+ dc.DrawRectangle(0, 0, size[0], size[1])
+ if node:
+ #Call with not paintBackground because if we are told not to paint the
+ #whole background, we have to paint in parts to undo selection coloring.
+ pb = paintBackground
+ self.paintWalk(node, dc, not pb)
+ dc.EndDrawing()
+
+ def GetDashPen(self):
+ return self.dashpen
+
+ def SetLinePen(self, pen):
+ Painter.SetLinePen(self, pen)
+ self.dashpen = wx.Pen(pen.GetColour(), 1, wx.DOT)
+
+ def paintWalk(self, node, dc, paintRects=0):
+ self.linePainter.Paint(node.parent, node, dc)
+ self.nodePainter.Paint(node, dc, drawRects = paintRects)
+ if node.expanded:
+ for kid in node.kids:
+ if not self.paintWalk(kid, dc, paintRects):
+ return False
+ for kid in node.kids:
+ px = (kid.projx - self.tree.layout.NODE_STEP) + 5
+ py = kid.projy + kid.height/2
+ if (not self.tree.model.IsLeaf(kid.data)) or ((kid.expanded or self.tree._assumeChildren) and len(kid.kids)):
+ dc.SetPen(self.linepen)
+ dc.SetBrush(self.bgbrush)
+ dc.DrawRectangle(px -4, py-4, 9, 9)
+ self.knobs.append( (kid, Rect(px -4, py -4, 9, 9)) )
+ dc.SetPen(self.textpen)
+ if not kid.expanded:
+ dc.DrawLine(px, py -2, px, py + 3)
+ dc.DrawLine(px -2, py, px + 3, py)
+ if node == self.tree.currentRoot:
+ px = (node.projx - self.tree.layout.NODE_STEP) + 5
+ py = node.projy + node.height/2
+ dc.SetPen(self.linepen)
+ dc.SetBrush(self.bgbrush)
+ dc.DrawRectangle(px -4, py-4, 9, 9)
+ self.knobs.append( (node, Rect(px -4, py -4, 9, 9)) )
+ dc.SetPen(self.textpen)
+ if not node.expanded:
+ dc.DrawLine(px, py -2, px, py + 3)
+ dc.DrawLine(px -2, py, px + 3, py)
+ return True
+
+ def OnMouse(self, evt):
+ Painter.OnMouse(self, evt)
+
+class TreeNodePainter(NodePainter):
+ def Paint(self, node, dc, location = None, drawRects = 0):
+ text = self.painter.textConverter.Convert(node)
+ extent = dc.GetTextExtent(text)
+ node.width = extent[0]
+ node.height = extent[1]
+ if node.selected:
+ dc.SetPen(self.painter.GetLinePen())
+ dc.SetBrush(self.painter.GetForegroundBrush())
+ dc.SetTextForeground(wx.NamedColour("WHITE"))
+ dc.DrawRectangle(node.projx -1, node.projy -1, node.width + 3, node.height + 3)
+ else:
+ if drawRects:
+ dc.SetBrush(self.painter.GetBackgroundBrush())
+ dc.SetPen(self.painter.GetBackgroundPen())
+ dc.DrawRectangle(node.projx -1, node.projy -1, node.width + 3, node.height + 3)
+ dc.SetTextForeground(self.painter.GetTextColour())
+ dc.DrawText(text, node.projx, node.projy)
+ self.painter.rectangles.append((node, Rect(node.projx, node.projy, node.width, node.height)))
+
+class TreeLinePainter(LinePainter):
+ def Paint(self, parent, child, dc):
+ dc.SetPen(self.painter.GetDashPen())
+ px = py = cx = cy = 0
+ if parent is None or child == self.painter.tree.currentRoot:
+ px = (child.projx - self.painter.tree.layout.NODE_STEP) + 5
+ py = child.projy + self.painter.tree.layout.NODE_HEIGHT/2 -2
+ cx = child.projx
+ cy = py
+ dc.DrawLine(px, py, cx, cy)
+ else:
+ px = parent.projx + 5
+ py = parent.projy + parent.height
+ cx = child.projx -5
+ cy = child.projy + self.painter.tree.layout.NODE_HEIGHT/2 -3
+ dc.DrawLine(px, py, px, cy)
+ dc.DrawLine(px, cy, cx, cy)
+
+#>> Event defs
+wxEVT_MVCTREE_BEGIN_EDIT = wx.NewEventType() #Start editing. Vetoable.
+wxEVT_MVCTREE_END_EDIT = wx.NewEventType() #Stop editing. Vetoable.
+wxEVT_MVCTREE_DELETE_ITEM = wx.NewEventType() #Item removed from model.
+wxEVT_MVCTREE_ITEM_EXPANDED = wx.NewEventType()
+wxEVT_MVCTREE_ITEM_EXPANDING = wx.NewEventType()
+wxEVT_MVCTREE_ITEM_COLLAPSED = wx.NewEventType()
+wxEVT_MVCTREE_ITEM_COLLAPSING = wx.NewEventType()
+wxEVT_MVCTREE_SEL_CHANGED = wx.NewEventType()
+wxEVT_MVCTREE_SEL_CHANGING = wx.NewEventType() #Vetoable.
+wxEVT_MVCTREE_KEY_DOWN = wx.NewEventType()
+wxEVT_MVCTREE_ADD_ITEM = wx.NewEventType() #Item added to model.
+
+EVT_MVCTREE_SEL_CHANGED = wx.PyEventBinder(wxEVT_MVCTREE_SEL_CHANGED, 1)
+EVT_MVCTREE_SEL_CHANGING = wx.PyEventBinder(wxEVT_MVCTREE_SEL_CHANGING, 1)
+EVT_MVCTREE_ITEM_EXPANDED = wx.PyEventBinder(wxEVT_MVCTREE_ITEM_EXPANDED, 1)
+EVT_MVCTREE_ITEM_EXPANDING = wx.PyEventBinder(wxEVT_MVCTREE_ITEM_EXPANDING, 1)
+EVT_MVCTREE_ITEM_COLLAPSED = wx.PyEventBinder(wxEVT_MVCTREE_ITEM_COLLAPSED, 1)
+EVT_MVCTREE_ITEM_COLLAPSING = wx.PyEventBinder(wxEVT_MVCTREE_ITEM_COLLAPSING, 1)
+EVT_MVCTREE_ADD_ITEM = wx.PyEventBinder(wxEVT_MVCTREE_ADD_ITEM, 1)
+EVT_MVCTREE_DELETE_ITEM = wx.PyEventBinder(wxEVT_MVCTREE_DELETE_ITEM, 1)
+EVT_MVCTREE_KEY_DOWN = wx.PyEventBinder(wxEVT_MVCTREE_KEY_DOWN, 1)
+
+class MVCTreeEvent(wx.PyCommandEvent):
+ def __init__(self, type, id, node = None, nodes = None, keyEvent = None, **kwargs):
+ apply(wx.PyCommandEvent.__init__, (self, type, id), kwargs)
+ self.node = node
+ self.nodes = nodes
+ self.keyEvent = keyEvent
+ def GetNode(self):
+ return self.node
+ def GetNodes(self):
+ return self.nodes
+ def getKeyEvent(self):
+ return self.keyEvent
+
+class MVCTreeNotifyEvent(MVCTreeEvent):
+ def __init__(self, type, id, node = None, nodes = None, **kwargs):
+ apply(MVCTreeEvent.__init__, (self, type, id, node, nodes), kwargs)
+ self.notify = wx.NotifyEvent(type, id)
+ def getNotifyEvent(self):
+ return self.notify
+
+class MVCTree(wx.ScrolledWindow):
+ """
+ The main mvc tree class.
+ """
+ def __init__(self, parent, id, model = None, layout = None, transform = None,
+ painter = None, *args, **kwargs):
+ apply(wx.ScrolledWindow.__init__, (self, parent, id), kwargs)
+ self.nodemap = {}
+ self._multiselect = False
+ self._selections = []
+ self._assumeChildren = False
+ self._scrollx = False
+ self._scrolly = False
+ self.doubleBuffered = False
+ self._lastPhysicalSize = self.GetSize()
+ self._editors = []
+ if not model:
+ model = BasicTreeModel()
+ model.SetRoot("Root")
+ self.SetModel(model)
+ if not layout:
+ layout = TreeLayout(self)
+ self.layout = layout
+ if not transform:
+ transform = NullTransform(self)
+ self.transform = transform
+ if not painter:
+ painter = TreePainter(self)
+ self.painter = painter
+ self.SetFont(wx.Font(9, wx.DEFAULT, wx.NORMAL, wx.NORMAL, False))
+ self.Bind(wx.EVT_MOUSE_EVENTS, self.OnMouse)
+ self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
+ self.doubleBuffered = True
+ self.Bind(wx.EVT_SIZE, self.OnSize)
+ self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
+ self.Bind(wx.EVT_PAINT, self.OnPaint)
+
+
+ def Refresh(self):
+ if self.doubleBuffered:
+ self.painter.ClearBuffer()
+ wx.ScrolledWindow.Refresh(self, False)
+
+ def GetPainter(self):
+ return self.painter
+
+ def GetLayoutEngine(self):
+ return self.layout
+
+ def GetTransform(self):
+ return self.transform
+
+ def __repr__(self):
+ return "<MVCTree instance at %s>" % str(hex(id(self)))
+
+ def __str__(self):
+ return self.__repr__()
+
+ def NodeAdded(self, parent, child):
+ e = MVCTreeEvent(wxEVT_MVCTREE_ADD_ITEM, self.GetId(), node = child, nodes = [parent, child])
+ self.GetEventHandler().ProcessEvent(e)
+ self.painter.ClearBuffer()
+
+ def NodeInserted(self, parent, child, index):
+ e = MVCTreeEvent(wxEVT_MVCTREE_ADD_ITEM, self.GetId(), node = child, nodes = [parent, child])
+ self.GetEventHandler().ProcessEvent(e)
+ self.painter.ClearBuffer()
+
+ def NodeRemoved(self, node):
+ e = MVCTreeEvent(wxEVT_MVCTREE_DELETE_ITEM, self.GetId(), node = child, nodes = [parent, child])
+ self.GetEventHandler().ProcessEvent(e)
+ self.painter.ClearBuffer()
+
+ def OnKeyDown(self, evt):
+ e = MVCTreeEvent(wxEVT_MVCTREE_KEY_DOWN, self.GetId(), keyEvent = evt)
+ self.GetEventHandler().ProcessEvent(e)
+
+ def SetFont(self, font):
+ self.painter.SetFont(font)
+ dc = wx.ClientDC(self)
+ dc.SetFont(font)
+ self.layout.SetHeight(dc.GetTextExtent("")[1] + 18)
+ self.painter.ClearBuffer()
+
+ def GetFont(self):
+ return self.painter.GetFont()
+
+ def AddEditor(self, editor):
+ self._editors.append(editor)
+
+ def RemoveEditor(self, editor):
+ self._editors.remove(editor)
+
+ def OnMouse(self, evt):
+ self.painter.OnMouse(evt)
+
+ def OnNodeClick(self, node, mouseEvent):
+ if node.selected and (self.IsMultiSelect() and mouseEvent.ControlDown()):
+ self.RemoveFromSelection(node.data)
+ else:
+ self.AddToSelection(node.data, mouseEvent.ControlDown(), mouseEvent.ShiftDown())
+
+ def OnKnobClick(self, node):
+ self.SetExpanded(node.data, not node.expanded)
+
+ def GetDisplayText(self, node):
+ treenode = self.nodemap[node]
+ return self.painter.textConverter.Convert(treenode)
+
+ def IsDoubleBuffered(self):
+ return self.doubleBuffered
+
+ def SetDoubleBuffered(self, bool):
+ """
+ By default MVCTree is double-buffered.
+ """
+ self.doubleBuffered = bool
+
+ def GetModel(self):
+ return self.model
+
+ def SetModel(self, model):
+ """
+ Completely change the data to be displayed.
+ """
+ self.model = model
+ model.tree = self
+ self.laidOut = 0
+ self.transformed = 0
+ self._selections = []
+ self.layoutRoot = MVCTreeNode()
+ self.layoutRoot.data = self.model.GetRoot()
+ self.layoutRoot.expanded = True
+ self.LoadChildren(self.layoutRoot)
+ self.currentRoot = self.layoutRoot
+ self.offset = [0,0]
+ self.rotation = 0
+ self._scrollset = None
+ self.Refresh()
+
+ def GetCurrentRoot(self):
+ return self.currentRoot
+
+ def LoadChildren(self, layoutNode):
+ if layoutNode.built:
+ return
+ else:
+ self.nodemap[layoutNode.data]=layoutNode
+ for i in range(self.GetModel().GetChildCount(layoutNode.data)):
+ p = MVCTreeNode("RAW", layoutNode, [])
+ layoutNode.Add(p)
+ p.data = self.GetModel().GetChildAt(layoutNode.data, i)
+ self.nodemap[p.data]=p
+ layoutNode.built = True
+ if not self._assumeChildren:
+ for kid in layoutNode.kids:
+ self.LoadChildren(kid)
+
+ def OnEraseBackground(self, evt):
+ pass
+
+ def OnSize(self, evt):
+ size = self.GetSize()
+ self.center = (size.width/2, size.height/2)
+ if self._lastPhysicalSize.width < size.width or self._lastPhysicalSize.height < size.height:
+ self.painter.ClearBuffer()
+ self._lastPhysicalSize = size
+
+ def GetSelection(self):
+ "Returns a tuple of selected nodes."
+ return tuple(self._selections)
+
+ def SetSelection(self, nodeTuple):
+ if type(nodeTuple) != type(()):
+ nodeTuple = (nodeTuple,)
+ e = MVCTreeNotifyEvent(wxEVT_MVCTREE_SEL_CHANGING, self.GetId(), nodeTuple[0], nodes = nodeTuple)
+ self.GetEventHandler().ProcessEvent(e)
+ if not e.notify.IsAllowed():
+ return
+ for node in nodeTuple:
+ treenode = self.nodemap[node]
+ treenode.selected = True
+ for node in self._selections:
+ treenode = self.nodemap[node]
+ node.selected = False
+ self._selections = list(nodeTuple)
+ e = MVCTreeEvent(wxEVT_MVCTREE_SEL_CHANGED, self.GetId(), nodeTuple[0], nodes = nodeTuple)
+ self.GetEventHandler().ProcessEvent(e)
+
+ def IsMultiSelect(self):
+ return self._multiselect
+
+ def SetMultiSelect(self, bool):
+ self._multiselect = bool
+
+ def IsSelected(self, node):
+ return self.nodemap[node].selected
+
+ def Edit(self, node):
+ if not self.model.IsEditable(node):
+ return
+ for ed in self._editors:
+ if ed.CanEdit(node):
+ e = MVCTreeNotifyEvent(wxEVT_MVCTREE_BEGIN_EDIT, self.GetId(), node)
+ self.GetEventHandler().ProcessEvent(e)
+ if not e.notify.IsAllowed():
+ return
+ ed.Edit(node)
+ self._currentEditor = ed
+ break
+
+ def EndEdit(self):
+ if self._currentEditor:
+ self._currentEditor.EndEdit
+ self._currentEditor = None
+
+ def _EditEnding(self, node):
+ e = MVCTreeNotifyEvent(wxEVT_MVCTREE_END_EDIT, self.GetId(), node)
+ self.GetEventHandler().ProcessEvent(e)
+ if not e.notify.IsAllowed():
+ return False
+ self._currentEditor = None
+ return True
+
+
+ def SetExpanded(self, node, bool):
+ treenode = self.nodemap[node]
+ if bool:
+ e = MVCTreeNotifyEvent(wxEVT_MVCTREE_ITEM_EXPANDING, self.GetId(), node)
+ self.GetEventHandler().ProcessEvent(e)
+ if not e.notify.IsAllowed():
+ return
+ if not treenode.built:
+ self.LoadChildren(treenode)
+ else:
+ e = MVCTreeNotifyEvent(wxEVT_MVCTREE_ITEM_COLLAPSING, self.GetId(), node)
+ self.GetEventHandler().ProcessEvent(e)
+ if not e.notify.IsAllowed():
+ return
+ treenode.expanded = bool
+ e = None
+ if treenode.expanded:
+ e = MVCTreeEvent(wxEVT_MVCTREE_ITEM_EXPANDED, self.GetId(), node)
+ else:
+ e = MVCTreeEvent(wxEVT_MVCTREE_ITEM_COLLAPSED, self.GetId(), node)
+ self.GetEventHandler().ProcessEvent(e)
+ self.layout.Layout(self.currentRoot)
+ self.transform.Transform(self.currentRoot, self.offset, self.rotation)
+ self.Refresh()
+
+ def IsExpanded(self, node):
+ return self.nodemap[node].expanded
+
+ def AddToSelection(self, nodeOrTuple, enableMulti = True, shiftMulti = False):
+ nodeTuple = nodeOrTuple
+ if type(nodeOrTuple)!= type(()):
+ nodeTuple = (nodeOrTuple,)
+ e = MVCTreeNotifyEvent(wxEVT_MVCTREE_SEL_CHANGING, self.GetId(), nodeTuple[0], nodes = nodeTuple)
+ self.GetEventHandler().ProcessEvent(e)
+ if not e.notify.IsAllowed():
+ return
+ changeparents = []
+ if not (self.IsMultiSelect() and (enableMulti or shiftMulti)):
+ for node in self._selections:
+ treenode = self.nodemap[node]
+ treenode.selected = False
+ changeparents.append(treenode)
+ node = nodeTuple[0]
+ self._selections = [node]
+ treenode = self.nodemap[node]
+ changeparents.append(treenode)
+ treenode.selected = True
+ else:
+ if shiftMulti:
+ for node in nodeTuple:
+ treenode = self.nodemap[node]
+ oldtreenode = self.nodemap[self._selections[0]]
+ if treenode.parent == oldtreenode.parent:
+ found = 0
+ for kid in oldtreenode.parent.kids:
+ if kid == treenode or kid == oldtreenode:
+ found = not found
+ kid.selected = True
+ self._selections.append(kid.data)
+ changeparents.append(kid)
+ elif found:
+ kid.selected = True
+ self._selections.append(kid.data)
+ changeparents.append(kid)
+ else:
+ for node in nodeTuple:
+ try:
+ self._selections.index(node)
+ except ValueError:
+ self._selections.append(node)
+ treenode = self.nodemap[node]
+ treenode.selected = True
+ changeparents.append(treenode)
+ e = MVCTreeEvent(wxEVT_MVCTREE_SEL_CHANGED, self.GetId(), nodeTuple[0], nodes = nodeTuple)
+ self.GetEventHandler().ProcessEvent(e)
+ dc = wx.ClientDC(self)
+ self.PrepareDC(dc)
+ for node in changeparents:
+ if node:
+ self.painter.Paint(dc, node, doubleBuffered = 0, paintBackground = 0)
+ self.painter.ClearBuffer()
+
+ def RemoveFromSelection(self, nodeTuple):
+ if type(nodeTuple) != type(()):
+ nodeTuple = (nodeTuple,)
+ changeparents = []
+ for node in nodeTuple:
+ self._selections.remove(node)
+ treenode = self.nodemap[node]
+ changeparents.append(treenode)
+ treenode.selected = False
+ e = MVCTreeEvent(wxEVT_MVCTREE_SEL_CHANGED, self.GetId(), node, nodes = nodeTuple)
+ self.GetEventHandler().ProcessEvent(e)
+ dc = wx.ClientDC(self)
+ self.PrepareDC(dc)
+ for node in changeparents:
+ if node:
+ self.painter.Paint(dc, node, doubleBuffered = 0, paintBackground = 0)
+ self.painter.ClearBuffer()
+
+
+ def GetBackgroundColour(self):
+ if hasattr(self, 'painter') and self.painter:
+ return self.painter.GetBackgroundColour()
+ else:
+ return wx.Window.GetBackgroundColour(self)
+ def SetBackgroundColour(self, color):
+ if hasattr(self, 'painter') and self.painter:
+ self.painter.SetBackgroundColour(color)
+ else:
+ wx.Window.SetBackgroundColour(self, color)
+ def GetForegroundColour(self):
+ if hasattr(self, 'painter') and self.painter:
+ return self.painter.GetForegroundColour()
+ else:
+ return wx.Window.GetBackgroundColour(self)
+ def SetForegroundColour(self, color):
+ if hasattr(self, 'painter') and self.painter:
+ self.painter.SetForegroundColour(color)
+ else:
+ wx.Window.SetBackgroundColour(self, color)
+
+ def SetAssumeChildren(self, bool):
+ self._assumeChildren = bool
+
+ def GetAssumeChildren(self):
+ return self._assumeChildren
+
+ def OnPaint(self, evt):
+ """
+ Ensures that the tree has been laid out and transformed, then calls the painter
+ to paint the control.
+ """
+ try:
+ self.EnableScrolling(False, False)
+ if not self.laidOut:
+ self.layout.Layout(self.currentRoot)
+ self.laidOut = True
+ self.transformed = False
+ if not self.transformed:
+ self.transform.Transform(self.currentRoot, self.offset, self.rotation)
+ self.transformed = True
+ tsize = None
+ tsize = list(self.transform.GetSize())
+ tsize[0] = tsize[0] + 50
+ tsize[1] = tsize[1] + 50
+ w, h = self.GetSize()
+ if tsize[0] > w or tsize[1] > h:
+ if not hasattr(self, '_oldsize') or (tsize[0] > self._oldsize[0] or tsize[1] > self._oldsize[1]):
+ self._oldsize = tsize
+ oldstart = self.GetViewStart()
+ self._lastPhysicalSize = self.GetSize()
+ self.SetScrollbars(10, 10, tsize[0]/10, tsize[1]/10)
+ self.Scroll(oldstart[0], oldstart[1])
+ dc = wx.PaintDC(self)
+ self.PrepareDC(dc)
+ dc.SetFont(self.GetFont())
+ self.painter.Paint(dc, self.currentRoot, self.doubleBuffered)
+ except:
+ traceback.print_exc()
+