class LinkChart(Frame): def __init__(self, parent, scrollX=True, scrollY=True, *args, **kw): self.parent = parent self.scrollX = scrollX self.scrollY = scrollY self.prototypes = [] self.nodes = [] self.node = None # Selected self.links = [] self.link = None # Selected self.objDict = {} # Map canvas to nodes, links self.cornerDict = {} self.editWidget = None self.mouseOver = None self.inMotion = None self.inMotion2 = None self.inMotion3 = None self.dragRegion = None self._resizeBusy = False self.selectedNodes = [] self.selectedLinks = [] self.highlightNodes = [] self.highlightLinks = [] kw['relief'] = 'flat' kw['borderwidth'] = 0 Frame.__init__(self, parent, *args, **kw) self.canvas = canvas = Canvas(self, relief='flat', borderwidth=0) self.canvas.configure(xscrollincrement=2, yscrollincrement=2, bg=self.cget('bg')) self.canvas.grid(row=0, column=0, sticky='nsew') if scrollX: self.horizScrollbar = Tkinter.Scrollbar(self, bg=self.cget('bg'), command=self.canvas.xview, orient=Tkinter.HORIZONTAL, borderwidth=1) self.canvas.configure(xscrollcommand=self.horizScrollbar.set) if scrollY: self.vertScrollbar = Tkinter.Scrollbar(self, bg=self.cget('bg'), command=self.canvas.yview, orient=Tkinter.VERTICAL, borderwidth=1) self.canvas.configure(yscrollcommand=self.vertScrollbar.set) canvas.bind('<Button-1>', self._mouseSingleClick) if not isWindowsOS(): canvas.bind('<Button-4>', self.scrollUp) canvas.bind('<Button-5>', self.scrollDown) else: canvas.bind('<MouseWheel>', self._windowsOsScroll) canvas.bind('<Double-1>', self._mouseDoubleClick) canvas.bind('<B1-Motion>', self._mouseDrag) canvas.bind('<ButtonRelease-1>', self._mouseRelease) canvas.bind('<Motion>', self._mouseEnter) canvas.bind('<Enter>', self._mouseEnter) canvas.bind('<Leave>', self._mouseLeave) canvas.bind('<KeyPress-Up>', self._moveUp) canvas.bind('<KeyPress-Down>', self._moveDown) canvas.bind('<KeyPress-Right>', self._moveRight) canvas.bind('<KeyPress-Left>', self._moveLeft) canvas.bind('<KeyPress-Delete>', self.deleteSelected) self.bind('<Configure>', self._resizeAfter) self.menu = Menu(self, tearoff=False) alignItems = [{ 'kind': 'command', 'label': 'Y axis', 'command': self._alignHorizontal }, { 'kind': 'command', 'label': 'X axis', 'command': self._alignVertical }] distributeItems = [{ 'kind': 'command', 'label': 'Horizontal', 'command': self._distributeHorizontal }, { 'kind': 'command', 'label': 'Verical', 'command': self._distributeVertical }] menuItems = [ { 'kind': 'cascade', 'label': 'Align', 'submenu': alignItems }, #0 { 'kind': 'cascade', 'label': 'Distribute', 'submenu': distributeItems }, #1 { 'kind': 'command', 'label': 'Link Selected', 'command': self.linkSelected }, #2 { 'kind': 'command', 'label': 'Reset Links', 'command': self._resetLinks }, #3 { 'kind': 'separator' }, { 'kind': 'command', 'label': 'Delete Links', #5 'command': self.deleteLinks }, { 'kind': 'command', 'label': 'Delete Items', #6 'command': self.deleteNodes }, { 'kind': 'separator' }, { 'kind': 'command', 'label': 'Save PostScript File', 'command': self.printCanvas }, ] self.menuItems = menuItems self.menu.setMenuItems(menuItems) canvas.bind('<ButtonPress-3>', self._popupMenu) self._drawAfter() def _resizeAfter(self, event): if self._resizeBusy: return self._resizeBusy = True self._popdownMenu(event) self.after_idle(lambda: self._refreshScrollbars(event)) def _refreshScrollbars(self, event): bbox = self.canvas.bbox('all') if not bbox: self._resizeBusy = False return cWidth = event.width cHeight = event.height vertScrollbar = self.vertScrollbar horizScrollbar = self.horizScrollbar xSpace = bbox[2] + bbox[0] # Symmetric space ySpace = bbox[3] + bbox[1] horizHeight = int(horizScrollbar.winfo_height()) vertWidth = int(vertScrollbar.winfo_width()) if cHeight < ySpace + horizHeight: vertScrollbar.grid(row=0, column=1, sticky='ns') cWidth -= vertWidth else: vertScrollbar.grid_forget() if cWidth < xSpace + vertWidth: horizScrollbar.grid(row=1, column=0, sticky='ew') cHeight -= horizHeight else: horizScrollbar.grid_forget() x1 = 0 y1 = 0 x2 = max(cWidth, xSpace) y2 = max(cHeight, ySpace) self.canvas.configure(width=cWidth, height=cHeight) self.canvas.configure(scrollregion=(x1, y1, x2, y2)) self.update_idletasks() self._resizeBusy = False def _popupMenu(self, event): self._finishEdit() self._configMenu() self.menu.popupMenu(event) def _popdownMenu(self, event): self._finishEdit() self.menu.unpost() def _configMenu(self): if self.selectedLinks: self.menu.entryconfig(5, state=ACTIVE) self.menu.entryconfig(3, state=ACTIVE) else: self.menu.entryconfig(5, state=INACTIVE) self.menu.entryconfig(3, state=INACTIVE) if self.selectedNodes: self.menu.entryconfig(2, state=ACTIVE) self.menu.entryconfig(6, state=ACTIVE) else: self.menu.entryconfig(2, state=INACTIVE) self.menu.entryconfig(6, state=INACTIVE) if self.selectedNodes or self.selectedLinks: self.menu.entryconfig(0, state=ACTIVE) self.menu.entryconfig(1, state=ACTIVE) else: self.menu.entryconfig(0, state=INACTIVE) self.menu.entryconfig(1, state=INACTIVE) def _resetLinks(self): for link in self.selectedLinks: link.resetCoords() def _alignSelected(self, vertical=False, horizontal=False): obj = self.node or self.link if not obj: return x0, y0 = obj.coords objs = self.selectedNodes + self.selectedLinks n = len(objs) if n < 2: return if horizontal: for obj in objs: obj.move(obj.coords[0], y0) if vertical: for obj in objs: obj.move(x0, obj.coords[1]) def _distributeSelected(self, vertical=False, horizontal=False): xCoords = [] yCoords = [] objs = self.selectedNodes + self.selectedLinks n = len(objs) if n < 3: return for obj in objs: x, y = obj.coords xCoords.append((x, obj)) yCoords.append((y, obj)) if horizontal: xCoords.sort() xA = xCoords[0][0] dx = (xCoords[-1][0] - xA) / float(n - 1) for i, (x0, obj) in enumerate(xCoords): y = obj.coords[1] x = xA + (i * dx) obj.move(x, y) if vertical: yCoords.sort() yA = yCoords[0][0] dy = (yCoords[-1][0] - yA) / float(n - 1) for i, (y0, obj) in enumerate(yCoords): x = obj.coords[0] y = yA + (i * dy) obj.move(x, y) def _alignHorizontal(self): self._alignSelected(horizontal=True) def _alignVertical(self): self._alignSelected(vertical=True) def _distributeHorizontal(self): self._distributeSelected(horizontal=True) def _distributeVertical(self): self._distributeSelected(vertical=True) def linkSelected(self): nodes = self.selectedNodes n = len(nodes) if n > 1: for i in range(n - 1): nodeA = nodes[i] for j in range(i + 1, n): nodeB = nodes[j] for link in nodeA.links: if nodeB in link.nodes: #print link, link.nodes, nodeA, nodeB break else: link = Link(self, nodeA, nodeB, text='*') #print link def deleteNodes(self): if self.selectedNodes: msg = 'Really delete %d selected nodes?' % len(self.selectedNodes) if showOkCancel('Confirm', msg, parent=self): for node in self.selectedNodes[:]: node.delete() self.selectedNodes = [] self.node = None def deleteLinks(self): if self.selectedLinks: msg = 'Really delete %d selected links?' % len(self.selectedLinks) if showOkCancel('Confirm', msg, parent=self): for link in self.selectedLinks[:]: link.delete() self.selectedLinks = [] self.link = None def deleteSelected(self, event=None): objs = self.selectedLinks + self.selectedNodes if objs: n1 = len(self.selectedNodes) n2 = len(self.selectedLinks) msg = 'Really delete %d selected objects?\n' % (n1 + n2) msg += '(%d nodes, %d links)' % (n1, n2) if showOkCancel('Confirm', msg, parent=self): for obj in objs: obj.delete() self.selectedLinks = [] self.link = None self.selectedNodes = [] self.node = None def printCanvas(self, *event): from memops.gui.FileSelect import FileType from memops.gui.FileSelectPopup import FileSelectPopup fileTypes = [FileType('PostScript', ['*.ps']), FileType('All', ['*'])] fileSelectPopup = FileSelectPopup(self, file_types=fileTypes, title='Print canvas to file', dismiss_text='Cancel', selected_file_must_exist=False) fileName = fileSelectPopup.getFile() self.postscript(colormode='color', file=fileName) def _clearSelection(self): for node in self.selectedNodes: node.isSelected = False for link in self.selectedLinks: link.isSelected = False self.selectedNodes = [] self.selectedLinks = [] self.link = None self.node = None def _clearHighlights(self): for node in self.highlightNodes: node.highlight = False for link in self.highlightLinks: link.highlight = False self.mouseOver = None self.highlightNodes = [] self.highlightLinks = [] def _mouseSingleClick(self, event): self._mouseClick(event) def _mouseDoubleClick(self, event): self._mouseClick(event, True) def _mouseClick(self, event, edit=False): #self.menu.popdownMenu(event) self._finishEdit() canvas = event.widget canvas.focus_force() x = canvas.canvasx(event.x) y = canvas.canvasy(event.y) items = canvas.find('overlapping', x - 1, y - 1, x + 1, y + 1) #item = self.find('closest',x,y)[0] if not items: self._clearSelection() self._clearHighlights() self.draw() return objs = [ self.objDict.get(item) for item in items if self.objDict.get(item) ] for obj in objs: if isinstance(obj, Node): if not (event.state & 4 or event.state & 1): self._clearSelection() obj.isSelected = True self.node = obj if obj not in self.selectedNodes: self.selectedNodes.append(obj) break else: self.node = None for obj in objs: if isinstance(obj, Link): if not (event.state & 4 or event.state & 1): self._clearSelection() obj.isSelected = True self.link = obj if obj not in self.selectedLinks: self.selectedLinks.append(obj) break else: self.link = None self._drawAfter() if not obj: return if edit and obj.editWidget: obj.edit() self.editWidget = obj.editWidget def _clearDragRegion(self, canvas): if self.dragRegion: canvas.delete(self.dragRegion[0]) self.dragRegion = None def _mouseEnter(self, event): self.menu.popdownMenu(event) canvas = event.widget canvas.focus_force() x = canvas.canvasx(event.x) y = canvas.canvasy(event.y) items = canvas.find('overlapping', x - 1, y - 1, x + 1, y + 1) #item = self.find('closest',x,y)[0] if not items: self._clearHighlights() self.draw() return obj = self.objDict.get(items[0]) self._clearHighlights() if obj is None: self.draw() return if obj is self.mouseOver: if self.inMotion2: for item in items: if self.cornerDict.has_key(item): self.inMotion2 = None break return objs = [ self.objDict.get(item) for item in items if self.objDict.get(item) ] for obj in objs: if isinstance(obj, Node): self.highlightNodes = [ obj, ] break else: for obj in objs: if isinstance(obj, Link): self.highlightLinks = [ obj, ] obj.highlight = True self.mouseOver = obj self.draw() def _mouseLeave(self, event): #self._mouseRelease(event) #self._finishEdit() self._clearHighlights() self.draw() def _mouseDrag(self, event): self.menu.popdownMenu(event) canvas = event.widget canvas.focus_force() x = canvas.canvasx(event.x) y = canvas.canvasy(event.y) #width = int(canvas.winfo_width()) #height = int (canvas.winfo_height()) # #if not (0 < x < width) or not (0 < y < height): # self._mouseRelease(event) # return inMotion = self.inMotion objs = self.selectedNodes + self.selectedLinks if objs: items = canvas.find('overlapping', x - 1, y - 1, x + 1, y + 1) if not inMotion: for item in items: if self.inMotion2 or self.cornerDict.has_key(item): obj = self.node x1, y1 = obj.coords # offsets dx = abs(x - x1) dy = abs(y - y1) size = max(dx, dy) + 1 obj.width = size obj.draw() self.inMotion2 = True return if inMotion: offsets = inMotion # Get offsets else: offsets = [] for obj in objs: x1, y1 = obj.coords # offsets dx = x - x1 # Offset relative to centre dy = y - y1 if isinstance(obj, Node) and obj.isPrototype: obj = obj.copy() offsets.append((dx, dy, obj)) self.inMotion = offsets # Store click offset and Node #print event, event.widget._root() for dx, dy, obj in offsets: x2 = x - dx y2 = y - dy obj.move(x2, y2) x0, y0 = event.x_root, event.y_root under = self.winfo_containing(x0, y0) if isinstance(under, Canvas): under = under.parent if isinstance(under, LinkChart) and (under is not self): for dx, dy, obj in offsets: if not isinstance(obj, Node): continue original = obj.original if original and original.isPrototype: xT = x0 - under.winfo_rootx() - dx yT = y0 - under.winfo_rooty() - dy if original.spawn: original.spawn.move(xT, yT) else: original.spawn = original.copy(under, coords=(xT, yT)) else: for dx, dy, obj in offsets: if not isinstance(obj, Node): continue original = obj.original if original and original.spawn: original.spawn.delete() original.spawn = None else: if self.inMotion3: x0, y0, = self.inMotion3 else: #self._mouseRelease(event) x0 = x y0 = y self.inMotion3 = (x, y) if self.dragRegion: item = self.dragRegion[0] canvas.coords(item, x0, y0, x, y) else: item = canvas.create_rectangle(x0, y0, x, y, outline='#008000', fill='#00E000', stipple='gray25') self.dragRegion = (item, x0, y0, x, y) def _mouseRelease(self, event): canvas = event.widget if self.dragRegion: item, x1, y1, x2, y2 = self.dragRegion xCoords = [x1, x2] yCoords = [y1, y2] xCoords.sort() yCoords.sort() x1, x2 = xCoords y1, y2 = yCoords self.node = None self.selectedNodes = [] for node in self.nodes: x0, y0 = node.coords if (x1 < x0 < x2) and (y1 < y0 < y2): self.selectedNodes.append(node) node.isSelected = True self.node = node else: node.isSelected = False self.link = None self.selectedLinks = [] for link in self.links: x0, y0 = link.coords if (x1 < x0 < x2) and (y1 < y0 < y2): self.selectedLinks.append(link) link.isSelected = True self.link = link else: link.isSelected = False self._clearDragRegion(canvas) self.draw() if self.inMotion: for dx, dy, obj in self.inMotion: if isinstance( obj, Node) and obj.original and obj.original.isPrototype: if obj.original.spawn: obj.original.spawn.original = None obj.original.spawn = None obj.delete() self.inMotion = None self.inMotion2 = None self.inMotion3 = None def _finishEdit(self): if self.editWidget: self.focus_force() self.editWidget.commit() self.editWidget.hide() self.editWidget = None def _moveUp(self, event): self._moveSelected(event, 0, -1) def _moveDown(self, event): self._moveSelected(event, 0, 1) def _moveRight(self, event): self._moveSelected(event, 1, 0) def _moveLeft(self, event): self._moveSelected(event, -1, 0) def _moveSelected(self, event, dx, dy): if not (event.state & 4 or event.state & 1): dx *= MOVE_STEP dy *= MOVE_STEP for node in self.selectedNodes: x0, y0 = node.coords node.move(x0 + dx, y0 + dy) for link in self.selectedLinks: x0, y0 = link.coords link.move(x0 + dx, y0 + dy) def scrollUp(self, event=None): pass def scrollDown(self, event): pass def _windowsOsScroll(self, event=None): delta = event.delta if delta > 0: self.scrollUp(event) elif delta < 0: self.scrollDown(event) def _drawAfter(self): self.after_idle(self.draw) def draw(self): for link in self.links: link.draw() for node in self.nodes: node.draw()
class Tree(Frame): def __init__(self, parent, iconSize='medium', multiSelect=False, font='Helvetica 8', doubleCallback=None, *args, **kw): Frame.__init__(self, parent, *args, **kw) self.multiSelect = multiSelect self.icons = {} self.openDict = {} self.scrollbarWidth = 15 self.canvasList = [] self.canvasDict = {} self.canvasLines = [] self._wait = False self.vOffset = 0 self.nodes = [] # Just a list of nodes self.nodeDict = {} # To fetch nodes via objects self.font = font self.doubleCallback = doubleCallback #bg = self.cget('background') iconItems = [{ 'kind': 'command', 'label': 'Small', 'command': self.smallIcons }, { 'kind': 'command', 'label': 'Medium', 'command': self.medIcons }, { 'kind': 'command', 'label': 'Large', 'command': self.largeIcons }] self.menu = Menu(self, tearoff=False) menu_items = [ { 'kind': 'cascade', 'label': 'Icon size', 'submenu': iconItems }, ] self.menu.setMenuItems(menu_items) self.rowHeight = None self.visibleRows = 80 self.setIconSize(iconSize, redraw=False) self.canvas = Canvas(self, relief='flat', borderwidth=0, background=BG_COLOR, xscrollcommand=self._setHScrollbar, *args, **kw) self.canvas.pack() self.xScrollbar = Scrollbar(self, orient='horizontal', width=self.scrollbarWidth, borderwidth=1, callback=self._moveHScrollbar, background=BG_COLOR) self.yScrollbar = Scrollbar(self, orient='vertical', width=self.scrollbarWidth, borderwidth=1, callback=self._setVScrollbar, background=BG_COLOR) self.canvas.bind('<Button-1>', self._mouseClick) self.canvas.bind('<Double-1>', self._mouseDoubleClick) if isWindowsOS: self.canvas.bind('<MouseWheel>', self._windowsOsScroll) else: self.canvas.bind('<Button-4>', self._mouseUp) self.canvas.bind('<Button-5>', self._mouseDown) self.canvas.bind('<p>', self._printCanvas) self.canvas.bind('<KeyPress-Prior>', self._pageUp) self.canvas.bind('<KeyPress-Next>', self._pageDown) self.canvas.bind('<KeyPress-Up>', self._keyUp) self.canvas.bind('<KeyPress-Down>', self._keyDown) self.canvas.bind('<KeyPress-Home>', self._keyHome) self.canvas.bind('<KeyPress-End>', self._keyEnd) self.canvas.bind('<Enter>', self._enter) self.canvas.bind('<ButtonPress-3>', self._popupMenu) self.bind('<Configure>', self._changeSizeAfter) def _popupMenu(self, event): self.menu.popupMenu(event) def smallIcons(self): self.setIconSize('small') def medIcons(self): self.setIconSize('medium') def largeIcons(self): self.setIconSize('large') def setIconSize(self, iconSize, redraw=True): size = ICON_SIZE_DICT.get(iconSize, 22) if size != self.rowHeight: self.rowHeight = size + 1 self.font = 'Helvetica %d' % (size / 2) ccpnDir = getTopDirectory() iconDir = '%dx%d' % (size, size) imageDir = path.join(ccpnDir, 'python', 'memops', 'gui', 'graphics', iconDir) files = [f for f in listdir(imageDir) if f.endswith('.gif')] for file in files: imageFile = path.join(imageDir, file) self.icons[file[:-4]] = PhotoImage(file=imageFile) if redraw: self._changeSize() return size def _setHScrollbar(self, start, end): self.xScrollbar.set(float(start), float(end)) def _moveHScrollbar(self, start, end): self.canvas.xview('moveto', start) def _enter(self, event): self.menu.popdownMenu() self.canvas.focus_force() def _printCanvas(self, *event): from memops.gui.FileSelect import FileType from memops.gui.FileSelectPopup import FileSelectPopup fileTypes = [FileType('PostScript', ['*.ps']), FileType('All', ['*'])] fileSelectPopup = FileSelectPopup(self, file_types=fileTypes, title='Print canvas to file', dismiss_text='Cancel', selected_file_must_exist=False) fileName = fileSelectPopup.getFile() self.canvas.postscript(colormode='color', file=fileName) def _windowsOsScroll(self, event): delta = event.delta if delta > 0: self._mouseUp(event) elif delta < 0: self._mouseDown(event) def _keyHome(self, event): self.vOffset = 0 self._drawAfter() def _keyEnd(self, event): numNodes = len(self.nodes) visNodes = self.visibleRows self.vOffset = max(0, numNodes - visNodes) self._drawAfter() def _scroll(self, delta=1): height = int(self.winfo_height()) numNodes = len(self.nodes) visNodes = self.visibleRows if numNodes <= visNodes: return vOffset = self.vOffset + delta vOffset = max(0, vOffset) vOffset = min(numNodes - visNodes, vOffset) if vOffset != self.vOffset: self.vOffset = vOffset self._drawAfter() def _pageUp(self, event): self._scroll(-1 * self.visibleRows) def _pageDown(self, event): self._scroll(self.visibleRows) def _mouseUp(self, event): self._scroll(-8) def _mouseDown(self, event): self._scroll(8) def _keyUp(self, event): self._scroll(-1) def _keyDown(self, event): self._scroll(1) def _setVScrollbar(self, start, end): self.cancelEdits() height = int(self.winfo_height()) gaps = start + 1 - end nRows = float(len(self.nodes)) if gaps == 0 or nRows == 0.0: self.yScrollbar.set(0, 1) self.vOffset = 0 else: # Have to do this so that the bar keeps up with the mouse overlap = int(nRows - self.visibleRows) size = min(1.0, self.visibleRows / nRows) self.yScrollbar.set(min(gaps, max(0, start)), max(size, min(1.0, start + size))) self.vOffset = min( max(0, int('%1.1d' % (start / gaps * (overlap + 1)))), overlap) #vOffset = max(0, start*nRows) #vOffset = min(nRows-self.visibleRows, vOffset) #self.vOffset = int(vOffset) self._drawAfter() def _updateScrollBars(self): allRows = len(self.nodes) visRows = self.visibleRows if allRows < visRows: self.vOffset = 0 self.yScrollbar.set(0, 1) self.yScrollbar.place_forget() else: size = self.scrollbarWidth width = int(self.winfo_width()) - 1 height = int(self.winfo_height()) - 1 start = float(self.vOffset) / allRows end = float(self.vOffset + visRows) / allRows self.yScrollbar.set(start, end) self.yScrollbar.place(x=width - size + 1, y=1, width=size, height=height) self.update_idletasks() def _mouseClick(self, event): self.cancelEdits() x = self.canvas.canvasx(event.x) y = self.canvas.canvasy(event.y) obj = self.canvas.find('closest', x, y)[0] node, typ = self.canvasDict.get(obj, (None, None)) if node: if typ is TOGGLE: node.toggle() else: if self.multiSelect and (event.state & 4 or event.state & 1): if event.state & 4: node.isSelected = not node.isSelected elif event.state & 1: indices = [ self.nodes.index(node), ] for i, node2 in enumerate(self.nodes): if node2.isSelected: indices.append(i) for node2 in self.nodes[min(indices):max(indices) + 1]: node2.isSelected = True else: for node2 in self.nodes: if node2.isSelected: node2.isSelected = False node.isSelected = True self._drawAfter() def _mouseDoubleClick(self, event): self.cancelEdits() x = self.canvas.canvasx(event.x) y = self.canvas.canvasy(event.y) obj = self.canvas.find('closest', x, y)[0] node, typ = self.canvasDict.get(obj, (None, None)) if node: if typ is TOGGLE: #node.toggle() return elif self.doubleCallback: self.doubleCallback(node) self._drawAfter() def _drawAfter(self): if self._wait: return self._wait = True self.after_idle(self._draw) def _draw(self): self._updateScrollBars() delta = self.rowHeight canvas = self.canvas cImage = canvas.create_image cList = self.canvasList cDict = self.canvasDict cLines = self.canvasLines cConfig = canvas.itemconfigure cDel = canvas.delete cCoords = canvas.coords cText = canvas.create_text cRect = canvas.create_rectangle cBbox = canvas.bbox cLine = canvas.create_line cLift = canvas.lift cLower = canvas.lower cPoly = canvas.create_polygon nodes = self.nodes nodeDict = self.nodeDict visibleRows = self.visibleRows icons = self.icons font = self.font vOffset = self.vOffset bg = BG_COLOR # self.cget('bg') roots = [n for n in nodes if not n.parent] if not roots: raise Exception('No rooted nodes') for item in cLines: cDel(item) stack = [(node, 0) for node in roots] nodes = [] while stack: node, level = stack.pop(0) nodes.append((node, level)) if node.isOpen: level2 = level + 1 stack = [(c, level2) for c in node.children] + stack yDict = {} pLevel = 0 pNode = None pad = 2 + (delta / 2) bSpace = 4 nodes = nodes[vOffset:] for row, data in enumerate(nodes): node, level = data y = pad + row * delta x = pad + level * delta x1 = x + delta x2 = x1 + delta - bSpace yDict[node] = y, x1 yP, xP = yDict.get(node.parent, (0, x)) if yP is not None: line = cLine(xP, yP, xP, y, fill=LINE_COLOR, width=1) cLower(line) cLines.append(line) if row > visibleRows: break iconImage = icons[node.icon] if node.isSelected: bgColor = SELECT_BG_COLOR fgColor = SELECT_FG_COLOR else: bgColor = bg fgColor = bg if row < len(cList): line, rect, tog1, tog2, box, icon, text = cList[row] cConfig(rect, fill=bgColor, outline=fgColor) cConfig(icon, image=iconImage) cConfig(text, text=node.label or '', font=font) cCoords(icon, x1, y) cCoords(text, x2, y) cCoords(line, x, y, x1, y) else: line = cLine(x, y, x1, y, fill=LINE_COLOR) rect = cRect(x, y, x2, y, fill=bgColor, outline=fgColor) box = cRect(-2, -2, -2, -2, fill=NODE_BG_COLOR) tog1 = cLine(-2, -2, -2, -2) tog2 = cLine(-2, -2, -2, -2) icon = cImage(x1, y, image=iconImage) text = cText(x2, y, text=node.label or '', justify='left', anchor='w', font=font) cList.append((line, rect, tog1, tog2, box, icon, text)) if node.callback: cCoords(box, x - 4, y - 4, x + 4, y + 4) if node.isOpen: cCoords(tog1, -2, -2, -2, -2) cCoords(tog2, x - 2, y, x + 3, y) else: cCoords(tog1, x, y - 2, x, y + 3) cCoords(tog2, x - 2, y, x + 3, y) else: cCoords(tog1, -2, -2, -2, -2) cCoords(tog2, -2, -2, -2, -2) cCoords(box, -2, -2, -2, -2) bbox = cBbox(text) if bbox: cCoords(rect, bbox[0] - 2, bbox[1], bbox[2] + 2, bbox[3]) cLift(box) cLift(tog1) cLift(tog2) cDict[tog1] = (node, TOGGLE) cDict[tog2] = (node, TOGGLE) cDict[box] = (node, TOGGLE) cDict[text] = (node, TEXT) cDict[icon] = (node, ICON) nRows = min(visibleRows, len(nodes)) if len(cList) > nRows: for row in range(nRows, len(cList)): for item in cList[row]: if cDict.get(item): del cDict[item] cDel(item) self.canvasList = cList[:nRows] self._wait = False def setFont(self, font): self.font = font self._drawAfter() def _changeSizeAfter(self, event): self.after_idle(lambda: self._changeSize(event)) def _changeSize(self, event=None): self.cancelEdits() if event: width = event.width height = event.height else: width = int(self.winfo_width()) height = int(self.winfo_height()) self.canvas.config(width=width, height=height) self.visibleRows = height / self.rowHeight bbox = self.canvas.bbox('all') if bbox: # None at startup: nothing drawn canvasWidth = bbox[2] - bbox[0] if width < canvasWidth: size = self.scrollbarWidth end = width / float(canvasWidth) self.canvas.config(scrollregion="0 0 %s %s" % (canvasWidth, height)) if self.visibleRows < len(self.nodes): self.xScrollbar.place(x=0, y=height - size, width=width - size, height=size) else: self.xScrollbar.place(x=0, y=height - size, width=width, height=size) self.xScrollbar.set(0, end) else: self.xScrollbar.place_forget() else: self.xScrollbar.place_forget() self._drawAfter() # Public def cancelEdits(self): self.menu.popdownMenu() def getSelected(self): return [node.object for node in self.nodes if node.isSelected] def select(self, object, expand=True): node = self.nodeDict.get(object) if node is not None: node.isSelected = True if expand: parent = node.parent while parent: parent.isOpen = True parent = parent.parent self._drawAfter() def selectAll(self, expand=True): for node in self.nodes: node.isSelected = True if expand: node.isOpen = expand def selectDeep(self, object, expand=True): node = self.nodeDict.get(object) if node is not None: stack = [ node, ] nodes = [] while stack: node2 = stack.pop() stack.extend(node2.children) nodes.append(node2) for node2 in nodes: if expand: node2.isOpen = True node2.isSelected = True self._drawAfter() def setSelected(self, objects, expand=True): select = self.select for object in objects: select(object, expand) def expand(self, object): node = self.nodeDict.get(object) if node is not None: node.expand() self._drawAfter() def expandAll(self): for node in self.nodes: node.expand() self._drawAfter() def expandDeep(self, object): node = self.nodeDict.get(object) if node is not None: stack = [ node, ] nodes = [] while stack: node2 = stack.pop() stack.extend(node2.children) nodes.append(node2) for node2 in nodes: node2.iexpand() self._drawAfter() def contract(self, object): node = self.nodeDict.get(object) if node is not None: node.collapse() self._drawAfter() def contractAll(self): for node in self.nodes: node.collapse() self._drawAfter() def contractDeep(self, object): node = self.nodeDict.get(object) if node is not None: stack = [ node, ] nodes = [] while stack: node2 = stack.pop() stack.extend(node2.children) nodes.append(node2) for node2 in nodes: node2.collapse() self._drawAfter() def update(self, parents, objects, labels=None, icons=None, callbacks=None, editWidgets=None): n = len(parents) for test in (objects, callbacks, labels, icons, editWidgets): if test is not None: assert len(test) == n self.nodes = nodes = [] self.nodeDict = nodeDict = {} self.openDict = {} if not labels: labels = [None] * n if not icons: icons = [None] * n if not callbacks: callbacks = [None] * n if not editWidgets: editWidgets = [[] for x in xrange(n)] for i, parent in enumerate(parents): object = objects[i] node = Node(self, parent, object, labels[i], icons[i], callbacks[i], False, False, editWidgets[i]) # JMCI FIXME: need to understand how the redraw works self._draw() def add(self, parent, object, label=None, icon='media-playback-stop', callback=None, isOpen=False, isSelected=False, editWidgets=None): nodeDict = self.nodeDict if nodeDict.get(object): nodeDict[object].delete() #raise Exception('Node already present for object %s' % object) node = Node(self, parent, object, label, icon, callback, isOpen, isSelected, editWidgets) def remove(self, object): node = self.nodeDict.get(object) if node is not None: if self.openDict.get(object): del self.openDict[object] node.delete() self._drawAfter()