def sort_column(tree: ttk.Treeview, col: int, descending: bool): """ Sort tree contents when a column header is clicked on. :param tree: the root tree to sort :param col: the column of the tree to sort :param descending: If True sort in descending order, otherwise sort in ascending order """ data = [(tree.set(child, col), child) for child in tree.get_children('')] try_date = False try: data = [(float(d[0]), d[1]) for d in data] except ValueError: try_date = True if try_date: try: data = [(parser.parse(d[0]), d[1]) for d in data] except ValueError: pass data.sort(reverse=descending) for ix, item in enumerate(data): tree.move(item[1], '', ix) tree.heading(col, command=lambda c=col: sort_column(tree, c, not descending))
def treeview_sort_column(treeview: ttk.Treeview, col, reverse=False): l = [(treeview.set(k, col), k) for k in treeview.get_children('')] l.sort(reverse=reverse) # Rearranging items in sorted positions for index, (val, k) in enumerate(l): treeview.move(k, '', index) treeview.heading( col, command=lambda: treeview_sort_column(treeview, col, not reverse))
def tv_column_sort(tv: ttk.Treeview, col: str, reverse: bool) -> None: l = [(tv.set(k, col), k) for k in tv.get_children('')] try: l.sort(key=lambda t: int(t[0]), reverse=reverse) except ValueError: l.sort(reverse=reverse) for index, (val, k) in enumerate(l): tv.move(k, '', index) tv.heading(col, command=lambda: tv_column_sort(tv, col, not reverse))
def sort_by(self, col_name: str, is_descending: bool, tree: ttk.Treeview = None): if tree is None: tree = self.tree data_list = [(tree.set(child_id, col_name), child_id) for child_id in tree.get_children('')] data_list.sort(reverse=is_descending) for new_idx, (item_name, item_id) in enumerate(data_list): tree.move(item_id, '', new_idx) # switch the heading so it will sort in the opposite direction tree.heading(col_name, command=lambda col=col_name: self.sort_by( col, (not is_descending), tree))
class BioInfo(Tk): def __init__(self): Tk.__init__(self) self.wm_title("BioInfo : comparaison des listes") self.resizable(width=FALSE, height=FALSE) self.SortDir = False # Lists Types self.typeList1 = None self.typeList2 = None # Frame content self.frameContent = Frame(self) self.frameContent.pack(side=TOP, fill=X) # ScrollBar scrollbar = Scrollbar(self.frameContent, orient=VERTICAL) scrollbar.pack(side=RIGHT, fill=Y) # Result Content self.dataCols = ('microArn_A', 'microArn_B', 'FoldC', 'p-Value', 'Note') self.tree = Treeview(self.frameContent, columns=self.dataCols, show = 'headings', yscrollcommand=scrollbar.set) # configure column headings for c in self.dataCols: self.tree.heading(c, text=c, command=lambda c=c: self.columnSort(c, self.SortDir)) self.tree.column(c, width=10) self.tree.pack(side=LEFT, fill=X, expand="yes") scrollbar.config(command=self.tree.yview) # Frame Lists self.frameLists = Frame(self) self.frameLists.pack(side=LEFT) # Frame Forms self.frameForms = Frame(self) self.frameForms.pack(side=LEFT, padx=20) #Liste n°1 selection self.frameList1 = Frame(self.frameLists) self.frameList1.pack() self.typeListStr1 = StringVar(self.frameList1) self.typeListStr1.set(str(ListBioType.TypeA)) self.buttonTypeList1 = OptionMenu(self.frameList1, self.typeListStr1, str(ListBioType.TypeA), str(ListBioType.TypeB)).pack(side=LEFT) self.entrylist1 = Entry(self.frameList1, width=30) self.entrylist1.pack(side=LEFT) self.buttonBrowseList1 = Button(self.frameList1, text="Parcourir", command=self.load_fileList1, width=10) self.buttonBrowseList1.pack(side=LEFT, padx=5) # List n°2 selection self.frameList2 = Frame(self.frameLists) self.frameList2.pack(side=BOTTOM) self.typeListStr2 = StringVar(self.frameList2) self.typeListStr2.set(str(ListBioType.TypeB)) self.buttonTypeList2 = OptionMenu(self.frameList2, self.typeListStr2, str(ListBioType.TypeA), str(ListBioType.TypeB)).pack(side=LEFT) self.entrylist2 = Entry(self.frameList2, width=30) self.entrylist2.pack(side=LEFT) self.buttonBrowseList2 = Button(self.frameList2, text="Parcourir", command=self.load_fileList2, width=10) self.buttonBrowseList2.pack(side=LEFT, padx=5) # Form pValue self.framePVal = Frame(self.frameForms) self.framePVal.pack() Label(self.framePVal, text="pValue").pack(side=LEFT) self.entryPVal = Entry(self.framePVal, width=6) self.entryPVal.pack(side=LEFT) # Form foldC self.frameFoldC = Frame(self.frameForms) self.frameFoldC.pack() Label(self.frameFoldC, text="foldCh").pack(side=LEFT) self.entryFoldC = Entry(self.frameFoldC, width=6) self.entryFoldC.pack(side=LEFT) # Form note self.frameNote = Frame(self.frameForms) self.frameNote.pack() Label(self.frameNote, text="note ").pack(side=LEFT) self.entryNote = Entry(self.frameNote, width=6) self.entryNote.pack(side=LEFT) # Bouton comparer self.buttonComparer = Button(self, text="Comparer", command=self.compare, width=10, state=DISABLED) self.buttonComparer.pack(fill= X, expand="yes", padx=20, pady=(10,0)) #Bouton exporter self.buttonExport = Button(self, text="Exporter", command=self.export, width=10, state=DISABLED) self.buttonExport.pack(fill= X, expand="yes", padx=20) # Réinitialiser self.buttonReset = Button(self, text="Réinitialiser", command=self.reset, width=10) self.buttonReset.pack(fill= X, expand="yes", padx=20, pady=(0,10)) # file members self.list1 = None self.list2 = None def load_fileList1(self): fname = askopenfilename(filetypes=(("CSV files", "*.csv"), ("All files", "*.*") )) if fname: self.entrylist1.delete(0, END) self.list1 = fname self.entrylist1.insert(0,fname) self.buttonComparer.config(state=NORMAL) def load_fileList2(self): fname = askopenfilename(filetypes=(("CSV files", "*.csv"), ("All files", "*.*") )) if fname: self.entrylist2.delete(0, END) self.list2 = fname self.entrylist2.insert(0,fname) self.buttonComparer.config(state=NORMAL) else: showerror("Erreur : fichier B", "La liste B est introuvable") def resetTree (self): for i in self.tree.get_children(): self.tree.delete(i) def reset(self): self.list1 = None self.entrylist1.delete(0, END) self.list2 = None self.entrylist2.delete(0, END) self.entryPVal.delete(0,END) self.entryFoldC.delete(0, END) self.entryNote.delete(0, END) self.typeList1 = None self.typeList2 = None self.buttonExport.config(state=DISABLED) self.buttonComparer.config(state=DISABLED) self.resetTree() def isValidfoldC(self, s): try: float(s) return True except ValueError: return False def isValidPValue(self, s): try: f = float(s) if f >= 0 and f <= 1: return True else: return False except: return False def isValidNote (self, s): try: f = int(s) return True except: return False def compare(self): self.buttonExport.config(state=NORMAL) # Détermination type Listes # List 1 if self.typeListStr1.get() == str(ListBioType.TypeA): self.typeList1 = ListBioType.TypeA elif self.typeListStr1.get() == str(ListBioType.TypeB): self.typeList1 = ListBioType.TypeB else: self.typeList1 = None # List 2 if self.typeListStr2.get() == str(ListBioType.TypeA): self.typeList2 = ListBioType.TypeA elif self.typeListStr2.get() == str(ListBioType.TypeB): self.typeList2 = ListBioType.TypeB else: self.typeList2 = None if not self.isValidfoldC(self.entryFoldC.get()) and not self.entryFoldC.get() == "": showerror("Erreur : foldC","La valeur fold Change n'est pas un nombre") elif not self.isValidPValue(self.entryPVal.get()) and not self.entryPVal.get() == "": showerror("Erreur : pValue","La valeur pValue n'est pas un nombre compris entre 0 et 1") elif not self.isValidNote(self.entryNote.get()) and not self.entryNote.get() == "": showerror("Erreur : note", "La valeur note n'est pas un nombre entier") # (List A and No List) or (No List and List A) elif ((self.list1 is not None and self.typeList1 == ListBioType.TypeA) and (self.list2 is None)) or\ ((self.list2 is not None and self.typeList2 == ListBioType.TypeA) and (self.list1 is None)): self.resetTree() try: listComp = ListComparator(self.list1, self.list2, self.entryPVal.get(), self.entryFoldC.get(), self.entryNote.get()) for e in listComp.getFilterListA(): self.tree.insert('', 'end', values=e) except IndexError: showerror("Erreur : liste A invalide", "Le fichier liste A n'est pas un fichier valide") # (List B and No List) or (No List and List B) elif ((self.list1 is not None and self.typeList1 == ListBioType.TypeB) and (self.list2 is None)) or\ ((self.list2 is not None and self.typeList2 == ListBioType.TypeB) and (self.list1 is None)): self.resetTree() try: listComp = ListComparator(self.list1, self.list2, self.entryPVal.get(), self.entryFoldC.get()) for e in listComp.getFilterListB(): self.tree.insert('', 'end', values=e) except IndexError: showerror("Erreur : liste A invalide", "Le fichier liste A n'est pas un fichier valide") # (List A and List B) or (List B and List A) elif ((self.list1 is not None and self.typeList1 == ListBioType.TypeA) and \ (self.list2 is not None and self.typeList2 == ListBioType.TypeB)) or \ ((self.list1 is not None and self.typeList1 == ListBioType.TypeB) and \ (self.list2 is not None and self.typeList2 == ListBioType.TypeA)): self.resetTree() listA = "" listB = "" if self.typeList1 == ListBioType.TypeA: listA = self.list1 else: listA = self.list2 if self.typeList1 == ListBioType.TypeB: listB = self.list1 else: listB = self.list2 try: listComp = ListComparator(listA, listB, self.entryPVal.get(), self.entryFoldC.get(), self.entryNote.get()) for e in listComp.getDiffAandB(): self.tree.insert('', 'end', values=e) except IndexError: showerror("Erreur : liste A ou B invalide", "Le fichier liste A ou B n'est pas un fichier valide") # (List A and List A) elif ((self.list1 is not None and self.typeList1 == ListBioType.TypeA) and \ (self.list2 is not None and self.typeList2 == ListBioType.TypeA)): self.resetTree() try: listComp = ListComparator(self.list1, self.list2, self.entryPVal.get(), self.entryFoldC.get(), self.entryNote.get()) for e in listComp.getDiffAandA(): self.tree.insert('', 'end', values=e) except IndexError: showerror("Erreur : liste A ou B invalide", "Le fichier liste A ou B n'est pas un fichier valide") # (List B and List B) elif ((self.list1 is not None and self.typeList1 == ListBioType.TypeB) and \ (self.list2 is not None and self.typeList2 == ListBioType.TypeB)): self.resetTree() try: listComp = ListComparator(self.list1, self.list2, self.entryPVal.get(), self.entryFoldC.get()) for e in listComp.getDiffBandB(): self.tree.insert('', 'end', values=e) except IndexError: showerror("Erreur : liste A ou B invalide", "Le fichier liste A ou B n'est pas un fichier valide") else: showerror("Erreur : Combinaisons de listes invalides", "Votre choix de types de listes ne correspond à aucune combinaison possible, contacter le developpeur") def export(self): if len(self.tree.get_children()) == 0: showinfo("Export", "Il n'y a rien à exporter") return fname = asksaveasfilename(filetypes=(("CSV files", "*.csv"), ("All files", "*.*") )) if fname: resExp = [] for it in self.tree.get_children(): resExp.append(self.tree.item(it)["values"]) expTabToCSV = TreeExportator(resExp, fname) expTabToCSV.export() showinfo("Export", "Exportation au format CSV réussi") def columnSort (self, col, descending=False): data = [(self.tree.set(child, col), child) for child in self.tree.get_children('')] data.sort(reverse=descending) for indx, item in enumerate(data): self.tree.move(item[1], '', indx) # reverse sort direction for next sort operation self.SortDir = not descending
class Manager(Toplevel): def __init__(self, master): Toplevel.__init__(self, master, class_=APP_NAME) self.title(_("Manage Feeds")) self.grab_set() self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) self.im_moins = PhotoImage(master=self, file=IM_MOINS) self.im_moins_sel = PhotoImage(master=self, file=IM_MOINS_SEL) self.im_moins_clicked = PhotoImage(master=self, file=IM_MOINS_CLICKED) self.im_plus = PhotoImage(master=self, file=IM_PLUS) self._no_edit_entry = self.register(lambda: False) self.change_made = False self.categories = set(LATESTS.sections()) self.categories.remove('All') self.categories.add('') # --- treeview self.tree = Treeview(self, columns=('Title', 'URL', 'Category', 'Remove'), style='manager.Treeview', selectmode='none') self.tree.heading('Title', text=_('Title'), command=lambda: self._sort_column('Title', False)) self.tree.heading('URL', text=_('URL'), command=lambda: self._sort_column('URL', False)) self.tree.heading('Category', text=_('Category'), command=lambda: self._sort_column('Category', False)) self.tree.column('#0', width=6) self.tree.column('Title', width=250) self.tree.column('URL', width=350) self.tree.column('Category', width=150) self.tree.column('Remove', width=20, minwidth=20, stretch=False) y_scroll = AutoScrollbar(self, orient='vertical', command=self.tree.yview) x_scroll = AutoScrollbar(self, orient='horizontal', command=self.tree.xview) self.tree.configure(xscrollcommand=x_scroll.set, yscrollcommand=y_scroll.set) self.tree.bind('<Motion>', self._highlight_active) self.tree.bind('<Leave>', self._leave) self._last_active_item = None # --- populate treeview for title in sorted(FEEDS.sections(), key=lambda x: x.lower()): item = self.tree.insert('', 'end', values=(title, FEEDS.get(title, 'url'), FEEDS.get(title, 'category', fallback=''), '')) if FEEDS.getboolean(title, 'active', fallback=True): self.tree.selection_add(item) self.tree.item(item, tags=item) self.tree.tag_configure(item, image=self.im_moins) self.tree.tag_bind( item, '<ButtonRelease-1>', lambda event, i=item: self._click_release(event, i)) self.tree.tag_bind(item, '<ButtonPress-1>', lambda event, i=item: self._press(event, i)) self.tree.tag_bind(item, '<Double-1>', lambda event, i=item: self._edit(event, i)) self.tree.grid(row=0, column=0, sticky='ewsn') x_scroll.grid(row=1, column=0, sticky='ew') y_scroll.grid(row=0, column=1, sticky='ns') Button(self, image=self.im_plus, command=self.feed_add, style='manager.TButton').grid(row=2, column=0, columnspan=2, sticky='e', padx=4, pady=4) self._check_add_id = '' def destroy(self): try: self.after_cancel(self._check_add_id) except ValueError: pass Toplevel.destroy(self) def _edit(self, event, item): """Edit feed title.""" column = self.tree.identify_column(event.x) if column in ['#1', '#2']: bbox = self.tree.bbox(item, column) entry = Entry(self.tree) entry.place(x=bbox[0], y=bbox[1], width=bbox[2], height=bbox[3], anchor='nw') entry.bind('<Escape>', lambda e: entry.destroy()) entry.bind('<FocusOut>', lambda e: entry.destroy()) if column == '#1': entry.insert(0, self.tree.item(item, 'values')[0]) entry.configure(style='manager.TEntry') def ok(event): name = entry.get() if name: name = self.master.feed_rename( self.tree.set(item, 'Title'), name) self.tree.set(item, 'Title', name) entry.destroy() entry.bind('<Return>', ok) else: entry.insert(0, self.tree.item(item, 'values')[1]) entry.configure(style='no_edit.TEntry', validate='key', validatecommand=self._no_edit_entry) entry.selection_range(0, 'end') entry.focus_set() elif column == '#3': def focus_out(event): x, y = self.tree.winfo_pointerxy() x0 = combo.winfo_rootx() x1 = x0 + combo.winfo_width() y0 = combo.winfo_rooty() y1 = y0 + combo.winfo_height() if not (x0 <= x <= x1 and y0 <= y <= y1): combo.destroy() def ok(event): category = combo.get().strip() self.categories.add(category) self.master.feed_change_cat(self.tree.set(item, 'Title'), self.tree.set(item, 'Category'), category) self.tree.set(item, 'Category', category) combo.destroy() bbox = self.tree.bbox(item, column) cat = list(self.categories) combo = AutoCompleteCombobox(self.tree, values=cat, allow_other_values=True) combo.place(x=bbox[0], y=bbox[1], width=bbox[2], height=bbox[3], anchor='nw') combo.bind('<Escape>', lambda e: combo.destroy()) combo.bind('<FocusOut>', focus_out) combo.bind('<Return>', ok) combo.bind('<<ComboboxSelected>>', ok) combo.current(cat.index(self.tree.set(item, '#3'))) def _press(self, event, item): if self.tree.identify_column(event.x) == '#4': self.tree.tag_configure(item, image=self.im_moins_clicked) def _click_release(self, event, item): """Handle click on items.""" if self.tree.identify_row(event.y) == item: if self.tree.identify_column(event.x) == '#4': title = self.tree.item(item, 'values')[0] rep = True if CONFIG.getboolean('General', 'confirm_remove', fallback=True): rep = askokcancel( _('Confirmation'), _('Do you want to remove the feed {feed}?').format( feed=title)) if rep: self.master.feed_remove(title) self.tree.delete(item) self.change_made = True elif self.tree.identify_element( event.x, event.y) == 'Checkbutton.indicator': sel = self.tree.selection() if item in sel: self.tree.selection_remove(item) self.master.feed_set_active(self.tree.set(item, '#1'), False) else: self.tree.selection_add(item) self.master.feed_set_active(self.tree.set(item, '#1'), True) self.change_made = True else: self.tree.tag_configure(item, image=self.im_moins) def _leave(self, event): """Remove highlight when mouse leave the treeview.""" if self._last_active_item is not None: self.tree.tag_configure(self._last_active_item, image=self.im_moins) def _highlight_active(self, event): """Highlight minus icon under the mouse.""" if self._last_active_item is not None: self.tree.tag_configure(self._last_active_item, image=self.im_moins) if self.tree.identify_column(event.x) == '#4': item = self.tree.identify_row(event.y) if item: self.tree.tag_configure(item, image=self.im_moins_sel) self._last_active_item = item else: self._last_active_item = None else: self._last_active_item = None def _sort_column(self, column, reverse): """Sort column by (reversed) alphabetical order.""" l = [(self.tree.set(c, column), c) for c in self.tree.get_children('')] l.sort(reverse=reverse, key=lambda x: x[0].lower()) for index, (val, c) in enumerate(l): self.tree.move(c, "", index) self.tree.heading( column, command=lambda: self._sort_column(column, not reverse)) def feed_add(self): dialog = Add(self) self.wait_window(dialog) url = dialog.url if url: self.configure(cursor='watch') queue = self.master.feed_add(url, manager=True) self._check_add_id = self.after(1000, self._check_add_finished, url, queue) def _check_add_finished(self, url, queue): if queue.empty(): self._check_add_id = self.after(1000, self._check_add_finished, url, queue) else: title = queue.get(False) if title: item = self.tree.insert('', 'end', values=(title, url, '')) self.tree.item(item, tags=item) self.tree.tag_configure(item, image=self.im_moins) self.tree.tag_bind( item, '<ButtonRelease-1>', lambda event: self._click_release(event, item)) self.tree.tag_bind(item, '<ButtonPress-1>', lambda event: self._press(event, item)) self.tree.tag_bind(item, '<Double-1>', lambda event: self._edit(event, item)) self.tree.selection_add(item) self.change_made = True self.configure(cursor='arrow') self.focus_set() self.grab_set()
class TreeviewPanel(tk.Frame): def __init__(self, parent: tk.BaseWidget, data: Mapping, *args) -> None: super().__init__(parent, *args) self.display = "trunc" self.harmonic = tk.StringVar(self) harmonic_label = tk.Label(self, textvariable=self.harmonic) harmonic_label.pack(side=tk.TOP) self.tv = Treeview( self, columns=("glyph", "angle", "trunc"), show=[], height=13, displaycolumn=["glyph", self.display], ) self.tv.column("glyph", anchor="center", minwidth=40, width=40, stretch=False) self.tv.column("angle", anchor="center", minwidth=120, width=120, stretch=False) self.tv.column("trunc", anchor="center", minwidth=120, width=120, stretch=False) self.tv.pack(side=tk.TOP) self.button_panel = tk.Frame(self) self.button_panel.pack(side=tk.TOP) self.up = tk.Button(self.button_panel, text="\u25b2", command=self.scroll_up) self.up.pack(side=tk.LEFT) self.toggle = tk.Button(self.button_panel, text="Toggle", command=self.toggle_column) self.toggle.pack(side=tk.LEFT) self.down = tk.Button(self.button_panel, text="\u25bc", command=self.scroll_down) self.down.pack(side=tk.LEFT) self.harm_panel = tk.Frame(self) self.harm_panel.pack(side=tk.TOP) self.harm_text = tk.Label(self.harm_panel, text="Harmonic:") self.harm_text.pack(side=tk.LEFT) self.harm = tk.Spinbox(self.harm_panel, from_=1, to=300, width=5) self.harm.pack(side=tk.LEFT, padx=5) self.harm.bind("<Key>", partial(spinbox_handler, self)) self.data = data self.fill_data(harmonic=1) def fill_data(self, harmonic: int) -> None: self.harmonic.set("Current harmonic: %d" % harmonic) for item in self.tv.get_children(): self.tv.delete(item) harm_angles = harmonics(self.data["base_angles"], harmonic) trunc_angles = map(str, map(truncate_rounding, harm_angles)) rounded_angles = map(lambda x: "%.2f°" % x, harm_angles) for row in zip(self.data["glyphs"], rounded_angles, trunc_angles): self.tv.insert("", tk.END, values=row) def spinbox_command(self): pass def toggle_column(self): if self.display == "angle": self.display = "trunc" else: self.display = "angle" self.tv.configure(displaycolumn=["glyph", self.display]) def scroll_down(self): last_item = self.tv.get_children()[-1] self.tv.move(last_item, "", 0) def scroll_up(self): first_item = self.tv.get_children()[0] self.tv.move(first_item, "", tk.END) def apply_command(self): val = self.harm.get() try: val = int(val) if val >= 1: self.fill_data(harmonic=val) except ValueError: messagebox.showwarning( "Harmonic", "The harmonic number must be a positive integer")
class Multicolumn_Listbox(Frame): _style_index = 0 class List_Of_Rows(object): def __init__(self, multicolumn_listbox): self._multicolumn_listbox = multicolumn_listbox def data(self, index): return self._multicolumn_listbox.row_data(index) def get(self, index): return Row(self._multicolumn_listbox, index) def insert(self, data, index=None): self._multicolumn_listbox.insert_row(data, index) def delete(self, index): self._multicolumn_listbox.delete_row(index) def update(self, index, data): self._multicolumn_listbox.update_row(index, data) def select(self, index): self._multicolumn_listbox.select_row(index) def deselect(self, index): self._multicolumn_listbox.deselect_row(index) def set_selection(self, indices): self._multicolumn_listbox.set_selection(indices) def __getitem__(self, index): return self.get(index) def __setitem__(self, index, value): return self._multicolumn_listbox.update_row(index, value) def __delitem__(self, index): self._multicolumn_listbox.delete_row(index) def __len__(self): return self._multicolumn_listbox.number_of_rows class List_Of_Columns(object): def __init__(self, multicolumn_listbox): self._multicolumn_listbox = multicolumn_listbox def data(self, index): return self._multicolumn_listbox.get_column(index) def get(self, index): return Column(self._multicolumn_listbox, index) def delete(self, index): self._multicolumn_listbox.delete_column(index) def update(self, index, data): self._multicolumn_listbox.update_column(index, data) def __getitem__(self, index): return self.get(index) def __setitem__(self, index, value): return self._multicolumn_listbox.update_column(index, value) def __delitem__(self, index): self._multicolumn_listbox.delete_column(index) def __len__(self): return self._multicolumn_listbox.number_of_columns def __init__(self, master, columns, data=None, command=None, sort=True, select_mode=None, heading_anchor=CENTER, cell_anchor=W, style=None, height=None, padding=None, adjust_heading_to_content=False, stripped_rows=None, selection_background=None, selection_foreground=None, field_background=None, heading_font=None, heading_background=None, heading_foreground=None, cell_pady=2, cell_background=None, cell_foreground=None, cell_font=None, headers=True): self._stripped_rows = stripped_rows self._columns = columns self._number_of_rows = 0 self._number_of_columns = len(columns) self.row = self.List_Of_Rows(self) self.column = self.List_Of_Columns(self) s = Style() if style is None: style_name = "Multicolumn_Listbox%s.Treeview" % self._style_index self._style_index += 1 else: style_name = style style_map = {} if selection_background is not None: style_map["background"] = [('selected', selection_background)] if selection_foreground is not None: style_map["foeground"] = [('selected', selection_foreground)] if style_map: s.map(style_name, **style_map) style_config = {} if cell_background is not None: style_config["background"] = cell_background if cell_foreground is not None: style_config["foreground"] = cell_foreground if cell_font is None: font_name = s.lookup(style_name, "font") cell_font = nametofont(font_name) else: if not isinstance(cell_font, Font): if isinstance(cell_font, basestring): cell_font = nametofont(cell_font) else: if len(Font) == 1: cell_font = Font(family=cell_font[0]) elif len(Font) == 2: cell_font = Font(family=cell_font[0], size=cell_font[1]) elif len(Font) == 3: cell_font = Font(family=cell_font[0], size=cell_font[1], weight=cell_font[2]) else: raise ValueError( "Not possible more than 3 values for font") style_config["font"] = cell_font self._cell_font = cell_font self._rowheight = cell_font.metrics("linespace") + cell_pady style_config["rowheight"] = self._rowheight if field_background is not None: style_config["fieldbackground"] = field_background s.configure(style_name, **style_config) heading_style_config = {} if heading_font is not None: heading_style_config["font"] = heading_font if heading_background is not None: heading_style_config["background"] = heading_background if heading_foreground is not None: heading_style_config["foreground"] = heading_foreground heading_style_name = style_name + ".Heading" s.configure(heading_style_name, **heading_style_config) treeview_kwargs = {"style": style_name} if height is not None: treeview_kwargs["height"] = height if padding is not None: treeview_kwargs["padding"] = padding if headers: treeview_kwargs["show"] = "headings" else: treeview_kwargs["show"] = "" if select_mode is not None: treeview_kwargs["selectmode"] = select_mode self.interior = Treeview(master, columns=columns, **treeview_kwargs) if command is not None: self._command = command self.interior.bind("<<TreeviewSelect>>", self._on_select) for i in range(0, self._number_of_columns): if sort: self.interior.heading( i, text=columns[i], anchor=heading_anchor, command=lambda col=i: self.sort_by(col, descending=False)) else: self.interior.heading(i, text=columns[i], anchor=heading_anchor) if adjust_heading_to_content: self.interior.column(i, width=Font().measure(columns[i])) self.interior.column(i, anchor=cell_anchor) if data is not None: for row in data: self.insert_row(row) @property def row_height(self): return self._rowheight @property def font(self): return self._cell_font def configure_column(self, index, width=None, minwidth=None, anchor=None, stretch=None): kwargs = {} for config_name in ("width", "anchor", "stretch", "minwidth"): config_value = locals()[config_name] if config_value is not None: kwargs[config_name] = config_value self.interior.column('#%s' % (index + 1), **kwargs) def row_data(self, index): try: item_ID = self.interior.get_children()[index] except IndexError: raise ValueError("Row index out of range: %d" % index) return self.item_ID_to_row_data(item_ID) def update_row(self, index, data): try: item_ID = self.interior.get_children()[index] except IndexError: raise ValueError("Row index out of range: %d" % index) if len(data) == len(self._columns): self.interior.item(item_ID, values=data) else: raise ValueError("The multicolumn listbox has only %d columns" % self._number_of_columns) def delete_row(self, index): list_of_items = self.interior.get_children() try: item_ID = list_of_items[index] except IndexError: raise ValueError("Row index out of range: %d" % index) self.interior.delete(item_ID) self._number_of_rows -= 1 if self._stripped_rows: for i in range(index, self._number_of_rows): self.interior.tag_configure(list_of_items[i + 1], background=self._stripped_rows[i % 2]) def insert_row(self, data, index=None): if len(data) != self._number_of_columns: raise ValueError("The multicolumn listbox has only %d columns" % self._number_of_columns) if index is None: index = self._number_of_rows - 1 item_ID = self.interior.insert('', index, values=data) self.interior.item(item_ID, tags=item_ID) self._number_of_rows += 1 if self._stripped_rows: list_of_items = self.interior.get_children() self.interior.tag_configure(item_ID, background=self._stripped_rows[index % 2]) for i in range(index + 1, self._number_of_rows): self.interior.tag_configure(list_of_items[i], background=self._stripped_rows[i % 2]) def column_data(self, index): return [ self.interior.set(child_ID, index) for child_ID in self.interior.get_children('') ] def update_column(self, index, data): for i, item_ID in enumerate(self.interior.get_children()): data_row = self.item_ID_to_row_data(item_ID) data_row[index] = data[i] self.interior.item(item_ID, values=data_row) return data def clear(self): # Another possibility: # self.interior.delete(*self.interior.get_children()) for row in self.interior.get_children(): self.interior.delete(row) self._number_of_rows = 0 def update(self, data): self.clear() for row in data: self.insert_row(row) def focus(self, index=None): if index is None: return self.interior.item(self.interior.focus()) else: item = self.interior.get_children()[index] self.interior.focus(item) def state(self, state=None): if state is None: return self.interior.state() else: self.interior.state(state) @property def number_of_rows(self): return self._number_of_rows @property def number_of_columns(self): return self._number_of_columns def toogle_selection(self, index): list_of_items = self.interior.get_children() try: item_ID = list_of_items[index] except IndexError: raise ValueError("Row index out of range: %d" % index) self.interior.selection_toggle(item_ID) def select_row(self, index): list_of_items = self.interior.get_children() try: item_ID = list_of_items[index] except IndexError: raise ValueError("Row index out of range: %d" % index) self.interior.selection_add(item_ID) def deselect_row(self, index): list_of_items = self.interior.get_children() try: item_ID = list_of_items[index] except IndexError: raise ValueError("Row index out of range: %d" % index) self.interior.selection_remove(item_ID) def deselect_all(self): self.interior.selection_remove(self.interior.selection()) def set_selection(self, indices): list_of_items = self.interior.get_children() self.interior.selection_set(" ".join(list_of_items[row_index] for row_index in indices)) @property def selected_rows(self): data = [] for item_ID in self.interior.selection(): data_row = self.item_ID_to_row_data(item_ID) data.append(data_row) return data @property def indices_of_selected_rows(self): list_of_indices = [] for index, item_ID in enumerate(self.interior.get_children()): if item_ID in self.interior.selection(): list_of_indices.append(index) return list_of_indices def delete_all_selected_rows(self): selected_items = self.interior.selection() for item_ID in selected_items: self.interior.delete(item_ID) number_of_deleted_rows = len(selected_items) self._number_of_rows -= number_of_deleted_rows return number_of_deleted_rows def _on_select(self, event): for item_ID in event.widget.selection(): data_row = self.item_ID_to_row_data(item_ID) self._command(data_row) def item_ID_to_row_data(self, item_ID): item = self.interior.item(item_ID) return item["values"] @property def table_data(self): data = [] for item_ID in self.interior.get_children(): data_row = self.item_ID_to_row_data(item_ID) data.append(data_row) return data @table_data.setter def table_data(self, data): self.update(data) def cell_data(self, row, column): """Get the value of a table cell""" try: item = self.interior.get_children()[row] except IndexError: raise ValueError("Row index out of range: %d" % row) return self.interior.set(item, column) def update_cell(self, row, column, value): """Set the value of a table cell""" item_ID = self.interior.get_children()[row] data = self.item_ID_to_row_data(item_ID) data[column] = value self.interior.item(item_ID, values=data) def __getitem__(self, index): if isinstance(index, tuple): row, column = index return self.cell_data(row, column) else: raise Exception("Row and column indices are required") def __setitem__(self, index, value): if isinstance(index, tuple): row, column = index self.update_cell(row, column, value) else: raise Exception("Row and column indices are required") def bind(self, event, handler): self.interior.bind(event, handler) def sort_by(self, col, descending): """ sort tree contents when a column header is clicked """ # grab values to sort data = [(self.interior.set(child_ID, col), child_ID) for child_ID in self.interior.get_children('')] # if the data to be sorted is numeric change to float try: data = [(float(number), child_ID) for number, child_ID in data] except ValueError: pass # now sort the data in place data.sort(reverse=descending) for idx, item in enumerate(data): self.interior.move(item[1], '', idx) # switch the heading so that it will sort in the opposite direction self.interior.heading( col, command=lambda col=col: self.sort_by(col, not descending)) if self._stripped_rows: list_of_items = self.interior.get_children('') for i in range(len(list_of_items)): self.interior.tag_configure(list_of_items[i], background=self._stripped_rows[i % 2]) def destroy(self): self.interior.destroy() def item_ID(self, index): return self.interior.get_children()[index]
class ElementListWidget(Frame): def __init__(self, parent, label, columns, showError): Frame.__init__(self, parent) self.showError = showError self.columnconfigure(0, weight = 1) self.rowconfigure(1, weight = 1) # Название таблицы self.titleLabel = Label(self, text = label) self.titleLabel.grid(column = 0, row = 0, sticky = W + E) # Таблица значений columns = ("Метка", "№") + columns self.tree = Treeview(self, columns = columns, displaycolumns = columns, selectmode = "browse") self.tree.grid(column = 0, row = 1, sticky = W + N + E + S) # Настраиваем внешний вид таблицы (первые колонки) self.tree.column("#0", width = 0, stretch = 0) # Прячем колонку с иконкой self.tree.column( columns[0], anchor = W, width = 150) self.tree.heading(columns[0], anchor = W, text = columns[0]) self.tree.column( columns[1], anchor = E, width = 80) self.tree.heading(columns[1], anchor = E, text = columns[1]) self.tree.bind("<<TreeviewSelect>>", self.onSelectionChanged) # Панель с кнопками self.buttonPanel = Frame(self) self.buttonPanel.grid(column = 0, row = 2, sticky = W + E) self.buttonPanel.columnconfigure(0, weight = 1) self.buttonPanel.columnconfigure(3, minsize = emptySpaceSize, weight = 0) self.buttonPanel.columnconfigure(6, minsize = emptySpaceSize, weight = 0) self.buttonPanel.columnconfigure(9, weight = 1) # Кнопки добавления/удаления элемента self.buttonAdd = Button(self.buttonPanel, text = "+", width = 3, command = self.onButtonAddClicked) self.buttonAdd.grid(column = 1, row = 0) self.buttonRemove = Button(self.buttonPanel, text = "-", width = 3, state = DISABLED, command = self.onButtonRemoveClicked) self.buttonRemove.grid(column = 2, row = 0) # Кнопки перемещения элемента self.buttonUp = Button(self.buttonPanel, text = "↑", width = 3, state = DISABLED, command = self.onButtonUpClicked) self.buttonUp.grid(column = 4, row = 0) self.buttonDown = Button(self.buttonPanel, text = "↓", width = 3, state = DISABLED, command = self.onButtonDownClicked) self.buttonDown.grid(column = 5, row = 0) # Кнопки применить/отменить (для выбранного элемента) self.buttonCancel = Button(self.buttonPanel, text = "✗", width = 3, command = self.updateSelectedFrame) self.buttonCancel.grid(column = 7, row = 0) self.buttonApply = Button(self.buttonPanel, text = "✓", width = 3, command = self.onButtonApplyClicked) self.buttonApply.grid(column = 8, row = 0) # Редактирование выделенного элемента self.i = StringVar() self.label = (StringVar(), StringVar()) self.selectedFrame = Frame(self) self.selectedFrame.grid(column = 0, row = 3, sticky = W + E) # Номер Label(self.selectedFrame, text = "№:") \ .grid(column = 0, row = 0) Label(self.selectedFrame, textvariable = self.i, width = 3, justify = RIGHT) \ .grid(column = 1, row = 0) # Пустое пространство self.selectedFrame.columnconfigure(2, minsize = emptySpaceSize, weight = 0) # Метка Entry(self.selectedFrame, textvariable = self.label[0]) \ .grid(column = 3, row = 0, sticky = W + E) Entry(self.selectedFrame, textvariable = self.label[1], bg = defaultValueBG) \ .grid(column = 4, row = 0, sticky = W + E) # Виджет для элементов классов-потомков self.detailFrame = Frame(self.selectedFrame) self.detailFrame.grid(column = 3, row = 1, columnspan = 2, sticky = W + N + E + S) self.selectedFrame.columnconfigure(3, weight = 1) self.selectedFrame.columnconfigure(4, weight = 1) self.selectedFrame.rowconfigure(1, weight = 1) def onButtonUpClicked(self): item = self.selectedItem() if item is None: return prev = self.tree.prev(item) if prev != "": parent, index = self.tree.parent(item), self.tree.index(item) self.tree.move(item, parent, index - 1) # Корректируем номера элементов self.tree.set(item, "№", index - 1) self.tree.set(prev, "№", index) self.updateSelectedFrame(item) def onButtonDownClicked(self): item = self.selectedItem() if item is None: return next = self.tree.next(item) if next != "": parent, index = self.tree.parent(item), self.tree.index(item) self.tree.move(item, parent, index + 1) # Корректируем номера элементов self.tree.set(item, "№", index + 1) self.tree.set(next, "№", index) self.updateSelectedFrame(item) def onButtonAddClicked(self): pass def onButtonRemoveClicked(self): item = self.selectedItem() if item is None: return next = self.tree.next(item) self.tree.delete(item) while next != "": i = int(self.tree.set(next, "№")) self.tree.set(next, "№", i - 1) next = self.tree.next(next) self.onSelectionChanged() def onButtonApplyClicked(self, item = None): if item is None: item = self.selectedItem() if item is None: return None label = self.label[0].get() self.tree.set(item, "Метка", label) return item def onSelectionChanged(self, event = None): item = self.selectedItem() # Обновляем состояние кнопок state = DISABLED if item is None else NORMAL for x in (self.buttonRemove, self.buttonUp, self.buttonDown): x["state"] = state self.updateSelectedFrame(item) def selectedItem(self): selection = self.tree.selection() return None if type(selection) == type("") else selection[0] def clear(self): for item in self.tree.get_children(): self.tree.delete(item) def updateSelectedFrame(self, item = None, values = None): if item is None: item = self.selectedItem() values = None if item is None: i = "" label = "" else: if values is None: values = self.tree.set(item) i = values["№"] label = values["Метка"] self.i.set(i) self.label[0].set(label) return (item, values) def addElement(self, values): self.tree.insert(parent = "", index = END, values = values) def setDefaultElement(self, label): self.label[1].set(label) def elementsCount(self): return len(self.tree.get_children()) def elements(self, transform): return [ transform(item) for item in self.tree.get_children() ]
class FormChildTemplate: def __init__(self, frm_parent, connection): self.connection = connection self.directive = Message() self.decide_template = True self.id_selected = 0 self.frm_child_list = LabelFrame(frm_parent) self.frm_child_crud = LabelFrame(frm_parent) self.frm_child_crud.config(fg=TEXT_COLOR, font=SUBTITLE_FONT) self.initialize_components() def initialize_components(self): """ Method that initialize the visual components for each form associated with the local administration """ # Resources for the Forms self.new_icon = PhotoImage(file=r"./Resources/create.png") self.modify_icon = PhotoImage(file=r"./Resources/modify.png") self.remove_icon = PhotoImage(file=r"./Resources/delete.png") self.save_icon = PhotoImage(file=r"./Resources/save.png") self.cancel_icon = PhotoImage(file=r"./Resources/cancel.png") self.add_icon = PhotoImage(file=r"./Resources/right.png") self.delete_icon = PhotoImage(file=r"./Resources/left.png") self.up_arrow = PhotoImage(file=r"./Resources/up_arrow.png") self.down_arrow = PhotoImage(file=r"./Resources/down_arrow.png") self.star_icon = PhotoImage(file=r"./Resources/star.png") self.back_icon = PhotoImage(file=r"./Resources/back.png") self.view_icon = PhotoImage(file=r"./Resources/view.png") # Components for List FRM lbl_sep1 = Label(self.frm_child_list) lbl_sep1.grid(row=0, column=0, padx=10, pady=25) self.trv_available = Treeview(self.frm_child_list, height=20, columns=('N', 'Name')) self.trv_available.heading('#0', text='ID', anchor=CENTER) self.trv_available.heading('#1', text='N', anchor=CENTER) self.trv_available.heading('#2', text='Name', anchor=CENTER) self.trv_available.column('#0', width=0, minwidth=50, stretch=NO) self.trv_available.column('#1', width=20, minwidth=20, stretch=NO) self.trv_available.column('#2', width=375, minwidth=375, stretch=NO) self.trv_available.bind("<ButtonRelease-1>", self.select_template_summary) self.trv_available.grid(row=0, column=1, sticky=W, pady=25) vsb_trv_av = Scrollbar(self.frm_child_list, orient="vertical", command=self.trv_available.yview) vsb_trv_av.grid(row=0, column=2, pady=25, sticky=NS) self.trv_available.configure(yscrollcommand=vsb_trv_av.set) frm_aux4 = Frame(self.frm_child_list) btn_new = Button(frm_aux4, image=self.new_icon, command=self.click_new) btn_new.grid(row=0, column=0, pady=5, padx=5, sticky=E) btn_new_ttp = CreateToolTip(btn_new, 'New template') btn_view = Button(frm_aux4, image=self.view_icon, command=self.click_view) btn_view.grid(row=1, column=0, pady=5, padx=5, sticky=E) btn_view_ttp = CreateToolTip(btn_new, 'View template') btn_edit = Button(frm_aux4, image=self.modify_icon, command=self.click_update) btn_edit.grid(row=2, column=0, pady=5, padx=5, sticky=E) btn_edit_ttp = CreateToolTip(btn_edit, 'Edit template') btn_delete = Button(frm_aux4, image=self.remove_icon, command=self.click_delete) btn_delete.grid(row=3, column=0, pady=5, padx=5, sticky=E) btn_delete_ttp = CreateToolTip(btn_delete, 'Delete template') frm_aux4.grid(row=0, column=3, pady=25, padx=25, sticky=NW) sep_template = Separator(self.frm_child_list, orient=VERTICAL) sep_template.grid(row=0, column=4, sticky=NS, padx=25) frm_aux3 = Frame(self.frm_child_list) lbl_sep3 = Label(frm_aux3) lbl_sep3.grid(row=0, column=0, padx=10, pady=25, rowspan=3) lbl_details = Label(frm_aux3, text='Details') lbl_details.config(fg=TEXT_COLOR, font=SUBTITLE_FONT) lbl_details.grid(row=0, column=1, sticky=W, pady=25, columnspan=2) self.txt_summary = Text(frm_aux3, height=22, width=50) self.txt_summary.config(font=TEXT_FONT, bg=DISABLED_COLOR) self.txt_summary.grid(row=1, column=1) vsb_txt_sum = Scrollbar(frm_aux3, orient="vertical", command=self.txt_summary.yview) vsb_txt_sum.grid(row=1, column=2, sticky=NS) self.txt_summary.configure(yscrollcommand=vsb_txt_sum.set) lbl_sep4 = Label(frm_aux3) lbl_sep4.grid(row=0, column=3, padx=10, pady=25, rowspan=3) lbl_sep5 = Label(frm_aux3) lbl_sep5.grid(row=2, column=1, pady=5, columnspan=2) frm_aux3.grid(row=0, column=5) # Components for CRUD FRM lbl_sep6 = Label(self.frm_child_crud) lbl_sep6.grid(row=0, column=0, padx=10, pady=25, rowspan=10) lbl_name = Label(self.frm_child_crud, text='Name*') lbl_name.config(fg=TEXT_COLOR, font=LABEL_FONT) lbl_name.grid(row=0, column=1, pady=25, sticky=NW) lbl_description = Label(self.frm_child_crud, text='Description*') lbl_description.config(fg=TEXT_COLOR, font=LABEL_FONT) lbl_description.grid(row=0, column=6, pady=25, sticky=NW) lbl_sep3 = Label(self.frm_child_crud) lbl_sep3.grid(row=0, column=2, padx=10, pady=25) self.txt_name = Entry(self.frm_child_crud, width=30, font=TEXT_FONT) self.txt_name.grid(row=0, column=3, pady=25, sticky=NW) lbl_sep4 = Label(self.frm_child_crud) lbl_sep4.grid(row=0, column=7, padx=10, pady=25) self.txt_description = Text(self.frm_child_crud, height=5, width=49) self.txt_description.config(font=TEXT_FONT) self.txt_description.grid(row=0, column=8, pady=25, sticky=W) vsb_txt_desc = Scrollbar(self.frm_child_crud, orient="vertical", command=self.txt_description.yview) vsb_txt_desc.grid(row=0, column=9, pady=25, sticky=NS) self.txt_description.configure(yscrollcommand=vsb_txt_desc.set) lbl_sep7 = Label(self.frm_child_crud) lbl_sep7.grid(row=0, column=5, padx=10, pady=25, rowspan=3) lbl_sep8 = Label(self.frm_child_crud) lbl_sep8.grid(row=0, column=10, padx=10, pady=25, rowspan=2) lbl_available_d = Label(self.frm_child_crud, text='Available sections') lbl_available_d.config(fg=TEXT_COLOR, font=LABEL_FONT) lbl_available_d.grid(row=1, column=1, pady=10, sticky=W, columnspan=4) lbl_selected_d = Label(self.frm_child_crud, text='Selected sections*') lbl_selected_d.config(fg=TEXT_COLOR, font=LABEL_FONT) lbl_selected_d.grid(row=1, column=6, pady=10, sticky=W, columnspan=4) self.trv_available_sections = Treeview(self.frm_child_crud, height=10, columns=('N', 'Name', 'Data Type')) self.trv_available_sections.heading('#0', text='ID', anchor=CENTER) self.trv_available_sections.heading('#1', text='N', anchor=CENTER) self.trv_available_sections.heading('#2', text='Name', anchor=CENTER) self.trv_available_sections.heading('#3', text='Data Type', anchor=CENTER) self.trv_available_sections.column('#0', width=0, minwidth=20, stretch=NO) self.trv_available_sections.column('#1', width=20, minwidth=20, stretch=NO) self.trv_available_sections.column('#2', width=150, minwidth=150, stretch=NO) self.trv_available_sections.column('#3', width=120, minwidth=120, stretch=NO) self.trv_available_sections.bind("<Button-1>", self.click_trv_asections) self.trv_available_sections.grid(row=2, column=1, rowspan=7, columnspan=3, sticky=W, pady=10) vsb_trv_avs = Scrollbar(self.frm_child_crud, orient="vertical", command=self.trv_available_sections.yview) vsb_trv_avs.grid(row=2, column=4, rowspan=7, pady=10, sticky=NS) self.trv_available_sections.configure(yscrollcommand=vsb_trv_avs.set) self.trv_selected_sections = Treeview(self.frm_child_crud, height=10, columns=('N', 'Name', 'Data type', 'Mandatory', 'Main')) self.trv_selected_sections.heading('#0', text='ID', anchor=CENTER) self.trv_selected_sections.heading('#1', text='N', anchor=CENTER) self.trv_selected_sections.heading('#2', text='Name', anchor=CENTER) self.trv_selected_sections.heading('#3', text='Data type', anchor=CENTER) self.trv_selected_sections.heading('#4', text='Mandatory', anchor=CENTER) self.trv_selected_sections.heading('#5', text='Main', anchor=CENTER) self.trv_selected_sections.column('#0', width=0, minwidth=20, stretch=NO) self.trv_selected_sections.column('#1', width=20, minwidth=20, stretch=NO) self.trv_selected_sections.column('#2', width=150, minwidth=150, stretch=NO) self.trv_selected_sections.column('#3', width=120, minwidth=120, stretch=NO) self.trv_selected_sections.column('#4', width=80, minwidth=80, stretch=NO) self.trv_selected_sections.column('#5', width=80, minwidth=80, stretch=NO) self.trv_selected_sections.bind("<Button-1>", self.click_trv_ssections) self.trv_selected_sections.bind("<Double-1>", self.click_switch_mandatory) self.trv_selected_sections.grid(row=2, column=6, rowspan=7, columnspan=3, sticky=W, pady=10) vsb_trv_ses = Scrollbar(self.frm_child_crud, orient="vertical", command=self.trv_selected_sections.yview) vsb_trv_ses.grid(row=2, column=9, rowspan=7, pady=10, sticky=NS) self.trv_selected_sections.configure(yscrollcommand=vsb_trv_ses.set) self.lbl_note_optional = Label( self.frm_child_crud, text= 'NOTES:\tTo switch between optional and mandatory, double click ' 'on selected section.\n\tChoose one or up to three main sections ' 'by first selecting the target sections\n\tand then clicking the ' 'star button.\n') self.lbl_note_optional.config(fg=TEXT_COLOR, font=NOTE_FONT, justify=LEFT) self.btn_add = Button(self.frm_child_crud, image=self.add_icon, command=self.click_add) btn_add_ttp = CreateToolTip(self.btn_add, 'Add section') self.btn_remove = Button(self.frm_child_crud, image=self.delete_icon, command=self.click_remove) btn_remove_ttp = CreateToolTip(self.btn_remove, 'Remove section') self.btn_main_section = Button(self.frm_child_crud, image=self.star_icon, command=self.click_main_section) btn_main_section_ttp = CreateToolTip(self.btn_main_section, 'Main section(s)') self.btn_up = Button(self.frm_child_crud, image=self.up_arrow, command=self.click_up) btn_up_ttp = CreateToolTip(self.btn_up, 'Move up') self.btn_down = Button(self.frm_child_crud, image=self.down_arrow, command=self.click_down) btn_down_ttp = CreateToolTip(self.btn_down, 'Move down') sep_aux1 = Separator(self.frm_child_crud, orient=VERTICAL) sep_aux1.grid(row=0, column=11, sticky=NS, rowspan=10) frm_aux1 = Frame(self.frm_child_crud) self.btn_save = Button(frm_aux1, image=self.save_icon, command=self.click_save) btn_save_ttp = CreateToolTip(self.btn_save, 'Save template') self.btn_back = Button(frm_aux1, image=self.back_icon, command=self.click_back) btn_back_ttp = CreateToolTip(self.btn_back, 'Go back') self.btn_cancel = Button(frm_aux1, image=self.cancel_icon, command=self.click_cancel) btn_cancel_ttp = CreateToolTip(self.btn_cancel, 'Cancel') frm_aux1.grid(row=0, column=12, pady=10, padx=25, sticky=NW, rowspan=10) def retrieve_list(self): # Remove existing elements in the list for item in self.trv_available.get_children(): self.trv_available.delete(item) self.directive = Message(action=37) self.connection = self.directive.send_directive(self.connection) # Adding elements into the list for index, item in enumerate(self.connection.message.information): elements = item.split('¥') self.trv_available.insert('', 'end', text=elements[0], values=(index + 1, summarize_text(elements[1], 375))) if len(self.trv_available.get_children()) != 0: self.trv_available.selection_set( self.trv_available.get_children()[0]) self.select_template_summary() def select_template_summary(self, event=None): """ Function activated when the event of selecting an item in the available templates TV is generated. It fills the summary text box with information of the selected template :param event: """ if self.trv_available.item( self.trv_available.selection())['text'] != '': # Clear summary txt box self.txt_summary['state'] = NORMAL self.txt_summary.delete('1.0', 'end-1c') self.id_selected = int( self.trv_available.item(self.trv_available.selection()) ['text']) # Retrieve id of selected item from TreeView self.directive = Message(action=40, information=[self.id_selected ]) # ask for the template self.connection = self.directive.send_directive(self.connection) # Insert template's name and description self.txt_summary.insert( 'end-1c', "Name:\n{}\n\n".format(self.connection.message.information[0])) self.txt_summary.insert( 'end-1c', "Description:\n{}\n\nSections:\n".format( self.connection.message.information[1])) self.directive = Message(action=77, information=[ self.id_selected ]) # ask for the sections of the selected template self.connection = self.directive.send_directive(self.connection) # Adding elements in the summary text box index = 0 for item in self.connection.message.information: elements = item.split('¥') self.txt_summary.insert('end-1c', "{}) {}\t\t{}\t\t{}\n".format(index + 1, elements[3], 'optional' \ if elements[7] == '' else 'mandatory', '' if elements[8] == '' else '(MAIN)')) index += 1 self.txt_summary['state'] = DISABLED def show_frm(self): self.retrieve_list() self.frm_child_list.grid(row=1, column=0, columnspan=9, rowspan=8, pady=10, padx=10) def hide_frm(self): self.clear_fields() self.frm_child_list.grid_forget() self.frm_child_crud.grid_forget() def click_new(self): self.view_decision = False # Decision when viewing a template self.template = Template() self.retrieve_sections() self.txt_name.focus_set() self.show_cu_buttons() self.frm_child_crud['text'] = 'New template' self.frm_child_list.grid_forget() self.frm_child_crud.grid(row=1, column=0, columnspan=9, rowspan=8, pady=10, padx=10) def click_view(self): if len(self.trv_available.selection()) == 1: self.view_decision = True # Decision when viewing a template self.directive = Message(action=40, information=[self.id_selected]) self.connection = self.directive.send_directive(self.connection) self.template = Template( id=self.id_selected, name=self.connection.message.information[0], description=self.connection.message.information[1], sections=self.connection.message.information[2]) self.txt_name.insert(0, self.template.name) self.txt_description.insert('1.0', self.template.description) self.retrieve_sections(self.template.sections) self.txt_name.focus_set() self.disable_visual_components() self.btn_back.grid(row=0, column=0, padx=5, pady=5, sticky=W) self.frm_child_list.grid_forget() self.frm_child_crud['text'] = 'View template' self.frm_child_crud.grid(row=1, column=0, columnspan=9, rowspan=8, pady=10, padx=10) else: messagebox.showwarning(parent=self.frm_child_list, title='No selection', message='You must select one item') def click_update(self): if len(self.trv_available.selection()) == 1: self.view_decision = False # Decision when viewing a template self.directive = Message( action=40, information=[self.id_selected, 'validate']) self.connection = self.directive.send_directive(self.connection) if self.connection.message.action == 5: # An error ocurred while trying to update the item messagebox.showerror( parent=self.frm_child_list, title='Can not edit the template', message=self.connection.message.information[0]) else: self.template = Template( id=self.id_selected, name=self.connection.message.information[0], description=self.connection.message.information[1], sections=self.connection.message.information[2]) self.txt_name.insert(0, self.template.name) self.txt_description.insert('1.0', self.template.description) self.retrieve_sections(self.template.sections) self.txt_name.focus_set() self.show_cu_buttons() self.frm_child_crud['text'] = 'Update template' self.frm_child_list.grid_forget() self.frm_child_crud.grid(row=1, column=0, columnspan=9, rowspan=8, pady=10, padx=10) else: messagebox.showwarning(parent=self.frm_child_list, title='No selection', message='You must select one item') def click_delete(self): if len(self.trv_available.selection()) == 1: decision = messagebox.askyesno( parent=self.frm_child_list, title='Confirmation', message='Are you sure you want to delete the item?') if decision: self.directive = Message(action=39, information=[self.id_selected]) self.connection = self.directive.send_directive( self.connection) if self.connection.message.action == 5: # An error ocurred while deleting the item messagebox.showerror( parent=self.frm_child_list, title='Can not delete the item', message=self.connection.message.information[0]) else: self.retrieve_list() else: messagebox.showwarning(parent=self.frm_child_list, title='No selection', message='You must select one item') def show_cu_buttons(self): self.btn_add.grid(row=5, column=5, padx=25) self.btn_remove.grid(row=6, column=5, padx=25) self.btn_main_section.grid(row=2, column=10, padx=25) self.btn_down.grid(row=6, column=10, padx=25) self.btn_up.grid(row=5, column=10, padx=25) self.btn_save.grid(row=0, column=0, padx=5, pady=5, sticky=W) self.btn_cancel.grid(row=1, column=0, padx=5, pady=5, sticky=W) self.lbl_note_optional.grid(row=9, column=6, columnspan=4, sticky=W) def disable_visual_components(self): self.txt_name['bg'] = DISABLED_COLOR self.txt_description['bg'] = DISABLED_COLOR self.txt_name['state'] = DISABLED self.txt_description['state'] = DISABLED self.btn_add.grid_forget() self.btn_remove.grid_forget() self.btn_main_section.grid_forget() self.btn_down.grid_forget() self.btn_up.grid_forget() def retrieve_sections(self, s_sections=None): if s_sections is None: s_sections = [] self.directive = Message(action=32) self.connection = self.directive.send_directive(self.connection) a_sections = self.connection.message.information for item in self.trv_available_sections.get_children(): self.trv_available_sections.delete(item) for item in self.trv_selected_sections.get_children(): self.trv_selected_sections.delete(item) for item in s_sections: item_aux1 = item.split('¥')[2:6] item_aux2 = [item.split('¥')[-1]] item = item_aux1 + item_aux2 item = '¥'.join(item) if item in a_sections: a_sections.remove(item) for index, item in enumerate(a_sections): elements = item.split('¥') self.trv_available_sections.insert( '', 'end', text=elements[0], values=(index + 1, summarize_text(elements[1], 150), summarize_text(elements[3], 120))) for index, item in enumerate(s_sections): elements = item.split('¥') self.trv_selected_sections.insert( '', 'end', text=elements[2], values=(index + 1, summarize_text(elements[3], 150), summarize_text(elements[5], 120), elements[7], elements[8])) def click_add(self): """ Function that moves a 'Section' from available tree view to selected tree view (in frm_child_crud) """ if len(self.trv_available_sections.selection()) != 0 and len( self.trv_selected_sections.selection()) == 0: if len(self.trv_selected_sections.get_children()) != 0: index = self.trv_selected_sections.item( self.trv_selected_sections.get_children()[-1])['values'][0] else: index = 0 for row in self.trv_available_sections.selection(): index += 1 values = self.trv_available_sections.item(row)['values'] self.trv_selected_sections.insert( '', 'end', text=self.trv_available_sections.item(row)['text'], values=(index, values[1], values[2], '✓', '')) self.trv_available_sections.delete(row) def click_remove(self): """ Function that moves a 'Section' from selected tree view to available tree view (in frm_child_crud) """ if len(self.trv_selected_sections.selection()) != 0 and len( self.trv_available_sections.selection()) == 0: if len(self.trv_available_sections.get_children()) != 0: index = self.trv_available_sections.item( self.trv_available_sections.get_children() [-1])['values'][0] else: index = 0 for row in self.trv_selected_sections.selection(): index += 1 values = self.trv_selected_sections.item(row)['values'] self.trv_available_sections.insert( '', 'end', text=self.trv_selected_sections.item(row)['text'], values=(index, values[1], values[2])) self.trv_selected_sections.delete(row) def click_up(self): # Make sure only one item in 'selected sections' is selected if len(self.trv_selected_sections.selection()) == 1 and len( self.trv_available_sections.selection()) == 0: item = self.trv_selected_sections.selection() index = self.trv_selected_sections.index(item) self.trv_selected_sections.move(item, '', index - 1) def click_down(self): # Make sure only one item in 'selected sections' is selected if len(self.trv_selected_sections.selection()) == 1 and len( self.trv_available_sections.selection()) == 0: item = self.trv_selected_sections.selection() index = self.trv_selected_sections.index(item) self.trv_selected_sections.move(item, '', index + 1) def click_main_section(self): # Make sure a max of three items of 'selected sections' are selected if 4 > len(self.trv_selected_sections.selection()) > 0 and len( self.trv_available_sections.selection()) == 0: # First change all sections as normal (not main) for item in self.trv_selected_sections.get_children(): if self.trv_selected_sections.item(item)['values'][3] != '': values = self.trv_selected_sections.item(item)['values'] self.trv_selected_sections.item( item, values=(values[0], values[1], values[2], values[3], '')) # Set new main sections cont_error = False for row in self.trv_selected_sections.selection(): values = self.trv_selected_sections.item(row)['values'] if values[2] == 'Classification' or values[2] == 'Text': self.trv_selected_sections.item( row, values=(values[0], values[1], values[2], values[3], '✓')) else: # File sections can not be main cont_error = True if cont_error: messagebox.showwarning( parent=self.frm_child_crud, title='Main section(s)', message= "Main section(s) must be of 'Text' or 'Classification' data type" ) else: messagebox.showwarning( parent=self.frm_child_crud, title='Main section(s)', message= 'You must select a minimum of one and a maximum of three sections' ) def click_trv_asections(self, event): """ Function that removes selection from 'available' tree view when 'selected' tree view is selected (in frm_child_crud) """ self.trv_selected_sections.selection_remove( self.trv_selected_sections.selection()) def click_trv_ssections(self, event): """ Function that removes selection from 'selected' tree view when 'available' tree view is selected (in frm_child_crud) """ self.trv_available_sections.selection_remove( self.trv_available_sections.selection()) def click_save(self): if self.validate_fields(): self.template.name = self.txt_name.get() self.template.description = self.txt_description.get( '1.0', 'end-1c') if self.template.id == 0: # Creating a template self.directive = Message(action=36, information=[ self.template.name, self.template.description, [], [], [] ]) for item in self.trv_selected_sections.get_children(): values = self.trv_selected_sections.item(item)['values'] self.directive.information[2].append( int(self.trv_selected_sections.item(item)['text'])) self.directive.information[3].append(values[4]) if values[4] != '': self.directive.information[4].append('✓') else: self.directive.information[4].append(values[3]) else: # Updating a template self.directive = Message(action=38, information=[ self.template.id, self.template.name, self.template.description, [], [], [] ]) for item in self.trv_selected_sections.get_children(): values = self.trv_selected_sections.item(item)['values'] self.directive.information[3].append( int(self.trv_selected_sections.item(item)['text'])) self.directive.information[4].append(values[4]) if values[4] != '': self.directive.information[5].append('✓') else: self.directive.information[5].append(values[3]) self.connection = self.directive.send_directive(self.connection) self.clear_fields() self.frm_child_crud.grid_forget() self.show_frm() def click_cancel(self): decision = True if self.txt_name.get() != self.template.name or \ self.txt_description.get('1.0', 'end-1c') != self.template.description or \ len(self.trv_selected_sections.get_children()) != len(self.template.sections): decision = messagebox.askyesno( parent=self.frm_child_crud, title='Cancel', message='Are you sure you want to cancel?') if decision: self.click_back() def click_back(self): self.clear_fields() self.frm_child_crud.grid_forget() self.show_frm() def validate_fields(self): text_section = False if len(self.txt_name.get()) == 0: messagebox.showwarning(parent=self.frm_child_crud, title='Missing information', message='You must provide a name') return False if len(self.txt_description.get('1.0', 'end-1c')) == 0: messagebox.showwarning(parent=self.frm_child_crud, title='Missing information', message='You must provide a description') return False if len(self.trv_selected_sections.get_children()) == 0: messagebox.showwarning( parent=self.frm_child_crud, title='Missing information', message='You must select at least one section') return False for item in self.trv_selected_sections.get_children(): values = self.trv_selected_sections.item(item)['values'] if values[2] == 'Text' or values[2] == 'Classification': text_section = True break if not text_section: messagebox.showwarning( parent=self.frm_child_crud, title='Missing information', message='At least one section has to be of text type') return False for item in self.trv_selected_sections.get_children(): values = self.trv_selected_sections.item(item)['values'] if values[4] == '✓': if values[2] == 'Text' or values[2] == 'Classification': return True else: messagebox.showwarning( parent=self.frm_child_crud, title='Main section', message= 'The main section has to be of text or classification type' ) return False messagebox.showwarning( parent=self.frm_child_crud, title='Main section', message='You must set one of the selected section as main') return False def clear_fields(self): self.txt_name['state'] = NORMAL self.txt_description['state'] = NORMAL self.txt_name['bg'] = ENABLED_COLOR self.txt_description['bg'] = ENABLED_COLOR self.txt_name.delete(0, END) self.txt_description.delete('1.0', 'end-1c') self.btn_save.grid_forget() self.btn_cancel.grid_forget() self.btn_back.grid_forget() self.lbl_note_optional.grid_forget() def click_switch_mandatory(self, event): if not self.view_decision: # Only if not viewing a template # Make sure only one item in 'selected sections' is selected if len(self.trv_selected_sections.selection()) == 1 and len( self.trv_available_sections.selection()) == 0: values = self.trv_selected_sections.item( self.trv_selected_sections.focus())['values'] if values[3] == '': self.trv_selected_sections.item( self.trv_selected_sections.focus(), values=(values[0], values[1], values[2], '✓', values[4])) else: self.trv_selected_sections.item( self.trv_selected_sections.focus(), values=(values[0], values[1], values[2], '', values[4]))
class EventScheduler(Tk): def __init__(self): Tk.__init__(self, className='Scheduler') logging.info('Start') self.protocol("WM_DELETE_WINDOW", self.hide) self._visible = BooleanVar(self, False) self.withdraw() self.icon_img = PhotoImage(master=self, file=ICON48) self.iconphoto(True, self.icon_img) # --- systray icon self.icon = TrayIcon(ICON, fallback_icon_path=ICON_FALLBACK) # --- menu self.menu_widgets = SubMenu(parent=self.icon.menu) self.menu_eyes = Eyes(self.icon.menu, self) self.icon.menu.add_checkbutton(label=_('Manager'), command=self.display_hide) self.icon.menu.add_cascade(label=_('Widgets'), menu=self.menu_widgets) self.icon.menu.add_cascade(label=_("Eyes' rest"), menu=self.menu_eyes) self.icon.menu.add_command(label=_('Settings'), command=self.settings) self.icon.menu.add_separator() self.icon.menu.add_command(label=_('About'), command=lambda: About(self)) self.icon.menu.add_command(label=_('Quit'), command=self.exit) self.icon.bind_left_click(lambda: self.display_hide(toggle=True)) add_trace(self._visible, 'write', self._visibility_trace) self.menu = Menu(self, tearoff=False) self.menu.add_command(label=_('Edit'), command=self._edit_menu) self.menu.add_command(label=_('Delete'), command=self._delete_menu) self.right_click_iid = None self.menu_task = Menu(self.menu, tearoff=False) self._task_var = StringVar(self) menu_in_progress = Menu(self.menu_task, tearoff=False) for i in range(0, 110, 10): prog = '{}%'.format(i) menu_in_progress.add_radiobutton(label=prog, value=prog, variable=self._task_var, command=self._set_progress) for state in ['Pending', 'Completed', 'Cancelled']: self.menu_task.add_radiobutton(label=_(state), value=state, variable=self._task_var, command=self._set_progress) self._img_dot = tkPhotoImage(master=self) self.menu_task.insert_cascade(1, menu=menu_in_progress, compound='left', label=_('In Progress'), image=self._img_dot) self.title('Scheduler') self.rowconfigure(1, weight=1) self.columnconfigure(0, weight=1) self.scheduler = BackgroundScheduler(coalesce=False, misfire_grace_time=86400) self.scheduler.add_jobstore('sqlalchemy', url='sqlite:///%s' % JOBSTORE) self.scheduler.add_jobstore('memory', alias='memo') # --- style self.style = Style(self) self.style.theme_use("clam") self.style.configure('title.TLabel', font='TkdefaultFont 10 bold') self.style.configure('title.TCheckbutton', font='TkdefaultFont 10 bold') self.style.configure('subtitle.TLabel', font='TkdefaultFont 9 bold') self.style.configure('white.TLabel', background='white') self.style.configure('border.TFrame', background='white', border=1, relief='sunken') self.style.configure("Treeview.Heading", font="TkDefaultFont") bgc = self.style.lookup("TButton", "background") fgc = self.style.lookup("TButton", "foreground") bga = self.style.lookup("TButton", "background", ("active", )) self.style.map('TCombobox', fieldbackground=[('readonly', 'white'), ('readonly', 'focus', 'white')], background=[("disabled", "active", "readonly", bgc), ("!disabled", "active", "readonly", bga)], foreground=[('readonly', '!disabled', fgc), ('readonly', '!disabled', 'focus', fgc), ('readonly', 'disabled', 'gray40'), ('readonly', 'disabled', 'focus', 'gray40') ], arrowcolor=[("disabled", "gray40")]) self.style.configure('menu.TCombobox', foreground=fgc, background=bgc, fieldbackground=bgc) self.style.map('menu.TCombobox', fieldbackground=[('readonly', bgc), ('readonly', 'focus', bgc)], background=[("disabled", "active", "readonly", bgc), ("!disabled", "active", "readonly", bga)], foreground=[('readonly', '!disabled', fgc), ('readonly', '!disabled', 'focus', fgc), ('readonly', 'disabled', 'gray40'), ('readonly', 'disabled', 'focus', 'gray40') ], arrowcolor=[("disabled", "gray40")]) self.style.map('DateEntry', arrowcolor=[("disabled", "gray40")]) self.style.configure('cal.TFrame', background='#424242') self.style.configure('month.TLabel', background='#424242', foreground='white') self.style.configure('R.TButton', background='#424242', arrowcolor='white', bordercolor='#424242', lightcolor='#424242', darkcolor='#424242') self.style.configure('L.TButton', background='#424242', arrowcolor='white', bordercolor='#424242', lightcolor='#424242', darkcolor='#424242') active_bg = self.style.lookup('TEntry', 'selectbackground', ('focus', )) self.style.map('R.TButton', background=[('active', active_bg)], bordercolor=[('active', active_bg)], darkcolor=[('active', active_bg)], lightcolor=[('active', active_bg)]) self.style.map('L.TButton', background=[('active', active_bg)], bordercolor=[('active', active_bg)], darkcolor=[('active', active_bg)], lightcolor=[('active', active_bg)]) self.style.configure('txt.TFrame', background='white') self.style.layout('down.TButton', [('down.TButton.downarrow', { 'side': 'right', 'sticky': 'ns' })]) self.style.map('TRadiobutton', indicatorforeground=[('disabled', 'gray40')]) self.style.map('TCheckbutton', indicatorforeground=[('disabled', 'gray40')], indicatorbackground=[ ('pressed', '#dcdad5'), ('!disabled', 'alternate', 'white'), ('disabled', 'alternate', '#a0a0a0'), ('disabled', '#dcdad5') ]) self.style.map('down.TButton', arrowcolor=[("disabled", "gray40")]) self.style.map('TMenubutton', arrowcolor=[('disabled', self.style.lookup('TMenubutton', 'foreground', ['disabled']))]) bg = self.style.lookup('TFrame', 'background', default='#ececec') self.configure(bg=bg) self.option_add('*Toplevel.background', bg) self.option_add('*Menu.background', bg) self.option_add('*Menu.tearOff', False) # toggle text self._open_image = PhotoImage(name='img_opened', file=IM_OPENED, master=self) self._closed_image = PhotoImage(name='img_closed', file=IM_CLOSED, master=self) self._open_image_sel = PhotoImage(name='img_opened_sel', file=IM_OPENED_SEL, master=self) self._closed_image_sel = PhotoImage(name='img_closed_sel', file=IM_CLOSED_SEL, master=self) self.style.element_create( "toggle", "image", "img_closed", ("selected", "!disabled", "img_opened"), ("active", "!selected", "!disabled", "img_closed_sel"), ("active", "selected", "!disabled", "img_opened_sel"), border=2, sticky='') self.style.map('Toggle', background=[]) self.style.layout('Toggle', [('Toggle.border', { 'children': [('Toggle.padding', { 'children': [('Toggle.toggle', { 'sticky': 'nswe' })], 'sticky': 'nswe' })], 'sticky': 'nswe' })]) # toggle sound self._im_sound = PhotoImage(master=self, file=IM_SOUND) self._im_mute = PhotoImage(master=self, file=IM_MUTE) self._im_sound_dis = PhotoImage(master=self, file=IM_SOUND_DIS) self._im_mute_dis = PhotoImage(master=self, file=IM_MUTE_DIS) self.style.element_create( 'mute', 'image', self._im_sound, ('selected', '!disabled', self._im_mute), ('selected', 'disabled', self._im_mute_dis), ('!selected', 'disabled', self._im_sound_dis), border=2, sticky='') self.style.layout('Mute', [('Mute.border', { 'children': [('Mute.padding', { 'children': [('Mute.mute', { 'sticky': 'nswe' })], 'sticky': 'nswe' })], 'sticky': 'nswe' })]) self.style.configure('Mute', relief='raised') # widget scrollbar self._im_trough = {} self._im_slider_vert = {} self._im_slider_vert_prelight = {} self._im_slider_vert_active = {} self._slider_alpha = Image.open(IM_SCROLL_ALPHA) for widget in ['Events', 'Tasks']: bg = CONFIG.get(widget, 'background', fallback='gray10') fg = CONFIG.get(widget, 'foreground') widget_bg = self.winfo_rgb(bg) widget_fg = tuple( round(c * 255 / 65535) for c in self.winfo_rgb(fg)) active_bg = active_color(*widget_bg) active_bg2 = active_color(*active_color(*widget_bg, 'RGB')) slider_vert = Image.new('RGBA', (13, 28), active_bg) slider_vert_active = Image.new('RGBA', (13, 28), widget_fg) slider_vert_prelight = Image.new('RGBA', (13, 28), active_bg2) self._im_trough[widget] = tkPhotoImage(width=15, height=15, master=self) self._im_trough[widget].put(" ".join( ["{" + " ".join([bg] * 15) + "}"] * 15)) self._im_slider_vert_active[widget] = PhotoImage( slider_vert_active, master=self) self._im_slider_vert[widget] = PhotoImage(slider_vert, master=self) self._im_slider_vert_prelight[widget] = PhotoImage( slider_vert_prelight, master=self) self.style.element_create('%s.Vertical.Scrollbar.trough' % widget, 'image', self._im_trough[widget]) self.style.element_create( '%s.Vertical.Scrollbar.thumb' % widget, 'image', self._im_slider_vert[widget], ('pressed', '!disabled', self._im_slider_vert_active[widget]), ('active', '!disabled', self._im_slider_vert_prelight[widget]), border=6, sticky='ns') self.style.layout( '%s.Vertical.TScrollbar' % widget, [('%s.Vertical.Scrollbar.trough' % widget, { 'children': [('%s.Vertical.Scrollbar.thumb' % widget, { 'expand': '1' })], 'sticky': 'ns' })]) # --- tree columns = { _('Summary'): ({ 'stretch': True, 'width': 300 }, lambda: self._sort_by_desc(_('Summary'), False)), _('Place'): ({ 'stretch': True, 'width': 200 }, lambda: self._sort_by_desc(_('Place'), False)), _('Start'): ({ 'stretch': False, 'width': 150 }, lambda: self._sort_by_date(_('Start'), False)), _('End'): ({ 'stretch': False, 'width': 150 }, lambda: self._sort_by_date(_("End"), False)), _('Category'): ({ 'stretch': False, 'width': 100 }, lambda: self._sort_by_desc(_('Category'), False)) } self.tree = Treeview(self, show="headings", columns=list(columns)) for label, (col_prop, cmd) in columns.items(): self.tree.column(label, **col_prop) self.tree.heading(label, text=label, anchor="w", command=cmd) self.tree.tag_configure('0', background='#ececec') self.tree.tag_configure('1', background='white') self.tree.tag_configure('outdated', foreground='red') scroll = AutoScrollbar(self, orient='vertical', command=self.tree.yview) self.tree.configure(yscrollcommand=scroll.set) # --- toolbar toolbar = Frame(self) self.img_plus = PhotoImage(master=self, file=IM_ADD) Button(toolbar, image=self.img_plus, padding=1, command=self.add).pack(side="left", padx=4) Label(toolbar, text=_("Filter by")).pack(side="left", padx=4) # --- TODO: add filter by start date (after date) self.filter_col = Combobox( toolbar, state="readonly", # values=("",) + self.tree.cget('columns')[1:], values=("", _("Category")), exportselection=False) self.filter_col.pack(side="left", padx=4) self.filter_val = Combobox(toolbar, state="readonly", exportselection=False) self.filter_val.pack(side="left", padx=4) Button(toolbar, text=_('Delete All Outdated'), padding=1, command=self.delete_outdated_events).pack(side="right", padx=4) # --- grid toolbar.grid(row=0, columnspan=2, sticky='we', pady=4) self.tree.grid(row=1, column=0, sticky='eswn') scroll.grid(row=1, column=1, sticky='ns') # --- restore data data = {} self.events = {} self.nb = 0 try: with open(DATA_PATH, 'rb') as file: dp = Unpickler(file) data = dp.load() except Exception: l = [ f for f in os.listdir(os.path.dirname(BACKUP_PATH)) if f.startswith('data.backup') ] if l: l.sort(key=lambda x: int(x[11:])) shutil.copy(os.path.join(os.path.dirname(BACKUP_PATH), l[-1]), DATA_PATH) with open(DATA_PATH, 'rb') as file: dp = Unpickler(file) data = dp.load() self.nb = len(data) backup() now = datetime.now() for i, prop in enumerate(data): iid = str(i) self.events[iid] = Event(self.scheduler, iid=iid, **prop) self.tree.insert('', 'end', iid, values=self.events[str(i)].values()) tags = [str(self.tree.index(iid) % 2)] self.tree.item(iid, tags=tags) if not prop['Repeat']: for rid, d in list(prop['Reminders'].items()): if d < now: del self.events[iid]['Reminders'][rid] self.after_id = self.after(15 * 60 * 1000, self.check_outdated) # --- bindings self.bind_class("TCombobox", "<<ComboboxSelected>>", self.clear_selection, add=True) self.bind_class("TCombobox", "<Control-a>", self.select_all) self.bind_class("TEntry", "<Control-a>", self.select_all) self.tree.bind('<3>', self._post_menu) self.tree.bind('<1>', self._select) self.tree.bind('<Double-1>', self._edit_on_click) self.menu.bind('<FocusOut>', lambda e: self.menu.unpost()) self.filter_col.bind("<<ComboboxSelected>>", self.update_filter_val) self.filter_val.bind("<<ComboboxSelected>>", self.apply_filter) # --- widgets self.widgets = {} prop = { op: CONFIG.get('Calendar', op) for op in CONFIG.options('Calendar') } self.widgets['Calendar'] = CalendarWidget(self, locale=CONFIG.get( 'General', 'locale'), **prop) self.widgets['Events'] = EventWidget(self) self.widgets['Tasks'] = TaskWidget(self) self.widgets['Timer'] = Timer(self) self.widgets['Pomodoro'] = Pomodoro(self) self._setup_style() for item, widget in self.widgets.items(): self.menu_widgets.add_checkbutton( label=_(item), command=lambda i=item: self.display_hide_widget(i)) self.menu_widgets.set_item_value(_(item), widget.variable.get()) add_trace(widget.variable, 'write', lambda *args, i=item: self._menu_widgets_trace(i)) self.icon.loop(self) self.tk.eval(""" apply {name { set newmap {} foreach {opt lst} [ttk::style map $name] { if {($opt eq "-foreground") || ($opt eq "-background")} { set newlst {} foreach {st val} $lst { if {($st eq "disabled") || ($st eq "selected")} { lappend newlst $st $val } } if {$newlst ne {}} { lappend newmap $opt $newlst } } else { lappend newmap $opt $lst } } ttk::style map $name {*}$newmap }} Treeview """) # react to scheduler --update-date in command line signal.signal(signal.SIGUSR1, self.update_date) # update selected date in calendar and event list every day self.scheduler.add_job(self.update_date, CronTrigger(hour=0, minute=0, second=1), jobstore='memo') self.scheduler.start() def _setup_style(self): # scrollbars for widget in ['Events', 'Tasks']: bg = CONFIG.get(widget, 'background', fallback='gray10') fg = CONFIG.get(widget, 'foreground', fallback='white') widget_bg = self.winfo_rgb(bg) widget_fg = tuple( round(c * 255 / 65535) for c in self.winfo_rgb(fg)) active_bg = active_color(*widget_bg) active_bg2 = active_color(*active_color(*widget_bg, 'RGB')) slider_vert = Image.new('RGBA', (13, 28), active_bg) slider_vert.putalpha(self._slider_alpha) slider_vert_active = Image.new('RGBA', (13, 28), widget_fg) slider_vert_active.putalpha(self._slider_alpha) slider_vert_prelight = Image.new('RGBA', (13, 28), active_bg2) slider_vert_prelight.putalpha(self._slider_alpha) self._im_trough[widget].put(" ".join( ["{" + " ".join([bg] * 15) + "}"] * 15)) self._im_slider_vert_active[widget].paste(slider_vert_active) self._im_slider_vert[widget].paste(slider_vert) self._im_slider_vert_prelight[widget].paste(slider_vert_prelight) for widget in self.widgets.values(): widget.update_style() def report_callback_exception(self, *args): err = ''.join(traceback.format_exception(*args)) logging.error(err) showerror('Exception', str(args[1]), err, parent=self) def save(self): logging.info('Save event database') data = [ev.to_dict() for ev in self.events.values()] with open(DATA_PATH, 'wb') as file: pick = Pickler(file) pick.dump(data) def update_date(self, *args): """Update Calendar's selected day and Events' list.""" self.widgets['Calendar'].update_date() self.widgets['Events'].display_evts() self.update_idletasks() # --- bindings def _select(self, event): if not self.tree.identify_row(event.y): self.tree.selection_remove(*self.tree.selection()) def _edit_on_click(self, event): sel = self.tree.selection() if sel: sel = sel[0] self.edit(sel) # --- class bindings @staticmethod def clear_selection(event): combo = event.widget combo.selection_clear() @staticmethod def select_all(event): event.widget.selection_range(0, "end") return "break" # --- show / hide def _menu_widgets_trace(self, item): self.menu_widgets.set_item_value(_(item), self.widgets[item].variable.get()) def display_hide_widget(self, item): value = self.menu_widgets.get_item_value(_(item)) if value: self.widgets[item].show() else: self.widgets[item].hide() def hide(self): self._visible.set(False) self.withdraw() self.save() def show(self): self._visible.set(True) self.deiconify() def _visibility_trace(self, *args): self.icon.menu.set_item_value(_('Manager'), self._visible.get()) def display_hide(self, toggle=False): value = self.icon.menu.get_item_value(_('Manager')) if toggle: value = not value self.icon.menu.set_item_value(_('Manager'), value) self._visible.set(value) if not value: self.withdraw() self.save() else: self.deiconify() # --- event management def event_add(self, event): self.nb += 1 iid = str(self.nb) self.events[iid] = event self.tree.insert('', 'end', iid, values=event.values()) self.tree.item(iid, tags=str(self.tree.index(iid) % 2)) self.widgets['Calendar'].add_event(event) self.widgets['Events'].display_evts() self.widgets['Tasks'].display_tasks() self.save() def event_configure(self, iid): self.tree.item(iid, values=self.events[iid].values()) self.widgets['Calendar'].add_event(self.events[iid]) self.widgets['Events'].display_evts() self.widgets['Tasks'].display_tasks() self.save() def add(self, date=None): iid = str(self.nb + 1) if date is not None: event = Event(self.scheduler, iid=iid, Start=date) else: event = Event(self.scheduler, iid=iid) Form(self, event, new=True) def delete(self, iid): index = self.tree.index(iid) self.tree.delete(iid) for k, item in enumerate(self.tree.get_children('')[index:]): tags = [ t for t in self.tree.item(item, 'tags') if t not in ['1', '0'] ] tags.append(str((index + k) % 2)) self.tree.item(item, tags=tags) self.events[iid].reminder_remove_all() self.widgets['Calendar'].remove_event(self.events[iid]) del (self.events[iid]) self.widgets['Events'].display_evts() self.widgets['Tasks'].display_tasks() self.save() def edit(self, iid): self.widgets['Calendar'].remove_event(self.events[iid]) Form(self, self.events[iid]) def check_outdated(self): """Check for outdated events every 15 min.""" now = datetime.now() for iid, event in self.events.items(): if not event['Repeat'] and event['Start'] < now: tags = list(self.tree.item(iid, 'tags')) if 'outdated' not in tags: tags.append('outdated') self.tree.item(iid, tags=tags) self.after_id = self.after(15 * 60 * 1000, self.check_outdated) def delete_outdated_events(self): now = datetime.now() outdated = [] for iid, prop in self.events.items(): if prop['End'] < now: if not prop['Repeat']: outdated.append(iid) elif prop['Repeat']['Limit'] != 'always': end = prop['End'] enddate = datetime.fromordinal( prop['Repeat']['EndDate'].toordinal()) enddate.replace(hour=end.hour, minute=end.minute) if enddate < now: outdated.append(iid) for item in outdated: self.delete(item) logging.info('Deleted outdated events') def refresh_reminders(self): """ Reschedule all reminders. Required when APScheduler is updated. """ for event in self.events.values(): reminders = [date for date in event['Reminders'].values()] event.reminder_remove_all() for date in reminders: event.reminder_add(date) logging.info('Refreshed reminders') # --- sorting def _move_item(self, item, index): self.tree.move(item, "", index) tags = [t for t in self.tree.item(item, 'tags') if t not in ['1', '0']] tags.append(str(index % 2)) self.tree.item(item, tags=tags) @staticmethod def to_datetime(date): date_format = get_date_format("short", CONFIG.get("General", "locale")).pattern dayfirst = date_format.startswith("d") yearfirst = date_format.startswith("y") return parse(date, dayfirst=dayfirst, yearfirst=yearfirst) def _sort_by_date(self, col, reverse): l = [(self.to_datetime(self.tree.set(k, col)), k) for k in self.tree.get_children('')] l.sort(reverse=reverse) # rearrange items in sorted positions for index, (val, k) in enumerate(l): self._move_item(k, index) # reverse sort next time self.tree.heading(col, command=lambda: self._sort_by_date(col, not reverse)) def _sort_by_desc(self, col, reverse): l = [(self.tree.set(k, col), k) for k in self.tree.get_children('')] l.sort(reverse=reverse, key=lambda x: x[0].lower()) # rearrange items in sorted positions for index, (val, k) in enumerate(l): self._move_item(k, index) # reverse sort next time self.tree.heading(col, command=lambda: self._sort_by_desc(col, not reverse)) # --- filter def update_filter_val(self, event): col = self.filter_col.get() self.filter_val.set("") if col: l = set() for k in self.events: l.add(self.tree.set(k, col)) self.filter_val.configure(values=tuple(l)) else: self.filter_val.configure(values=[]) self.apply_filter(event) def apply_filter(self, event): col = self.filter_col.get() val = self.filter_val.get() items = list(self.events.keys()) if not col: for item in items: self._move_item(item, int(item)) else: i = 0 for item in items: if self.tree.set(item, col) == val: self._move_item(item, i) i += 1 else: self.tree.detach(item) # --- manager's menu def _post_menu(self, event): self.right_click_iid = self.tree.identify_row(event.y) self.tree.selection_remove(*self.tree.selection()) self.tree.selection_add(self.right_click_iid) if self.right_click_iid: try: self.menu.delete(_('Progress')) except TclError: pass state = self.events[self.right_click_iid]['Task'] if state: self._task_var.set(state) if '%' in state: self._img_dot = PhotoImage(master=self, file=IM_DOT) else: self._img_dot = tkPhotoImage(master=self) self.menu_task.entryconfigure(1, image=self._img_dot) self.menu.insert_cascade(0, menu=self.menu_task, label=_('Progress')) self.menu.tk_popup(event.x_root, event.y_root) def _delete_menu(self): if self.right_click_iid: self.delete(self.right_click_iid) def _edit_menu(self): if self.right_click_iid: self.edit(self.right_click_iid) def _set_progress(self): if self.right_click_iid: self.events[self.right_click_iid]['Task'] = self._task_var.get() self.widgets['Tasks'].display_tasks() if '%' in self._task_var.get(): self._img_dot = PhotoImage(master=self, file=IM_DOT) else: self._img_dot = tkPhotoImage(master=self) self.menu_task.entryconfigure(1, image=self._img_dot) # --- icon menu def exit(self): self.save() rep = self.widgets['Pomodoro'].stop(self.widgets['Pomodoro'].on) if not rep: return self.menu_eyes.quit() self.after_cancel(self.after_id) try: self.scheduler.shutdown() except SchedulerNotRunningError: pass self.destroy() def settings(self): splash_supp = CONFIG.get('General', 'splash_supported', fallback=True) dialog = Settings(self) self.wait_window(dialog) self._setup_style() if splash_supp != CONFIG.get('General', 'splash_supported'): for widget in self.widgets.values(): widget.update_position() # --- week schedule def get_next_week_events(self): """Return events scheduled for the next 7 days """ locale = CONFIG.get("General", "locale") next_ev = {} today = datetime.now().date() for d in range(7): day = today + timedelta(days=d) evts = self.widgets['Calendar'].get_events(day) if evts: evts = [self.events[iid] for iid in evts] evts.sort(key=lambda ev: ev.get_start_time()) desc = [] for ev in evts: if ev["WholeDay"]: date = "" else: date = "%s - %s " % ( format_time(ev['Start'], locale=locale), format_time(ev['End'], locale=locale)) place = "(%s)" % ev['Place'] if place == "()": place = "" desc.append(("%s%s %s\n" % (date, ev['Summary'], place), ev['Description'])) next_ev[day.strftime('%A')] = desc return next_ev # --- tasks def get_tasks(self): # TODO: find events with repetition in the week # TODO: better handling of events on several days tasks = [] for event in self.events.values(): if event['Task']: tasks.append(event) return tasks