]> git.saurik.com Git - wxWidgets.git/blob - wxPython/wx/lib/mvctree.py
reduce the number of objects for a quicker startup time
[wxWidgets.git] / wxPython / wx / lib / mvctree.py
1 # 12/09/2003 - Jeff Grimmett (grimmtooth@softhome.net)
2 #
3 # o 2.5 compatability update.
4 # o I'm a little nervous about some of it though.
5 #
6 # 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net)
7 #
8 # o wxTreeModel -> TreeModel
9 # o wxMVCTree -> MVCTree
10 # o wxMVCTreeEvent -> MVCTreeEvent
11 # o wxMVCTreeNotifyEvent -> MVCTreeNotifyEvent
12 #
13
14 """
15 MVCTree is a control which handles hierarchical data. It is constructed
16 in model-view-controller architecture, so the display of that data, and
17 the content of the data can be changed greatly without affecting the other parts.
18
19 MVCTree actually is even more configurable than MVC normally implies, because
20 almost every aspect of it is pluggable:
21 MVCTree - Overall controller, and the window that actually gets placed
22 in the GUI.
23 Painter - Paints the control. The 'view' part of MVC.
24 NodePainter - Paints just the nodes
25 LinePainter - Paints just the lines between the nodes
26 TextConverter - Figures out what text to print for each node
27 Editor - Edits the contents of a node, if the model is editable.
28 LayoutEngine - Determines initial placement of nodes
29 Transform - Adjusts positions of nodes for movement or special effects.
30 TreeModel - Contains the data which the rest of the control acts
31 on. The 'model' part of MVC.
32
33 Author/Maintainer - Bryn Keller <xoltar@starship.python.net>
34
35
36 NOTE: This module is *not* supported in any way. Use it however you
37 wish, but be warned that dealing with any consequences is
38 entirly up to you.
39 --Robin
40 """
41
42 #------------------------------------------------------------------------
43 import os
44 import sys
45 import traceback
46 import warnings
47
48 import wx
49 #------------------------------------------------------------------------
50
51 warningmsg = r"""\
52
53 ################################################\
54 # This module is not supported in any way! |
55 # |
56 # See cource code for wx.lib.mvctree for more |
57 # information. |
58 ################################################/
59
60 """
61
62 warnings.warn(warningmsg, DeprecationWarning, stacklevel=2)
63 #------------------------------------------------------------------------
64
65 class MVCTreeNode:
66 """
67 Used internally by MVCTree to manage its data. Contains information about
68 screen placement, the actual data associated with it, and more. These are
69 the nodes passed to all the other helper parts to do their work with.
70 """
71 def __init__(self, data=None, parent = None, kids = None, x = 0, y = 0):
72 self.x = 0
73 self.y = 0
74 self.projx = 0
75 self.projy = 0
76 self.parent = parent
77 self.kids = kids
78 if self.kids is None:
79 self.kids = []
80 self.data = data
81 self.expanded = False
82 self.selected = False
83 self.built = False
84 self.scale = 0
85
86 def GetChildren(self):
87 return self.kids
88
89 def GetParent(self):
90 return self.parent
91
92 def Remove(self, node):
93 try:
94 self.kids.remove(node)
95 except:
96 pass
97 def Add(self, node):
98 self.kids.append(node)
99 node.SetParent(self)
100
101 def SetParent(self, parent):
102 if self.parent and not (self.parent is parent):
103 self.parent.Remove(self)
104 self.parent = parent
105 def __str__(self):
106 return "Node: " + str(self.data) + " (" + str(self.x) + ", " + str(self.y) + ")"
107 def __repr__(self):
108 return str(self.data)
109 def GetTreeString(self, tabs=0):
110 s = tabs * '\t' + str(self) + '\n'
111 for kid in self.kids:
112 s = s + kid.GetTreeString(tabs + 1)
113 return s
114
115
116 class Editor:
117 def __init__(self, tree):
118 self.tree = tree
119 def Edit(self, node):
120 raise NotImplementedError
121 def EndEdit(self, node, commit):
122 raise NotImplementedError
123 def CanEdit(self, node):
124 raise NotImplementedError
125
126 class LayoutEngine:
127 """
128 Interface for layout engines.
129 """
130 def __init__(self, tree):
131 self.tree = tree
132 def Layout(self, node):
133 raise NotImplementedError
134 def GetNodeList(self):
135 raise NotImplementedError
136
137 class Transform:
138 """
139 Transform interface.
140 """
141 def __init__(self, tree):
142 self.tree = tree
143 def Transform(self, node, offset, rotation):
144 """
145 This method should only change the projx and projy attributes of
146 the node. These represent the position of the node as it should
147 be drawn on screen. Adjusting the x and y attributes can and
148 should cause havoc.
149 """
150 raise NotImplementedError
151
152 def GetSize(self):
153 """
154 Returns the size of the entire tree as laid out and transformed
155 as a tuple
156 """
157 raise NotImplementedError
158
159 class Painter:
160 """
161 This is the interface that MVCTree expects from painters. All painters should
162 be Painter subclasses.
163 """
164 def __init__(self, tree):
165 self.tree = tree
166 self.textcolor = wx.NamedColour("BLACK")
167 self.bgcolor = wx.NamedColour("WHITE")
168 self.fgcolor = wx.NamedColour("BLUE")
169 self.linecolor = wx.NamedColour("GREY")
170 self.font = wx.Font(9, wx.DEFAULT, wx.NORMAL, wx.NORMAL, False)
171 self.bmp = None
172
173 def GetFont(self):
174 return self.font
175
176 def SetFont(self, font):
177 self.font = font
178 self.tree.Refresh()
179 def GetBuffer(self):
180 return self.bmp
181 def ClearBuffer(self):
182 self.bmp = None
183 def Paint(self, dc, node, doubleBuffered=1, paintBackground=1):
184 raise NotImplementedError
185 def GetTextColour(self):
186 return self.textcolor
187 def SetTextColour(self, color):
188 self.textcolor = color
189 self.textbrush = wx.Brush(color)
190 self.textpen = wx.Pen(color, 1, wx.SOLID)
191 def GetBackgroundColour(self):
192 return self.bgcolor
193 def SetBackgroundColour(self, color):
194 self.bgcolor = color
195 self.bgbrush = wx.Brush(color)
196 self.bgpen = wx.Pen(color, 1, wx.SOLID)
197 def GetForegroundColour(self):
198 return self.fgcolor
199 def SetForegroundColour(self, color):
200 self.fgcolor = color
201 self.fgbrush = wx.Brush(color)
202 self.fgpen = wx.Pen(color, 1, wx.SOLID)
203 def GetLineColour(self):
204 return self.linecolor
205 def SetLineColour(self, color):
206 self.linecolor = color
207 self.linebrush = wx.Brush(color)
208 self.linepen = wx.Pen( color, 1, wx.SOLID)
209 def GetForegroundPen(self):
210 return self.fgpen
211 def GetBackgroundPen(self):
212 return self.bgpen
213 def GetTextPen(self):
214 return self.textpen
215 def GetForegroundBrush(self):
216 return self.fgbrush
217 def GetBackgroundBrush(self):
218 return self.bgbrush
219 def GetTextBrush(self):
220 return self.textbrush
221 def GetLinePen(self):
222 return self.linepen
223 def GetLineBrush(self):
224 return self.linebrush
225 def OnMouse(self, evt):
226 if evt.LeftDClick():
227 x, y = self.tree.CalcUnscrolledPosition(evt.GetX(), evt.GetY())
228 for item in self.rectangles:
229 if item[1].Contains((x,y)):
230 self.tree.Edit(item[0].data)
231 self.tree.OnNodeClick(item[0], evt)
232 return
233 elif evt.ButtonDown():
234 x, y = self.tree.CalcUnscrolledPosition(evt.GetX(), evt.GetY())
235 for item in self.rectangles:
236 if item[1].Contains((x, y)):
237 self.tree.OnNodeClick(item[0], evt)
238 return
239 for item in self.knobs:
240 if item[1].Contains((x, y)):
241 self.tree.OnKnobClick(item[0])
242 return
243 evt.Skip()
244
245
246 class TreeModel:
247 """
248 Interface for tree models
249 """
250 def GetRoot(self):
251 raise NotImplementedError
252 def SetRoot(self, root):
253 raise NotImplementedError
254 def GetChildCount(self, node):
255 raise NotImplementedError
256 def GetChildAt(self, node, index):
257 raise NotImplementedError
258 def GetParent(self, node):
259 raise NotImplementedError
260 def AddChild(self, parent, child):
261 if hasattr(self, 'tree') and self.tree:
262 self.tree.NodeAdded(parent, child)
263 def RemoveNode(self, child):
264 if hasattr(self, 'tree') and self.tree:
265 self.tree.NodeRemoved(child)
266 def InsertChild(self, parent, child, index):
267 if hasattr(self, 'tree') and self.tree:
268 self.tree.NodeInserted(parent, child, index)
269 def IsLeaf(self, node):
270 raise NotImplementedError
271
272 def IsEditable(self, node):
273 return False
274
275 def SetEditable(self, node):
276 return False
277
278 class NodePainter:
279 """
280 This is the interface expected of a nodepainter.
281 """
282 def __init__(self, painter):
283 self.painter = painter
284 def Paint(self, node, dc, location = None):
285 """
286 location should be provided only to draw in an unusual position
287 (not the node's normal position), otherwise the node's projected x and y
288 coordinates will be used.
289 """
290 raise NotImplementedError
291
292 class LinePainter:
293 """
294 The linepainter interface.
295 """
296 def __init__(self, painter):
297 self.painter = painter
298 def Paint(self, parent, child, dc):
299 raise NotImplementedError
300
301 class TextConverter:
302 """
303 TextConverter interface.
304 """
305 def __init__(self, painter):
306 self.painter = painter
307 def Convert(node):
308 """
309 Should return a string. The node argument will be an
310 MVCTreeNode.
311 """
312 raise NotImplementedError
313
314
315 class BasicTreeModel(TreeModel):
316 """
317 A very simple treemodel implementation, but flexible enough for many needs.
318 """
319 def __init__(self):
320 self.children = {}
321 self.parents = {}
322 self.root = None
323 def GetRoot(self):
324 return self.root
325 def SetRoot(self, root):
326 self.root = root
327 def GetChildCount(self, node):
328 if self.children.has_key(node):
329 return len(self.children[node])
330 else:
331 return 0
332 def GetChildAt(self, node, index):
333 return self.children[node][index]
334
335 def GetParent(self, node):
336 return self.parents[node]
337
338 def AddChild(self, parent, child):
339 self.parents[child]=parent
340 if not self.children.has_key(parent):
341 self.children[parent]=[]
342 self.children[parent].append(child)
343 TreeModel.AddChild(self, parent, child)
344 return child
345
346 def RemoveNode(self, node):
347 parent = self.parents[node]
348 del self.parents[node]
349 self.children[parent].remove(node)
350 TreeModel.RemoveNode(self, node)
351
352 def InsertChild(self, parent, child, index):
353 self.parents[child]=parent
354 if not self.children.has_key(parent):
355 self.children[parent]=[]
356 self.children[parent].insert(child, index)
357 TreeModel.InsertChild(self, parent, child, index)
358 return child
359
360 def IsLeaf(self, node):
361 return not self.children.has_key(node)
362
363 def IsEditable(self, node):
364 return False
365
366 def SetEditable(self, node, bool):
367 return False
368
369
370 class FileEditor(Editor):
371 def Edit(self, node):
372 treenode = self.tree.nodemap[node]
373 self.editcomp = wxTextCtrl(self.tree, -1)
374 for rect in self.tree.painter.rectangles:
375 if rect[0] == treenode:
376 self.editcomp.SetPosition((rect[1][0], rect[1][1]))
377 break
378 self.editcomp.SetValue(node.fileName)
379 self.editcomp.SetSelection(0, len(node.fileName))
380 self.editcomp.SetFocus()
381 self.treenode = treenode
382 # self.editcomp.Bind(wx.EVT_KEY_DOWN, self._key)
383 self.editcomp.Bind(wx.EVT_KEY_UP, self._key)
384 self.editcomp.Bind(wx.EVT_LEFT_DOWN, self._mdown)
385 self.editcomp.CaptureMouse()
386
387 def CanEdit(self, node):
388 return isinstance(node, FileWrapper)
389
390 def EndEdit(self, commit):
391 if not self.tree._EditEnding(self.treenode.data):
392 return
393 if commit:
394 node = self.treenode.data
395 try:
396 os.rename(node.path + os.sep + node.fileName, node.path + os.sep + self.editcomp.GetValue())
397 node.fileName = self.editcomp.GetValue()
398 except:
399 traceback.print_exc()
400 self.editcomp.ReleaseMouse()
401 self.editcomp.Destroy()
402 del self.editcomp
403 self.tree.Refresh()
404
405
406 def _key(self, evt):
407 if evt.GetKeyCode() == wx.WXK_RETURN:
408 self.EndEdit(True)
409 elif evt.GetKeyCode() == wx.WXK_ESCAPE:
410 self.EndEdit(False)
411 else:
412 evt.Skip()
413
414 def _mdown(self, evt):
415 if evt.IsButton():
416 x, y = evt.GetPosition()
417 w, h = self.editcomp.GetSize()
418 if x < 0 or y < 0 or x > w or y > h:
419 self.EndEdit(False)
420
421
422 class FileWrapper:
423 """
424 Node class for FSTreeModel.
425 """
426 def __init__(self, path, fileName):
427 self.path = path
428 self.fileName = fileName
429
430 def __str__(self):
431 return self.fileName
432
433 class FSTreeModel(BasicTreeModel):
434 """
435 This treemodel models the filesystem starting from a given path.
436 """
437 def __init__(self, path):
438 BasicTreeModel.__init__(self)
439 fw = FileWrapper(path, path.split(os.sep)[-1])
440 self._Build(path, fw)
441 self.SetRoot(fw)
442 self._editable = True
443 def _Build(self, path, fileWrapper):
444 for name in os.listdir(path):
445 fw = FileWrapper(path, name)
446 self.AddChild(fileWrapper, fw)
447 childName = path + os.sep + name
448 if os.path.isdir(childName):
449 self._Build(childName, fw)
450
451 def IsEditable(self, node):
452 return self._editable
453
454 def SetEditable(self, node, bool):
455 self._editable = bool
456
457 class LateFSTreeModel(FSTreeModel):
458 """
459 This treemodel models the filesystem starting from a given path.
460 It retrieves the directory list as requested.
461 """
462 def __init__(self, path):
463 BasicTreeModel.__init__(self)
464 name = path.split(os.sep)[-1]
465 pathpart = path[:-len(name)]
466 fw = FileWrapper(pathpart, name)
467 self._Build(path, fw)
468 self.SetRoot(fw)
469 self._editable = True
470 self.children = {}
471 self.parents = {}
472 def _Build(self, path, parent):
473 ppath = parent.path + os.sep + parent.fileName
474 if not os.path.isdir(ppath):
475 return
476 for name in os.listdir(ppath):
477 fw = FileWrapper(ppath, name)
478 self.AddChild(parent, fw)
479 def GetChildCount(self, node):
480 if self.children.has_key(node):
481 return FSTreeModel.GetChildCount(self, node)
482 else:
483 self._Build(node.path, node)
484 return FSTreeModel.GetChildCount(self, node)
485
486 def IsLeaf(self, node):
487 return not os.path.isdir(node.path + os.sep + node.fileName)
488
489 class StrTextConverter(TextConverter):
490 def Convert(self, node):
491 return str(node.data)
492
493 class NullTransform(Transform):
494 def GetSize(self):
495 return tuple(self.size)
496
497 def Transform(self, node, offset, rotation):
498 self.size = [0,0]
499 list = self.tree.GetLayoutEngine().GetNodeList()
500 for node in list:
501 node.projx = node.x + offset[0]
502 node.projy = node.y + offset[1]
503 if node.projx > self.size[0]:
504 self.size[0] = node.projx
505 if node.projy > self.size[1]:
506 self.size[1] = node.projy
507
508 class Rect:
509 def __init__(self, x, y, width, height):
510 self.x = x
511 self.y = y
512 self.width = width
513 self.height = height
514 def __getitem__(self, index):
515 return (self.x, self.y, self.width, self.height)[index]
516
517 def __setitem__(self, index, value):
518 name = ['x', 'y', 'width', 'height'][index]
519 setattr(self, name, value)
520
521 def Contains(self, other):
522 if type(other) == type(()):
523 other = Rect(other[0], other[1], 0, 0)
524 if other.x >= self.x:
525 if other.y >= self.y:
526 if other.width + other.x <= self.width + self.x:
527 if other.height + other.y <= self.height + self.y:
528 return True
529 return False
530
531 def __str__(self):
532 return "Rect: " + str([self.x, self.y, self.width, self.height])
533
534 class TreeLayout(LayoutEngine):
535 def SetHeight(self, num):
536 self.NODE_HEIGHT = num
537
538 def __init__(self, tree):
539 LayoutEngine.__init__(self, tree)
540 self.NODE_STEP = 20
541 self.NODE_HEIGHT = 20
542 self.nodelist = []
543
544 def Layout(self, node):
545 self.nodelist = []
546 self.NODE_HEIGHT = self.tree.GetFont().GetPointSize() * 2
547 self.layoutwalk(node)
548
549 def GetNodeList(self):
550 return self.nodelist
551
552 def layoutwalk(self, node):
553 if node == self.tree.currentRoot:
554 node.level = 1
555 self.lastY = (-self.NODE_HEIGHT)
556 node.x = self.NODE_STEP * node.level
557 node.y = self.lastY + self.NODE_HEIGHT
558 self.lastY = node.y
559 self.nodelist.append(node)
560 if node.expanded:
561 for kid in node.kids:
562 kid.level = node.level + 1
563 self.layoutwalk(kid)
564
565 class TreePainter(Painter):
566 """
567 The default painter class. Uses double-buffering, delegates the painting of nodes and
568 lines to helper classes deriving from NodePainter and LinePainter.
569 """
570 def __init__(self, tree, nodePainter = None, linePainter = None, textConverter = None):
571 Painter.__init__(self, tree)
572 if not nodePainter:
573 nodePainter = TreeNodePainter(self)
574 self.nodePainter = nodePainter
575 if not linePainter:
576 linePainter = TreeLinePainter(self)
577 self.linePainter = linePainter
578 if not textConverter:
579 textConverter = StrTextConverter(self)
580 self.textConverter = textConverter
581 self.charWidths = []
582
583 def Paint(self, dc, node, doubleBuffered=1, paintBackground=1):
584 if not self.charWidths:
585 self.charWidths = []
586 for i in range(25):
587 self.charWidths.append(dc.GetTextExtent("D")[0] * i)
588 self.charHeight = dc.GetTextExtent("D")[1]
589 self.textpen = wx.Pen(self.GetTextColour(), 1, wx.SOLID)
590 self.fgpen = wx.Pen(self.GetForegroundColour(), 1, wx.SOLID)
591 self.bgpen = wx.Pen(self.GetBackgroundColour(), 1, wx.SOLID)
592 self.linepen = wx.Pen(self.GetLineColour(), 1, wx.SOLID)
593 self.dashpen = wx.Pen(self.GetLineColour(), 1, wx.DOT)
594 self.textbrush = wx.Brush(self.GetTextColour(), wx.SOLID)
595 self.fgbrush = wx.Brush(self.GetForegroundColour(), wx.SOLID)
596 self.bgbrush = wx.Brush(self.GetBackgroundColour(), wx.SOLID)
597 self.linebrush = wx.Pen(self.GetLineColour(), 1, wx.SOLID)
598 treesize = self.tree.GetSize()
599 size = self.tree.transform.GetSize()
600 size = (max(treesize.width, size[0]+50), max(treesize.height, size[1]+50))
601 dc.BeginDrawing()
602 if doubleBuffered:
603 mem_dc = wx.MemoryDC()
604 if not self.GetBuffer():
605 self.knobs = []
606 self.rectangles = []
607 self.bmp = wx.EmptyBitmap(size[0], size[1])
608 mem_dc.SelectObject(self.GetBuffer())
609 mem_dc.SetPen(self.GetBackgroundPen())
610 mem_dc.SetBrush(self.GetBackgroundBrush())
611 mem_dc.DrawRectangle(0, 0, size[0], size[1])
612 mem_dc.SetFont(self.tree.GetFont())
613 self.paintWalk(node, mem_dc)
614 else:
615 mem_dc.SelectObject(self.GetBuffer())
616 xstart, ystart = self.tree.CalcUnscrolledPosition(0,0)
617 size = self.tree.GetClientSizeTuple()
618 dc.Blit(xstart, ystart, size[0], size[1], mem_dc, xstart, ystart)
619 else:
620 if node == self.tree.currentRoot:
621 self.knobs = []
622 self.rectangles = []
623 dc.SetPen(self.GetBackgroundPen())
624 dc.SetBrush(self.GetBackgroundBrush())
625 dc.SetFont(self.tree.GetFont())
626 if paintBackground:
627 dc.DrawRectangle(0, 0, size[0], size[1])
628 if node:
629 #Call with not paintBackground because if we are told not to paint the
630 #whole background, we have to paint in parts to undo selection coloring.
631 pb = paintBackground
632 self.paintWalk(node, dc, not pb)
633 dc.EndDrawing()
634
635 def GetDashPen(self):
636 return self.dashpen
637
638 def SetLinePen(self, pen):
639 Painter.SetLinePen(self, pen)
640 self.dashpen = wx.Pen(pen.GetColour(), 1, wx.DOT)
641
642 def paintWalk(self, node, dc, paintRects=0):
643 self.linePainter.Paint(node.parent, node, dc)
644 self.nodePainter.Paint(node, dc, drawRects = paintRects)
645 if node.expanded:
646 for kid in node.kids:
647 if not self.paintWalk(kid, dc, paintRects):
648 return False
649 for kid in node.kids:
650 px = (kid.projx - self.tree.layout.NODE_STEP) + 5
651 py = kid.projy + kid.height/2
652 if (not self.tree.model.IsLeaf(kid.data)) or ((kid.expanded or self.tree._assumeChildren) and len(kid.kids)):
653 dc.SetPen(self.linepen)
654 dc.SetBrush(self.bgbrush)
655 dc.DrawRectangle(px -4, py-4, 9, 9)
656 self.knobs.append( (kid, Rect(px -4, py -4, 9, 9)) )
657 dc.SetPen(self.textpen)
658 if not kid.expanded:
659 dc.DrawLine(px, py -2, px, py + 3)
660 dc.DrawLine(px -2, py, px + 3, py)
661 if node == self.tree.currentRoot:
662 px = (node.projx - self.tree.layout.NODE_STEP) + 5
663 py = node.projy + node.height/2
664 dc.SetPen(self.linepen)
665 dc.SetBrush(self.bgbrush)
666 dc.DrawRectangle(px -4, py-4, 9, 9)
667 self.knobs.append( (node, Rect(px -4, py -4, 9, 9)) )
668 dc.SetPen(self.textpen)
669 if not node.expanded:
670 dc.DrawLine(px, py -2, px, py + 3)
671 dc.DrawLine(px -2, py, px + 3, py)
672 return True
673
674 def OnMouse(self, evt):
675 Painter.OnMouse(self, evt)
676
677 class TreeNodePainter(NodePainter):
678 def Paint(self, node, dc, location = None, drawRects = 0):
679 text = self.painter.textConverter.Convert(node)
680 extent = dc.GetTextExtent(text)
681 node.width = extent[0]
682 node.height = extent[1]
683 if node.selected:
684 dc.SetPen(self.painter.GetLinePen())
685 dc.SetBrush(self.painter.GetForegroundBrush())
686 dc.SetTextForeground(wx.NamedColour("WHITE"))
687 dc.DrawRectangle(node.projx -1, node.projy -1, node.width + 3, node.height + 3)
688 else:
689 if drawRects:
690 dc.SetBrush(self.painter.GetBackgroundBrush())
691 dc.SetPen(self.painter.GetBackgroundPen())
692 dc.DrawRectangle(node.projx -1, node.projy -1, node.width + 3, node.height + 3)
693 dc.SetTextForeground(self.painter.GetTextColour())
694 dc.DrawText(text, node.projx, node.projy)
695 self.painter.rectangles.append((node, Rect(node.projx, node.projy, node.width, node.height)))
696
697 class TreeLinePainter(LinePainter):
698 def Paint(self, parent, child, dc):
699 dc.SetPen(self.painter.GetDashPen())
700 px = py = cx = cy = 0
701 if parent is None or child == self.painter.tree.currentRoot:
702 px = (child.projx - self.painter.tree.layout.NODE_STEP) + 5
703 py = child.projy + self.painter.tree.layout.NODE_HEIGHT/2 -2
704 cx = child.projx
705 cy = py
706 dc.DrawLine(px, py, cx, cy)
707 else:
708 px = parent.projx + 5
709 py = parent.projy + parent.height
710 cx = child.projx -5
711 cy = child.projy + self.painter.tree.layout.NODE_HEIGHT/2 -3
712 dc.DrawLine(px, py, px, cy)
713 dc.DrawLine(px, cy, cx, cy)
714
715 #>> Event defs
716 wxEVT_MVCTREE_BEGIN_EDIT = wx.NewEventType() #Start editing. Vetoable.
717 wxEVT_MVCTREE_END_EDIT = wx.NewEventType() #Stop editing. Vetoable.
718 wxEVT_MVCTREE_DELETE_ITEM = wx.NewEventType() #Item removed from model.
719 wxEVT_MVCTREE_ITEM_EXPANDED = wx.NewEventType()
720 wxEVT_MVCTREE_ITEM_EXPANDING = wx.NewEventType()
721 wxEVT_MVCTREE_ITEM_COLLAPSED = wx.NewEventType()
722 wxEVT_MVCTREE_ITEM_COLLAPSING = wx.NewEventType()
723 wxEVT_MVCTREE_SEL_CHANGED = wx.NewEventType()
724 wxEVT_MVCTREE_SEL_CHANGING = wx.NewEventType() #Vetoable.
725 wxEVT_MVCTREE_KEY_DOWN = wx.NewEventType()
726 wxEVT_MVCTREE_ADD_ITEM = wx.NewEventType() #Item added to model.
727
728 EVT_MVCTREE_SEL_CHANGED = wx.PyEventBinder(wxEVT_MVCTREE_SEL_CHANGED, 1)
729 EVT_MVCTREE_SEL_CHANGING = wx.PyEventBinder(wxEVT_MVCTREE_SEL_CHANGING, 1)
730 EVT_MVCTREE_ITEM_EXPANDED = wx.PyEventBinder(wxEVT_MVCTREE_ITEM_EXPANDED, 1)
731 EVT_MVCTREE_ITEM_EXPANDING = wx.PyEventBinder(wxEVT_MVCTREE_ITEM_EXPANDING, 1)
732 EVT_MVCTREE_ITEM_COLLAPSED = wx.PyEventBinder(wxEVT_MVCTREE_ITEM_COLLAPSED, 1)
733 EVT_MVCTREE_ITEM_COLLAPSING = wx.PyEventBinder(wxEVT_MVCTREE_ITEM_COLLAPSING, 1)
734 EVT_MVCTREE_ADD_ITEM = wx.PyEventBinder(wxEVT_MVCTREE_ADD_ITEM, 1)
735 EVT_MVCTREE_DELETE_ITEM = wx.PyEventBinder(wxEVT_MVCTREE_DELETE_ITEM, 1)
736 EVT_MVCTREE_KEY_DOWN = wx.PyEventBinder(wxEVT_MVCTREE_KEY_DOWN, 1)
737
738 class MVCTreeEvent(wx.PyCommandEvent):
739 def __init__(self, type, id, node = None, nodes = None, keyEvent = None, **kwargs):
740 apply(wx.PyCommandEvent.__init__, (self, type, id), kwargs)
741 self.node = node
742 self.nodes = nodes
743 self.keyEvent = keyEvent
744 def GetNode(self):
745 return self.node
746 def GetNodes(self):
747 return self.nodes
748 def getKeyEvent(self):
749 return self.keyEvent
750
751 class MVCTreeNotifyEvent(MVCTreeEvent):
752 def __init__(self, type, id, node = None, nodes = None, **kwargs):
753 apply(MVCTreeEvent.__init__, (self, type, id, node, nodes), kwargs)
754 self.notify = wx.NotifyEvent(type, id)
755 def getNotifyEvent(self):
756 return self.notify
757
758 class MVCTree(wx.ScrolledWindow):
759 """
760 The main mvc tree class.
761 """
762 def __init__(self, parent, id, model = None, layout = None, transform = None,
763 painter = None, *args, **kwargs):
764 apply(wx.ScrolledWindow.__init__, (self, parent, id), kwargs)
765 self.nodemap = {}
766 self._multiselect = False
767 self._selections = []
768 self._assumeChildren = False
769 self._scrollx = False
770 self._scrolly = False
771 self.doubleBuffered = False
772 self._lastPhysicalSize = self.GetSize()
773 self._editors = []
774 if not model:
775 model = BasicTreeModel()
776 model.SetRoot("Root")
777 self.SetModel(model)
778 if not layout:
779 layout = TreeLayout(self)
780 self.layout = layout
781 if not transform:
782 transform = NullTransform(self)
783 self.transform = transform
784 if not painter:
785 painter = TreePainter(self)
786 self.painter = painter
787 self.SetFont(wx.Font(9, wx.DEFAULT, wx.NORMAL, wx.NORMAL, False))
788 self.Bind(wx.EVT_MOUSE_EVENTS, self.OnMouse)
789 self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
790 self.doubleBuffered = True
791 self.Bind(wx.EVT_SIZE, self.OnSize)
792 self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
793 self.Bind(wx.EVT_PAINT, self.OnPaint)
794
795
796 def Refresh(self):
797 if self.doubleBuffered:
798 self.painter.ClearBuffer()
799 wx.ScrolledWindow.Refresh(self, False)
800
801 def GetPainter(self):
802 return self.painter
803
804 def GetLayoutEngine(self):
805 return self.layout
806
807 def GetTransform(self):
808 return self.transform
809
810 def __repr__(self):
811 return "<MVCTree instance at %s>" % str(hex(id(self)))
812
813 def __str__(self):
814 return self.__repr__()
815
816 def NodeAdded(self, parent, child):
817 e = MVCTreeEvent(wxEVT_MVCTREE_ADD_ITEM, self.GetId(), node = child, nodes = [parent, child])
818 self.GetEventHandler().ProcessEvent(e)
819 self.painter.ClearBuffer()
820
821 def NodeInserted(self, parent, child, index):
822 e = MVCTreeEvent(wxEVT_MVCTREE_ADD_ITEM, self.GetId(), node = child, nodes = [parent, child])
823 self.GetEventHandler().ProcessEvent(e)
824 self.painter.ClearBuffer()
825
826 def NodeRemoved(self, node):
827 e = MVCTreeEvent(wxEVT_MVCTREE_DELETE_ITEM, self.GetId(), node = child, nodes = [parent, child])
828 self.GetEventHandler().ProcessEvent(e)
829 self.painter.ClearBuffer()
830
831 def OnKeyDown(self, evt):
832 e = MVCTreeEvent(wxEVT_MVCTREE_KEY_DOWN, self.GetId(), keyEvent = evt)
833 self.GetEventHandler().ProcessEvent(e)
834
835 def SetFont(self, font):
836 self.painter.SetFont(font)
837 dc = wx.ClientDC(self)
838 dc.SetFont(font)
839 self.layout.SetHeight(dc.GetTextExtent("")[1] + 18)
840 self.painter.ClearBuffer()
841
842 def GetFont(self):
843 return self.painter.GetFont()
844
845 def AddEditor(self, editor):
846 self._editors.append(editor)
847
848 def RemoveEditor(self, editor):
849 self._editors.remove(editor)
850
851 def OnMouse(self, evt):
852 self.painter.OnMouse(evt)
853
854 def OnNodeClick(self, node, mouseEvent):
855 if node.selected and (self.IsMultiSelect() and mouseEvent.ControlDown()):
856 self.RemoveFromSelection(node.data)
857 else:
858 self.AddToSelection(node.data, mouseEvent.ControlDown(), mouseEvent.ShiftDown())
859
860 def OnKnobClick(self, node):
861 self.SetExpanded(node.data, not node.expanded)
862
863 def GetDisplayText(self, node):
864 treenode = self.nodemap[node]
865 return self.painter.textConverter.Convert(treenode)
866
867 def IsDoubleBuffered(self):
868 return self.doubleBuffered
869
870 def SetDoubleBuffered(self, bool):
871 """
872 By default MVCTree is double-buffered.
873 """
874 self.doubleBuffered = bool
875
876 def GetModel(self):
877 return self.model
878
879 def SetModel(self, model):
880 """
881 Completely change the data to be displayed.
882 """
883 self.model = model
884 model.tree = self
885 self.laidOut = 0
886 self.transformed = 0
887 self._selections = []
888 self.layoutRoot = MVCTreeNode()
889 self.layoutRoot.data = self.model.GetRoot()
890 self.layoutRoot.expanded = True
891 self.LoadChildren(self.layoutRoot)
892 self.currentRoot = self.layoutRoot
893 self.offset = [0,0]
894 self.rotation = 0
895 self._scrollset = None
896 self.Refresh()
897
898 def GetCurrentRoot(self):
899 return self.currentRoot
900
901 def LoadChildren(self, layoutNode):
902 if layoutNode.built:
903 return
904 else:
905 self.nodemap[layoutNode.data]=layoutNode
906 for i in range(self.GetModel().GetChildCount(layoutNode.data)):
907 p = MVCTreeNode("RAW", layoutNode, [])
908 layoutNode.Add(p)
909 p.data = self.GetModel().GetChildAt(layoutNode.data, i)
910 self.nodemap[p.data]=p
911 layoutNode.built = True
912 if not self._assumeChildren:
913 for kid in layoutNode.kids:
914 self.LoadChildren(kid)
915
916 def OnEraseBackground(self, evt):
917 pass
918
919 def OnSize(self, evt):
920 size = self.GetSize()
921 self.center = (size.width/2, size.height/2)
922 if self._lastPhysicalSize.width < size.width or self._lastPhysicalSize.height < size.height:
923 self.painter.ClearBuffer()
924 self._lastPhysicalSize = size
925
926 def GetSelection(self):
927 "Returns a tuple of selected nodes."
928 return tuple(self._selections)
929
930 def SetSelection(self, nodeTuple):
931 if type(nodeTuple) != type(()):
932 nodeTuple = (nodeTuple,)
933 e = MVCTreeNotifyEvent(wxEVT_MVCTREE_SEL_CHANGING, self.GetId(), nodeTuple[0], nodes = nodeTuple)
934 self.GetEventHandler().ProcessEvent(e)
935 if not e.notify.IsAllowed():
936 return
937 for node in nodeTuple:
938 treenode = self.nodemap[node]
939 treenode.selected = True
940 for node in self._selections:
941 treenode = self.nodemap[node]
942 node.selected = False
943 self._selections = list(nodeTuple)
944 e = MVCTreeEvent(wxEVT_MVCTREE_SEL_CHANGED, self.GetId(), nodeTuple[0], nodes = nodeTuple)
945 self.GetEventHandler().ProcessEvent(e)
946
947 def IsMultiSelect(self):
948 return self._multiselect
949
950 def SetMultiSelect(self, bool):
951 self._multiselect = bool
952
953 def IsSelected(self, node):
954 return self.nodemap[node].selected
955
956 def Edit(self, node):
957 if not self.model.IsEditable(node):
958 return
959 for ed in self._editors:
960 if ed.CanEdit(node):
961 e = MVCTreeNotifyEvent(wxEVT_MVCTREE_BEGIN_EDIT, self.GetId(), node)
962 self.GetEventHandler().ProcessEvent(e)
963 if not e.notify.IsAllowed():
964 return
965 ed.Edit(node)
966 self._currentEditor = ed
967 break
968
969 def EndEdit(self):
970 if self._currentEditor:
971 self._currentEditor.EndEdit
972 self._currentEditor = None
973
974 def _EditEnding(self, node):
975 e = MVCTreeNotifyEvent(wxEVT_MVCTREE_END_EDIT, self.GetId(), node)
976 self.GetEventHandler().ProcessEvent(e)
977 if not e.notify.IsAllowed():
978 return False
979 self._currentEditor = None
980 return True
981
982
983 def SetExpanded(self, node, bool):
984 treenode = self.nodemap[node]
985 if bool:
986 e = MVCTreeNotifyEvent(wxEVT_MVCTREE_ITEM_EXPANDING, self.GetId(), node)
987 self.GetEventHandler().ProcessEvent(e)
988 if not e.notify.IsAllowed():
989 return
990 if not treenode.built:
991 self.LoadChildren(treenode)
992 else:
993 e = MVCTreeNotifyEvent(wxEVT_MVCTREE_ITEM_COLLAPSING, self.GetId(), node)
994 self.GetEventHandler().ProcessEvent(e)
995 if not e.notify.IsAllowed():
996 return
997 treenode.expanded = bool
998 e = None
999 if treenode.expanded:
1000 e = MVCTreeEvent(wxEVT_MVCTREE_ITEM_EXPANDED, self.GetId(), node)
1001 else:
1002 e = MVCTreeEvent(wxEVT_MVCTREE_ITEM_COLLAPSED, self.GetId(), node)
1003 self.GetEventHandler().ProcessEvent(e)
1004 self.layout.Layout(self.currentRoot)
1005 self.transform.Transform(self.currentRoot, self.offset, self.rotation)
1006 self.Refresh()
1007
1008 def IsExpanded(self, node):
1009 return self.nodemap[node].expanded
1010
1011 def AddToSelection(self, nodeOrTuple, enableMulti = True, shiftMulti = False):
1012 nodeTuple = nodeOrTuple
1013 if type(nodeOrTuple)!= type(()):
1014 nodeTuple = (nodeOrTuple,)
1015 e = MVCTreeNotifyEvent(wxEVT_MVCTREE_SEL_CHANGING, self.GetId(), nodeTuple[0], nodes = nodeTuple)
1016 self.GetEventHandler().ProcessEvent(e)
1017 if not e.notify.IsAllowed():
1018 return
1019 changeparents = []
1020 if not (self.IsMultiSelect() and (enableMulti or shiftMulti)):
1021 for node in self._selections:
1022 treenode = self.nodemap[node]
1023 treenode.selected = False
1024 changeparents.append(treenode)
1025 node = nodeTuple[0]
1026 self._selections = [node]
1027 treenode = self.nodemap[node]
1028 changeparents.append(treenode)
1029 treenode.selected = True
1030 else:
1031 if shiftMulti:
1032 for node in nodeTuple:
1033 treenode = self.nodemap[node]
1034 oldtreenode = self.nodemap[self._selections[0]]
1035 if treenode.parent == oldtreenode.parent:
1036 found = 0
1037 for kid in oldtreenode.parent.kids:
1038 if kid == treenode or kid == oldtreenode:
1039 found = not found
1040 kid.selected = True
1041 self._selections.append(kid.data)
1042 changeparents.append(kid)
1043 elif found:
1044 kid.selected = True
1045 self._selections.append(kid.data)
1046 changeparents.append(kid)
1047 else:
1048 for node in nodeTuple:
1049 try:
1050 self._selections.index(node)
1051 except ValueError:
1052 self._selections.append(node)
1053 treenode = self.nodemap[node]
1054 treenode.selected = True
1055 changeparents.append(treenode)
1056 e = MVCTreeEvent(wxEVT_MVCTREE_SEL_CHANGED, self.GetId(), nodeTuple[0], nodes = nodeTuple)
1057 self.GetEventHandler().ProcessEvent(e)
1058 dc = wx.ClientDC(self)
1059 self.PrepareDC(dc)
1060 for node in changeparents:
1061 if node:
1062 self.painter.Paint(dc, node, doubleBuffered = 0, paintBackground = 0)
1063 self.painter.ClearBuffer()
1064
1065 def RemoveFromSelection(self, nodeTuple):
1066 if type(nodeTuple) != type(()):
1067 nodeTuple = (nodeTuple,)
1068 changeparents = []
1069 for node in nodeTuple:
1070 self._selections.remove(node)
1071 treenode = self.nodemap[node]
1072 changeparents.append(treenode)
1073 treenode.selected = False
1074 e = MVCTreeEvent(wxEVT_MVCTREE_SEL_CHANGED, self.GetId(), node, nodes = nodeTuple)
1075 self.GetEventHandler().ProcessEvent(e)
1076 dc = wx.ClientDC(self)
1077 self.PrepareDC(dc)
1078 for node in changeparents:
1079 if node:
1080 self.painter.Paint(dc, node, doubleBuffered = 0, paintBackground = 0)
1081 self.painter.ClearBuffer()
1082
1083
1084 def GetBackgroundColour(self):
1085 if hasattr(self, 'painter') and self.painter:
1086 return self.painter.GetBackgroundColour()
1087 else:
1088 return wx.Window.GetBackgroundColour(self)
1089 def SetBackgroundColour(self, color):
1090 if hasattr(self, 'painter') and self.painter:
1091 self.painter.SetBackgroundColour(color)
1092 else:
1093 wx.Window.SetBackgroundColour(self, color)
1094 def GetForegroundColour(self):
1095 if hasattr(self, 'painter') and self.painter:
1096 return self.painter.GetForegroundColour()
1097 else:
1098 return wx.Window.GetBackgroundColour(self)
1099 def SetForegroundColour(self, color):
1100 if hasattr(self, 'painter') and self.painter:
1101 self.painter.SetForegroundColour(color)
1102 else:
1103 wx.Window.SetBackgroundColour(self, color)
1104
1105 def SetAssumeChildren(self, bool):
1106 self._assumeChildren = bool
1107
1108 def GetAssumeChildren(self):
1109 return self._assumeChildren
1110
1111 def OnPaint(self, evt):
1112 """
1113 Ensures that the tree has been laid out and transformed, then calls the painter
1114 to paint the control.
1115 """
1116 try:
1117 self.EnableScrolling(False, False)
1118 if not self.laidOut:
1119 self.layout.Layout(self.currentRoot)
1120 self.laidOut = True
1121 self.transformed = False
1122 if not self.transformed:
1123 self.transform.Transform(self.currentRoot, self.offset, self.rotation)
1124 self.transformed = True
1125 tsize = None
1126 tsize = list(self.transform.GetSize())
1127 tsize[0] = tsize[0] + 50
1128 tsize[1] = tsize[1] + 50
1129 w, h = self.GetSize()
1130 if tsize[0] > w or tsize[1] > h:
1131 if not hasattr(self, '_oldsize') or (tsize[0] > self._oldsize[0] or tsize[1] > self._oldsize[1]):
1132 self._oldsize = tsize
1133 oldstart = self.GetViewStart()
1134 self._lastPhysicalSize = self.GetSize()
1135 self.SetScrollbars(10, 10, tsize[0]/10, tsize[1]/10)
1136 self.Scroll(oldstart[0], oldstart[1])
1137 dc = wx.PaintDC(self)
1138 self.PrepareDC(dc)
1139 dc.SetFont(self.GetFont())
1140 self.painter.Paint(dc, self.currentRoot, self.doubleBuffered)
1141 except:
1142 traceback.print_exc()
1143
1144
1145