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