updated wxMVCTree, VTK, and the demo
git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/trunk@4629 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775
This commit is contained in:
		| @@ -45,6 +45,15 @@ Or you can send mail directly to the list using this address: | ||||
|  | ||||
| ---------------------------------------------------------------------- | ||||
|  | ||||
| What's new in 2.1.12 | ||||
| -------------------- | ||||
| Updated wxMVCTree and added a demo for it. | ||||
|  | ||||
| Added a wrapper class for the Visualization ToolKit (or VTK) in the | ||||
| wxPython.lib.vtk module.  (http://www.kitware.com/) | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| What's new in 2.1.11 | ||||
| -------------------- | ||||
|   | ||||
| @@ -21,6 +21,8 @@ _useSplitter       = true | ||||
| _useNestedSplitter = true | ||||
|  | ||||
| _treeList = [ | ||||
|     ('New since last release', ['wxMVCTree', 'wxVTKRenderWindow']), | ||||
|  | ||||
|     ('Managed Windows', ['wxFrame', 'wxDialog', 'wxMiniFrame']), | ||||
|  | ||||
|     ('Non-Managed Windows', ['wxGrid', 'wxSashWindow', | ||||
| @@ -47,7 +49,7 @@ _treeList = [ | ||||
|  | ||||
|     ('wxPython Library', ['Layoutf', 'wxScrolledMessageDialog', | ||||
|                           'wxMultipleChoiceDialog', 'wxPlotCanvas', 'wxFloatBar', | ||||
|                           'PyShell', 'wxCalendar']), | ||||
|                           'PyShell', 'wxCalendar', 'wxMVCTree', 'wxVTKRenderWindow']), | ||||
|  | ||||
|     ('Cool Contribs', ['pyTree', 'hangman', 'SlashDot', 'XMLtreeview']), | ||||
|  | ||||
|   | ||||
							
								
								
									
										61
									
								
								utils/wxPython/demo/wxMVCTree.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								utils/wxPython/demo/wxMVCTree.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | ||||
|  | ||||
| import sys, os | ||||
| from wxPython.wx import * | ||||
| from wxPython.lib.mvctree import * | ||||
|  | ||||
|  | ||||
| logger = None | ||||
| def selchanging(evt): | ||||
|     logger.write("SelChanging!\n") | ||||
|  | ||||
| def selchanged(evt): | ||||
|     logger.write("SelChange!\n") | ||||
|     logger.write(str(evt.node)) | ||||
| def expanded(evt): | ||||
|     logger.write("Expanded\n") | ||||
| def closed(evt): | ||||
|     logger.write("Closed!\n") | ||||
| def key(evt): | ||||
|     logger.write("Key\n") | ||||
| def add(evt): | ||||
|     logger.write("Add\n") | ||||
| def delitem(evt): | ||||
|     logger.write("Delete\n") | ||||
|  | ||||
| def runTest(frame, nb, log): | ||||
|     #f = wxFrame(frame, -1, "wxMVCTree", wxPoint(0,0), wxSize(200,500)) | ||||
|     global logger | ||||
|     logger = log | ||||
|     p = wxMVCTree(nb, -1) | ||||
|     p.SetAssumeChildren(true) | ||||
|     p.SetModel(LateFSTreeModel(os.path.normpath(os.getcwd() + os.sep +'..'))) | ||||
|     #Uncomment this to enable live filename editing! | ||||
| #    p.AddEditor(FileEditor(p)) | ||||
|     p.SetMultiSelect(true) | ||||
|     EVT_MVCTREE_SEL_CHANGING(p, p.GetId(), selchanging) | ||||
|     EVT_MVCTREE_SEL_CHANGED(p, p.GetId(), selchanged) | ||||
|     EVT_MVCTREE_ITEM_EXPANDED(p, p.GetId(), expanded) | ||||
|     EVT_MVCTREE_ITEM_COLLAPSED(p, p.GetId(), closed) | ||||
|     EVT_MVCTREE_ADD_ITEM(p, p.GetId(), add) | ||||
|     EVT_MVCTREE_DELETE_ITEM(p, p.GetId(), delitem) | ||||
|     EVT_MVCTREE_KEY_DOWN(p, p.GetId(), key) | ||||
|     return p | ||||
|  | ||||
| overview = """\ | ||||
| wxMVCTree is a control which handles hierarchical data. It is constructed in model-view-controller architecture, so the display of that data, and the content of the data can be changed greatly without affecting the other parts. | ||||
|  | ||||
| Multiple selections are possible by holding down the Ctrl key. | ||||
|  | ||||
| This demo shows the wxPython directory structure. The interesting part is that the tree model is late-bound to the filesystem, so the filenames are not retrieved until the directory is expanded. In mvctree.py are models for generic data, and both the early and late-bound filesystem models. | ||||
|  | ||||
| There is also support for editing, though it's not enabled in this demo, to avoid accidentally renaming files! | ||||
|  | ||||
| """ | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
							
								
								
									
										60
									
								
								utils/wxPython/demo/wxVTKRenderWindow.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								utils/wxPython/demo/wxVTKRenderWindow.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
|  | ||||
| from wxPython.wx  import * | ||||
| try: | ||||
|     from wxPython.lib import vtk | ||||
|     haveVTK = true | ||||
| except ImportError: | ||||
|     haveVTK = false | ||||
|  | ||||
| #---------------------------------------------------------------------- | ||||
|  | ||||
| def runTest(frame, nb, log): | ||||
|     if haveVTK: | ||||
|         win = vtk.wxVTKRenderWindow(nb, -1) | ||||
|  | ||||
|         # Get the render window | ||||
|         renWin = win.GetRenderWindow() | ||||
|  | ||||
|         # Next, do the VTK stuff | ||||
|         ren = vtk.vtkRenderer() | ||||
|         renWin.AddRenderer(ren) | ||||
|         cone = vtk.vtkConeSource() | ||||
|         cone.SetResolution(80) | ||||
|         coneMapper = vtk.vtkPolyDataMapper() | ||||
|         coneMapper.SetInput(cone.GetOutput()) | ||||
|         coneActor = vtk.vtkActor() | ||||
|         coneActor.SetMapper(coneMapper) | ||||
|         ren.AddActor(coneActor) | ||||
|         coneMapper.GetLookupTable().Build() | ||||
|  | ||||
|         # Create a scalar bar | ||||
|         scalarBar = vtk.vtkScalarBarActor() | ||||
|         scalarBar.SetLookupTable(coneMapper.GetLookupTable()) | ||||
|         scalarBar.SetTitle("Temperature") | ||||
|         scalarBar.GetPositionCoordinate().SetCoordinateSystemToNormalizedViewport() | ||||
|         scalarBar.GetPositionCoordinate().SetValue(0.1, 0.01) | ||||
|         scalarBar.SetOrientationToHorizontal() | ||||
|         scalarBar.SetWidth(0.8) | ||||
|         scalarBar.SetHeight(0.17) | ||||
|         ren.AddActor2D(scalarBar) | ||||
|  | ||||
|         return win | ||||
|  | ||||
|  | ||||
|     else: | ||||
|         wxMessageBox("Unable to import VTK, which is a required component " | ||||
|                      "of this demo.  You need to download and install the " | ||||
|                      "Python extension module for VTK from http://www.kitware.com/", | ||||
|                      "Import Error") | ||||
|         return None | ||||
|  | ||||
| #---------------------------------------------------------------------- | ||||
|  | ||||
|  | ||||
| overview = """\ | ||||
| wxVTKRenderWindow is a wrapper around the vtkRenderWindow from the | ||||
| VTK Visualization Toolkit.  The VTK Python extensions are required, | ||||
| they can be obtained from http://www.kitware.com/ where you can also | ||||
| find some nifty pictures and stuff. | ||||
|  | ||||
| """ | ||||
| @@ -2,10 +2,8 @@ | ||||
| wxMVCTree is a control which handles hierarchical data. It is constructed | ||||
| in model-view-controller architecture, so the display of that data, and | ||||
| the content of the data can be changed greatly without affecting the other parts. | ||||
| This module contains the wxMVCTree class (the 'controller' of the MVC trio) | ||||
| and PathfinderNode, which it uses internally to manage its info. | ||||
|  | ||||
| Pathfinder actually is even more configurable than MVC normally implies, because | ||||
| wxMVCTree actually is even more configurable than MVC normally implies, because | ||||
| almost every aspect of it is pluggable: | ||||
|     wxMVCTree - Overall controller, and the window that actually gets placed | ||||
|     in the GUI. | ||||
| @@ -24,7 +22,7 @@ Author/Maintainer - Bryn Keller <xoltar@starship.python.net> | ||||
|  | ||||
| #------------------------------------------------------------------------ | ||||
| from wxPython.wx import * | ||||
| import os, sys | ||||
| import os, sys, traceback | ||||
| #------------------------------------------------------------------------ | ||||
|  | ||||
| class MVCTreeNode: | ||||
| @@ -94,6 +92,8 @@ class LayoutEngine: | ||||
|         self.tree = tree | ||||
|     def layout(self, node): | ||||
|         raise NotImplementedError | ||||
|     def GetNodeList(self): | ||||
|         raise NotImplementedError | ||||
|  | ||||
| class Transform: | ||||
|     """ | ||||
| @@ -110,6 +110,13 @@ class Transform: | ||||
|         """ | ||||
|         raise NotImplementedError | ||||
|  | ||||
|     def GetSize(self): | ||||
|         """ | ||||
|         Returns the size of the entire tree as laid out and transformed | ||||
|         as a tuple | ||||
|         """ | ||||
|         raise NotImplementedError | ||||
|  | ||||
| class Painter: | ||||
|     """ | ||||
|     This is the interface that wxMVCTree expects from painters. All painters should | ||||
| @@ -122,9 +129,7 @@ class Painter: | ||||
|         self.fgcolor = wxNamedColour("BLUE") | ||||
|         self.linecolor = wxNamedColour("GREY") | ||||
|         self.font = wxFont(9, wxDEFAULT, wxNORMAL, wxNORMAL, false) | ||||
|         self.knobs = [] | ||||
|         self.rectangles = [] | ||||
|         self.minx = self.maxx = self.miny = self.maxy = 0 | ||||
|         self.bmp = None | ||||
|  | ||||
|     def GetFont(self): | ||||
|         return self.font | ||||
| @@ -132,8 +137,11 @@ class Painter: | ||||
|     def SetFont(self, font): | ||||
|         self.font = font | ||||
|         self.tree.Refresh() | ||||
|  | ||||
|     def paint(self, dc, node): | ||||
|     def GetBuffer(self): | ||||
|         return self.bmp | ||||
|     def ClearBuffer(self): | ||||
|         self.bmp = None | ||||
|     def paint(self, dc, node, doubleBuffered=1, paintBackground=1): | ||||
|         raise NotImplementedError | ||||
|     def GetTextColour(self): | ||||
|         return self.textcolor | ||||
| @@ -177,19 +185,20 @@ class Painter: | ||||
|         return self.linebrush | ||||
|     def OnMouse(self, evt): | ||||
|         if evt.LeftDClick(): | ||||
|             x, y = self.tree.CalcUnscrolledPosition(evt.GetX(), evt.GetY()) | ||||
|             for item in self.rectangles: | ||||
|                 if item[1].contains((evt.GetX(), evt.GetY())): | ||||
|                 if item[1].contains((x,y)): | ||||
|                     self.tree.Edit(item[0].data) | ||||
|                     self.tree.OnNodeClick(item[0], evt) | ||||
|                     return | ||||
|         elif evt.ButtonDown(): | ||||
|             #self.oldpos = (evt.GetX(), evt.GetY()) | ||||
|             x, y = self.tree.CalcUnscrolledPosition(evt.GetX(), evt.GetY()) | ||||
|             for item in self.rectangles: | ||||
|                 if item[1].contains((evt.GetX(), evt.GetY())): | ||||
|                 if item[1].contains((x, y)): | ||||
|                     self.tree.OnNodeClick(item[0], evt) | ||||
|                     return | ||||
|             for item in self.knobs: | ||||
|                 if item[1].contains((evt.GetX(), evt.GetY())): | ||||
|                 if item[1].contains((x, y)): | ||||
|                     self.tree.OnKnobClick(item[0]) | ||||
|                     return | ||||
|         evt.Skip() | ||||
| @@ -331,7 +340,8 @@ class FileEditor(Editor): | ||||
|         self.editcomp.SetSelection(0, len(node.fileName)) | ||||
|         self.editcomp.SetFocus() | ||||
|         self.treenode = treenode | ||||
|         EVT_KEY_DOWN(self.editcomp, self._key) | ||||
| #        EVT_KEY_DOWN(self.editcomp, self._key) | ||||
|         EVT_KEY_UP(self.editcomp, self._key) | ||||
|         EVT_LEFT_DOWN(self.editcomp, self._mdown) | ||||
|         self.editcomp.CaptureMouse() | ||||
|  | ||||
| @@ -347,10 +357,11 @@ class FileEditor(Editor): | ||||
|                 os.rename(node.path + os.sep + node.fileName, node.path + os.sep + self.editcomp.GetValue()) | ||||
|                 node.fileName = self.editcomp.GetValue() | ||||
|             except: | ||||
|                 import traceback;traceback.print_exc() | ||||
|                 traceback.print_exc() | ||||
|         self.editcomp.ReleaseMouse() | ||||
|         self.editcomp.Destroy() | ||||
|         del self.editcomp | ||||
|         self.tree.Refresh() | ||||
|  | ||||
|  | ||||
|     def _key(self, evt): | ||||
| @@ -364,7 +375,6 @@ class FileEditor(Editor): | ||||
|     def _mdown(self, evt): | ||||
|         if evt.IsButton(): | ||||
|             pos = evt.GetPosition() | ||||
|             print pos.x, pos.y | ||||
|             edsize = self.editcomp.GetSize() | ||||
|             if pos.x < 0 or pos.y < 0 or pos.x > edsize.width or pos.y > edsize.height: | ||||
|                 self.EndEdit(false) | ||||
| @@ -416,7 +426,6 @@ class LateFSTreeModel(FSTreeModel): | ||||
|         import string | ||||
|         name = string.split(path, os.sep)[-1] | ||||
|         pathpart = path[:-len(name)] | ||||
|         print pathpart | ||||
|         fw = FileWrapper(pathpart, name) | ||||
|         self._Build(path, fw) | ||||
|         self.SetRoot(fw) | ||||
| @@ -445,11 +454,19 @@ class StrTextConverter(TextConverter): | ||||
|         return str(node.data) | ||||
|  | ||||
| class NullTransform(Transform): | ||||
|     def GetSize(self): | ||||
|         return tuple(self.size) | ||||
|  | ||||
|     def transform(self, node, offset, rotation): | ||||
|         self.size = [0,0] | ||||
|         list = self.tree.GetLayoutEngine().GetNodeList() | ||||
|         for node in list: | ||||
|             node.projx = node.x + offset[0] | ||||
|             node.projy = node.y + offset[1] | ||||
|         for kid in node.kids: | ||||
|             self.transform(kid, offset, rotation) | ||||
|             if node.projx > self.size[0]: | ||||
|                 self.size[0] = node.projx | ||||
|             if node.projy > self.size[1]: | ||||
|                 self.size[1] = node.projy | ||||
|  | ||||
| class Rect: | ||||
|     def __init__(self, x, y, width, height): | ||||
| @@ -485,17 +502,27 @@ class TreeLayout(LayoutEngine): | ||||
|         LayoutEngine.__init__(self, tree) | ||||
|         self.NODE_STEP = 20 | ||||
|         self.NODE_HEIGHT = 20 | ||||
|         self.nodelist = [] | ||||
|  | ||||
|     def layout(self, node): | ||||
|         self.nodelist = [] | ||||
|         self.layoutwalk(node) | ||||
|  | ||||
|     def GetNodeList(self): | ||||
|         return self.nodelist | ||||
|  | ||||
|     def layoutwalk(self, node): | ||||
|         if node == self.tree.currentRoot: | ||||
|             node.level = 1 | ||||
|             self.lastY = (-self.NODE_HEIGHT) | ||||
|         node.x = self.NODE_STEP * node.level | ||||
|         node.y = self.lastY + self.NODE_HEIGHT | ||||
|         self.lastY = node.y | ||||
|         self.nodelist.append(node) | ||||
|         if node.expanded: | ||||
|             for kid in node.kids: | ||||
|                 kid.level = node.level + 1 | ||||
|                 self.layout(kid) | ||||
|                 self.layoutwalk(kid) | ||||
|  | ||||
| class TreePainter(Painter): | ||||
|     """ | ||||
| @@ -515,7 +542,7 @@ class TreePainter(Painter): | ||||
|         self.textConverter = textConverter | ||||
|         self.charWidths = [] | ||||
|  | ||||
|     def paint(self, dc, node): | ||||
|     def paint(self, dc, node, doubleBuffered=1, paintBackground=1): | ||||
|         if not self.charWidths: | ||||
|             self.charWidths = [] | ||||
|             for i in range(25): | ||||
| @@ -530,15 +557,41 @@ class TreePainter(Painter): | ||||
|         self.fgbrush = wxBrush(self.GetForegroundColour(), wxSOLID) | ||||
|         self.bgbrush = wxBrush(self.GetBackgroundColour(), wxSOLID) | ||||
|         self.linebrush = wxPen(self.GetLineColour(), 1, wxSOLID) | ||||
|         self.rectangles = [] | ||||
|         self.knobs = [] | ||||
|         treesize = self.tree.GetSize() | ||||
|         size = self.tree.transform.GetSize() | ||||
|         size = (max(treesize.width, size[0]+50), max(treesize.height, size[1]+50)) | ||||
|         dc.BeginDrawing() | ||||
|         if doubleBuffered: | ||||
|             mem_dc = wxMemoryDC() | ||||
|             if not self.GetBuffer(): | ||||
|                 self.knobs = [] | ||||
|                 self.rectangles = [] | ||||
|                 self.bmp = wxEmptyBitmap(size[0], size[1]) | ||||
|                 mem_dc.SelectObject(self.GetBuffer()) | ||||
|                 mem_dc.SetPen(self.GetBackgroundPen()) | ||||
|                 mem_dc.SetBrush(self.GetBackgroundBrush()) | ||||
|                 mem_dc.DrawRectangle(0, 0, size[0], size[1]) | ||||
|                 mem_dc.SetFont(self.tree.GetFont()) | ||||
|                 self.paintWalk(node, mem_dc) | ||||
|             else: | ||||
|                 mem_dc.SelectObject(self.GetBuffer()) | ||||
|             xstart, ystart = self.tree.CalcUnscrolledPosition(0,0) | ||||
|             size = self.tree.GetClientSizeTuple() | ||||
|             dc.Blit(xstart, ystart, size[0], size[1], mem_dc, xstart, ystart) | ||||
|         else: | ||||
|             if node == self.tree.currentRoot: | ||||
|                 self.knobs = [] | ||||
|                 self.rectangles = [] | ||||
|             dc.SetPen(self.GetBackgroundPen()) | ||||
|             dc.SetBrush(self.GetBackgroundBrush()) | ||||
|         size = self.tree.GetSize() | ||||
|         dc.DrawRectangle(0, 0, size.width, size.height) | ||||
|             dc.SetFont(self.tree.GetFont()) | ||||
|             if paintBackground: | ||||
|                 dc.DrawRectangle(0, 0, size[0], size[1]) | ||||
|             if node: | ||||
|             self.paintWalk(node, dc) | ||||
|                 #Call with not paintBackground because if we are told not to paint the | ||||
|                 #whole background, we have to paint in parts to undo selection coloring. | ||||
|                 pb = paintBackground | ||||
|                 self.paintWalk(node, dc, not pb) | ||||
|         dc.EndDrawing() | ||||
|  | ||||
|     def GetDashPen(self): | ||||
| @@ -548,9 +601,28 @@ class TreePainter(Painter): | ||||
|         Painter.SetLinePen(self, pen) | ||||
|         self.dashpen = wxPen(pen.GetColour(), 1, wxDOT) | ||||
|  | ||||
|     def drawBox(self, px, py, node, dc): | ||||
|         if self.tree.model.IsLeaf(node.data) or ((node.expanded or not self.tree._assumeChildren) and not len(node.kids)): | ||||
|             return | ||||
|     def paintWalk(self, node, dc, paintRects=0): | ||||
|         self.linePainter.paint(node.parent, node, dc) | ||||
|         self.nodePainter.paint(node, dc, drawRects = paintRects) | ||||
|         if node.expanded: | ||||
|             for kid in node.kids: | ||||
|                 if not self.paintWalk(kid, dc, paintRects): | ||||
|                     return false | ||||
|             for kid in node.kids: | ||||
|                 px = (kid.projx - self.tree.layout.NODE_STEP) + 5 | ||||
|                 py = kid.projy + kid.height/2 | ||||
|                 if (not self.tree.model.IsLeaf(kid.data)) or ((kid.expanded or self.tree._assumeChildren) and len(kid.kids)): | ||||
|                     dc.SetPen(self.linepen) | ||||
|                     dc.SetBrush(self.bgbrush) | ||||
|                     dc.DrawRectangle(px -4, py-4, 9, 9) | ||||
|                     self.knobs.append(kid, Rect(px -4, py -4, 9, 9)) | ||||
|                     dc.SetPen(self.textpen) | ||||
|                     if not kid.expanded: | ||||
|                         dc.DrawLine(px, py -2, px, py + 3) | ||||
|                     dc.DrawLine(px -2, py, px + 3, py) | ||||
|         if node == self.tree.currentRoot: | ||||
|             px = (node.projx - self.tree.layout.NODE_STEP) + 5 | ||||
|             py = node.projy + node.height/2 | ||||
|             dc.SetPen(self.linepen) | ||||
|             dc.SetBrush(self.bgbrush) | ||||
|             dc.DrawRectangle(px -4, py-4, 9, 9) | ||||
| @@ -559,49 +631,27 @@ class TreePainter(Painter): | ||||
|             if not node.expanded: | ||||
|                 dc.DrawLine(px, py -2, px, py + 3) | ||||
|             dc.DrawLine(px -2, py, px + 3, py) | ||||
|  | ||||
|     def paintWalk(self, node, dc): | ||||
|         self.linePainter.paint(node.parent, node, dc) | ||||
|         self.nodePainter.paint(node, dc) | ||||
|         if node.expanded: | ||||
|             for kid in node.kids: | ||||
|                 if not self.paintWalk(kid, dc): | ||||
|                     return false | ||||
|             for kid in node.kids: | ||||
|                 px = (kid.projx - self.tree.layout.NODE_STEP) + 5 | ||||
|                 py = kid.projy + kid.height/2 | ||||
|                 self.drawBox(px, py, kid, dc) | ||||
|         if node == self.tree.currentRoot: | ||||
|             px = (node.projx - self.tree.layout.NODE_STEP) + 5 | ||||
|             py = node.projy + node.height/2 | ||||
|             self.drawBox(px, py, node, dc) | ||||
|         return true | ||||
|  | ||||
|     def OnMouse(self, evt): | ||||
|         Painter.OnMouse(self, evt) | ||||
|  | ||||
| class TreeNodePainter(NodePainter): | ||||
|     def paint(self, node, dc, location = None): | ||||
|     def paint(self, node, dc, location = None, drawRects = 0): | ||||
|         text = self.painter.textConverter.convert(node) | ||||
|         extent = dc.GetTextExtent(text) | ||||
|         node.width = extent[0] | ||||
|         node.height = extent[1] | ||||
|         if node == self.painter.tree.currentRoot: | ||||
|             self.painter.minx = self.painter.maxx = self.painter.miny = self.painter.maxy = 0 | ||||
|         if node.projx < self.painter.minx: | ||||
|             self.painter.minx = node.projx | ||||
|         elif node.projx + node.width > self.painter.maxx: | ||||
|             self.painter.maxx = node.projx + node.width | ||||
|         if node.projy < self.painter.miny: | ||||
|             self.painter.miny = node.projy | ||||
|         elif node.projy + node.height > self.painter.maxy: | ||||
|             self.painter.maxy = node.projy + node.height | ||||
|         if node.selected: | ||||
|             dc.SetPen(self.painter.GetLinePen()) | ||||
|             dc.SetBrush(self.painter.GetForegroundBrush()) | ||||
|             dc.SetTextForeground(wxNamedColour("WHITE")) | ||||
|             dc.DrawRectangle(node.projx -1, node.projy -1, node.width + 3, node.height + 3) | ||||
|         else: | ||||
|             if drawRects: | ||||
|                 dc.SetBrush(self.painter.GetBackgroundBrush()) | ||||
|                 dc.SetPen(self.painter.GetBackgroundPen()) | ||||
|                 dc.DrawRectangle(node.projx -1, node.projy -1, node.width + 3, node.height + 3) | ||||
|             dc.SetTextForeground(self.painter.GetTextColour()) | ||||
|         dc.DrawText(text, node.projx, node.projy) | ||||
|         self.painter.rectangles.append((node, Rect(node.projx, node.projy, node.width, node.height))) | ||||
| @@ -612,7 +662,7 @@ class TreeLinePainter(LinePainter): | ||||
|         px = py = cx = cy = 0 | ||||
|         if parent is None or child == self.painter.tree.currentRoot: | ||||
|             px = (child.projx - self.painter.tree.layout.NODE_STEP) + 5 | ||||
|             py = child.projy + self.painter.tree.layout.NODE_HEIGHT/2 | ||||
|             py = child.projy + self.painter.tree.layout.NODE_HEIGHT/2 -2 | ||||
|             cx = child.projx | ||||
|             cy = py | ||||
|             dc.DrawLine(px, py, cx, cy) | ||||
| @@ -620,7 +670,7 @@ class TreeLinePainter(LinePainter): | ||||
|             px = parent.projx + 5 | ||||
|             py = parent.projy + parent.height | ||||
|             cx = child.projx -5 | ||||
|             cy = child.projy + self.painter.tree.layout.NODE_HEIGHT/2 | ||||
|             cy = child.projy + self.painter.tree.layout.NODE_HEIGHT/2 -3 | ||||
|             dc.DrawLine(px, py, px, cy) | ||||
|             dc.DrawLine(px, cy, cx, cy) | ||||
|  | ||||
| @@ -671,27 +721,35 @@ class wxMVCTreeEvent(wxPyCommandEvent): | ||||
|         self.node = node | ||||
|         self.nodes = nodes | ||||
|         self.keyEvent = keyEvent | ||||
|  | ||||
|     def GetNode(self): | ||||
|         return self.node | ||||
|     def GetNodes(self): | ||||
|         return self.nodes | ||||
|     def getKeyEvent(self): | ||||
|         return self.keyEvent | ||||
|  | ||||
| class wxMVCTreeNotifyEvent(wxMVCTreeEvent): | ||||
|     def __init__(self, type, id, node = None, nodes = None, **kwargs): | ||||
|         apply(wxMVCTreeEvent.__init__, (self, type, id), kwargs) | ||||
|         self.notify = wxNotifyEvent(type, id) | ||||
|     def getNotifyEvent(self): | ||||
|         return self.notify | ||||
|  | ||||
| class wxMVCTree(wxWindow): | ||||
| class wxMVCTree(wxScrolledWindow): | ||||
|     """ | ||||
|     The main mvcTree class. | ||||
|     The main mvc tree class. | ||||
|     """ | ||||
|     def __init__(self, parent, id, model = None, layout = None, transform = None, | ||||
|                  painter = None, *args, **kwargs): | ||||
|         apply(wxWindow.__init__, (self, parent, id), kwargs) | ||||
|         apply(wxScrolledWindow.__init__, (self, parent, id), kwargs) | ||||
|         self.nodemap = {} | ||||
|         self._multiselect = false | ||||
|         self._selections = [] | ||||
|         self._assumeChildren = false | ||||
|         self._scrollx = false | ||||
|         self._scrolly = false | ||||
|         self.doubleBuffered = true | ||||
|         self.doubleBuffered = false | ||||
|         self._lastPhysicalSize = self.GetSize() | ||||
|         self._editors = [] | ||||
|         if not model: | ||||
|             model = BasicTreeModel() | ||||
| @@ -708,8 +766,22 @@ class wxMVCTree(wxWindow): | ||||
|         self.painter = painter | ||||
|         self.SetFont(wxFont(9, wxDEFAULT, wxNORMAL, wxNORMAL, false)) | ||||
|         EVT_MOUSE_EVENTS(self, self.OnMouse) | ||||
|         EVT_SCROLLWIN(self, self.OnScroll) | ||||
|         EVT_KEY_DOWN(self, self.OnKeyDown) | ||||
|         self.doubleBuffered = true | ||||
|  | ||||
|     def Refresh(self): | ||||
|         if self.doubleBuffered: | ||||
|             self.painter.ClearBuffer() | ||||
|         wxScrolledWindow.Refresh(self) | ||||
|  | ||||
|     def GetPainter(self): | ||||
|         return self.painter | ||||
|  | ||||
|     def GetLayoutEngine(self): | ||||
|         return self.layout | ||||
|  | ||||
|     def GetTransform(self): | ||||
|         return self.transform | ||||
|  | ||||
|     def __repr__(self): | ||||
|         return "<wxMVCTree instance at %s>" % str(hex(id(self))) | ||||
| @@ -720,15 +792,16 @@ class wxMVCTree(wxWindow): | ||||
|     def NodeAdded(self, parent, child): | ||||
|         e = wxMVCTreeEvent(wxEVT_MVCTREE_ADD_ITEM, self.GetId(), node = child, nodes = [parent, child]) | ||||
|         self.GetEventHandler().ProcessEvent(e) | ||||
|         self.painter.ClearBuffer() | ||||
|  | ||||
|     def NodeInserted(self, parent, child, index): | ||||
|         e = wxMVCTreeEvent(wxEVT_MVCTREE_ADD_ITEM, self.GetId(), node = child, nodes = [parent, child]) | ||||
|         self.GetEventHandler().ProcessEvent(e) | ||||
|  | ||||
|         self.painter.ClearBuffer() | ||||
|     def NodeRemoved(self, node): | ||||
|         e = wxMVCTreeEvent(wxEVT_MVCTREE_DELETE_ITEM, self.GetId(), node = child, nodes = [parent, child]) | ||||
|         self.GetEventHandler().ProcessEvent(e) | ||||
|  | ||||
|         self.painter.ClearBuffer() | ||||
|     def OnKeyDown(self, evt): | ||||
|         e = wxMVCTreeEvent(wxEVT_MVCTREE_KEY_DOWN, self.GetId(), keyEvent = evt) | ||||
|         self.GetEventHandler().ProcessEvent(e) | ||||
| @@ -738,7 +811,7 @@ class wxMVCTree(wxWindow): | ||||
|         dc = wxClientDC(self) | ||||
|         dc.SetFont(font) | ||||
|         self.layout.SetHeight(dc.GetTextExtent("")[1] + 18) | ||||
|  | ||||
|         self.painter.ClearBuffer() | ||||
|     def GetFont(self): | ||||
|         return self.painter.GetFont() | ||||
|  | ||||
| @@ -752,11 +825,10 @@ class wxMVCTree(wxWindow): | ||||
|         self.painter.OnMouse(evt) | ||||
|  | ||||
|     def OnNodeClick(self, node, mouseEvent): | ||||
|         if node.selected: | ||||
|         if node.selected and (self.IsMultiSelect() and mouseEvent.ControlDown()): | ||||
|             self.RemoveFromSelection(node.data) | ||||
|         else: | ||||
|             self.AddToSelection(node.data, mouseEvent.ControlDown()) | ||||
|         self.Refresh() | ||||
|             self.AddToSelection(node.data, mouseEvent.ControlDown(), mouseEvent.ShiftDown()) | ||||
|  | ||||
|     def OnKnobClick(self, node): | ||||
|         self.SetExpanded(node.data, not node.expanded) | ||||
| @@ -793,6 +865,7 @@ class wxMVCTree(wxWindow): | ||||
|         self.currentRoot = self.layoutRoot | ||||
|         self.offset = [0,0] | ||||
|         self.rotation = 0 | ||||
|         self._scrollset = None | ||||
|         self.Refresh() | ||||
|  | ||||
|     def GetCurrentRoot(self): | ||||
| @@ -817,12 +890,11 @@ class wxMVCTree(wxWindow): | ||||
|         pass | ||||
|  | ||||
|     def OnSize(self, evt): | ||||
|         try: | ||||
|             size = self.GetSizeTuple() | ||||
|             self.center = (size[0]/2, size[1]/2) | ||||
|             del self.bmp | ||||
|         except: | ||||
|             pass | ||||
|         size = self.GetSize() | ||||
|         self.center = (size.width/2, size.height/2) | ||||
|         if self._lastPhysicalSize.width < size.width or self._lastPhysicalSize.height < size.height: | ||||
|             self.painter.ClearBuffer() | ||||
|         self._lastPhysicalSize = size | ||||
|  | ||||
|     def GetSelection(self): | ||||
|         "Returns a tuple of selected nodes." | ||||
| @@ -909,7 +981,7 @@ class wxMVCTree(wxWindow): | ||||
|     def IsExpanded(self, node): | ||||
|         return self.nodemap[node].expanded | ||||
|  | ||||
|     def AddToSelection(self, nodeOrTuple, enableMulti = true): | ||||
|     def AddToSelection(self, nodeOrTuple, enableMulti = true, shiftMulti = false): | ||||
|         nodeTuple = nodeOrTuple | ||||
|         if type(nodeOrTuple)!= type(()): | ||||
|             nodeTuple = (nodeOrTuple,) | ||||
| @@ -917,14 +989,34 @@ class wxMVCTree(wxWindow): | ||||
|         self.GetEventHandler().ProcessEvent(e) | ||||
|         if not e.notify.IsAllowed(): | ||||
|             return | ||||
|         if not self.IsMultiSelect() or not enableMulti: | ||||
|         changeparents = [] | ||||
|         if not (self.IsMultiSelect() and (enableMulti or shiftMulti)): | ||||
|             for node in self._selections: | ||||
|                 treenode = self.nodemap[node] | ||||
|                 treenode.selected = false | ||||
|                 changeparents.append(treenode) | ||||
|             node = nodeTuple[0] | ||||
|             self._selections = [node] | ||||
|             treenode = self.nodemap[node] | ||||
|             changeparents.append(treenode) | ||||
|             treenode.selected = true | ||||
|         else: | ||||
|             if shiftMulti: | ||||
|                 for node in nodeTuple: | ||||
|                     treenode = self.nodemap[node] | ||||
|                     oldtreenode = self.nodemap[self._selections[0]] | ||||
|                     if treenode.parent == oldtreenode.parent: | ||||
|                         found = 0 | ||||
|                         for kid in oldtreenode.parent.kids: | ||||
|                             if kid == treenode or kid == oldtreenode: | ||||
|                                 found = not found | ||||
|                                 kid.selected = true | ||||
|                                 self._selections.append(kid.data) | ||||
|                                 changeparents.append(kid) | ||||
|                             elif found: | ||||
|                                 kid.selected = true | ||||
|                                 self._selections.append(kid.data) | ||||
|                                 changeparents.append(kid) | ||||
|             else: | ||||
|                 for node in nodeTuple: | ||||
|                     try: | ||||
| @@ -933,22 +1025,32 @@ class wxMVCTree(wxWindow): | ||||
|                         self._selections.append(node) | ||||
|                         treenode = self.nodemap[node] | ||||
|                         treenode.selected = true | ||||
|                         changeparents.append(treenode) | ||||
|         e = wxMVCTreeEvent(wxEVT_MVCTREE_SEL_CHANGED, self.GetId(), nodeTuple[0], nodes = nodeTuple) | ||||
|         self.GetEventHandler().ProcessEvent(e) | ||||
|  | ||||
|         dc = wxClientDC(self) | ||||
|         self.PrepareDC(dc) | ||||
|         for node in changeparents: | ||||
|             if node: | ||||
|                 self.painter.paint(dc, node, doubleBuffered = 0, paintBackground = 0) | ||||
|         self.painter.ClearBuffer() | ||||
|     def RemoveFromSelection(self, nodeTuple): | ||||
|         if type(nodeTuple) != type(()): | ||||
|             nodeTuple = (nodeTuple,) | ||||
|         changeparents = [] | ||||
|         for node in nodeTuple: | ||||
|             try: | ||||
|                 self._selections.index(node) | ||||
|             except IndexError: | ||||
|             self._selections.remove(node) | ||||
|             treenode = self.nodemap[node] | ||||
|                 node.selected = false | ||||
|             changeparents.append(treenode) | ||||
|             treenode.selected = false | ||||
|         e = wxMVCTreeEvent(wxEVT_MVCTREE_SEL_CHANGED, self.GetId(), node, nodes = nodeTuple) | ||||
|         self.GetEventHandler().ProcessEvent(e) | ||||
|  | ||||
|         dc = wxClientDC(self) | ||||
|         self.PrepareDC(dc) | ||||
|         for node in changeparents: | ||||
|             if node: | ||||
|                 self.painter.paint(dc, node, doubleBuffered = 0, paintBackground = 0) | ||||
|         self.painter.ClearBuffer() | ||||
|  | ||||
|  | ||||
|     def GetBackgroundColour(self): | ||||
| @@ -978,126 +1080,39 @@ class wxMVCTree(wxWindow): | ||||
|     def GetAssumeChildren(self): | ||||
|         return self._assumeChildren | ||||
|  | ||||
|     def OnScroll(self, evt): | ||||
|         type = evt.GetEventType() | ||||
|         field = [self.painter.maxx - self.painter.minx, self.painter.maxy - self.painter.miny] | ||||
|         size = self.GetSizeTuple() | ||||
|         index = 1 | ||||
|         if evt.GetOrientation() == wxHORIZONTAL: | ||||
|             index = 0 | ||||
|             self._scrollx = true | ||||
|         else: | ||||
|             self._scrolly = true | ||||
|             index = 1 | ||||
|         if type ==  wxEVT_SCROLLWIN_TOP: | ||||
|             self.offset[index] = 0 | ||||
|         elif type == wxEVT_SCROLLWIN_LINEUP: | ||||
|             self.offset[index] = self.offset[index] + 1 | ||||
|         elif type == wxEVT_SCROLLWIN_LINEDOWN: | ||||
|             self.offset[index] = self.offset[index] - 1 | ||||
|         elif type == wxEVT_SCROLLWIN_PAGEUP: | ||||
|             self.offset[index] = self.offset[index] + int(20 * float(field[index])/float(size[index])) | ||||
|         elif type == wxEVT_SCROLLWIN_PAGEDOWN: | ||||
|             self.offset[index] = self.offset[index] - int(20 * float(field[index])/float(size[index])) | ||||
|         elif type == wxEVT_SCROLLWIN_THUMBTRACK: | ||||
|             self.offset[index] = -(evt.GetPosition()) | ||||
|         elif type == wxEVT_SCROLLWIN_BOTTOM: | ||||
|             self.offset[index] = field[index] | ||||
|         self.transformed = false | ||||
|         self.Refresh() | ||||
|  | ||||
|     def OnPaint(self, evt): | ||||
|         """ | ||||
|         Ensures that the tree has been laid out and transformed, then calls the painter | ||||
|         to paint the control. | ||||
|         """ | ||||
|         try: | ||||
|             self.EnableScrolling(false, false) | ||||
|             if not self.laidOut: | ||||
|                 self.layout.layout(self.currentRoot) | ||||
|                 self.laidOut = true | ||||
|                 self.transformed = false | ||||
|             if not self.transformed: | ||||
|                 self.transform.transform(self.currentRoot, self.offset, self.rotation) | ||||
|                 self.transformed = true | ||||
|             dc = wxPaintDC(self) | ||||
|             dc.SetFont(self.GetFont()) | ||||
|             if self.doubleBuffered: | ||||
|                 size = self.GetSize() | ||||
|                 if not hasattr(self, 'bmp'): | ||||
|                     self.bmp = bmp =wxEmptyBitmap(size.width, size.height) | ||||
|                 else: | ||||
|                     bmp = self.bmp | ||||
|                 mem_dc = wxMemoryDC() | ||||
|                 mem_dc.SetFont(self.GetFont()) | ||||
|                 mem_dc.SelectObject(bmp) | ||||
|                 self.painter.paint(mem_dc, self.currentRoot) | ||||
|                 dc.Blit(0, 0, size.width, size.height, mem_dc, 0, 0); | ||||
|             else: | ||||
|                 self.painter.paint(dc, self.currentRoot) | ||||
|             tsize = None | ||||
|             tsize = list(self.transform.GetSize()) | ||||
|             tsize[0] = tsize[0] + 50 | ||||
|             tsize[1] = tsize[1] + 50 | ||||
|             size = self.GetSizeTuple() | ||||
|             if self._scrollx or self.painter.minx < 0 or self.painter.maxx > size[0]: | ||||
|                 field = self.painter.maxx - self.painter.minx | ||||
|                 self.SetScrollbar(wxHORIZONTAL, -self.offset[0], size[0]/field, field, true) | ||||
|                 self._scrollx = false | ||||
|             if self._scrolly or self.painter.miny < 0 or self.painter.maxy > size[1]: | ||||
|                 field = self.painter.maxy - self.painter.miny | ||||
|                 self.SetScrollbar(wxVERTICAL, -self.offset[1], size[1]/field, field, true) | ||||
|                 self._scrolly = false | ||||
|             if tsize[0] > size[0] or tsize[1] > size[1]: | ||||
|                 if not hasattr(self, '_oldsize') or (tsize[0] > self._oldsize[0] or tsize[1] > self._oldsize[1]): | ||||
|                     self._oldsize = tsize | ||||
|                     oldstart = self.ViewStart() | ||||
|                     self._lastPhysicalSize = self.GetSize() | ||||
|                     self.SetScrollbars(10, 10, tsize[0]/10, tsize[1]/10) | ||||
|                     self.Scroll(oldstart[0], oldstart[1]) | ||||
|             dc = wxPaintDC(self) | ||||
|             self.PrepareDC(dc) | ||||
|             dc.SetFont(self.GetFont()) | ||||
|             self.painter.paint(dc, self.currentRoot, self.doubleBuffered) | ||||
|         except: | ||||
|             import traceback;traceback.print_exc() | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     def exit(evt): | ||||
|         import sys;sys.exit() | ||||
|  | ||||
|     block = 0 | ||||
|  | ||||
|     def selchanging(evt): | ||||
|         print "SelChanging!" | ||||
|         print evt.node | ||||
|         global block | ||||
|         if block: | ||||
|             evt.notify.Veto() | ||||
|         block = not block | ||||
|  | ||||
|     def selchanged(evt): | ||||
|         print "SelChange!" | ||||
|         print evt.node | ||||
|     def expanded(evt): | ||||
|         print "Expanded!" | ||||
|     def closed(evt): | ||||
|         print "Closed!" | ||||
|     def key(evt): | ||||
|         print "Key" | ||||
|     def add(evt): | ||||
|         print "Add" | ||||
|     def delitem(evt): | ||||
|         print "Delete" | ||||
|  | ||||
|     class MyApp(wxApp): | ||||
|         def OnInit(self): | ||||
|             f = wxFrame(NULL, -1, "wxMVCTree") | ||||
|             p = None | ||||
|             p = wxMVCTree(f, -1) | ||||
|             p.SetAssumeChildren(true) | ||||
|             if len(sys.argv) > 1: | ||||
|                 p.SetModel(LateFSTreeModel(sys.argv[1])) | ||||
|                 p.AddEditor(FileEditor(p)) | ||||
|             p.SetMultiSelect(true) | ||||
|             f.Show(true) | ||||
|             EVT_CLOSE(f, exit) | ||||
|             EVT_MVCTREE_SEL_CHANGED(p, p.GetId(), selchanged) | ||||
|             EVT_MVCTREE_SEL_CHANGING(p, p.GetId(), selchanging) | ||||
|             EVT_MVCTREE_ITEM_EXPANDED(p, p.GetId(), expanded) | ||||
|             EVT_MVCTREE_ITEM_COLLAPSED(p, p.GetId(), closed) | ||||
|             EVT_MVCTREE_ADD_ITEM(p, p.GetId(), add) | ||||
|             EVT_MVCTREE_DELETE_ITEM(p, p.GetId(), delitem) | ||||
|             EVT_MVCTREE_KEY_DOWN(p, p.GetId(), key) | ||||
|             p.SetForegroundColour(wxNamedColour("GREEN")) | ||||
|             self.SetTopWindow(f) | ||||
|             return true | ||||
|  | ||||
|     app = MyApp(false) | ||||
|     app.MainLoop() | ||||
|             traceback.print_exc() | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -109,49 +109,50 @@ class wxVTKRenderWindow(wxWindow): | ||||
|  | ||||
|  | ||||
|  | ||||
| ## testcode is now in the demo ## | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     class TestFrame(wxFrame): | ||||
|         def __init__(self, parent): | ||||
|             wxFrame.__init__(self, parent, -1, "VTK Test", size=(450,450)) | ||||
| ## if __name__ == '__main__': | ||||
| ##     class TestFrame(wxFrame): | ||||
| ##         def __init__(self, parent): | ||||
| ##             wxFrame.__init__(self, parent, -1, "VTK Test", size=(450,450)) | ||||
|  | ||||
|             rw = wxVTKRenderWindow(self, -1) | ||||
| ##             rw = wxVTKRenderWindow(self, -1) | ||||
|  | ||||
|             # Get the render window | ||||
|             renWin = rw.GetRenderWindow() | ||||
| ##             # Get the render window | ||||
| ##             renWin = rw.GetRenderWindow() | ||||
|  | ||||
|             # Next, do the VTK stuff | ||||
|             ren = vtkRenderer() | ||||
|             renWin.AddRenderer(ren) | ||||
|             cone = vtkConeSource() | ||||
|             cone.SetResolution(80) | ||||
|             coneMapper = vtkPolyDataMapper() | ||||
|             coneMapper.SetInput(cone.GetOutput()) | ||||
|             coneActor = vtkActor() | ||||
|             coneActor.SetMapper(coneMapper) | ||||
|             ren.AddActor(coneActor) | ||||
|             coneMapper.GetLookupTable().Build() | ||||
| ##             # Next, do the VTK stuff | ||||
| ##             ren = vtkRenderer() | ||||
| ##             renWin.AddRenderer(ren) | ||||
| ##             cone = vtkConeSource() | ||||
| ##             cone.SetResolution(80) | ||||
| ##             coneMapper = vtkPolyDataMapper() | ||||
| ##             coneMapper.SetInput(cone.GetOutput()) | ||||
| ##             coneActor = vtkActor() | ||||
| ##             coneActor.SetMapper(coneMapper) | ||||
| ##             ren.AddActor(coneActor) | ||||
| ##             coneMapper.GetLookupTable().Build() | ||||
|  | ||||
|             # Create a scalar bar | ||||
|             scalarBar = vtkScalarBarActor() | ||||
|             scalarBar.SetLookupTable(coneMapper.GetLookupTable()) | ||||
|             scalarBar.SetTitle("Temperature") | ||||
|             scalarBar.GetPositionCoordinate().SetCoordinateSystemToNormalizedViewport() | ||||
|             scalarBar.GetPositionCoordinate().SetValue(0.1, 0.01) | ||||
|             scalarBar.SetOrientationToHorizontal() | ||||
|             scalarBar.SetWidth(0.8) | ||||
|             scalarBar.SetHeight(0.17) | ||||
|             ren.AddActor2D(scalarBar) | ||||
| ##             # Create a scalar bar | ||||
| ##             scalarBar = vtkScalarBarActor() | ||||
| ##             scalarBar.SetLookupTable(coneMapper.GetLookupTable()) | ||||
| ##             scalarBar.SetTitle("Temperature") | ||||
| ##             scalarBar.GetPositionCoordinate().SetCoordinateSystemToNormalizedViewport() | ||||
| ##             scalarBar.GetPositionCoordinate().SetValue(0.1, 0.01) | ||||
| ##             scalarBar.SetOrientationToHorizontal() | ||||
| ##             scalarBar.SetWidth(0.8) | ||||
| ##             scalarBar.SetHeight(0.17) | ||||
| ##             ren.AddActor2D(scalarBar) | ||||
|  | ||||
|  | ||||
|  | ||||
|     class TestApp(wxApp): | ||||
|         def OnInit(self): | ||||
|             f = TestFrame(None) | ||||
|             self.SetTopWindow(f) | ||||
|             f.Show(true) | ||||
|             return true | ||||
| ##     class TestApp(wxApp): | ||||
| ##         def OnInit(self): | ||||
| ##             f = TestFrame(None) | ||||
| ##             self.SetTopWindow(f) | ||||
| ##             f.Show(true) | ||||
| ##             return true | ||||
|  | ||||
|     app = TestApp(0) | ||||
|     app.MainLoop() | ||||
| ##     app = TestApp(0) | ||||
| ##     app.MainLoop() | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user