]> git.saurik.com Git - wxWidgets.git/blame - wxPython/wx/lib/mvctree.py
wxCheckListBox doesn't require wxUSE_OWNER_DRAWN when using WXUNIVERSAL
[wxWidgets.git] / wxPython / wx / lib / mvctree.py
CommitLineData
b881fc78
RD
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#
d4b73b1b
RD
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#
b881fc78 13
d14a1e28 14"""
d4b73b1b 15MVCTree is a control which handles hierarchical data. It is constructed
d14a1e28
RD
16in model-view-controller architecture, so the display of that data, and
17the content of the data can be changed greatly without affecting the other parts.
18
d4b73b1b 19MVCTree actually is even more configurable than MVC normally implies, because
d14a1e28 20almost every aspect of it is pluggable:
d4b73b1b 21 MVCTree - Overall controller, and the window that actually gets placed
d14a1e28
RD
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
33Author/Maintainer - Bryn Keller <xoltar@starship.python.net>
34
35
36NOTE: 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#------------------------------------------------------------------------
b881fc78
RD
43import os
44import sys
45import traceback
46import warnings
47
48import wx
49#------------------------------------------------------------------------
50
51warningmsg = 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
62warnings.warn(warningmsg, DeprecationWarning, stacklevel=2)
d14a1e28
RD
63#------------------------------------------------------------------------
64
65class MVCTreeNode:
66 """
d4b73b1b 67 Used internally by MVCTree to manage its data. Contains information about
d14a1e28
RD
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
116class 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
126class 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
137class 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
159class Painter:
160 """
d4b73b1b 161 This is the interface that MVCTree expects from painters. All painters should
d14a1e28
RD
162 be Painter subclasses.
163 """
164 def __init__(self, tree):
165 self.tree = tree
b881fc78
RD
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)
d14a1e28
RD
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
b881fc78
RD
189 self.textbrush = wx.Brush(color)
190 self.textpen = wx.Pen(color, 1, wx.SOLID)
d14a1e28
RD
191 def GetBackgroundColour(self):
192 return self.bgcolor
193 def SetBackgroundColour(self, color):
194 self.bgcolor = color
b881fc78
RD
195 self.bgbrush = wx.Brush(color)
196 self.bgpen = wx.Pen(color, 1, wx.SOLID)
d14a1e28
RD
197 def GetForegroundColour(self):
198 return self.fgcolor
199 def SetForegroundColour(self, color):
200 self.fgcolor = color
b881fc78
RD
201 self.fgbrush = wx.Brush(color)
202 self.fgpen = wx.Pen(color, 1, wx.SOLID)
d14a1e28
RD
203 def GetLineColour(self):
204 return self.linecolor
205 def SetLineColour(self, color):
206 self.linecolor = color
b881fc78
RD
207 self.linebrush = wx.Brush(color)
208 self.linepen = wx.Pen( color, 1, wx.SOLID)
d14a1e28
RD
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
d4b73b1b 246class TreeModel:
d14a1e28
RD
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
278class 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
292class 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
301class 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
d4b73b1b 315class BasicTreeModel(TreeModel):
d14a1e28
RD
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)
d4b73b1b 343 TreeModel.AddChild(self, parent, child)
d14a1e28
RD
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)
d4b73b1b 350 TreeModel.RemoveNode(self, node)
d14a1e28
RD
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)
d4b73b1b 357 TreeModel.InsertChild(self, parent, child, index)
d14a1e28
RD
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
370class 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
b881fc78
RD
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)
d14a1e28
RD
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):
b881fc78 407 if evt.KeyCode() == wx.WXK_RETURN:
d14a1e28 408 self.EndEdit(True)
b881fc78 409 elif evt.KeyCode() == wx.WXK_ESCAPE:
d14a1e28
RD
410 self.EndEdit(False)
411 else:
412 evt.Skip()
413
414 def _mdown(self, evt):
415 if evt.IsButton():
b881fc78
RD
416 x, y = evt.GetPosition()
417 w, h = self.editcomp.GetSize()
418 if x < 0 or y < 0 or x > w or y > h:
d14a1e28
RD
419 self.EndEdit(False)
420
421
422class 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
433class 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
457class 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
489class StrTextConverter(TextConverter):
490 def Convert(self, node):
491 return str(node.data)
492
493class 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
508class 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
534class 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
565class 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]
b881fc78
RD
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)
d14a1e28
RD
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:
b881fc78 603 mem_dc = wx.MemoryDC()
d14a1e28
RD
604 if not self.GetBuffer():
605 self.knobs = []
606 self.rectangles = []
b881fc78 607 self.bmp = wx.EmptyBitmap(size[0], size[1])
d14a1e28
RD
608 mem_dc.SelectObject(self.GetBuffer())
609 mem_dc.SetPen(self.GetBackgroundPen())
610 mem_dc.SetBrush(self.GetBackgroundBrush())
d7403ad2 611 mem_dc.DrawRectangle(0, 0, size[0], size[1])
d14a1e28
RD
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()
d7403ad2 618 dc.Blit(xstart, ystart, size[0], size[1], mem_dc, xstart, ystart)
d14a1e28
RD
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:
d7403ad2 627 dc.DrawRectangle(0, 0, size[0], size[1])
d14a1e28
RD
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)
b881fc78 640 self.dashpen = wx.Pen(pen.GetColour(), 1, wx.DOT)
d14a1e28
RD
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)
d7403ad2 655 dc.DrawRectangle(px -4, py-4, 9, 9)
d14a1e28
RD
656 self.knobs.append( (kid, Rect(px -4, py -4, 9, 9)) )
657 dc.SetPen(self.textpen)
658 if not kid.expanded:
d7403ad2
RD
659 dc.DrawLine(px, py -2, px, py + 3)
660 dc.DrawLine(px -2, py, px + 3, py)
d14a1e28
RD
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)
d7403ad2 666 dc.DrawRectangle(px -4, py-4, 9, 9)
d14a1e28
RD
667 self.knobs.append( (node, Rect(px -4, py -4, 9, 9)) )
668 dc.SetPen(self.textpen)
669 if not node.expanded:
d7403ad2
RD
670 dc.DrawLine(px, py -2, px, py + 3)
671 dc.DrawLine(px -2, py, px + 3, py)
d14a1e28
RD
672 return True
673
674 def OnMouse(self, evt):
675 Painter.OnMouse(self, evt)
676
677class 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())
b881fc78 686 dc.SetTextForeground(wx.NamedColour("WHITE"))
d7403ad2 687 dc.DrawRectangle(node.projx -1, node.projy -1, node.width + 3, node.height + 3)
d14a1e28
RD
688 else:
689 if drawRects:
690 dc.SetBrush(self.painter.GetBackgroundBrush())
691 dc.SetPen(self.painter.GetBackgroundPen())
d7403ad2 692 dc.DrawRectangle(node.projx -1, node.projy -1, node.width + 3, node.height + 3)
d14a1e28 693 dc.SetTextForeground(self.painter.GetTextColour())
d7403ad2 694 dc.DrawText(text, node.projx, node.projy)
d14a1e28
RD
695 self.painter.rectangles.append((node, Rect(node.projx, node.projy, node.width, node.height)))
696
697class 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
d7403ad2 706 dc.DrawLine(px, py, cx, cy)
d14a1e28
RD
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
d7403ad2
RD
712 dc.DrawLine(px, py, px, cy)
713 dc.DrawLine(px, cy, cx, cy)
b881fc78
RD
714
715#>> Event defs
716wxEVT_MVCTREE_BEGIN_EDIT = wx.NewEventType() #Start editing. Vetoable.
717wxEVT_MVCTREE_END_EDIT = wx.NewEventType() #Stop editing. Vetoable.
718wxEVT_MVCTREE_DELETE_ITEM = wx.NewEventType() #Item removed from model.
719wxEVT_MVCTREE_ITEM_EXPANDED = wx.NewEventType()
720wxEVT_MVCTREE_ITEM_EXPANDING = wx.NewEventType()
721wxEVT_MVCTREE_ITEM_COLLAPSED = wx.NewEventType()
722wxEVT_MVCTREE_ITEM_COLLAPSING = wx.NewEventType()
723wxEVT_MVCTREE_SEL_CHANGED = wx.NewEventType()
724wxEVT_MVCTREE_SEL_CHANGING = wx.NewEventType() #Vetoable.
725wxEVT_MVCTREE_KEY_DOWN = wx.NewEventType()
726wxEVT_MVCTREE_ADD_ITEM = wx.NewEventType() #Item added to model.
727
728EVT_MVCTREE_SEL_CHANGED = wx.PyEventBinder(wxEVT_MVCTREE_SEL_CHANGED, 1)
729EVT_MVCTREE_SEL_CHANGING = wx.PyEventBinder(wxEVT_MVCTREE_SEL_CHANGING, 1)
730EVT_MVCTREE_ITEM_EXPANDED = wx.PyEventBinder(wxEVT_MVCTREE_ITEM_EXPANDED, 1)
731EVT_MVCTREE_ITEM_EXPANDING = wx.PyEventBinder(wxEVT_MVCTREE_ITEM_EXPANDING, 1)
732EVT_MVCTREE_ITEM_COLLAPSED = wx.PyEventBinder(wxEVT_MVCTREE_ITEM_COLLAPSED, 1)
733EVT_MVCTREE_ITEM_COLLAPSING = wx.PyEventBinder(wxEVT_MVCTREE_ITEM_COLLAPSING, 1)
734EVT_MVCTREE_ADD_ITEM = wx.PyEventBinder(wxEVT_MVCTREE_ADD_ITEM, 1)
735EVT_MVCTREE_DELETE_ITEM = wx.PyEventBinder(wxEVT_MVCTREE_DELETE_ITEM, 1)
736EVT_MVCTREE_KEY_DOWN = wx.PyEventBinder(wxEVT_MVCTREE_KEY_DOWN, 1)
737
d4b73b1b 738class MVCTreeEvent(wx.PyCommandEvent):
d14a1e28 739 def __init__(self, type, id, node = None, nodes = None, keyEvent = None, **kwargs):
b881fc78 740 apply(wx.PyCommandEvent.__init__, (self, type, id), kwargs)
d14a1e28
RD
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
d4b73b1b 751class MVCTreeNotifyEvent(MVCTreeEvent):
d14a1e28 752 def __init__(self, type, id, node = None, nodes = None, **kwargs):
d4b73b1b 753 apply(MVCTreeEvent.__init__, (self, type, id, node, nodes), kwargs)
b881fc78 754 self.notify = wx.NotifyEvent(type, id)
d14a1e28
RD
755 def getNotifyEvent(self):
756 return self.notify
757
d4b73b1b 758class MVCTree(wx.ScrolledWindow):
d14a1e28
RD
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):
b881fc78 764 apply(wx.ScrolledWindow.__init__, (self, parent, id), kwargs)
d14a1e28
RD
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
b881fc78
RD
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)
d14a1e28 790 self.doubleBuffered = True
b881fc78
RD
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)
d14a1e28
RD
794
795
796 def Refresh(self):
797 if self.doubleBuffered:
798 self.painter.ClearBuffer()
b881fc78 799 wx.ScrolledWindow.Refresh(self, False)
d14a1e28
RD
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):
d4b73b1b 811 return "<MVCTree instance at %s>" % str(hex(id(self)))
d14a1e28
RD
812
813 def __str__(self):
814 return self.__repr__()
815
816 def NodeAdded(self, parent, child):
d4b73b1b 817 e = MVCTreeEvent(wxEVT_MVCTREE_ADD_ITEM, self.GetId(), node = child, nodes = [parent, child])
d14a1e28
RD
818 self.GetEventHandler().ProcessEvent(e)
819 self.painter.ClearBuffer()
820
821 def NodeInserted(self, parent, child, index):
d4b73b1b 822 e = MVCTreeEvent(wxEVT_MVCTREE_ADD_ITEM, self.GetId(), node = child, nodes = [parent, child])
d14a1e28
RD
823 self.GetEventHandler().ProcessEvent(e)
824 self.painter.ClearBuffer()
825
826 def NodeRemoved(self, node):
d4b73b1b 827 e = MVCTreeEvent(wxEVT_MVCTREE_DELETE_ITEM, self.GetId(), node = child, nodes = [parent, child])
d14a1e28
RD
828 self.GetEventHandler().ProcessEvent(e)
829 self.painter.ClearBuffer()
830
831 def OnKeyDown(self, evt):
d4b73b1b 832 e = MVCTreeEvent(wxEVT_MVCTREE_KEY_DOWN, self.GetId(), keyEvent = evt)
d14a1e28
RD
833 self.GetEventHandler().ProcessEvent(e)
834
835 def SetFont(self, font):
836 self.painter.SetFont(font)
b881fc78 837 dc = wx.ClientDC(self)
d14a1e28
RD
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 """
d4b73b1b 872 By default MVCTree is double-buffered.
d14a1e28
RD
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,)
d4b73b1b 933 e = MVCTreeNotifyEvent(wxEVT_MVCTREE_SEL_CHANGING, self.GetId(), nodeTuple[0], nodes = nodeTuple)
d14a1e28
RD
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)
d4b73b1b 944 e = MVCTreeEvent(wxEVT_MVCTREE_SEL_CHANGED, self.GetId(), nodeTuple[0], nodes = nodeTuple)
d14a1e28
RD
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):
d4b73b1b 961 e = MVCTreeNotifyEvent(wxEVT_MVCTREE_BEGIN_EDIT, self.GetId(), node)
d14a1e28
RD
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):
d4b73b1b 975 e = MVCTreeNotifyEvent(wxEVT_MVCTREE_END_EDIT, self.GetId(), node)
d14a1e28
RD
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:
d4b73b1b 986 e = MVCTreeNotifyEvent(wxEVT_MVCTREE_ITEM_EXPANDING, self.GetId(), node)
d14a1e28
RD
987 self.GetEventHandler().ProcessEvent(e)
988 if not e.notify.IsAllowed():
989 return
990 if not treenode.built:
991 self.LoadChildren(treenode)
992 else:
d4b73b1b 993 e = MVCTreeNotifyEvent(wxEVT_MVCTREE_ITEM_COLLAPSING, self.GetId(), node)
d14a1e28
RD
994 self.GetEventHandler().ProcessEvent(e)
995 if not e.notify.IsAllowed():
996 return
997 treenode.expanded = bool
998 e = None
999 if treenode.expanded:
d4b73b1b 1000 e = MVCTreeEvent(wxEVT_MVCTREE_ITEM_EXPANDED, self.GetId(), node)
d14a1e28 1001 else:
d4b73b1b 1002 e = MVCTreeEvent(wxEVT_MVCTREE_ITEM_COLLAPSED, self.GetId(), node)
d14a1e28
RD
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,)
d4b73b1b 1015 e = MVCTreeNotifyEvent(wxEVT_MVCTREE_SEL_CHANGING, self.GetId(), nodeTuple[0], nodes = nodeTuple)
d14a1e28
RD
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)
d4b73b1b 1056 e = MVCTreeEvent(wxEVT_MVCTREE_SEL_CHANGED, self.GetId(), nodeTuple[0], nodes = nodeTuple)
d14a1e28 1057 self.GetEventHandler().ProcessEvent(e)
b881fc78 1058 dc = wx.ClientDC(self)
d14a1e28
RD
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
d4b73b1b 1074 e = MVCTreeEvent(wxEVT_MVCTREE_SEL_CHANGED, self.GetId(), node, nodes = nodeTuple)
d14a1e28 1075 self.GetEventHandler().ProcessEvent(e)
b881fc78 1076 dc = wx.ClientDC(self)
d14a1e28
RD
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:
b881fc78 1088 return wx.Window.GetBackgroundColour(self)
d14a1e28
RD
1089 def SetBackgroundColour(self, color):
1090 if hasattr(self, 'painter') and self.painter:
1091 self.painter.SetBackgroundColour(color)
1092 else:
b881fc78 1093 wx.Window.SetBackgroundColour(self, color)
d14a1e28
RD
1094 def GetForegroundColour(self):
1095 if hasattr(self, 'painter') and self.painter:
1096 return self.painter.GetForegroundColour()
1097 else:
b881fc78 1098 return wx.Window.GetBackgroundColour(self)
d14a1e28
RD
1099 def SetForegroundColour(self, color):
1100 if hasattr(self, 'painter') and self.painter:
1101 self.painter.SetForegroundColour(color)
1102 else:
b881fc78 1103 wx.Window.SetBackgroundColour(self, color)
d14a1e28
RD
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
b881fc78
RD
1129 w, h = self.GetSize()
1130 if tsize[0] > w or tsize[1] > h:
d14a1e28
RD
1131 if not hasattr(self, '_oldsize') or (tsize[0] > self._oldsize[0] or tsize[1] > self._oldsize[1]):
1132 self._oldsize = tsize
b881fc78 1133 oldstart = self.GetViewStart()
d14a1e28
RD
1134 self._lastPhysicalSize = self.GetSize()
1135 self.SetScrollbars(10, 10, tsize[0]/10, tsize[1]/10)
1136 self.Scroll(oldstart[0], oldstart[1])
b881fc78 1137 dc = wx.PaintDC(self)
d14a1e28
RD
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