class Gallery(Frame): def __init__( self, master, notebook, graphics_tab, root, canvas, *args, **kwargs): Frame.__init__(self, master, *args, **kwargs) self.parent = master self.nbook = notebook self.t7 = graphics_tab self.root = root self.canvas = canvas self.counter = 0 self.thumb_labels = [] self.width_strings = [] self.height_strings = [] formats = make_formats_dict() self.current_person = get_current_person()[0] set_window_max_size(self.parent) self.filter_pix_data() self.make_widgets() def make_widgets(self): if self.parent.winfo_class() == 'Toplevel': gallery_canvas = Canvas( self.parent, bd=0, highlightthickness=0) self.gallery_content = Frame(gallery_canvas) gallery_canvas.grid(row=0, column=0, sticky='nsew') self.parent.grid_columnconfigure(0, weight=1) self.parent.grid_rowconfigure(0, weight=1) else: self.gallery_content = Frame(self.parent) self.gallery_content.grid(column=0, row=0) self.thumb_canvas = Canvas( self.gallery_content, bd=0, highlightthickness=0, bg=formats['bg']) self.thumb_canvas.pack(padx=12, pady=12) self.thumbstrip = Frame(self.thumb_canvas) self.thumbstrip.pack(side='top') self.previous_img = Button(self.gallery_content, text='<<', width=6) self.pic_canvas = Canvas( self.gallery_content, highlightbackground=formats['bg']) self.pic_canvas.bind('<Button-1>', self.focus_clicked) self.pic_canvas.bind('<Button-1>', self.scroll_start, add='+') self.pic_canvas.bind('<B1-Motion>', self.scroll_move) self.img_path = '{}treebard_gps\data\{}\images\{}'.format( root_drive, self.image_dir, self.main_pic) img_big = Image.open(self.img_path) self.tk_img = ImageTk.PhotoImage(img_big) self.pic_canvas.image = self.tk_img z = 0 self.current_pictures = sorted(self.current_pictures) for img in self.current_pictures: pic_col = Frame(self.thumbstrip) pic_col.pack(side='left', expand=1, fill='y') pic_file = img self.img_path = '{}treebard_gps\data\{}\images\{}'.format(root_drive, self.image_dir, pic_file) idx = len(pic_file) bare = pic_file[0:idx-4] thumbsy = Image.open(self.img_path) self.width_strings.append(thumbsy.width) self.height_strings.append(thumbsy.height) thumbsy.thumbnail((185,85)) thumb_path = 'images/{}_tmb.png'.format(bare) # overwrites file by same name if it exists thumbsy.save(thumb_path) small = ImageTk.PhotoImage(file=thumb_path, master=self.thumbstrip) thumb = Label( pic_col, image=small) thumb.pack(expand=1, fill='y') thumb.image = small self.thumb_labels.append(thumb) # lambda used to save value in loop if self.parent.winfo_class() == 'Toplevel': rad = Radiobutton( pic_col, takefocus=0, value=pic_file, command=lambda pic_file=pic_file: self.set_main_pic( pic_file)) rad.pack() if rad['value'] == self.main_pic: rad.select() else: rad = Frame( pic_col, height=24, width=24) rad.pack(expand=1, fill='both') if self.parent.winfo_name() == 'source_tab': pic_file = '{}, {}'.format(self.source, pic_file) create_tooltip(rad, pic_file) z += 1 self.pil_img = img_big self.fit_canvas_to_pic() self.thumb_dict = dict(zip(self.thumb_labels, self.current_pictures)) self.next_img = Button(self.gallery_content, text='>>', width=6) panel = Frame(self.gallery_content) subject = LabelH3(panel, text=self.curr_entity) subject.grid(column=0, row=0, sticky='ew') # labels with selectable multiline text self.caption_lab = MessageCopiable(panel, width=36) self.picfile_lab = MessageCopiable(panel, width=36) self.picsize_lab = MessageCopiable(panel, width=36) edit = Button( panel, text='EDIT', width=8, command=lambda graphics=self.t7: self.go_to_graphics(graphics)) self.previous_img.config(command=self.back) self.next_img.config(command=self.forward) panel.grid_rowconfigure(0, weight=2) panel.grid_rowconfigure(1, weight=1) panel.grid_rowconfigure(4, weight=2) self.caption_lab.grid(column=0, row=1, pady=(12,12), sticky='ew') self.caption_lab.insert(1.0, self.caption_text) self.picfile_lab.grid(column=0, row=2, pady=(12,0), sticky='ew') self.picfile_lab.insert(1.0, self.img_path) self.picsize_lab.grid(column=0, row=3, pady=(0,24), sticky='ew') self.picsize_lab.insert( 1.0, 'width: {}, height: {}'.format( self.pil_img.width, self.pil_img.height)) edit.grid(column=0, row=4) self.caption_lab.set_height() self.picfile_lab.set_height() self.picsize_lab.set_height() self.previous_img.pack(side='left', padx=12) self.pic_canvas.pack(side='left', expand=1, fill='y') self.next_img.pack(side='left', padx=12) panel.pack(side='left', expand=1, fill='y') for thumb in self.thumb_labels: thumb.bind('<Button-1>', self.show_clicked) self.pic_canvas.bind('<Key-Left>', lambda evt: self.back()) self.pic_canvas.bind('<Key-Right>', lambda evt: self.forward()) # add statusbar-tooltips self.visited = ( (self.thumbstrip, "Thumbnail Views", "Click thumbnail to display. Hover below to see " "file name. If radio, click to make main image."), (self.pic_canvas, "Image", "Arrow keys change image when it's in focus."), (self.previous_img, "Left Button", "Click with mouse or when highlighted click with spacebar."), (self.next_img, "Right Button", "Click with mouse or when highlighted click with spacebar.")) if self.parent.winfo_class() == 'Toplevel': box = Frame(self.parent) box.grid(column=0, row=1, pady=12) close = Button( box, text='CLOSE', width=8, command=self.cancel_gallery) close.grid() self.parent.protocol('WM_DELETE_WINDOW', self.cancel_gallery) self.thumb_canvas.create_window(0, 0, anchor='nw', window=self.thumbstrip) self.thumb_canvas.config( scrollregion=( 0, 0, self.thumbstrip.winfo_reqwidth(), self.thumbstrip.winfo_reqheight())) self.thumb_canvas.config( width=self.root.maxsize()[0], height=self.thumbstrip.winfo_reqheight()) scroll_width = int(self.thumb_canvas['scrollregion'].split(' ')[2]) if scroll_width >= int(self.thumb_canvas['width']): for child in self.thumbstrip.winfo_children(): for gchild in child.winfo_children(): gchild.bind("<Enter>", self.thumb_start) gchild.bind("<Motion>", self.thumb_move) if self.parent.winfo_class() == 'Toplevel': gallery_canvas.create_window( 0, 0, anchor=tk.NW, window=self.gallery_content) self.resize_scrollbar() self.resize_window() self.config_labels() def resize_scrollbar(self): self.parent.update_idletasks() self.pic_canvas.config(scrollregion=self.pic_canvas.bbox("all")) def resize_window(self): self.parent.update_idletasks() page_x = self.gallery_content.winfo_reqwidth() page_y = self.gallery_content.winfo_reqheight()+48 self.parent.geometry('{}x{}'.format(page_x, page_y)) def cancel_gallery(self, event=None): self.root.focus_set() self.parent.destroy() def focus_clicked(self, evt): evt.widget.focus_set() def hilite(evt): evt.widget.config(bg=formats['highlight_bg']) def unhilite(evt): evt.widget.config(bg=formats['bg']) def show_clicked(self, evt): select_pic = self.current_pictures.index(self.thumb_dict[evt.widget]) self.chosen_picfile = self.current_pictures[select_pic] current_dir = files.get_current_file()[1] self.img_path = '{}treebard_gps\data\{}\images\{}'.format(root_drive, current_dir, self.chosen_picfile) pix_data = self.get_current_pix_data() for tup in pix_data: if tup[1] == self.chosen_picfile: self.caption_text = tup[2] new = Image.open(self.img_path) self.tk_img = ImageTk.PhotoImage(new) self.pil_img = new self.fit_canvas_to_pic() self.pic_canvas.image = self.tk_img self.pic_canvas.config( scrollregion=(0, 0, self.pil_img.width, self.pil_img.height)) self.config_labels() self.counter = select_pic def scroll_start(self, event): self.pic_canvas.scan_mark(event.x, event.y) def scroll_move(self, event): self.pic_canvas.scan_dragto(event.x, event.y, gain=5) def thumb_start(self, event): self.thumb_canvas.scan_mark(event.x, event.y) def thumb_move(self, event): self.thumb_canvas.scan_dragto(event.x, event.y, gain=1) def go_to_graphics(self, graphics): # if frame with this name already exists it's replaced # https://stackoverflow.com/questions/59518905/naming-a-widget-to-auto-destroy-replace-it picwin = Frame(graphics, name='exists') picwin.pack() curr_pic = self.picfile_lab.get(1.0, 'end') curr_pic = curr_pic.strip('\n') img_path = curr_pic edit_pic = Image.open(img_path) edit_img = ImageTk.PhotoImage(edit_pic) editlab = LabelStay( picwin, image=edit_img) editlab.image = edit_img self.nbook.select(graphics) # scroll to top so controls are seen when tab opens self.canvas.yview_moveto(0.0) if self.parent.winfo_class() == 'Toplevel': self.parent.lower(belowThis=self.nbook) editlab.pack() # When this grids a big pic, the whole notebook gets big # prevent large pics from blowing up size of the whole notebook # when placed here by edit button on a gallery # Will need more attention when ready to make the graphics tab. editlab.config(width=700, height=700) def filter_pix_data(self): def second_item(s): return s[1] pix_data = self.get_current_pix_data() pix_data = sorted(pix_data, key=second_item) for tup in pix_data: if tup[3] == 1: self.main_pic = tup[1] self.caption_text = tup[2] if self.parent.winfo_name() == 'source_tab': self.source = tup[4] self.current_pictures = [] for tup in pix_data: self.current_pictures.append(tup[1]) curr_entity = tup[0] self.curr_entity = curr_entity self.caption_path = [] for tup in pix_data: self.caption_path.append((tup[1], tup[2])) def back(self, evt=None): if self.counter == 0: self.counter = len(self.caption_path) self.counter -= 1 current_dir = files.get_current_file()[1] self.img_path = '{}treebard_gps\data\{}\images\{}'.format( root_drive, current_dir, self.caption_path[self.counter][0]) self.caption_text = self.caption_path[self.counter][1] new = Image.open(self.img_path) self.tk_img = ImageTk.PhotoImage(new) self.pil_img = new self.fit_canvas_to_pic() self.pic_canvas.image = self.tk_img self.pic_canvas.config( scrollregion=(0, 0, self.pil_img.width, self.pil_img.height)) self.config_labels() def forward(self, evt=None): self.counter += 1 if self.counter == len(self.caption_path): self.counter = 0 current_dir = files.get_current_file()[1] self.img_path = '{}treebard_gps\\data\\{}\\images\\{}'.format( root_drive, current_dir, self.caption_path[self.counter][0]) self.caption_text = self.caption_path[self.counter][1] new = Image.open(self.img_path) self.tk_img = ImageTk.PhotoImage(new) self.pil_img = new self.fit_canvas_to_pic() self.pic_canvas.image = self.tk_img self.pic_canvas.config( scrollregion=(0, 0, self.pil_img.width, self.pil_img.height)) self.config_labels() def get_current_pix_data(self): current_file_tup = files.get_current_file() current_file = current_file_tup[0] self.image_dir = current_file_tup[1] conn = sqlite3.connect(current_file) cur = conn.cursor() if self.parent.winfo_name() == 'place_tab': cur.execute(select_all_place_images) # cur.execute(''' # SELECT places, images, caption, main_image # FROM images_entities # JOIN place # ON images_entities.place_id = place.place_id # JOIN current # ON current.place_id = place.place_id # JOIN image # ON image.image_id = images_entities.image_id # ''') elif self.parent.winfo_name() == 'source_tab': cur.execute(select_all_source_images) # cur.execute(''' # SELECT citations, images, caption, main_image, sources # FROM images_entities # JOIN source # ON citation.source_id = source.source_id # JOIN citation # ON images_entities.citation_id = citation.citation_id # JOIN current # ON current.citation_id = citation.citation_id # JOIN image # ON image.image_id = images_entities.image_id # ''') elif self.parent.winfo_class() == 'Toplevel': # person images cur.execute(select_all_person_images, (self.current_person,)) # cur.execute( # ''' # SELECT names, images, caption, main_image # FROM images_entities # JOIN person # ON images_entities.person_id = person.person_id # JOIN image # ON image.image_id = images_entities.image_id # JOIN name # ON person.person_id = name.person_id # WHERE images_entities.person_id = ? # AND name_type_id = 1 # ''', # (self.current_person,)) if self.parent.winfo_class() != 'Toplevel': pix_data = cur.fetchall() else: pix_data = cur.fetchall() pix_data = [list(i) for i in pix_data] cur.close() conn.close() return pix_data def fit_canvas_to_pic(self): # make the buttons stay in one place as pics change max_wd = max(self.width_strings) max_ht = max(self.height_strings) scr_wd = self.root.winfo_screenwidth() scr_ht = self.root.winfo_screenheight() FULL = 0.55 if max_wd <= scr_wd * FULL: gallery_wd = max_wd else: gallery_wd = scr_wd * FULL if max_ht <= scr_ht * FULL: gallery_ht = max_ht else: gallery_ht = scr_ht * FULL self.pic_canvas.config( scrollregion=(0, 0, self.pil_img.width, self.pil_img.height), width=gallery_wd, height=gallery_ht) if (self.pil_img.width >= gallery_wd and self.pil_img.height >= gallery_ht): image = self.pic_canvas.create_image( 0, 0, anchor='nw', image=self.tk_img) elif (self.pil_img.width <= gallery_wd and self.pil_img.height >= gallery_ht): image = self.pic_canvas.create_image( self.pic_canvas.winfo_reqwidth()/2, 0, anchor='n', image=self.tk_img) elif (self.pil_img.width >= gallery_wd and self.pil_img.height <= gallery_ht): image = self.pic_canvas.create_image( 0, self.pic_canvas.winfo_reqheight()/2, anchor='w', image=self.tk_img) elif (self.pil_img.width <= gallery_wd and self.pil_img.height <= gallery_ht): image = self.pic_canvas.create_image( self.pic_canvas.winfo_reqwidth()/2, self.pic_canvas.winfo_reqheight()/2, anchor='center', image=self.tk_img) def config_labels(self): for widg in (self.caption_lab, self.picfile_lab, self.picsize_lab): widg.config(state='normal') widg.delete(1.0, 'end') self.caption_lab.insert(1.0, self.caption_text) self.picfile_lab.insert(1.0, self.img_path) self.picsize_lab.insert( 1.0, 'width: {}, height: {}'.format( self.pil_img.width, self.pil_img.height)) for widg in (self.caption_lab, self.picfile_lab, self.picsize_lab): widg.set_height() def set_main_pic(self, val): radio_value = (val,) current_file = files.get_current_file()[0] conn = sqlite3.connect(current_file) conn.execute("PRAGMA foreign_keys = 1") cur = conn.cursor() cur.execute(select_current_person_id) # cur.execute(''' # SELECT person_id # FROM current WHERE current_id = 1''') curr_per = cur.fetchone() curr_per = curr_per cur.execute(select_current_person_image, curr_per) # cur.execute(''' # SELECT images # FROM image # JOIN images_entities # ON image.image_id = images_entities.image_id # WHERE main_image = 1 # AND images_entities.person_id = ?''', curr_per) old_top_pic = cur.fetchone() cur.execute(update_images_entities_zero, curr_per) # cur.execute(''' # UPDATE images_entities # SET main_image = 0 # WHERE main_image = 1 # AND images_entities.person_id = ?''', curr_per) conn.commit() cur.execute(update_images_entities_one, radio_value) # cur.execute(''' # UPDATE images_entities # SET main_image = 1 # WHERE image_id = ( # SELECT image_id # FROM image WHERE images = ?) # AND person_id = # (SELECT current.person_id # FROM current WHERE current_id = 1)''', radio_value) conn.commit() cur.close() conn.close()
class Combobox(FrameHilited3): hive = [] def __init__(self, master, root, callback=None, height=480, values=[], scrollbar_size=24, *args, **kwargs): FrameHilited3.__init__(self, master, *args, **kwargs) ''' This is a replacement for ttk.Combobox. ''' self.master = master self.callback = callback self.root = root self.height = height self.values = values self.scrollbar_size = scrollbar_size self.formats = make_formats_dict() self.buttons = [] self.selected = None self.result_string = '' self.entered = None self.lenval = len(self.values) self.owt = None self.scrollbar_clicked = False self.typed = None self.screen_height = self.winfo_screenheight() self.config(bd=0) # simulate <<ComboboxSelected>>: self.var = tk.StringVar() self.var.trace_add('write', lambda *args, **kwargs: self.combobox_selected()) # simulate ttk.Combobox.current() self.current = 0 self.make_widgets() self.master.bind_all('<ButtonRelease-1>', self.close_dropdown, add='+') # self.root.bind('<Configure>', self.hide_all_drops) # DO NOT DELETE # Above binding closes dropdown if Windows title bar is clicked, it # has no other purpose. But it causes minor glitches e.g. if a # dropdown button is highlighted and focused, the Entry has to be # clicked twice to put it back into the alternating drop/undrop # cycle as expected. Without this binding, the click on the title # bar lowers the dropdown below the root window which is good # enough for now. To get around it, use the custom_window_border.py. # expose only unique methods of Entry e.g. not self.config (self is a Frame and # the Entry, Toplevel, Canvas, and window have to be configured together) so # to size the entry use instance.config_drop_width(72) self.insert = self.entry.insert self.delete = self.entry.delete self.get = self.entry.get def make_widgets(self): self.entry = Entry(self, textvariable=self.var) self.arrow = LabelHilited(self, text='\u25BC', width=2) # self.arrow = ComboboxArrow(self, text='\u25BC', width=2) self.entry.grid(column=0, row=0) self.arrow.grid(column=1, row=0) self.update_idletasks() self.width = self.winfo_reqwidth() self.drop = ToplevelHilited(self, bd=0) self.drop.bind('<Destroy>', self.clear_reference_to_dropdown) self.drop.withdraw() Combobox.hive.append(self.drop) for widg in (self.master, self.drop): widg.bind('<Escape>', self.hide_all_drops, add='+') self.drop.grid_columnconfigure(0, weight=1) self.drop.grid_rowconfigure(0, weight=1) self.canvas = CanvasHilited(self.drop) self.canvas.grid(column=0, row=0, sticky='news') self.scrollv_combo = Scrollbar(self.drop, hideable=True, command=self.canvas.yview) self.canvas.config(yscrollcommand=self.scrollv_combo.set) self.content = Frame(self.canvas) self.content.grid_columnconfigure(0, weight=1) self.content.grid_rowconfigure('all', weight=1) self.scrollv_combo.grid(column=1, row=0, sticky='ns') self.entry.bind('<KeyPress>', self.open_or_close_dropdown) self.entry.bind('<Tab>', self.open_or_close_dropdown) for widg in (self.entry, self.arrow): widg.bind('<Button-1>', self.open_or_close_dropdown, add='+') self.arrow.bind('<Button-1>', self.focus_entry_on_arrow_click, add='+') for frm in (self, self.content): frm.bind('<FocusIn>', self.arrow.highlight) frm.bind('<FocusOut>', self.arrow.unhighlight) self.drop.bind('<FocusIn>', self.focus_dropdown) self.drop.bind('<Unmap>', self.unhighlight_all_drop_items) self.current_combo_parts = [ self, self.entry, self.arrow, self.scrollv_combo ] for part in self.current_combo_parts: part.bind('<Enter>', self.unbind_combo_parts) part.bind('<Leave>', self.rebind_combo_parts) self.config_values(self.values) config_generic(self.drop) def unbind_combo_parts(self, evt): self.master.unbind_all('<ButtonRelease-1>') def rebind_combo_parts(self, evt): self.master.bind_all('<ButtonRelease-1>', self.close_dropdown, add='+') def unhighlight_all_drop_items(self, evt): for child in self.content.winfo_children(): child.config(bg=self.formats['highlight_bg']) def clear_reference_to_dropdown(self, evt): dropdown = evt.widget if dropdown in Combobox.hive: idx = Combobox.hive.index(dropdown) del Combobox.hive[idx] dropdown = None def config_values(self, values): ''' The vertical scrollbar, when there is one, overlaps the dropdown button highlight but both still work. To change this, the button width can be changed when the scrollbar appears and disappears. ''' # a sample button is made to get its height, then destroyed b = ButtonFlatHilited(self.content, text='Sample') one_height = b.winfo_reqheight() b.destroy() self.fit_height = one_height * len(values) self.values = values self.lenval = len(self.values) for button in self.buttons: button.destroy() self.buttons = [] host_width = self.winfo_reqwidth() self.window = self.canvas.create_window(0, 0, anchor='nw', window=self.content, width=host_width) self.canvas.config(scrollregion=(0, 0, host_width, self.fit_height)) c = 0 for item in values: bt = ButtonFlatHilited(self.content, text=item, anchor='w') bt.grid(column=0, row=c, sticky='ew') for event in ('<Button-1>', '<Return>', '<space>'): bt.bind(event, self.get_clicked, add='+') bt.bind('<Enter>', self.highlight) bt.bind('<Leave>', self.unhighlight) bt.bind('<Tab>', self.tab_out_of_dropdown_fwd) bt.bind('<Shift-Tab>', self.tab_out_of_dropdown_back) bt.bind('<KeyPress>', self.traverse_on_arrow) bt.bind('<FocusOut>', self.unhighlight) bt.bind('<FocusOut>', self.get_tip_widg, add='+') bt.bind('<FocusIn>', self.get_tip_widg) bt.bind('<Enter>', self.get_tip_widg, add='+') bt.bind('<Leave>', self.get_tip_widg, add='+') self.buttons.append(bt) c += 1 for b in self.buttons: b.config(command=self.callback) def get_tip_widg(self, evt): ''' '10' is FocusOut, '9' is FocusIn ''' if self.winfo_reqwidth() <= evt.widget.winfo_reqwidth(): widg = evt.widget evt_type = evt.type if evt_type in ('7', '9'): self.show_overwidth_tip(widg) elif evt_type in ('8', '10'): self.hide_overwidth_tip() def show_overwidth_tip(self, widg): ''' Instead of a horizontal scrollbar, if a dropdown item doesn't all show in the space allotted, the full text will appear in a tooltip on highlight. Most of this code is borrowed from Michael Foord. ''' text = widg.cget('text') if self.owt: return x, y, cx, cy = widg.bbox() x = x + widg.winfo_rootx() + 32 y = y + cy + widg.winfo_rooty() + 32 self.owt = ToplevelHilited(self) self.owt.wm_overrideredirect(1) l = LabelTip2(self.owt, text=text) l.pack(ipadx=6, ipady=3) self.owt.wm_geometry('+{}+{}'.format(x, y)) def hide_overwidth_tip(self): tip = self.owt self.owt = None if tip: tip.destroy() def highlight_arrow(self, evt): self.arrow.config(bg=self.formats['head_bg']) def unhighlight_arrow(self, evt): self.arrow.config(bg=self.formats['highlight_bg']) def focus_entry_on_arrow_click(self, evt): self.focus_set() self.entry.select_range(0, 'end') def hide_other_drops(self): for dropdown in Combobox.hive: if dropdown != self.drop: dropdown.withdraw() def hide_all_drops(self, evt=None): for dropdown in Combobox.hive: dropdown.withdraw() def close_dropdown(self, evt): ''' Runs only on ButtonRelease-1. In the case of a destroyable combobox in a dialog, after the combobox is destroyed, this event will cause an error because the dropdown no longer exists. I think this is harmless so I added the try/except to pass on it instead of figuring out how to prevent the error. ''' widg = evt.widget if widg == self.scrollv_combo: self.scrollbar_clicked = True try: self.drop.withdraw() except tk.TclError: pass def config_drop_width(self, new_width): self.entry.config(width=new_width) self.update_idletasks() self.width = self.winfo_reqwidth() self.drop.geometry('{}x{}'.format(self.width, self.height)) self.scrollregion_width = new_width self.canvas.itemconfigure(self.window, width=self.width) self.canvas.configure(scrollregion=(0, 0, new_width, self.fit_height)) def open_or_close_dropdown(self, evt=None): if evt is None: # dropdown item clicked--no evt bec. of Button command option if self.callback: self.callback(self.selected) self.drop.withdraw() return if len(self.buttons) == 0: return evt_type = evt.type evt_sym = evt.keysym if evt_sym == 'Tab': self.drop.withdraw() return elif evt_sym == 'Escape': self.hide_all_drops() return first = None last = None if len(self.buttons) != 0: first = self.buttons[0] last = self.buttons[len(self.buttons) - 1] # self.drop.winfo_ismapped() gets the wrong value # if the scrollbar was the last thing clicked # so drop_is_open has to be used also. if evt_type == '4': if self.drop.winfo_ismapped() == 1: drop_is_open = True elif self.drop.winfo_ismapped() == 0: drop_is_open = False if self.scrollbar_clicked is True: drop_is_open = True self.scrollbar_clicked = False if drop_is_open is True: self.drop.withdraw() drop_is_open = False return elif drop_is_open is False: pass elif evt_type == '2': if evt_sym not in ('Up', 'Down'): return elif first is None or last is None: pass elif evt_sym == 'Down': first.config(bg=self.formats['bg']) first.focus_set() self.canvas.yview_moveto(0.0) elif evt_sym == 'Up': last.config(bg=self.formats['bg']) last.focus_set() self.canvas.yview_moveto(1.0) self.update_idletasks() x = self.winfo_rootx() y = self.winfo_rooty() combo_height = self.winfo_reqheight() self.fit_height = self.content.winfo_reqheight() self.drop.wm_overrideredirect(1) fly_up = self.get_vertical_pos(combo_height, evt) if fly_up[0] is False: y = y + combo_height else: y = fly_up[1] self.drop.geometry('{}x{}+{}+{}'.format(self.width, self.height, x, y)) self.drop.deiconify() self.hide_other_drops() def get_vertical_pos(self, combo_height, evt): fly_up = False vert_pos = evt.y_root - evt.y clearance = self.screen_height - (vert_pos + combo_height) if clearance < self.height: fly_up = True return (fly_up, vert_pos - self.height) def highlight(self, evt): for widg in self.buttons: widg.config(bg=self.formats['highlight_bg']) widget = evt.widget widget.config(bg=self.formats['bg']) self.selected = widget widget.focus_set() def unhighlight(self, evt): x, y = self.winfo_pointerxy() hovered = self.winfo_containing(x, y) if hovered in self.buttons: evt.widget.config(bg=self.formats['highlight_bg']) def hide_drops_on_title_bar_click(self, evt): x, y = self.winfo_pointerxy() hovered = self.winfo_containing(x, y) def focus_dropdown(self, evt): for widg in self.buttons: widg.config(takefocus=1) def handle_tab_out_of_dropdown(self, go): for widg in self.buttons: widg.config(takefocus=0) self.entry.delete(0, 'end') self.entry.insert(0, self.selected.cget('text')) self.drop.withdraw() self.entry.focus_set() if go == 'fwd': goto = self.entry.tk_focusNext() elif go == 'back': goto = self.entry.tk_focusPrev() goto.focus_set() def tab_out_of_dropdown_fwd(self, evt): self.selected = evt.widget self.handle_tab_out_of_dropdown('fwd') def tab_out_of_dropdown_back(self, evt): self.selected = evt.widget self.handle_tab_out_of_dropdown('back') def get_clicked(self, evt): self.selected = evt.widget self.current = self.selected.grid_info()['row'] self.entry.delete(0, 'end') self.entry.insert(0, self.selected.cget('text')) self.entry.select_range(0, 'end') self.open_or_close_dropdown() def get_typed(self): self.typed = self.var.get() def highlight_on_traverse(self, evt, next_item=None, prev_item=None): evt_type = evt.type evt_sym = evt.keysym # 2 is key press, 4 is button press for widg in self.buttons: widg.config(bg=self.formats['highlight_bg']) if evt_type == '4': self.selected = evt.widget elif evt_type == '2' and evt_sym == 'Down': self.selected = next_item elif evt_type == '2' and evt_sym == 'Up': self.selected = prev_item self.selected.config(bg=self.formats['bg']) self.widg_height = int(self.fit_height / self.lenval) widg_screenpos = self.selected.winfo_rooty() widg_listpos = self.selected.winfo_y() win_top = self.drop.winfo_rooty() win_bottom = win_top + self.height win_ratio = self.height / self.fit_height list_ratio = widg_listpos / self.fit_height widg_ratio = self.widg_height / self.fit_height up_ratio = list_ratio - win_ratio + widg_ratio if widg_screenpos > win_bottom - 0.75 * self.widg_height: self.canvas.yview_moveto(float(list_ratio)) elif widg_screenpos < win_top: self.canvas.yview_moveto(float(up_ratio)) self.selected.focus_set() def traverse_on_arrow(self, evt): if evt.keysym not in ('Up', 'Down'): return widg = evt.widget sym = evt.keysym self.widg_height = int(self.fit_height / self.lenval) self.trigger_down = self.height - self.widg_height * 3 self.trigger_up = self.height - self.widg_height * 2 self.update_idletasks() next_item = widg.tk_focusNext() prev_item = widg.tk_focusPrev() rel_ht = widg.winfo_y() if sym == 'Down': if next_item in self.buttons: self.highlight_on_traverse(evt, next_item=next_item) else: next_item = self.buttons[0] next_item.focus_set() next_item.config(bg=self.formats['bg']) self.canvas.yview_moveto(0.0) elif sym == 'Up': if prev_item in self.buttons: self.highlight_on_traverse(evt, prev_item=prev_item) else: prev_item = self.buttons[self.lenval - 1] prev_item.focus_set() prev_item.config(bg=self.formats['bg']) self.canvas.yview_moveto(1.0) def colorize(self): # the widgets that don't respond to events are working # the scrollbar, which has its own colorize method, is working # the arrow label has its own highlight methods, it's working self.config(bg=self.formats['bg']) self.entry.config(bg=self.formats['highlight_bg']) self.drop.config(bg=self.formats['highlight_bg']) self.content.config(bg=self.formats['highlight_bg']) # The dropdown buttons respond to so many events that it might be # a sort of minor miracle to make them colorize instantly. For # now it's enough that they colorize on reload and they are not # on top, they're only seen on dropdown. def callback(self): ''' A function specified on instantiation. ''' print('this will not print if overridden (callback)') def combobox_selected(self): ''' A function specified on instantiation will run when the selection is made. Similar to ttk's <<ComboboxSelected>> but instead of binding to a virtual event. ''' print('this will not print if overridden (combobox_selected)')