class EditRow(Frame): def __init__(self, master, command, *args, **kwargs): Frame.__init__(self, master, *args, **kwargs) self.ent = Entry(self, width=36) spacer = Label(self, width=3) ok_butt = Button(self, text='OK', command=command) cancel_butt = Button(self, text='CANCEL', command=self.remove_edit_row) spacer.grid(column=0, row=0) self.ent.grid(column=1, row=0, padx=3, pady=3) ok_butt.grid(column=2, row=0, padx=6, pady=6) cancel_butt.grid(column=3, row=0, padx=6, pady=6) def remove_edit_row(self): self.grid_forget()
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.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()) self.make_widgets() master.bind_all('<ButtonRelease-1>', self.close_dropdown, add='+') self.root.bind('<Configure>', self.hide_all_drops) def close_dropdown(self, evt): ''' Runs only on ButtonRelease-1. ''' widg = evt.widget if widg == self.canvas.vert: self.scrollbar_clicked = True if widg in (self, self.arrow, self.entry, self.canvas.vert): return self.drop.withdraw() def make_widgets(self): self.entry = Entry(self, textvariable=self.var) self.arrow = LabelHilited(self, text='\u25BC', width=2) self.entry.grid(column=0, row=0) self.arrow.grid(column=1, row=0, padx=0, pady=0) self.next_on_tab = self.tk_focusNext() self.prev_on_tab = self.tk_focusPrev() self.update_idletasks() self.width = self.winfo_reqwidth() self.drop = ToplevelHilited( self, bd=0) self.drop.withdraw() Combobox.hive.append(self.drop) self.master.bind('<Escape>', self.hide_all_drops) self.drop.grid_columnconfigure(0, weight=1) self.drop.grid_rowconfigure(0, weight=1) self.canvas = CanvasScrolledBG2( self.drop, fixed_width=True, scrollregion_width=self.width, scrollbar='vert') self.canvas.grid(column=0, row=0, sticky='news') self.canvas.content.grid_columnconfigure(0, weight=1) self.canvas.content.grid_rowconfigure('all', weight=1) self.canvas.vert.grid(column=1, row=0, sticky='ns') self.entry.bind('<KeyPress>', self.open_or_close_dropdown) for widg in (self.entry, self.arrow): widg.bind('<Button-1>', self.open_or_close_dropdown) self.arrow.bind('<Button-1>', self.focus_entry_on_arrow_click, add='+') for frm in (self, self.canvas.content): frm.bind('<FocusIn>', self.highlight_arrow) frm.bind('<FocusOut>', self.unhighlight_arrow) self.drop.bind('<FocusIn>', self.focus_dropdown) self.config_values(self.values) config_generic(self.drop) def config_values(self, values): b = ButtonFlatHilited(self.canvas.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 = [] CanvasScrolledBG2.config_fixed_width_canvas(self) c = 0 for item in values: bt = ButtonFlatHilited(self.canvas.content, text=item, anchor='w') bt.grid(column=0, row=c, sticky='ew') # for event in ('<Return>', '<space>'): for event in ('<Button-1>', '<Return>', '<space>'): bt.bind(event, self.get_clicked) 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. Some of this code is borrowed from Michael Foord. ''' text=widg.cget('text') 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=formats['head_bg']) def unhighlight_arrow(self, evt): self.arrow.config(bg=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): for dropdown in Combobox.hive: dropdown.withdraw() 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 evt_type = evt.type evt_sym = evt.keysym first = self.buttons[0] print('245 self.lenval is', self.lenval) last = self.buttons[self.lenval - 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 evt_sym == 'Down': first.config(bg=formats['bg']) first.focus_set() self.canvas.yview_moveto(0.0) elif evt_sym == 'Up': last.config(bg=formats['bg']) last.focus_set() self.canvas.yview_moveto(1.0) self.update_idletasks() x = self.winfo_rootx() y = self.winfo_rooty() y_off = self.winfo_reqheight() self.fit_height = self.canvas.content.winfo_reqheight() self.drop.wm_overrideredirect(1) fly_up = self.get_vertical_pos() if fly_up[0] is False: y = y + y_off 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): fly_up = False self.update_idletasks() vertical_pos = self.winfo_rooty() vertical_rel = self.winfo_y() combo_height = self.winfo_reqheight() top_o_drop = vertical_pos + vertical_rel + combo_height clearance = self.screen_height - top_o_drop if clearance < self.height: fly_up = True return (fly_up, vertical_pos - self.height) def highlight(self, evt): widg = evt.widget self.update_idletasks() widg.config(bg=formats['bg']) def unhighlight(self, evt): widg = evt.widget widg.config(bg=formats['highlight_bg']) def focus_dropdown(self, evt): for widg in self.buttons: widg.config(takefocus=1) def tab_out_of_dropdown_fwd(self, evt): for widg in self.buttons: widg.config(takefocus=0) self.selected = evt.widget self.entry.delete(0, 'end') self.entry.insert(0, self.selected.cget('text')) next_on_tab = self.tk_focusNext() next_on_tab = next_on_tab.tk_focusNext() next_on_tab.focus_set() def tab_out_of_dropdown_back(self, evt): for widg in self.buttons: widg.config(takefocus=0) self.selected = evt.widget self.entry.delete(0, 'end') self.entry.insert(0, self.selected.cget('text')) prev_on_tab = self.tk_focusPrev() prev_on_tab.focus_set() def get_clicked(self, evt): self.selected = evt.widget 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=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=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=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=formats['bg']) self.canvas.yview_moveto(1.0) 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, just pass the name of the function in the constructor. ''' print('this will not print if overridden (combobox_selected)')
class PersonAdd(Frame): def __init__(self, parent, autofill, root, *args, **kwargs): Frame.__init__(self, parent, *args, **kwargs) self.parent = parent self.root = root self.autofill = autofill self.config(padx=24, pady=24) self.gender = 'unknown' self.new_person_id = None self.role_person_edited = False self.findings_roles_id = None self.rc_menu = RightClickMenu(self.root) self.make_widgets() def make_widgets(self): all_pics = self.get_all_pics() lab1 = Label(self, text='Gender:') self.gender_input = ClearableReadonlyCombobox(self, values=GENDER_TYPES) lab2 = Label(self, text='Main Image:') self.image_input = ClickAnywhereCombo(self, values=all_pics) lab3 = Label(self, text='Name Type:') self.name_type_input = ClearableReadonlyCombobox(self, values=self.get_name_types()) lab4 = Label(self, text='Full Name') self.name_input = Entry(self, width=65) self.how = LabelH3( self, text='Alphabetize name: after clicking auto-sort, tab into ' 'auto-filled name fields to modify\nsort order with ' 'arrow keys or if sort order is correct, just click ADD.') autosort = Button( self, text='AUTOSORT', command=self.show_sort_order) self.order_frm = Frame(self) s = 0 for stg in range(20): mov = LabelMovable(self.order_frm) mov.grid(column=s, row=0, padx=3) s += 1 self.buttonbox = Frame(self) self.add_butt = Button(self.buttonbox, text='ADD', width=8) lab1.grid(column=0, row=3) self.gender_input.grid( column=1, row=3, padx=12, pady=12, sticky='e') lab2.grid(column=2, row=3) self.image_input.grid(column=3, row=3, padx=12, pady=12) lab3.grid(column=0, row=4) self.name_type_input.grid( column=1, row=4, padx=12, pady=12, sticky='e') lab4.grid(column=2, row=4) self.name_input.grid(column=3, row=4, padx=12, pady=12) self.how.grid(column=0, row=5, padx=6, pady=6, columnspan=4) autosort.grid(column=0, row=6, padx=6, pady=6) self.order_frm.grid(column=1, row=6, columnspan=4, pady=24) self.buttonbox.grid(column=1, row=7, sticky='e') self.add_butt.grid(column=0, row=0, padx=6, pady=6, sticky='e') self.new_person_statusbar = StatusbarTooltips(self.parent, resizer=False) visited = ( (self.gender_input, "Gender Input", "'Unknown' used if left blank."), (self.image_input, "Image Input", "Use an old photo of person's home town if no photo available."), (self.name_type_input, "Name Type Input", "Choose the name type."), (self.name_input, "Name Input", "Autofills but you can change it."), (autosort, "Autosort Button", "Click to auto-create a sortable name."), (self.order_frm, "", "Tab to focus name element. Arrow to change order.") ) run_statusbar_tooltips( visited, self.new_person_statusbar.status_label, self.new_person_statusbar.tooltip_label) self.preset() rcm_widgets = (self.name_input, self.name_type_input) make_rc_menus( rcm_widgets, self.rc_menu, person_add_msg) def preset(self): self.gender_input.config(state='normal') self.gender_input.delete(0, 'end') self.gender_input.config(state='readonly') self.image_input.delete(0, 'end') self.image_input.insert(0, 'no_photo_001.gif') self.name_type_input.config(state='normal') self.name_type_input.delete(0, 'end') self.name_type_input.insert(0, 'birth name') self.name_type_input.config(state='readonly') def reset(self): self.preset() self.name_input.delete(0, 'end') for child in self.order_frm.winfo_children(): child['text'] = '' self.dupe_check = True def add_person(self, findings_roles_id=None): self.get_entered_values() self.findings_roles_id = findings_roles_id self.check_for_dupes() def get_entered_values(self): ''' ''' if len(self.gender_input.get()) != 0: self.gender = self.gender_input.get() self.full_name = self.name_input.get() self.selected_image = self.image_input.get() self.name_type = self.name_type_input.get() def check_for_dupes(self): ''' If birth name already exists in database, open dialog. ''' conn = sqlite3.connect(current_file) cur = conn.cursor() cur.execute(select_all_person_ids) all_people = cur.fetchall() cur.close() conn.close() all_people = [[i[0]] for i in all_people] names_only = [] for id in all_people: display_name = get_name_with_id(id[0]) names_only.append(display_name) id.insert(0, display_name) people_vals = [] for lst in all_people: if not lst[0]: lst[0] = '' people_vals.append(' #'.join([lst[0], str(lst[1])])) if self.full_name not in names_only: self.make_new_person() self.make_sort_order() self.save_new_name(self.new_person_id) self.reset() else: self.dupe_check = msg.YesNoMessage( self, title='Duplicate Check', message="This birth name already exists. To create a " "new person by the same name, click SUBMIT. The " "two persons can be merged later if desired.", show_input=self.full_name).show() if self.dupe_check is True: self.make_new_person() self.make_sort_order() self.save_new_name(self.new_person_id) self.reset() else: self.reset() def make_new_person(self): conn = sqlite3.connect(current_file) conn.execute('PRAGMA foreign_keys = 1') cur = conn.cursor() cur.execute(insert_person_null, (self.gender,)) conn.commit() cur.execute("SELECT seq FROM SQLITE_SEQUENCE WHERE name = 'person' ") new_person_id = cur.fetchone() if new_person_id: self.new_person_id = new_person_id[0] if self.role_person_edited is True: cur.execute( update_findings_roles_person, (self.new_person_id, self.findings_roles_id)) conn.commit() self.role_person_edited = False cur.close() conn.close() def make_sort_order(self): self.order = [] for child in self.order_frm.winfo_children(): text = child['text'] self.order.append(text) self.order = ' '.join(self.order) self.order = self.order.replace(' , ', ', ') self.order = self.order.strip(', ') def save_new_name(self, subject_id): conn = sqlite3.connect(current_file) cur = conn.cursor() conn.execute('PRAGMA foreign_keys = 1') cur.execute(select_image_id, (self.selected_image,)) img_id = cur.fetchone()[0] cur.execute(insert_images_entities, (img_id, subject_id)) conn.commit() cur.execute(select_name_type_id, (self.name_type,)) name_type_id = cur.fetchone() name_type_id = name_type_id[0] cur.execute( insert_name, (subject_id, self.full_name, name_type_id, self.order)) conn.commit() new_list = make_values_list_for_person_select() self.autofill.values = new_list cur.close() conn.close() self.image_input.delete(0, 'end') self.image_input.insert(0, 'no_photo_001.gif') for widg in (self.name_type_input, self.name_input): widg.config(state='normal') widg.delete(0, 'end') self.name_type_input.config(state='readonly') for child in self.order_frm.winfo_children(): child.config(text='') self.gender_input.config(state='normal') self.gender_input.delete(0, 'end') self.gender_input.config(state='readonly') self.gender_input.focus_set() def show_sort_order(self): self.got = self.name_input.get() self.new_name = self.got self.got = self.got.split() if len(self.got) == 0: return else: length = len(self.got)-1 word = self.got[length].lower() self.got.insert(0, ',') length += 1 if word not in NAME_SUFFIXES: self.got.insert(0, self.got.pop()) elif word in NAME_SUFFIXES and self.got[length].lower() == word: self.got.insert(0, self.got.pop()) self.got.insert(0, self.got.pop()) for child in self.order_frm.winfo_children(): child.config(text='') v = 0 for name in self.got: self.order_frm.winfo_children()[v].config(text=name) v += 1 def get_name_types(self): conn = sqlite3.connect(current_file) cur = conn.cursor() cur.execute(select_all_name_types) name_types = cur.fetchall() cur.close() conn.close() name_types = [i[0] for i in name_types] return name_types def get_all_pics(self): conn = sqlite3.connect(current_file) cur = conn.cursor() picvals = cur.fetchall() cur.close() conn.close() return picvals
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)')
class MessageModel(Toplevelx): ''' parent: modal dialog has no minimize/maximize buttons, goes where parent goes gparent: parent of parent might need to be destroyed on click of msg button title: goes on title bar of dialog using w.title('title') method prompt: tells the user to enter some data in an entry or combobox message: multiline Message widget text to inform user why dialog opened input: bad input from user displayed in dialog so he sees his typo/bad data x = inst.show(): data input by user into dialog is returned on dialog close ''' def __init__(self, parent, gparent=None, title=None, prompt=None, message=None, input_text=None, *args, **kwargs): Toplevelx.__init__(self, parent, title=None, *args, **kwargs) self.edit_input = False self.add_data_to_db = True self.parent = parent self.gparent = gparent self.prompt = prompt self.message = message self.input_text = input_text if title: self.title(title) self.grab_set() self.transient(parent) self.make_widgets() def make_widgets(self): self.frm = Frame(self) self.frm.grid(column=0, row=0, pady=(0, 18)) self.errlab = Label(self.frm, text='Input was: ') self.errlab.grid(column=0, row=0, padx=24, pady=(24, 0), sticky='w') self.errshow = LabelItalic(self.frm) self.errshow.grid(column=1, row=0, pady=(24, 0), sticky='w') self.errmsg = tk.Message(self.frm, text=self.message, bd=3, relief='raised', aspect=400) self.errmsg.grid(column=0, row=1, padx=36, pady=(0, 18), columnspan=2) self.promptlab = Label(self.frm) self.var = tk.StringVar() self.altentry = Entry(self.frm) self.altentry.config(textvariable=self.var) self.promptlab.grid(column=0, row=2, pady=24, padx=24, sticky='w') self.altentry.grid(column=1, row=2, pady=24, sticky='w') self.promptlab.grid_remove() self.altentry.grid_remove() self.altentry.focus_set() self.altentry.bind('<Return>', self.on_ok) self.bbox = Frame(self) self.bbox.grid(column=0, row=3, columnspan=2) self.done = Button(self.bbox, text='DONE', command=self.on_ok, width=9) self.done.grid(column=0, row=0, padx=24, pady=(12, 24)) self.done.grid_remove() self.stop = Button(self.bbox, text='CANCEL1', command=self.on_cancel, width=9) self.stop.grid(column=1, row=0, padx=24, pady=(12, 24)) self.stop.grid_remove() self.done.focus_set() self.accept = Button(self.bbox, text='Accept Original Input', command=self.on_ok, width=36) self.accept.grid(column=0, row=0, padx=24, pady=12) self.accept.grid_remove() self.edit = Button(self.bbox, text='Submit Edited Input', command=self.on_alt, width=36) self.edit.grid(column=0, row=1, padx=24, pady=12) self.edit.grid_remove() self.cancel_all = Button(self.bbox, text="Cancel (Don't Submit Anything)", command=self.on_cancel, width=36) self.cancel_all.grid(column=0, row=2, padx=24, pady=(12, 36)) self.cancel_all.grid_remove() self.bind('<Escape>', self.on_cancel) PlaySound('SystemHand', SND_ASYNC) self.protocol("WM_DELETE_WINDOW", self.on_cancel) def on_ok(self, event=None): print('original input is OK; input is:', self.input) def on_alt(self, event=None): self.edit_input = True self.destroy() def on_cancel(self, event=None): self.add_data_to_db = False self.destroy()
class FontPicker(Frame): def __init__(self, master, main, *args, **kwargs): Frame.__init__(self, master, *args, **kwargs) self.master = master self.root = main.root self.canvas = main.canvas self.content = main self.canvas_docs = main.canvas_docs self.content_docs = main.content_docs self.all_fonts = font.families() conn = sqlite3.connect(current_file) cur = conn.cursor() cur.execute(select_font_scheme) font_scheme = cur.fetchall() cur.close() conn.close() self.font_scheme = list(font_scheme[0]) self.make_widgets() def make_widgets(self): def combobox_selected(combo): ''' The reason this function is nested is that I have no experience with overriding methods. When I tried to add `self` as the first parameter, there was an error and I didn't know what to do. I nested it so I wouldn't have to use `self`. ''' if combo == self.combos["select_input_font"]: input_sample.config(font=(self.all_fonts[combo.current], self.fontSize)) elif combo == self.combos["select_output_font"]: output_sample.config(font=(self.all_fonts[combo.current], self.fontSize)) else: print("case not handled") # update_idletasks() seems to speed up the redrawing of the # app with the new font self.update_idletasks() sample_text = ["Sample", "Text ABCDEFGHxyz 0123456789 iIl1 o0O"] self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) sample = Frame(self) self.output_sample = Label(sample, text=" Output ".join(sample_text)) self.input_sample = Entry(sample, width=50) self.input_sample.insert(0, " Input ".join(sample_text)) self.fontSizeVar = tk.IntVar() self.fontSize = self.font_scheme[0] self.font_size = Scale( self, from_=8.0, to=26.0, tickinterval=6.0, label="Text Size", # Can this be centered over the Scale? orient="horizontal", length=200, variable=self.fontSizeVar, command=self.show_font_size) self.font_size.set(self.fontSize) combo_names = ["select_output_font", "select_input_font"] self.combos = {} j = 2 for name in combo_names: cbo = Combobox(self, self.root, values=self.all_fonts, height=300, scrollbar_size=12) self.combos[name] = cbo name = name.replace("_", " ").title() lab = Label(self, text=name) lab.grid(column=0, row=j, pady=(24, 6)) cbo.grid(column=0, row=j + 1, pady=(6, 20)) j += 2 self.apply_button = Button(self, text="APPLY", command=self.apply) sample.grid(column=0, row=0) self.output_sample.grid(padx=24, pady=20) self.input_sample.grid(padx=24, pady=20) self.font_size.grid(column=0, row=1, pady=24) self.apply_button.grid(column=0, row=7, sticky="e", padx=(0, 24), pady=(0, 24)) Combobox.combobox_selected = combobox_selected def apply(self): def resize_scrollbar(): self.root.update_idletasks() self.canvas_docs.config(scrollregion=self.canvas_docs.bbox('all')) self.font_scheme[0] = self.fontSizeVar.get() if len(self.combos["select_output_font"].get()) != 0: self.font_scheme[1] = self.combos["select_output_font"].get() if len(self.combos["select_input_font"].get()) != 0: self.font_scheme[2] = self.combos["select_input_font"].get() conn = sqlite3.connect(current_file) conn.execute('PRAGMA foreign_keys = 1') cur = conn.cursor() cur.execute(update_format_fonts, tuple(self.font_scheme)) conn.commit() cur.close() conn.close() config_generic(self.root) resize_scrollbar() def show_font_size(self, evt): self.fontSize = self.fontSizeVar.get()
class NotesDialog(Toplevel): def __init__(self, master, current_person, finding_id=None, header=None, pressed=None, *args, **kwargs): Toplevel.__init__(self, master, *args, **kwargs) self.birth_name = get_name_with_id(current_person) self.title('{} ({})'.format('Notes for an Event', self.birth_name)) self.current_person = current_person self.root = master self.finding_id = finding_id self.header = header self.pressed = pressed # self.self.new_index = None self.current_note_text = '' self.current_subtopic = '' self.current_note_id = None self.new_subtopic_label_text = 'New Note' self.current_subtopic_index = None self.new_subtopic_name = '' self.current_file = get_current_file()[0] self.bind('<Escape>', self.close_roles_dialog) self.subtopics = [] self.privacy = tk.IntVar() self.refresh_notes_per_finding() self.rc_menu = RightClickMenu(self.root) self.make_widgets() self.protocol('WM_DELETE_WINDOW', self.close_roles_dialog) def make_widgets(self): self.title('{}{} ({})'.format('Notes for Conclusion #', self.finding_id, self.birth_name)) self.grid_columnconfigure(0, weight=1) self.grid_rowconfigure(1, weight=1) header = Frame(self) header.grid_columnconfigure(1, weight=1) header.grid_rowconfigure(0, weight=1) self.notes_dialog_header = Frame(header) content = Frame(self) left_panel = Frame(content) topiclab = Label(left_panel, text='Note Subtopics') self.toc = Listbox(left_panel, self.sorted_subtopics, view_height=424, view_width=180, scrollbar=False) self.new_index = len(self.subtopics) self.toc.insert(self.new_index, self.new_subtopic_label_text) self.toc.focus_set() self.toc.selection_set(0) self.toc.store_new_subtopic_name = self.store_new_subtopic_name self.toc.pass_ctrl_click = self.open_links_dialog self.toc.pass_delete_key = self.delete_note self.selected_subtopic = Label(content, text='New Note') self.note_input = ScrolledText(content) note_submit = Button(content, text='Submit Changes', command=self.save_changes) order = Button(content, text='Change Order of Subtopics', command=self.reorder_notes) radframe = LabelFrame(content, text='Make selected note...') self.public = Radiobutton(radframe, text='...public', anchor='w', variable=self.privacy, value=0, command=self.save_privacy_setting) self.private = Radiobutton(radframe, text='...private', anchor='w', variable=self.privacy, value=1, command=self.save_privacy_setting) close = Button(content, text='DONE', command=self.close_roles_dialog) # notes_statusbar = StatusbarTooltips(self) # visited = ( # (above, # 'this is a notes_statusbar message', # 'this is a tooltip'), # (below, # 'this is also a notes_statusbar message', # 'this is another tooltip')) # run_statusbar_tooltips( # visited, # notes_statusbar.status_label, # notes_statusbar.tooltip_label) # grid in self header.grid(column=0, row=0, columnspan=2, pady=12, sticky='we') content.grid(column=0, row=1, sticky='news', padx=(0, 6)) # notes_statusbar.grid(column=0, row=2, sticky='ew') # grid in header self.notes_dialog_header.grid(column=1, row=0, sticky='ew') self.notes_dialog_header.grid_columnconfigure(0, weight=1) # grid in content left_panel.grid(column=0, row=1, sticky='news', rowspan=2, pady=24) self.selected_subtopic.grid(column=1, row=1, sticky='w') self.note_input.grid(column=1, row=2, columnspan=3, sticky='nsew', padx=(0, 24), pady=12) note_submit.grid(column=3, row=3, sticky='se') self.grid_rowconfigure(0, weight=1) self.grid_columnconfigure(0, weight=1) content.grid_rowconfigure(1, weight=1) order.grid(column=1, row=4) radframe.grid(column=2, row=5, sticky='n', pady=24) close.grid(column=3, row=6, sticky='se', padx=24, pady=(0, 24)) # grid in left_panel topiclab.grid(column=0, row=0) self.toc.grid(column=0, row=1) for child in self.toc.listbox_content.winfo_children(): child.bind('<<ListboxSelected>>', self.switch_note) self.subtopic_widgets = self.toc.listbox_content.winfo_children() # grid in radframe self.public.grid(column=0, row=0, sticky='news', padx=24) self.private.grid(column=0, row=1, sticky='news', padx=24) # rcm_widgets = (self.subtopic_input, self.note_input.text) # make_rc_menus( # rcm_widgets, # self.rc_menu, # note_dlg_msg, # header_parent=self.notes_dialog_header, # header=self.header, # which_dlg='notes') config_generic(self) center_window(self) def open_links_dialog(self): links = Toplevel(self.root) links.title('Link Notes to Multiple Entities') instrux = LabelH3( links, text='The current note can be linked to any number of Entities\n' 'such as Persons, Places, Assertions, Conclusions, or Sources.') instrux.grid(padx=24, pady=24) def save_changes(self): self.save_new_subtopic() conn = sqlite3.connect(self.current_file) conn.execute('PRAGMA foreign_keys = 1') cur = conn.cursor() new_note_text = self.note_input.text.get(1.0, 'end-1c') subtopic = self.current_subtopic cur.execute(update_note, (new_note_text, subtopic)) conn.commit() cur.close() conn.close() self.master.current_note_text = new_note_text self.refresh() def save_new_subtopic(self): if self.selected_subtopic.cget('text') != self.new_subtopic_label_text: return self.subtopic_dialog = Toplevel(self.root) self.subtopic_dialog.title('New Note: Subtopic Input Dialog') headlab = LabelH3(self.subtopic_dialog, text='Unique subtopic name for new note:') self.subtopic_input = Entry(self.subtopic_dialog, width=64) self.subtopic_input.focus_set() buttonbox = Frame(self.subtopic_dialog) self.subtopic_dialog.grid_columnconfigure(0, weight=1) new_note_ok = Button(buttonbox, text='Submit Note and Subtitle', command=self.submit_new_note) new_note_cancel = Button(buttonbox, text='Cancel', command=self.close_subtopic_dialog) headlab.grid(column=0, row=0, pady=(24, 0), columnspan=2) self.subtopic_input.grid(column=0, row=1, padx=24, pady=24, columnspan=2) buttonbox.grid(column=1, row=2, sticky='we', padx=24, pady=24) new_note_ok.grid(column=0, row=0, sticky='e', padx=12) new_note_cancel.grid(column=1, row=0, sticky='e', padx=12) self.subtopic_dialog.bind('<Escape>', self.close_subtopic_dialog) self.subtopic_dialog.protocol('WM_DELETE_WINDOW', self.close_subtopic_dialog) def submit_new_note(self): conn = sqlite3.connect(self.current_file) conn.execute('PRAGMA foreign_keys = 1') cur = conn.cursor() new_note_text = self.note_input.text.get(1.0, 'end-1c') new_subtopic = self.subtopic_input.get() cur.execute(select_count_subtopic, (new_subtopic, )) count = cur.fetchone()[0] if count > 0: non_unique_subtopic = ErrorMessage( self, message="Each subtopic can be\nused once in a tree.") non_unique_subtopic.title('Non-unique Note Title Error') self.subtopic_input.focus_set() return if len(new_subtopic) == 0: blank_subtopic = ErrorMessage(self, message="Blank notes are OK\n" "but not blank note titles.") blank_subtopic.title('Blank Note Title Error') self.subtopic_input.focus_set() return cur.execute(insert_note, (new_note_text, new_subtopic)) conn.commit() cur.execute("SELECT seq FROM SQLITE_SEQUENCE WHERE name = 'note'") new_note_id = cur.fetchone()[0] cur.execute(insert_findings_notes, (self.finding_id, new_note_id, self.new_index)) conn.commit() cur.close() conn.close() self.master.current_note_text = new_note_text self.refresh_notes_per_finding() self.toc.insert(self.new_index, new_subtopic) items = self.toc.listbox_content.winfo_children() for child in items: child.bind('<<ListboxSelected>>', self.switch_note) self.toc.resize_scrollbar() self.toc.selection_set(self.new_index) self.switch_note() self.pressed.config(text=' ... ') self.new_index = len(self.subtopics) self.close_subtopic_dialog() self.refresh() def refresh_notes_per_finding(self): conn = sqlite3.connect(self.current_file) cur = conn.cursor() cur.execute(select_notes_refresh, (self.finding_id, )) notes = cur.fetchall() all_subtopics = [] if len(notes) != 0: for tup in notes: all_subtopics.append([tup[1], tup[3]]) all_subtopics.sort(key=lambda i: i[1]) self.sorted_subtopics = [] for lst in all_subtopics: self.sorted_subtopics.append(lst[0]) self.subtopics = self.sorted_subtopics self.notesdict = {} for tup in notes: self.notesdict[tup[0]] = [tup[1], tup[2]] cur.close() conn.close() def close_roles_dialog(self, evt=None): self.destroy() self.master.focus_set() def close_subtopic_dialog(self, evt=None): self.subtopic_dialog.destroy() def save_privacy_setting(self): privacy_setting = self.privacy.get() conn = sqlite3.connect(self.current_file) conn.execute('PRAGMA foreign_keys = 1') cur = conn.cursor() cur.execute(update_note_private, (privacy_setting, self.current_note_id)) conn.commit() cur.close() conn.close() def get_selected_subtopic(self): # "is not None" has to be explicit here if self.toc.curselection() is not None: self.current_subtopic_index = self.toc.curselection() else: return self.current_subtopic = self.toc.get(self.current_subtopic_index) if len(self.notesdict) != 0: for k, v in self.notesdict.items(): if v[0] == self.current_subtopic: break self.current_note_id = k self.current_note_text = v[1] def store_new_subtopic_name(self): conn = sqlite3.connect(self.current_file) conn.execute('PRAGMA foreign_keys = 1') cur = conn.cursor() cur.execute(update_note_subtopic, (self.toc.new_subtopic_name, self.current_note_id)) conn.commit() cur.close() conn.close() self.subtopic_widgets = self.toc.listbox_content.winfo_children() for child in self.subtopic_widgets: child.bind('<<ListboxSelected>>', self.switch_note) if child.winfo_reqwidth() > self.toc.view_width: create_tooltip(child, self.toc.new_subtopic_name) def switch_note(self, evt=None): self.get_selected_subtopic() if len(self.current_subtopic) == 0: return self.selected_subtopic.config(text=self.current_subtopic) if self.current_subtopic == self.new_subtopic_label_text: self.note_input.text.delete(1.0, 'end') return self.note_input.text.delete(1.0, 'end') self.note_input.text.insert(1.0, self.current_note_text) conn = sqlite3.connect(self.current_file) cur = conn.cursor() cur.execute(select_private_note, (self.current_subtopic, self.finding_id)) privacy_setting = cur.fetchone() if privacy_setting: privacy_setting = privacy_setting[0] cur.close() conn.close() if privacy_setting is None: return elif privacy_setting == 0: self.public.select() elif privacy_setting == 1: self.private.select() def delete_note(self): ''' Since any given note can be re-used more than once by linking it to any number of findings, delete note from note table only if the note_id no longer exists at all in the findings_notes table; not every time a note_id is deleted from the findings_notes table. ''' def ok_delete(): run_delete() cancel_delete() def cancel_delete(): self.focus_set() deleter.destroy() def run_delete(): note_count_per_finding = 0 self.toc.delete(selected) self.note_input.text.delete(1.0, 'end') self.selected_subtopic.config(text='') cur.execute(delete_findings_notes, (deletable, self.finding_id)) conn.commit() if delete_var.get() == 1: cur.execute(select_count_findings_notes_note_id, (deletable, )) linked_notes = cur.fetchone()[0] if linked_notes == 0: cur.execute(delete_note, (deletable, )) conn.commit() cur.execute(select_count_findings_notes_finding_id, (self.finding_id, )) note_count_per_finding = cur.fetchone()[0] cur.close() conn.close() self.toc.selection_clear() if note_count_per_finding == 0: self.pressed.config(text=' ') self.refresh() selected = self.toc.curselection() subtopic = self.toc.get(selected) if subtopic is None: return conn = sqlite3.connect(self.current_file) conn.execute('PRAGMA foreign_keys = 1') cur = conn.cursor() cur.execute(select_note_id, (subtopic, )) deletable = cur.fetchone() if deletable is None: return elif deletable is not None: deletable = deletable[0] delete_var = tk.IntVar() deleter = Toplevel(self) deleter.title('Delete Note Dialog') instrux = LabelH3( deleter, text='Any note can be linked to any number of entities.') radio1 = Radiobutton(deleter, text='Delete this instance of this note.', value=0, variable=delete_var) radio2 = Radiobutton(deleter, text='Delete all instances of this note.', value=1, variable=delete_var) instrux.grid(column=0, row=0, columnspan=2, padx=24, pady=24) radio1.grid(column=0, row=1) radio2.grid(column=1, row=1) buttonbox = Frame(deleter) buttonbox.grid(column=1, row=2, sticky='e') b1 = Button(buttonbox, text='OK', command=ok_delete) b2 = Button(buttonbox, text='Cancel', command=cancel_delete) b1.grid(column=0, row=0) b2.grid(column=1, row=0) def refresh(self): self.refresh_notes_per_finding() self.toc.make_listbox_content() self.subtopic_widgets = self.toc.listbox_content.winfo_children() for child in self.subtopic_widgets: child.bind('<<ListboxSelected>>', self.switch_note) def reorder_notes(self): ''' ''' if self.toc.size() < 2: return self.order_dlg = Toplevel(self) self.order_dlg.grab_set() self.order_dlg.protocol('WM_DELETE_WINDOW', self.ignore_changes) self.order_dlg.bind('<Return>', self.save_close_reorder_dlg) self.order_dlg.bind('<Escape>', self.ignore_changes) self.order_dlg.grid_columnconfigure(0, weight=1) self.order_dlg.title('Reorder Subtopics') instrux = ('Tab or Ctrl+Tab selects movable subtopic.\n' 'Arrow keys change subtopic order up or down.') top = LabelH3(self.order_dlg, text=instrux, anchor='center') self.labels = Frame(self.order_dlg) e = 0 for subtopic in self.subtopics: lab = LabelMovable(self.labels, text=subtopic, anchor='w') if e == 0: first = lab e += 1 lab.grid(column=0, row=e, padx=3, sticky='ew') first.focus_set() close2 = Button(self.order_dlg, text='OK', command=self.save_close_reorder_dlg) top.grid(column=0, row=0, pady=(24, 0), padx=24, columnspan=2) self.labels.grid(column=0, row=1, columnspan=2, padx=24, pady=24) self.labels.grid_columnconfigure(0, weight=1) close2.grid(column=1, row=2, sticky='se', padx=12, pady=(0, 12)) center_window(self.order_dlg) def ignore_changes(self, evt=None): self.order_dlg.grab_release() self.order_dlg.destroy() self.focus_set() def save_close_reorder_dlg(self, evt=None): ''' Replace the values list. ''' q = 0 new_order = [] save_order = [] for child in self.labels.winfo_children(): text = child.cget('text') new_order.append(text) save_order.append([text, q]) q += 1 conn = sqlite3.connect(self.current_file) conn.execute('PRAGMA foreign_keys = 1') cur = conn.cursor() for lst in save_order: cur.execute(select_note_id, (lst[0], )) note_id = cur.fetchone()[0] cur.execute(update_findings_notes, (lst[1], self.finding_id, note_id)) conn.commit() cur.close() conn.close() self.subtopics = self.toc.items = new_order self.refresh() self.order_dlg.grab_release() self.order_dlg.destroy() self.focus_set()
finding = 1 view = tk.Tk() treebard = view # mockup; this isn't what really happens entry = EntryAutofill(view, width=50) entry.config(textvariable=entry.var) entry.values = place_strings entry.autofill = True entry.grid() entry.focus_set() entry.bind("<FocusOut>", get_final) traverse = Entry(view) traverse.grid() frame = Frame(view) frame.grid() view.mainloop() # DO LIST # input to db # add a search box for ids by name or name by ids # test all cases and make more cases to test # concoct a case where there are # 2 contiguous insertions not at first or last # 2 contiguous insertions at first # 2 contiguous insertions at last # 2 non-contiguous insertions not at first or last
class Bilbobox(FrameHilited3): hive = [] def __init__(self, master, callback, height=480, values=[], scrollbar_size=24, *args, **kwargs): FrameHilited3.__init__(self, master, *args, **kwargs) ''' This is a replacement for ttk.Combobox. Configuration is done tkinter style, not with ttk.Style. Advantages over ttk.Combobox include: 1) It's not ttk so it's easy to style and it can be predicted what options will be available; 2) each dropdown item takes focus with arrow??? button and can run a command; 3) unlike Combobox, instead of clicking to open dropdown and clicking again to select a dropdown item, user hovers to open dropdown and unhovers or clicks to close it. The dropdown window can't have a border as it would constitute a gap between the entry and the dropdown and the dropdown would close when user tries to move the mouse from entry to dropdown. ''' self.master = master self.callback = callback self.height = height self.values = values self.scrollbar_size = scrollbar_size self.buttons = [] self.selected_button = None self.result_string = '' self.entered = None self.config(bd=0, relief='sunken') self.make_widgets() # without add='+' the first Bilbobox created can only be # withdrawn by clicking on one of its dropdown items or on the title bar # Can't be bound to button click; that would remove the button from its own event. master.bind_all('<ButtonRelease-1>', self.close_dropdown, add='+') def make_widgets(self): ''' The entry and arrow need to trigger the same events, but binding them to the same events would be a can of worms because of the Enter and Leave events. Instead, the binding is to self--the frame that both the entry and the arrow are in--and the underlying frame sees the events and responds. ''' self.entry = Entry(self) self.arrow = LabelHilited(self, text='\u25EF', width=2) self.arrow.bind('<Button-1>', self.focus_entry_on_arrow_click) self.entry.grid(column=0, row=0) self.arrow.grid(column=1, row=0, padx=0, pady=0) self.update_idletasks() self.width = self.winfo_reqwidth() self.drop = ToplevelHilited(self, bd=0) self.drop.withdraw() self.drop.bind('<Unmap>', self.focus_entry_on_unmap) Bilbobox.hive.append(self.drop) self.master.bind('<Escape>', self.hide_all_drops) self.drop.grid_columnconfigure(0, weight=1) self.drop.grid_rowconfigure(0, weight=1) self.canvas = CanvasScrolledBG2(self.drop, fixed_width=True, scrollregion_width=self.width, scrollbar='vert') self.canvas.grid(column=0, row=0, sticky='news') self.canvas.content.grid_columnconfigure(0, weight=1) self.canvas.content.grid_rowconfigure('all', weight=1) self.canvas.vert.grid(column=1, row=0, sticky='ns') self.bind('<Enter>', self.open_dropdown) for widg in (self, self.canvas.vert, self.canvas.content): widg.bind('<Leave>', self.hide_this_drop) widg.bind('<Enter>', self.detect_enter, add='+') for frm in (self, self.canvas.content): frm.bind('<FocusIn>', self.highlight_arrow) frm.bind('<FocusOut>', self.unhighlight_arrow) self.config_values(self.values) config_generic(self.drop) def detect_enter(self, evt): ''' Depending on which widget is left and which is entered by mouse, a delayed response from a Leave event closes the dropdown if the entered widget says it's OK to do so. ''' self.entered = evt.widget def hide_this_drop(self, evt): ''' The after() method is needed so that the Enter event has time to set self.entered before the Leave event closes the dropdown. Works if after() runs after 100 microseconds but if 500, it gives the user a chance to recover from overshooting the scrollbar (for example), yet without imposing a noticeable wait if the user was serious about leaving the widget. ''' def do_after_bool_set(): if self.entered is None or self.entered not in ( self, self.canvas.content, self.canvas.vert): self.drop.withdraw() self.entered = None self.entered = None evt.widget.after(500, do_after_bool_set) def config_values(self, values): b = ButtonFlatHilited(self.canvas.content, text='Sample') one_height = b.winfo_reqheight() b.destroy() self.fit_height = one_height * len(values) self.values = values for button in self.buttons: button.destroy() self.buttons = [] if self.drop in Bilbobox.hive: idx = Bilbobox.hive.index(self.drop) del Bilbobox.hive[idx] CanvasScrolledBG2.config_fixed_width_canvas(self) c = 0 for choice in values: bt = ButtonFlatHilited(self.canvas.content, text=choice, anchor='w') bt.grid(column=0, row=c, sticky='ew') # why t+1 in mockup? for event in ('<Button-1>', '<Return>', '<space>'): bt.bind(event, self.get_clicked) bt.bind('<Enter>', self.highlight) bt.bind('<Leave>', self.unhighlight) self.buttons.append(bt) c += 1 for b in self.buttons: b.config(command=self.callback) 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. Some of this code borrowed from Michael Foord. ''' self.owt = None if self.winfo_reqwidth() <= widg.winfo_reqwidth(): text = widg.cget('text') 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 = LabelTip(self.owt, text=text) l.pack(ipadx=6, ipady=3) self.owt.wm_geometry('+{}+{}'.format(x, y)) def hide_overwidth_tip(self, widg): tip = self.owt self.owt = None if tip: tip.destroy() def highlight_arrow(self, evt): self.arrow.config(bg=formats['head_bg']) def unhighlight_arrow(self, evt): self.arrow.config(bg=formats['highlight_bg']) def callback(self): print('this will not print if overridden') def focus_entry_on_unmap(self, evt): self.entry.focus_set() self.entry.select_range(0, 'end') def focus_entry_on_arrow_click(self, evt): self.focus_set() self.entry.select_range(0, 'end') def get_clicked(self, evt): self.selected_button = sb = evt.widget self.entry.delete(0, 'end') self.entry.insert(0, sb.cget('text')) def hide_other_drops(self): for dropdown in Bilbobox.hive: if dropdown != self.drop: dropdown.withdraw() def hide_all_drops(self, evt): for dropdown in Bilbobox.hive: dropdown.withdraw() def open_dropdown(self, evt=None): ''' Unlike a combobox, a Bilbobox drops down when hovered by the mouse. ''' self.update_idletasks() x = self.winfo_rootx() y = self.winfo_rooty() y_off = self.winfo_reqheight() self.fit_height = self.canvas.content.winfo_reqheight() self.drop.wm_overrideredirect(1) self.drop.geometry('{}x{}+{}+{}'.format(self.width, self.height, x, y + y_off)) self.drop.deiconify() self.hide_other_drops() def highlight(self, evt): widg = evt.widget self.update_idletasks() widg.config(bg=formats['bg']) self.show_overwidth_tip(widg) def unhighlight(self, evt): widg = evt.widget widg.config(bg=formats['highlight_bg']) self.hide_overwidth_tip(widg) def close_dropdown(self, evt): ''' Runs only on ButtonRelease-1. ''' if evt.widget == self.arrow: if self.drop.winfo_ismapped() == 1: self.drop.withdraw() elif self.drop.winfo_ismapped() == 0: self.open_dropdown() elif evt.widget in (self.canvas.vert, ): if evt.type == '8': self.drop.withdraw() else: pass # this pass is needed here, don't delete this condition else: self.drop.withdraw()