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 Spacer(Frame): def __init__(self, parent, relief='raised', *args, **kw): apply(Frame.__init__, (self, parent) + args, kw) #self.grid_rowconfigure(0, weight=1) #self.grid_columnconfigure(1, weight=1) self.bg = self.cget('bg') self.relief = relief self.grid_columnconfigure(0, weight=1) self.canvas = Canvas(self, background=self.bg, width=1, height=6) self.canvas.grid(row=0, column=0, sticky=Tkinter.NSEW) #self.canvas.bind('<Configure>', self.resizeCallback) self.grid(sticky=Tkinter.EW) self.canvasL1 = [] self.canvasL2 = [] self.canvasL3 = [] self.refresh = 0 self.event = None self.bind('<Configure>', self.resizeAfter) def resizeAfter(self, event): self.event = event if self.refresh: return else: self.refresh = 1 self.after_idle(lambda: self.resize(self.event)) def resize(self, event): m = 6 w = event.width h = event.height N = int(h / m) self.canvas.config(width=w, height=h) if self.relief == 'sunken': fill1 = 'grey65' fill2 = 'grey95' else: fill1 = 'grey95' fill2 = 'grey65' for i in range(N): y = 1 + (i / float(N)) * h if i >= len(self.canvasL1): rect1 = self.canvas.create_rectangle(4, y + 0, w - 5, y + 2, width=0, fill=fill1) rect2 = self.canvas.create_rectangle(5, y + 1, w - 4, y + 3, width=0, fill=fill2) rect3 = self.canvas.create_rectangle(5, y + 1, w - 5, y + 2, width=0, fill=self.bg) self.canvasL1.append(rect1) self.canvasL2.append(rect2) self.canvasL3.append(rect3) else: self.canvas.coords(self.canvasL1[i], 4, y + 0, w - 5, y + 2) self.canvas.coords(self.canvasL2[i], 5, y + 1, w - 4, y + 3) self.canvas.coords(self.canvasL3[i], 5, y + 1, w - 5, y + 2) if N > len(self.canvasL1): for i in range(N, len(self.canvasL1)): self.canvas.delete(self.canvasL1[i]) self.canvas.delete(self.canvasL2[i]) self.canvas.delete(self.canvasL3[i]) self.refresh = 0