class PulldownMenu(Frame): # if using indentation then should use list as stack only # in other words only delete or insert at end # entries entries must have attribute text_attr or if this is # None then assume that entries entries themselves provide name def __init__(self, parent, callback=None, entries=None, text_attr=None, colors=None, outline='#602000', fill='#B05848', label_color='#501000', selected_index=-1, indent='', extra='', sticky='w', do_initial_callback=True, force_callback=False, *args, **kw): if (entries is None): entries = [] Frame.__init__(self, parent, sticky=sticky, *args, **kw) self.text_attr = text_attr #indentation and extra string for popup information self.indent = indent self.extra = extra self.callback = callback self.do_initial_callback = do_initial_callback self.force_callback = force_callback frame = Frame(self) #frame = parent self.frame = frame self.bg = frame.cget('bg') self.outline = outline self.fill = fill self.label_color = label_color self.menu = Menu(parent, tearoff=0, borderwidth=1, activeborderwidth=1) self.menu.images = [] self.label = Label(self, foreground=label_color) self.first_pass = True #self.entries = entries self.entries = [] self.colors = [] self.setup(entries, selected_index, colors=colors) s = 10 self.canvas = Canvas(self, width=s, height=s, background=self.bg) self.label.bind("<Button-1>", self.labelPopup) # below does not work for some reason #self.bind("<Button-1>", self.labelPopup) # below does not work any more since can have submenus and # no way to reach those with below since involves leaving menu #self.menu.bind("<Leave>", self.popdown) self.menu.bind("<Leave>", self.leave) self.menu.bind("<Button-1>", self.buttonPress) self.poppedUpSubmenu = False self.canvas.bind("<Button-1>", self.canvasPopup) self.canvas.bind("<Configure>", self.resizeCallback) self.label.grid(row=0, column=0, sticky=Tkinter.W) self.canvas.grid(row=0, column=1, sticky=Tkinter.E, padx=2) def leave(self, event): if (not self.poppedUpSubmenu): x = event.x y = event.y x1 = self.menu.winfo_width() y1 = self.menu.winfo_height() if (x < 0) or (y < 0) or (x >= x1) or (y >= y1): self.popdown() def buttonPress(self, event): if (event.widget != self.menu): return for n in range(len(self.entries) - 1, -1, -1): if event.y >= self.menu.yposition(n): break else: return item = self.entries[n] if (isinstance(item, {}.__class__) and item['kind'] == 'cascade'): self.poppedUpSubmenu = True else: self.poppedUpSubmenu = False def resizeCallback(self, *event): w = self.canvas.winfo_width() / 2 h = self.canvas.winfo_height() / 2 self.canvas.delete('all') self.canvas.create_polygon(1, 1, w, 2 * h - 1, 2 * w - 1, 1, fill=self.fill, outline=self.outline) def getEntry(self, entry_index, entries=None): if not entries: entries = self.entries if type(entry_index) == types.TupleType: e = entry_index[0] if len(entry_index) > 1: items = entries[e]['submenu'] entry = self.getEntry(entry_index[1:], items) else: entry = entries[e]['label'] elif (entry_index >= 0): entry = entries[entry_index] else: entry = None return entry def getEntryText(self, entry_index, entries=None): if not entries: entries = self.entries if type(entry_index) in (types.TupleType, types.ListType): e = entry_index[0] if len(entry_index) > 1: items = entries[e]['submenu'] text = self.getEntryText(entry_index[1:], items) else: text = entries[e]['label'] elif (entry_index != -1): #print 'getEntryText', entry_index, entries entry = entries[entry_index] if (self.text_attr): text = getattr(entry, self.text_attr) else: text = entry else: text = '' # TBD: for now text returned should not be unicode text = str(text) # below did not work for one person (Chinese characters??) but above did ###text = text.encode('utf-8') return text def setup(self, entries, selected_index, first_pass=True, colors=None): #print 'setup', entries, selected_index if first_pass is not None: self.first_pass = first_pass self.clearMenuItems() if (entries): e = list(entries) else: e = [No_entries_label] self.entries = e self.colors = colors or [None] * len(e) if type(selected_index) in (types.TupleType, types.ListType): if (not selected_index or selected_index[0] >= len(entries)): selected_index = -1 else: if (selected_index >= len(entries)): selected_index = -1 if (selected_index == -1): selected_index = 0 self.selected_index = -1 # changed in setSelectedIndex() self.setMenuItems() self.setSelectedIndex(selected_index) def substituteCascadeCallbacks(self, item, selected_index): if (item['kind'] == 'cascade'): n = 0 for subitem in item['submenu']: self.substituteCascadeCallbacks(subitem, selected_index + [n]) n = n + 1 else: callback = item.get('command') if (callback): label = item['label'] item['command'] = lambda: self.cascadeCallback( callback, label, tuple(selected_index)) def cascadeCallback(self, callback, label, selected_index): self.selected_index = selected_index self.setLabel(selected_index) callback(selected_index, label) def setMenuItems(self): self.menu.images = [] for n in range(len(self.entries)): columnbreak = 0 if n and n % 20 == 0: columnbreak = 1 item = self.entries[n] color = None if n < len(self.colors): color = self.colors[n] if isinstance(item, {}.__class__): if (item['kind'] == 'cascade'): self.substituteCascadeCallbacks(item, [n]) else: t = (self.indent * n) + self.extra + str(self.getEntryText(n)) command = lambda n=n: self.setSelectedIndex(n) if color: image = self.makeColorTile(color) item = { 'kind': 'command', 'accelerator': t, 'command': command, 'image': image, 'columnbreak': columnbreak } else: item = { 'kind': 'command', 'label': t, 'command': command, 'columnbreak': columnbreak } self.menu.addMenuItem(item) def clearMenuItems(self): if self.entries: self.menu.delete(0, len(self.entries)) # below assumes entry_index is integer not tuple def insert(self, entry_index, entry, make_selected=False, color=None): if (self.haveNoEntries()): if (entry_index != 0): return elif ((entry_index < 0) or (entry_index >= len(self.entries))): return self.clearMenuItems() self.checkForNone() self.entries.insert(entry_index, entry) self.colors.insert(entry_index, color) self.setMenuItems() if (make_selected or (self.selected_index == -1)): self.setSelectedIndex(entry_index, force_callback=True) else: self.setSelectedIndex(self.selected_index) # below assumes entry is integer not tuple def append(self, entry, make_selected=False, color=None): self.clearMenuItems() self.checkForNone() self.entries.append(entry) self.colors.append(color) self.setMenuItems() if (make_selected or (self.selected_index == -1)): self.setSelectedIndex(len(self.entries) - 1, force_callback=True) else: self.setSelectedIndex(self.selected_index) def checkForNone(self): if (self.haveNoEntries()): self.entries = [] self.colors = [] self.menu.delete(0) def haveNoEntries(self): if (len(self.entries) == 1 and self.entries[0] == No_entries_label): return True else: return False def baseIndex(self, ind): if type(ind) == types.TupleType: ind = ind[0] return ind # below assumes entry_index is integer not tuple def delete(self, entry_index, n=1): if (self.haveNoEntries()): return if ((entry_index < 0) or (entry_index >= len(self.entries))): return self.clearMenuItems() n1 = entry_index n2 = n1 + n m = self.baseIndex(self.selected_index) del self.entries[n1:n2] del self.colors[n1:n2] if (not self.entries): self.entries = [No_entries_label] self.colors = [None] self.setMenuItems() if ((m >= n1) and (m < n2)): if (n1 == 0): selected_index = 0 else: selected_index = n1 - 1 self.setSelectedIndex(selected_index, force_callback=True) def replace(self, entries, selected_index=-1, colors=None): self.menu.delete(0, len(self.entries)) self.setup(entries, selected_index, first_pass=False, colors=colors) def popdown(self, *event): self.menu.unpost() def setLabel(self, selected_index): text = self.getEntryText(selected_index) if (not text): text = self.extra # arbitrary self.label.set(text=text) def labelPopup(self, event): self.poppedUpSubmenu = False s = self.baseIndex(self.selected_index) x = event.x_root - event.x + 2 y = event.y_root - event.y - \ max(0, s) * (self.label.winfo_height() + 1) self.menu.post(x, y) def canvasPopup(self, event): s = self.baseIndex(self.selected_index) x = event.x_root - event.x - self.label.winfo_width() + 2 y = event.y_root - event.y - \ max(0, s) * (self.label.winfo_height() + 1) + \ self.canvas.winfo_height() - self.label.winfo_height() self.menu.post(x, y) def getSelectedIndex(self): return self.selected_index def setSelectedIndex(self, selected_index, force_callback=False): if (force_callback or self.force_callback or \ (selected_index != self.selected_index)): #print 'setSelectedIndex', self.entries, selected_index, self.selected_index, self.first_pass self.selected_index = selected_index self.setLabel(selected_index) if (self.callback): if (self.haveNoEntries()): if (not self.first_pass): self.callback(-1, None) elif (selected_index != -1): if (not self.first_pass or self.do_initial_callback): self.callback(selected_index, self.entries[selected_index]) self.first_pass = False def get(self): return self.getSelected() def getSelected(self): ind = self.getSelectedIndex() if (ind == -1): return None else: return self.getEntry(ind) def set(self, selected): self.setSelected(selected) def findEntryIndex(self, entry, entries=None): #print 'findEntryIndex1', entry, entries if (entries): noEntries = True else: noEntries = False entries = self.entries try: ind = entries.index(entry) return ind except: pass #print 'findEntryIndex2', entry, entries for n in range(len(entries)): e = entries[n] #print 'findEntryIndex3', entry, e if e == entry: if noEntries: return [n] else: return n elif type(e) == types.DictType: if (len(entries) > 1): try: ind = self.findEntryIndex(entry, e['submenu']) return [n] + ind except: pass else: if (entry == e['label']): return [n] raise 'unknown entry "%s"' % entry # selects first item found in entries which matches def setSelected(self, selected): #print 'setSelected1', selected, type(selected), self.entries try: selected_index = self.findEntryIndex(selected) except: #selected_index = -1 return #print 'setSelected2', selected_index self.setSelectedIndex(selected_index) def makeColorTile(self, color): image = Tkinter.PhotoImage() self.menu.images.append(image) if type(color) == type([]): colors = [scaleColor(self.menu, c, 1.0) for c in color] else: colors = [ scaleColor(self.menu, color, 1.0), ] cols = max(8, len(colors)) for x in range(cols): i = x % len(colors) c = colors[i] for y in range(16): image.put('{%s %s}' % (c, c), to=(2 * x, y)) return image
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 ScrolledCanvas(Frame): def __init__(self, parent, resizeCallback=None, width=600, height=600, *args, **kw): self.bbox = None self.busy = 0 self.initialX = None self.initialY = None self.resizeCallback = resizeCallback apply(Frame.__init__, (self, parent) + args, kw) self.grid_rowconfigure(0, weight=1) self.grid_columnconfigure(0, weight=1) self.menu = Menu(self, tearoff=0, include_event=True) self.configMenu() self.canvas = Canvas(self, relief='flat', borderwidth=0, width=width, height=height) self.canvas.configure(xscrollincrement=2, yscrollincrement=2) self.canvas.grid(row=0, column=0, sticky=Tkinter.NSEW) self.horizScrollbar = Tkinter.Scrollbar(self, bg=self.cget('bg'), command=self.canvas.xview, orient=Tkinter.HORIZONTAL, borderwidth=1) self.vertScrollbar = Tkinter.Scrollbar(self, bg=self.cget('bg'), command=self.canvas.yview, orient=Tkinter.VERTICAL, borderwidth=1) self.canvas.configure(xscrollcommand=self.horizScrollbar.set, yscrollcommand=self.vertScrollbar.set) self.canvas.bind('<Configure>', self.resizeAfter) self.canvas.bind('<Button-1>', self.mouseButton1) self.canvas.bind('<Button-2>', self.mouseButton2) self.canvas.bind('<Button-3>', self.mouseButton3) self.canvas.bind('<ButtonRelease-1>', self.mouseButtonRelease1) self.canvas.bind('<ButtonRelease-2>', self.mouseButtonRelease2) self.canvas.bind('<B2-Motion>', self.mouseScroll) self.canvas.bind('<B3-Motion>', self.doNothing) self.canvas.bind('<Motion>', self.mouseEnter) self.canvas.bind('<Enter>', self.mouseEnter) def doNothing(self, event): pass def mouseEnter(self, event): if self.menu.winfo_ismapped(): self.removeMenu() def refresh(self): self.bbox = self.canvas.bbox('all') bbox = self.bbox if not bbox: self.busy = 0 return cWidth = int(self.canvas.cget('width')) cHeight = int(self.canvas.cget('height')) if cHeight > bbox[3] - bbox[1]: self.vertScrollbar.grid_forget() else: self.vertScrollbar.grid(row=0, column=1, sticky=Tkinter.NS) if cWidth > bbox[2] - bbox[0]: self.horizScrollbar.grid_forget() else: self.horizScrollbar.grid(row=1, column=0, sticky=Tkinter.EW) x1 = bbox[0] y1 = bbox[1] x2 = max(bbox[0] + cWidth, bbox[2]) y2 = max(bbox[1] + cHeight, bbox[3]) self.canvas.configure(scrollregion=(x1, y1, x2, y2)) self.update_idletasks() self.busy = 0 def printCanvas(self, *event): 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.bbox = bbox = self.canvas.bbox('all') w = bbox[2] - bbox[0] h = bbox[3] - bbox[1] self.canvas.postscript(colormode='color', file=fileName, x=bbox[0], y=bbox[1], width=w + 2, pagewidth='21.c', height=h + 2) def configMenu(self): items = [ { 'kind': 'command', 'label': 'Print to file', 'command': self.printCanvas }, ] self.menu.setMenuItems(items) def removeMenu(self, *event): self.menu.unpost() def mouseButton3(self, event): self.menu.popupMenu(event) def mouseButton1(self, event): self.removeMenu() if not self.initialX: self.initialX = event.x self.initialY = event.y def mouseButton2(self, event): self.removeMenu() if not self.initialX: self.initialX = event.x self.initialY = event.y self.initialPX = self.horizScrollbar.get()[0] self.initialPY = self.vertScrollbar.get()[0] def mouseButtonRelease1(self, event): self.initialX = None self.initialY = None def mouseButtonRelease2(self, event): self.initialX = None self.initialY = None self.initialPX = None self.initialPY = None def mouseScroll(self, event): self.menu.unpost() bbox = self.bbox if not bbox: return bW = float(bbox[2] - bbox[0]) bH = float(bbox[3] - bbox[1]) cWidth = int(self.canvas.cget('width')) cHeight = int(self.canvas.cget('height')) if cWidth < bW: dx = self.initialX - event.x prop = self.initialPX + (dx / bW) self.canvas.xview('moveto', prop) if cHeight < bH: dy = self.initialY - event.y prop = self.initialPY + (dy / bH) self.canvas.yview('moveto', prop) def resizeAfter(self, event): if self.busy: return else: self.busy = 1 self.removeMenu() self.after_idle(lambda: self.resize(event)) def resize(self, event): if self.resizeCallback: self.after_idle( lambda: self.resizeCallback(event.width, event.height)) self.canvas.configure(width=event.width, height=event.height) self.after_idle(self.refresh)
class PulldownList(Frame): # if using indentation then should use list as stack only # in other words only delete or insert at end # There is a 1:1 correspondance between the ordered lists of texts and # list of objects # categories is the category name for each text/object # objects with the same category will go under a submenu # of that name def __init__(self, parent, callback=None, texts=None, objects=None, categories=None, colors=None, index=0, prefix='', indent='', initCallback=False, forceCallback=False, numbering=False, arrowLine='#602000', arrowFill='#B05848', labelColor='#501000', menuBg='#F0F0FF', sticky='w', *args, **kw): Frame.__init__(self, parent, sticky=sticky, *args, **kw) self.callback = callback self.texts = texts or [] self.objects = objects or [] self.categories = categories or [] self.colors = colors or [] self.prefix = prefix self.indent = indent self.initCallback = initCallback self.numbering = numbering self.arrowLine = arrowLine self.arrowFill = arrowFill self.labelColor = labelColor # Current selection self.index = None self.object = NullText self.rows = [] self.bg = self.cget('bg') self.label = Label(self, foreground=labelColor) self.canvas = Canvas(self, width=12, height=12, background=self.bg) self.menu = Menu(self.canvas, tearoff=False, bg=menuBg, relief='solid', borderwidth=1, activeborderwidth=1) self.menu.images = [] # Photoimage has to remain referenced self.setup(self.texts, self.objects, index, self.colors, self.categories) self.label.bind("<Button-1>", self._labelClick) self.menu.bind("<Leave>", self._leave) self.canvas.bind("<Button-1>", self._canvasClick) self.canvas.bind("<Configure>", self._resizeCallback) self.grid_columnconfigure(0, weight=1) self.label.grid(row=0, column=0, sticky='w') self.canvas.grid(row=0, column=1, sticky='w', padx=2) # # Retrieval # def get(self): return (self.getText(), self.getObject()) def getSelected(self): return self.get() def getObject(self): return self._fetch(self.index, self.objects) def getText(self): return self._fetch(self.index, self.texts) def getSelectedIndex(self): return self.index # # Setting selected # def set(self, item): # Works with an object or a text index = None if item in self.texts: index = list(self.texts).index(item) elif item in self.objects: index = list(self.objects).index(item) if index is not None: self.setIndex(index) def setSelected(self, item): self.set(item) def setIndex(self, index, doCallback=False): self.index = index if self.objects: obj = self.objects[index] if obj is not self.object: self.object = obj if (doCallback or self.initCallback) and self.texts and self.callback: self.callback(obj) self._updateLabel() # # Bulk configuration # def clear(self): self.setup([], [], 0) def setup(self, texts, objects, index, colors=None, categories=None): self.texts = texts nTexts = len(texts) if not objects: objects = texts while len(objects) < nTexts: objects.append(None) self.objects = objects if colors is None: self.colors = [None] * nTexts else: while len(colors) < nTexts: colors.append(None) self.colors = colors if categories is None: self.categories = [None] * nTexts else: while len(categories) < nTexts: categories.append(None) self.categories = categories self._setMenuItems() self.setIndex(index or 0) # # In-place/minor configuration # def insert(self, index, text, object=None, color=None, category=None, select=False): index = max(0, min(len(self.texts), index)) self.texts.insert(index, text) self.objects.insert(index, object) self.colors.insert(index, color) self.categories.insert(index, category) self._setMenuItems() if select: self.setIndex(index) def append(self, text, object=None, color=None, category=None, select=False): self.insert(len(self.texts), text, object=None, color=None, category=None, select=False) def delete(self, index, howMany=1): if index < 0: return elif index >= len(self.texts): return end = min(index + howMany, len(self.texts)) self._clearMenu() del self.texts[index:end] del self.objects[index:end] del self.colors[index:end] del self.categories[index:end] self._setMenuItems() index = min(len(self.texts) - 1, self.index) self.setIndex(index) # # Internal methods # def _leave(self, event): x = event.x y = event.y x1 = self.menu.winfo_width() y1 = self.menu.winfo_height() if (x < 0) or (y < 0) or (x >= x1) or (y >= y1): self._popdown() def _resizeCallback(self, *event): c = self.canvas w = c.winfo_width() - 1 h = c.winfo_height() - 1 c.delete('all') c.create_rectangle(0, 0, 10, 2, fill=self.arrowFill, outline=self.arrowLine) c.create_polygon(0, 4, 0, 6, 5, 11, 10, 6, 10, 4, fill=self.arrowFill, outline=self.arrowLine) def _fetch(self, index, array): if index is None: return None if index < 0: index += len(array) if index < 0: return None elif index >= len(array): return None else: return array[index] def _setMenuItems(self): self._clearMenu() self.menu.images = [] # Clear photoimages if not self.texts and not self.menu.entrycget(1, 'label'): item = {'kind': 'command', 'label': NullText, 'command': None} self.menu.addMenuItem(item) self.rows = [0] return topList = [] categoryDict = {} for i in range(len(self.texts)): text = self.texts[i] color = self.colors[i] category = self.categories[i] if category: if categoryDict.get(category) is None: categoryDict[category] = [] topList.append((None, category, None, category)) categoryDict[category].append((i, text, color)) else: topList.append((i, text, color, None)) row = 0 for index, text, color, cat in topList: columnbreak = 0 if row and row % 20 == 0: columnbreak = 1 if cat: string = (self.indent * row) + self.prefix + text items = [] rowB = 0 for index2, text2, color2 in categoryDict.get(cat, []): columnbreakB = 0 if rowB and rowB % 20 == 0: columnbreakB = 1 if self.numbering: number = '%d%. ' % (index2 + 1) else: number = '' string2 = number + self.prefix + text2 command = lambda n=index2: self.setIndex(n, True) if color2: image = self._makeColorTile(color2) item2 = { 'kind': 'command', 'accelerator': string2, 'command': command, 'image': image, 'columnbreak': columnbreakB } else: item2 = { 'kind': 'command', 'label': string2, 'command': command, 'columnbreak': columnbreakB } items.append(item2) self.rows.append(row) rowB += 1 item = { 'kind': 'cascade', 'label': string, 'submenu': items, 'columnbreak': columnbreak } else: if self.numbering: number = '%d%. ' % (index + 1) else: number = '' string = (self.indent * row) + number + self.prefix + text command = lambda n=index: self.setIndex(n, True) if color: image = self._makeColorTile(color) item = { 'kind': 'command', 'accelerator': string, 'command': command, 'image': image, 'columnbreak': columnbreak } else: item = { 'kind': 'command', 'label': string, 'command': command, 'columnbreak': columnbreak } self.menu.addMenuItem(item) self.rows.append(row) row += 1 def _clearMenu(self): self.menu.delete(0, 'end') self.index = 0 self.rows = [] def _popdown(self, *event): self.menu.unpost() def _labelClick(self, event): s = self.rows[self.index] #x = event.x_root - event.x + 2 + self.label.winfo_width() x = event.x_root - 2 y = event.y_root - event.y - max(0, s) * (self.label.winfo_height() + 1) self.menu.post(x, y) def _canvasClick(self, event): s = self.rows[self.index] x = event.x_root - event.x + 2 y = event.y_root - event.y - max(0, s) * (self.label.winfo_height() + 1) self.menu.post(x, y) def _updateLabel(self): if self.texts: text = self.texts[self.index] or NullText else: text = NullText self.label.set(text=text) def _makeColorTile(self, color): image = Tkinter.PhotoImage() self.menu.images.append(image) if type(color) == type([]): colors = [scaleColor(self.menu, c, 1.0) for c in color] else: colors = [ scaleColor(self.menu, color, 1.0), ] cols = max(8, len(colors)) for x in range(cols): i = x % len(colors) c = colors[i] for y in range(16): image.put('{%s %s}' % (c, c), to=(2 * x, y)) return image