def _create_file_menu(self, master: Menu) -> Menu: menu = Menu(master, tearoff=0) menu.add_command(label="New", command=self.new, underline=0, accelerator=_KEYS["new"][0]) self.master.bind_all(_KEYS["new"][1], lambda event: self.new()) menu.add_command(label="Open", command=self.open, underline=0, accelerator=_KEYS["open"][0]) self.master.bind_all(_KEYS["open"][1], lambda event: self.open()) menu.add_command(label="Save", command=self.save, underline=0, accelerator=_KEYS["save"][0]) self.master.bind_all(_KEYS["save"][1], lambda event: self.save()) menu.entryconfigure(2, state=DISABLED) self._document.bind("<<Modified>>", lambda event: menu.entryconfigure( 2, state=_to_state(self._document.modified)), add=True) menu.add_command(label="Save As", command=self.save_as, underline=5) menu.add_command(label="Close", command=self.close, underline=0, accelerator=_KEYS["close"][0]) self.master.bind_all(_KEYS["close"][1], lambda event: self.close()) return menu
class Sticky(Toplevel): """ Sticky note class """ def __init__(self, master, key, **kwargs): """ Create a new sticky note. master: main app key: key identifying this note in master.note_data kwargs: dictionnary of the other arguments (title, txt, category, color, tags, geometry, locked, checkboxes, images, rolled) """ Toplevel.__init__(self, master) # --- window properties self.id = key self.is_locked = not (kwargs.get("locked", False)) self.images = [] self.links = {} self.latex = {} self.nb_links = 0 self.title('mynotes%s' % key) self.attributes("-type", "splash") self.attributes("-alpha", CONFIG.getint("General", "opacity") / 100) self.focus_force() # window geometry self.update_idletasks() self.geometry(kwargs.get("geometry", '220x235')) self.save_geometry = kwargs.get("geometry", '220x235') self.update() self.rowconfigure(1, weight=1) self.minsize(10, 10) self.protocol("WM_DELETE_WINDOW", self.hide) # --- style self.style = Style(self) self.style.configure(self.id + ".TCheckbutton", selectbackground="red") self.style.map('TEntry', selectbackground=[('!focus', '#c3c3c3')]) selectbg = self.style.lookup('TEntry', 'selectbackground', ('focus', )) self.style.configure("sel.TCheckbutton", background=selectbg) self.style.map("sel.TCheckbutton", background=[("active", selectbg)]) # --- note elements # title font_title = "%s %s" % (CONFIG.get("Font", "title_family").replace( " ", "\ "), CONFIG.get("Font", "title_size")) style = CONFIG.get("Font", "title_style").split(",") if style: font_title += " " font_title += " ".join(style) self.title_var = StringVar(master=self, value=kwargs.get("title", _("Title"))) self.title_label = Label(self, textvariable=self.title_var, anchor="center", style=self.id + ".TLabel", font=font_title) self.title_entry = Entry(self, textvariable=self.title_var, exportselection=False, justify="center", font=font_title) # buttons/icons self.roll = Label(self, image="img_roll", style=self.id + ".TLabel") self.close = Label(self, image="img_close", style=self.id + ".TLabel") self.im_lock = PhotoImage(master=self, file=IM_LOCK) self.cadenas = Label(self, style=self.id + ".TLabel") # corner grip self.corner = Sizegrip(self, style=self.id + ".TSizegrip") # texte font_text = "%s %s" % (CONFIG.get("Font", "text_family").replace( " ", "\ "), CONFIG.get("Font", "text_size")) self.txt = Text(self, wrap='word', undo=True, selectforeground='white', inactiveselectbackground=selectbg, selectbackground=selectbg, tabs=(10, 'right', 21, 'left'), relief="flat", borderwidth=0, highlightthickness=0, font=font_text) # tags self.txt.tag_configure("bold", font="%s bold" % font_text) self.txt.tag_configure("italic", font="%s italic" % font_text) self.txt.tag_configure("bold-italic", font="%s bold italic" % font_text) self.txt.tag_configure("underline", underline=True, selectforeground="white") self.txt.tag_configure("overstrike", overstrike=True, selectforeground="white") self.txt.tag_configure("center", justify="center") self.txt.tag_configure("left", justify="left") self.txt.tag_configure("right", justify="right") self.txt.tag_configure("link", foreground="blue", underline=True, selectforeground="white") self.txt.tag_configure("list", lmargin1=0, lmargin2=21, tabs=(10, 'right', 21, 'left')) self.txt.tag_configure("todolist", lmargin1=0, lmargin2=21, tabs=(10, 'right', 21, 'left')) margin = 2 * Font(self, font=font_text).measure("m") self.txt.tag_configure("enum", lmargin1=0, lmargin2=margin + 5, tabs=(margin, 'right', margin + 5, 'left')) for coul in TEXT_COLORS.values(): self.txt.tag_configure(coul, foreground=coul, selectforeground="white") self.txt.tag_configure(coul + "-underline", foreground=coul, selectforeground="white", underline=True) self.txt.tag_configure(coul + "-overstrike", foreground=coul, overstrike=True, selectforeground="white") # --- menus # --- * menu on title self.menu = Menu(self, tearoff=False) # note color menu_note_color = Menu(self.menu, tearoff=False) colors = list(COLORS.keys()) colors.sort() for coul in colors: menu_note_color.add_command( label=coul, command=lambda key=coul: self.change_color(key)) # category self.category = StringVar( self, kwargs.get("category", CONFIG.get("General", "default_category"))) self.menu_categories = Menu(self.menu, tearoff=False) categories = CONFIG.options("Categories") categories.sort() for cat in categories: self.menu_categories.add_radiobutton(label=cat.capitalize(), value=cat, variable=self.category, command=self.change_category) # position: normal, always above, always below self.position = StringVar( self, kwargs.get("position", CONFIG.get("General", "position"))) menu_position = Menu(self.menu, tearoff=False) menu_position.add_radiobutton(label=_("Always above"), value="above", variable=self.position, command=self.set_position_above) menu_position.add_radiobutton(label=_("Always below"), value="below", variable=self.position, command=self.set_position_below) menu_position.add_radiobutton(label=_("Normal"), value="normal", variable=self.position, command=self.set_position_normal) # mode: note, list, todo list menu_mode = Menu(self.menu, tearoff=False) self.mode = StringVar(self, kwargs.get("mode", "note")) menu_mode.add_radiobutton(label=_("Note"), value="note", variable=self.mode, command=self.set_mode_note) menu_mode.add_radiobutton(label=_("List"), value="list", variable=self.mode, command=self.set_mode_list) menu_mode.add_radiobutton(label=_("ToDo List"), value="todolist", variable=self.mode, command=self.set_mode_todolist) menu_mode.add_radiobutton(label=_("Enumeration"), value="enum", variable=self.mode, command=self.set_mode_enum) self.menu.add_command(label=_("Delete"), command=self.delete) self.menu.add_cascade(label=_("Category"), menu=self.menu_categories) self.menu.add_cascade(label=_("Color"), menu=menu_note_color) self.menu.add_command(label=_("Lock"), command=self.lock) self.menu.add_cascade(label=_("Position"), menu=menu_position) self.menu.add_cascade(label=_("Mode"), menu=menu_mode) # --- * menu on main text self.menu_txt = Menu(self.txt, tearoff=False) # style menu_style = Menu(self.menu_txt, tearoff=False) menu_style.add_command(label=_("Bold"), command=lambda: self.toggle_text_style("bold")) menu_style.add_command( label=_("Italic"), command=lambda: self.toggle_text_style("italic")) menu_style.add_command(label=_("Underline"), command=self.toggle_underline) menu_style.add_command(label=_("Overstrike"), command=self.toggle_overstrike) # text alignment menu_align = Menu(self.menu_txt, tearoff=False) menu_align.add_command(label=_("Left"), command=lambda: self.set_align("left")) menu_align.add_command(label=_("Right"), command=lambda: self.set_align("right")) menu_align.add_command(label=_("Center"), command=lambda: self.set_align("center")) # text color menu_colors = Menu(self.menu_txt, tearoff=False) colors = list(TEXT_COLORS.keys()) colors.sort() for coul in colors: menu_colors.add_command(label=coul, command=lambda key=coul: self. change_sel_color(TEXT_COLORS[key])) # insert menu_insert = Menu(self.menu_txt, tearoff=False) menu_insert.add_command(label=_("Symbols"), command=self.add_symbols) menu_insert.add_command(label=_("Checkbox"), command=self.add_checkbox) menu_insert.add_command(label=_("Image"), command=self.add_image) menu_insert.add_command(label=_("Date"), command=self.add_date) menu_insert.add_command(label=_("Link"), command=self.add_link) if LATEX: menu_insert.add_command(label="LaTex", command=self.add_latex) self.menu_txt.add_cascade(label=_("Style"), menu=menu_style) self.menu_txt.add_cascade(label=_("Alignment"), menu=menu_align) self.menu_txt.add_cascade(label=_("Color"), menu=menu_colors) self.menu_txt.add_cascade(label=_("Insert"), menu=menu_insert) # --- restore note content/appearence self.color = kwargs.get("color", CONFIG.get("Categories", self.category.get())) self.txt.insert('1.0', kwargs.get("txt", "")) self.txt.edit_reset() # clear undo stack # restore inserted objects (images and checkboxes) # we need to restore objects with increasing index to avoid placment errors indexes = list(kwargs.get("inserted_objects", {}).keys()) indexes.sort(key=sorting) for index in indexes: kind, val = kwargs["inserted_objects"][index] if kind == "checkbox": ch = Checkbutton(self.txt, takefocus=False, style=self.id + ".TCheckbutton") if val: ch.state(("selected", )) self.txt.window_create(index, window=ch) elif kind == "image": if os.path.exists(val): self.images.append(PhotoImage(master=self.txt, file=val)) self.txt.image_create(index, image=self.images[-1], name=val) # restore tags for tag, indices in kwargs.get("tags", {}).items(): if indices: self.txt.tag_add(tag, *indices) for link in kwargs.get("links", {}).values(): self.nb_links += 1 self.links[self.nb_links] = link self.txt.tag_bind("link#%i" % self.nb_links, "<Button-1>", lambda e, l=link: open_url(l)) for img, latex in kwargs.get("latex", {}).items(): self.latex[img] = latex if LATEX: self.txt.tag_bind(img, '<Double-Button-1>', lambda e, im=img: self.add_latex(im)) mode = self.mode.get() if mode != "note": self.txt.tag_add(mode, "1.0", "end") self.txt.focus_set() self.lock() if kwargs.get("rolled", False): self.rollnote() if self.position.get() == "above": self.set_position_above() elif self.position.get() == "below": self.set_position_below() # --- placement # titlebar if CONFIG.get("General", "buttons_position") == "right": # right = lock icon - title - roll - close self.columnconfigure(1, weight=1) self.roll.grid(row=0, column=2, sticky="e") self.close.grid(row=0, column=3, sticky="e", padx=(0, 2)) self.cadenas.grid(row=0, column=0, sticky="w") self.title_label.grid(row=0, column=1, sticky="ew", pady=(1, 0)) else: # left = close - roll - title - lock icon self.columnconfigure(2, weight=1) self.roll.grid(row=0, column=1, sticky="w") self.close.grid(row=0, column=0, sticky="w", padx=(2, 0)) self.cadenas.grid(row=0, column=3, sticky="e") self.title_label.grid(row=0, column=2, sticky="ew", pady=(1, 0)) # body self.txt.grid(row=1, columnspan=4, column=0, sticky="ewsn", pady=(1, 4), padx=4) self.corner.lift(self.txt) self.corner.place(relx=1.0, rely=1.0, anchor="se") # --- bindings self.bind("<FocusOut>", self.save_note) self.bind('<Configure>', self.bouge) self.bind('<Button-1>', self.change_focus, True) self.close.bind("<Button-1>", self.hide) self.close.bind("<Enter>", self.enter_close) self.close.bind("<Leave>", self.leave_close) self.roll.bind("<Button-1>", self.rollnote) self.roll.bind("<Enter>", self.enter_roll) self.roll.bind("<Leave >", self.leave_roll) self.title_label.bind("<Double-Button-1>", self.edit_title) self.title_label.bind("<ButtonPress-1>", self.start_move) self.title_label.bind("<ButtonRelease-1>", self.stop_move) self.title_label.bind("<B1-Motion>", self.move) self.title_label.bind('<Button-3>', self.show_menu) self.title_entry.bind("<Return>", lambda e: self.title_entry.place_forget()) self.title_entry.bind("<FocusOut>", lambda e: self.title_entry.place_forget()) self.title_entry.bind("<Escape>", lambda e: self.title_entry.place_forget()) self.txt.tag_bind("link", "<Enter>", lambda event: self.txt.configure(cursor="hand1")) self.txt.tag_bind("link", "<Leave>", lambda event: self.txt.configure(cursor="")) self.txt.bind("<FocusOut>", self.save_note) self.txt.bind('<Button-3>', self.show_menu_txt) # add binding to the existing class binding so that the selected text # is erased on pasting self.txt.bind("<Control-v>", self.paste) self.corner.bind('<ButtonRelease-1>', self.resize) # --- keyboard shortcuts self.txt.bind('<Control-b>', lambda e: self.toggle_text_style('bold')) self.txt.bind('<Control-i>', lambda e: self.toggle_text_style('italic')) self.txt.bind('<Control-u>', lambda e: self.toggle_underline()) self.txt.bind('<Control-r>', lambda e: self.set_align('right')) self.txt.bind('<Control-l>', lambda e: self.set_align('left')) def __setattr__(self, name, value): object.__setattr__(self, name, value) if name == "color": self.style.configure(self.id + ".TSizegrip", background=self.color) self.style.configure(self.id + ".TLabel", background=self.color) self.style.configure("close" + self.id + ".TLabel", background=self.color) self.style.configure("roll" + self.id + ".TLabel", background=self.color) self.style.map(self.id + ".TLabel", background=[("active", self.color)]) self.style.configure(self.id + ".TCheckbutton", background=self.color) self.style.map(self.id + ".TCheckbutton", background=[("active", self.color), ("disabled", self.color)]) self.style.map("close" + self.id + ".TLabel", background=[("active", self.color)]) self.style.map("roll" + self.id + ".TLabel", background=[("active", self.color)]) self.configure(bg=self.color) self.txt.configure(bg=self.color) def paste(self, event): """ delete selected text before pasting """ if self.txt.tag_ranges("sel"): self.txt.delete("sel.first", "sel.last") def delete(self, confirmation=True): """ Delete this note """ if confirmation: rep = askokcancel(_("Confirmation"), _("Delete the note?")) else: rep = True if rep: del (self.master.note_data[self.id]) del (self.master.notes[self.id]) self.master.save() self.destroy() def lock(self): """ Put note in read-only mode to avoid unwanted text insertion """ if self.is_locked: selectbg = self.style.lookup('TEntry', 'selectbackground', ('focus', )) self.txt.configure(state="normal", selectforeground='white', selectbackground=selectbg, inactiveselectbackground=selectbg) self.style.configure("sel.TCheckbutton", background=selectbg) self.style.map("sel.TCheckbutton", background=[("active", selectbg)]) self.is_locked = False for checkbox in self.txt.window_names(): ch = self.txt.children[checkbox.split(".")[-1]] ch.configure(state="normal") self.cadenas.configure(image="") self.menu.entryconfigure(3, label=_("Lock")) self.title_label.bind("<Double-Button-1>", self.edit_title) self.txt.bind('<Button-3>', self.show_menu_txt) else: self.txt.configure(state="disabled", selectforeground='black', inactiveselectbackground='#c3c3c3', selectbackground='#c3c3c3') self.style.configure("sel.TCheckbutton", background='#c3c3c3') self.style.map("sel.TCheckbutton", background=[("active", '#c3c3c3')]) self.cadenas.configure(image=self.im_lock) for checkbox in self.txt.window_names(): ch = self.txt.children[checkbox.split(".")[-1]] ch.configure(state="disabled") self.is_locked = True self.menu.entryconfigure(3, label=_("Unlock")) self.title_label.unbind("<Double-Button-1>") self.txt.unbind('<Button-3>') self.save_note() def save_info(self): """ Return the dictionnary containing all the note data """ data = {} data["txt"] = self.txt.get("1.0", "end")[:-1] data["tags"] = {} for tag in self.txt.tag_names(): if tag not in ["sel", "todolist", "list", "enum"]: data["tags"][tag] = [ index.string for index in self.txt.tag_ranges(tag) ] data["title"] = self.title_var.get() data["geometry"] = self.save_geometry data["category"] = self.category.get() data["color"] = self.color data["locked"] = self.is_locked data["mode"] = self.mode.get() data["inserted_objects"] = {} data["rolled"] = not self.txt.winfo_ismapped() data["position"] = self.position.get() data["links"] = {} for i, link in self.links.items(): if self.txt.tag_ranges("link#%i" % i): data["links"][i] = link data["latex"] = {} for img, latex in self.latex.items(): if self.txt.tag_ranges(img): data["latex"][img] = latex for image in self.txt.image_names(): data["inserted_objects"][self.txt.index(image)] = ( "image", image.split('#')[0]) for checkbox in self.txt.window_names(): ch = self.txt.children[checkbox.split(".")[-1]] data["inserted_objects"][self.txt.index(checkbox)] = ( "checkbox", "selected" in ch.state()) return data def change_color(self, key): self.color = COLORS[key] self.save_note() def change_category(self, category=None): if category: self.category.set(category) self.color = CONFIG.get("Categories", self.category.get()) self.save_note() def set_position_above(self): e = ewmh.EWMH() for w in e.getClientList(): if w.get_wm_name() == 'mynotes%s' % self.id: e.setWmState(w, 1, '_NET_WM_STATE_ABOVE') e.setWmState(w, 0, '_NET_WM_STATE_BELOW') e.display.flush() self.save_note() def set_position_below(self): e = ewmh.EWMH() for w in e.getClientList(): if w.get_wm_name() == 'mynotes%s' % self.id: e.setWmState(w, 0, '_NET_WM_STATE_ABOVE') e.setWmState(w, 1, '_NET_WM_STATE_BELOW') e.display.flush() self.save_note() def set_position_normal(self): e = ewmh.EWMH() for w in e.getClientList(): if w.get_wm_name() == 'mynotes%s' % self.id: e.setWmState(w, 0, '_NET_WM_STATE_BELOW') e.setWmState(w, 0, '_NET_WM_STATE_ABOVE') e.display.flush() self.save_note() def set_mode_note(self): self.txt.tag_remove("list", "1.0", "end") self.txt.tag_remove("todolist", "1.0", "end") self.txt.tag_remove("enum", "1.0", "end") self.save_note() def set_mode_list(self): end = int(self.txt.index("end").split(".")[0]) lines = self.txt.get("1.0", "end").splitlines() for i, l in zip(range(1, end), lines): # remove checkboxes try: ch = self.txt.window_cget("%i.0" % i, "window") self.txt.children[ch.split('.')[-1]].destroy() self.txt.delete("%i.0" % i) except TclError: # there is no checkbox # remove enumeration res = re.match('^\t[0-9]+\.\t', l) if res: self.txt.delete("%i.0" % i, "%i.%i" % (i, res.end())) if self.txt.get("%i.0" % i, "%i.3" % i) != "\t•\t": self.txt.insert("%i.0" % i, "\t•\t") self.txt.tag_add("list", "1.0", "end") self.txt.tag_remove("todolist", "1.0", "end") self.txt.tag_remove("enum", "1.0", "end") self.save_note() def set_mode_enum(self): self.txt.configure(autoseparators=False) self.txt.edit_separator() end = int(self.txt.index("end").split(".")[0]) lines = self.txt.get("1.0", "end").splitlines() for i, l in zip(range(1, end), lines): # remove checkboxes try: ch = self.txt.window_cget("%i.0" % i, "window") self.txt.children[ch.split('.')[-1]].destroy() self.txt.delete("%i.0" % i) except TclError: # there is no checkbox # remove bullets if self.txt.get("%i.0" % i, "%i.3" % i) == "\t•\t": self.txt.delete("%i.0" % i, "%i.3" % i) if not re.match('^\t[0-9]+\.', l): self.txt.insert("%i.0" % i, "\t0.\t") self.txt.tag_add("enum", "1.0", "end") self.txt.tag_remove("todolist", "1.0", "end") self.txt.tag_remove("list", "1.0", "end") self.update_enum() self.txt.configure(autoseparators=True) self.txt.edit_separator() self.save_note() def set_mode_todolist(self): end = int(self.txt.index("end").split(".")[0]) lines = self.txt.get("1.0", "end").splitlines() for i, l in zip(range(1, end), lines): res = re.match('^\t[0-9]+\.\t', l) if res: self.txt.delete("%i.0" % i, "%i.%i" % (i, res.end())) elif self.txt.get("%i.0" % i, "%i.3" % i) == "\t•\t": self.txt.delete("%i.0" % i, "%i.3" % i) try: ch = self.txt.window_cget("%i.0" % i, "window") except TclError: ch = Checkbutton(self.txt, takefocus=False, style=self.id + ".TCheckbutton") self.txt.window_create("%i.0" % i, window=ch) self.txt.tag_remove("enum", "1.0", "end") self.txt.tag_remove("list", "1.0", "end") self.txt.tag_add("todolist", "1.0", "end") self.save_note() # --- bindings def enter_roll(self, event): """ mouse is over the roll icon """ self.roll.configure(image="img_rollactive") def leave_roll(self, event): """ mouse leaves the roll icon """ self.roll.configure(image="img_roll") def enter_close(self, event): """ mouse is over the close icon """ self.close.configure(image="img_closeactive") def leave_close(self, event): """ mouse leaves the close icon """ self.close.configure(image="img_close") def change_focus(self, event): if not self.is_locked: event.widget.focus_force() def show_menu(self, event): self.menu.tk_popup(event.x_root, event.y_root) def show_menu_txt(self, event): self.menu_txt.tk_popup(event.x_root, event.y_root) def resize(self, event): self.save_geometry = self.geometry() def bouge(self, event): geo = self.geometry().split("+")[1:] self.save_geometry = self.save_geometry.split("+")[0] \ + "+%s+%s" % tuple(geo) def edit_title(self, event): self.title_entry.place(x=self.title_label.winfo_x() + 5, y=self.title_label.winfo_y(), anchor="nw", width=self.title_label.winfo_width() - 10) def start_move(self, event): self.x = event.x self.y = event.y self.configure(cursor='fleur') def stop_move(self, event): self.x = None self.y = None self.configure(cursor='') def move(self, event): if self.x is not None and self.y is not None: deltax = event.x - self.x deltay = event.y - self.y x = self.winfo_x() + deltax y = self.winfo_y() + deltay self.geometry("+%s+%s" % (x, y)) def save_note(self, event=None): data = self.save_info() data["visible"] = True self.master.note_data[self.id] = data self.master.save() def rollnote(self, event=None): if self.txt.winfo_ismapped(): self.txt.grid_forget() self.corner.place_forget() self.geometry("%sx22" % self.winfo_width()) else: self.txt.grid(row=1, columnspan=4, column=0, sticky="ewsn", pady=(1, 4), padx=4) self.corner.place(relx=1.0, rely=1.0, anchor="se") self.geometry(self.save_geometry) self.save_note() def hide(self, event=None): """ Hide note (can be displayed again via app menu) """ cat = self.category.get() self.master.add_note_to_menu(self.id, self.title_var.get().strip(), cat) data = self.save_info() data["visible"] = False self.master.note_data[self.id] = data del (self.master.notes[self.id]) self.master.save() self.destroy() # --- Settings update def update_title_font(self): font = "%s %s" % (CONFIG.get("Font", "title_family").replace( " ", "\ "), CONFIG.get("Font", "title_size")) style = CONFIG.get("Font", "title_style").split(",") if style: font += " " font += " ".join(style) self.title_label.configure(font=font) def update_text_font(self): font = "%s %s" % (CONFIG.get("Font", "text_family").replace( " ", "\ "), CONFIG.get("Font", "text_size")) self.txt.configure(font=font) self.txt.tag_configure("bold", font="%s bold" % font) self.txt.tag_configure("italic", font="%s italic" % font) self.txt.tag_configure("bold-italic", font="%s bold italic" % font) margin = 2 * Font(self, font=font).measure("m") self.txt.tag_configure("enum", lmargin1=0, lmargin2=margin + 5, tabs=(margin, 'right', margin + 5, 'left')) def update_menu_cat(self, categories): """ Update the category submenu """ self.menu_categories.delete(0, "end") for cat in categories: self.menu_categories.add_radiobutton(label=cat.capitalize(), value=cat, variable=self.category, command=self.change_category) def update_titlebar(self): if CONFIG.get("General", "buttons_position") == "right": # right = lock icon - title - roll - close self.columnconfigure(1, weight=1) self.columnconfigure(2, weight=0) self.roll.grid_configure(row=0, column=2, sticky="e") self.close.grid_configure(row=0, column=3, sticky="e", padx=(0, 2)) self.cadenas.grid_configure(row=0, column=0, sticky="w") self.title_label.grid_configure(row=0, column=1, sticky="ew", pady=(1, 0)) else: # left = close - roll - title - lock icon self.columnconfigure(2, weight=1) self.columnconfigure(1, weight=0) self.roll.grid_configure(row=0, column=1, sticky="w") self.close.grid_configure(row=0, column=0, sticky="w", padx=(2, 0)) self.cadenas.grid_configure(row=0, column=3, sticky="e") self.title_label.grid_configure(row=0, column=2, sticky="ew", pady=(1, 0)) # --- Text edition def add_link(self): def ok(eveny=None): lien = link.get() txt = text.get() if lien: if not txt: txt = lien self.nb_links += 1 if self.txt.tag_ranges("sel"): index = self.txt.index("sel.first") self.txt.delete('sel.first', 'sel.last') else: index = "current" tags = self.txt.tag_names(index) + ("link", "link#%i" % self.nb_links) self.txt.insert("current", txt, tags) if not lien[:4] == "http": lien = "http://" + lien self.links[self.nb_links] = lien self.txt.tag_bind("link#%i" % self.nb_links, "<Button-1>", lambda e: open_url(lien)) top.destroy() top = Toplevel(self) top.transient(self) top.update_idletasks() top.geometry("+%i+%i" % top.winfo_pointerxy()) top.grab_set() top.resizable(True, False) top.title(_("Link")) top.columnconfigure(1, weight=1) text = Entry(top) link = Entry(top) if self.txt.tag_ranges('sel'): txt = self.txt.get('sel.first', 'sel.last') else: txt = '' text.insert(0, txt) text.icursor("end") Label(top, text=_("Text")).grid(row=0, column=0, sticky="e", padx=4, pady=4) Label(top, text=_("Link")).grid(row=1, column=0, sticky="e", padx=4, pady=4) text.grid(row=0, column=1, sticky="ew", padx=4, pady=4) link.grid(row=1, column=1, sticky="ew", padx=4, pady=4) Button(top, text="Ok", command=ok).grid(row=2, columnspan=2, padx=4, pady=4) text.focus_set() text.bind("<Return>", ok) link.bind("<Return>", ok) def add_checkbox(self): ch = Checkbutton(self.txt, takefocus=False, style=self.id + ".TCheckbutton") self.txt.window_create("current", window=ch) def add_date(self): self.txt.insert("current", strftime("%x")) def add_latex(self, img_name=None): def ok(event): latex = r'%s' % text.get() if latex: if img_name is None: l = [ int(os.path.splitext(f)[0]) for f in os.listdir(PATH_LATEX) ] l.sort() if l: i = l[-1] + 1 else: i = 0 img = "%i.png" % i self.txt.tag_bind(img, '<Double-Button-1>', lambda e: self.add_latex(img)) self.latex[img] = latex else: img = img_name im = os.path.join(PATH_LATEX, img) try: math_to_image(latex, im, fontsize=CONFIG.getint("Font", "text_size") - 2) self.images.append(PhotoImage(file=im, master=self)) if self.txt.tag_ranges("sel"): index = self.txt.index("sel.first") self.txt.delete('sel.first', 'sel.last') else: index = self.txt.index("current") self.txt.image_create(index, image=self.images[-1], name=im) self.txt.tag_add(img, index) top.destroy() except Exception as e: showerror(_("Error"), str(e)) top = Toplevel(self) top.transient(self) top.update_idletasks() top.geometry("+%i+%i" % top.winfo_pointerxy()) top.grab_set() top.resizable(True, False) top.title("LaTex") text = Entry(top, justify='center') if img_name is not None: text.insert(0, self.latex[img_name]) else: if self.txt.tag_ranges('sel'): text.insert(0, self.txt.get('sel.first', 'sel.last')) else: text.insert(0, '$$') text.icursor(1) text.pack(fill='x', expand=True) text.bind('<Return>', ok) text.focus_set() def add_image(self): fichier = askopenfilename(defaultextension=".png", filetypes=[("PNG", "*.png")], initialdir="", initialfile="", title=_('Select PNG image')) if os.path.exists(fichier): self.images.append(PhotoImage(master=self.txt, file=fichier)) self.txt.image_create("current", image=self.images[-1], name=fichier) elif fichier: showerror("Erreur", "L'image %s n'existe pas" % fichier) def add_symbols(self): symbols = pick_symbol( self, CONFIG.get("Font", "text_family").replace(" ", "\ "), CONFIG.get("General", "symbols")) self.txt.insert("current", symbols) def toggle_text_style(self, style): '''Toggle the style of the selected text''' if self.txt.tag_ranges("sel"): current_tags = self.txt.tag_names("sel.first") if style in current_tags: # first char is in style so 'unstyle' the range self.txt.tag_remove(style, "sel.first", "sel.last") elif style == "bold" and "bold-italic" in current_tags: self.txt.tag_remove("bold-italic", "sel.first", "sel.last") self.txt.tag_add("italic", "sel.first", "sel.last") elif style == "italic" and "bold-italic" in current_tags: self.txt.tag_remove("bold-italic", "sel.first", "sel.last") self.txt.tag_add("bold", "sel.first", "sel.last") elif style == "bold" and "italic" in current_tags: self.txt.tag_remove("italic", "sel.first", "sel.last") self.txt.tag_add("bold-italic", "sel.first", "sel.last") elif style == "italic" and "bold" in current_tags: self.txt.tag_remove("bold", "sel.first", "sel.last") self.txt.tag_add("bold-italic", "sel.first", "sel.last") else: # first char is normal, so apply style to the whole selection self.txt.tag_add(style, "sel.first", "sel.last") def toggle_underline(self): if self.txt.tag_ranges("sel"): current_tags = self.txt.tag_names("sel.first") if "underline" in current_tags: # first char is in style so 'unstyle' the range self.txt.tag_remove("underline", "sel.first", "sel.last") for coul in TEXT_COLORS.values(): self.txt.tag_remove(coul + "-underline", "sel.first", "sel.last") else: self.txt.tag_add("underline", "sel.first", "sel.last") for coul in TEXT_COLORS.values(): r = text_ranges(self.txt, coul, "sel.first", "sel.last") if r: for deb, fin in zip(r[::2], r[1::2]): self.txt.tag_add(coul + "-underline", "sel.first", "sel.last") def toggle_overstrike(self): if self.txt.tag_ranges("sel"): current_tags = self.txt.tag_names("sel.first") if "overstrike" in current_tags: # first char is in style so 'unstyle' the range self.txt.tag_remove("overstrike", "sel.first", "sel.last") for coul in TEXT_COLORS.values(): self.txt.tag_remove(coul + "-overstrike", "sel.first", "sel.last") else: self.txt.tag_add("overstrike", "sel.first", "sel.last") for coul in TEXT_COLORS.values(): r = text_ranges(self.txt, coul, "sel.first", "sel.last") if r: for deb, fin in zip(r[::2], r[1::2]): self.txt.tag_add(coul + "-overstrike", "sel.first", "sel.last") def change_sel_color(self, color): """ change the color of the selection """ if self.txt.tag_ranges("sel"): for coul in TEXT_COLORS.values(): self.txt.tag_remove(coul, "sel.first", "sel.last") self.txt.tag_remove(coul + "-overstrike", "sel.first", "sel.last") self.txt.tag_remove(coul + "-underline", "sel.first", "sel.last") if not color == "black": self.txt.tag_add(color, "sel.first", "sel.last") underline = text_ranges(self.txt, "underline", "sel.first", "sel.last") overstrike = text_ranges(self.txt, "overstrike", "sel.first", "sel.last") for deb, fin in zip(underline[::2], underline[1::2]): self.txt.tag_add(color + "-underline", deb, fin) for deb, fin in zip(overstrike[::2], overstrike[1::2]): self.txt.tag_add(color + "-overstrike", deb, fin) def set_align(self, alignment): """ Align the text according to alignment (left, right, center) """ if self.txt.tag_ranges("sel"): line = self.txt.index("sel.first").split(".")[0] line2 = self.txt.index("sel.last").split(".")[0] deb, fin = line + ".0", line2 + ".end" if not "\t" in self.txt.get(deb, fin): # tabulations don't support right/center alignment # remove old alignment tag self.txt.tag_remove("left", deb, fin) self.txt.tag_remove("right", deb, fin) self.txt.tag_remove("center", deb, fin) # set new alignment tag self.txt.tag_add(alignment, deb, fin) def update_enum(self): """ update enumeration numbers """ lines = self.txt.get("1.0", "end").splitlines() indexes = [] for i, l in enumerate(lines): res = re.match('^\t[0-9]+\.\t', l) res2 = re.match('^\t[0-9]+\.', l) if res: indexes.append((i, res.end())) elif res2: indexes.append((i, res2.end())) for j, (i, end) in enumerate(indexes): self.txt.delete("%i.0" % (i + 1), "%i.%i" % (i + 1, end)) self.txt.insert("%i.0" % (i + 1), "\t%i.\t" % (j + 1)) self.txt.tag_add("enum", "1.0", "end")
class Program(Tk): """ Class for application that takes a network in extended newick format and displays trees. This program can save the network and trees as images or text file. """ def __init__(self): super().__init__() ORIGINAL_DPI = 96.0 #Scale the window depending on current monitor's dpi self.current_dpi = self._get_dpi() self.scale = self.current_dpi / ORIGINAL_DPI self.tk.call("tk", "scaling", self.scale + 0.5) self.net_frame = None self.net_directory = "" self.trees_directory = "" self.save_directory = "" self.net_fig = None self.graph_window = None self.title("PhyloProgram") self.scaled_width = self._scale_window(750) self.scaled_height = self._scale_window(600) self.geometry(f"{self.scaled_width}x{self.scaled_height}") self.protocol("WM_DELETE_WINDOW", self._exit) #prompt windows self.input_prompt = None self.select_leaves_prompt = None self._initialise_menu_bar() self._initialise_tool_bar() self._initialise_info_bar() self.main_frame = Frame(self) self.main_frame.pack(side="top", fill="both", expand=1) #initialise network figure canvas in main window self.net_fig = plt.figure("Input network") self.net_fig.gca().clear() self.net_canvas = FigureCanvasTkAgg(self.net_fig, master=self.main_frame) self._initialise_main_text_widget() try: # PyInstaller creates a temp folder and stores path in _MEIPASS base_path = sys._MEIPASS print(f"PyInstaller temp folder: {base_path}") print( f"The PyInstaller temp folder can be deleted after session is closed\n" ) except: pass def _get_dpi(self): """ For private use. Get the dpi of the current screen Returns ------- int DPI of current monitor """ screen = Tk() current_dpi = screen.winfo_fpixels("1i") screen.destroy() return current_dpi def _initialise_menu_bar(self): """For private use. Initialise the top menu bar and bind shortcuts""" menu_bar = Menu(self) file_menu = Menu(menu_bar, tearoff=0) file_menu.add_command(label="Enter network", command=self.new_network, accelerator="Ctrl+N") file_menu.add_command(label="Open network...", command=self.open_network, accelerator="Ctrl+O") file_menu.add_separator() #drSPR sub menu rspr_graph_menu = Menu(file_menu, tearoff=0) file_menu.add_cascade(label="Create rSPR graph", menu=rspr_graph_menu) rspr_graph_menu.add_command( label="Enter trees", command=lambda: self.new_trees("Create rSPR graph"), accelerator="Ctrl+G") rspr_graph_menu.add_command( label="Open trees...", command=lambda: self.open_trees("Create rSPR graph"), accelerator="Ctrl+Shift+G") drspr_menu = Menu(file_menu, tearoff=0) file_menu.add_cascade(label="Calculate drSPR", menu=drspr_menu) drspr_menu.add_command( label="Enter trees", command=lambda: self.new_trees("Calculate drSPR"), accelerator="Ctrl+D") drspr_menu.add_command( label="Open trees...", command=lambda: self.open_trees("Calculate drSPR"), accelerator="Ctrl+Shift+D") file_menu.add_separator() self.save_sub_menu = Menu(file_menu, tearoff=0) self.save_sub_menu.add_command(label="Text file (trees only)", command=self.save_trees_only_text, accelerator="Ctrl+T") self.save_sub_menu.add_command(label="Text file", command=self.save_text, accelerator="Ctrl+Shift+T") self.save_sub_menu.add_command(label="Images", command=self.save_image, accelerator="Ctrl+Shift+I") file_menu.add_cascade(label="Save as...", menu=self.save_sub_menu) self.save_sub_menu.entryconfigure("Text file (trees only)", state="disabled") self.save_sub_menu.entryconfigure("Text file", state="disabled") self.save_sub_menu.entryconfigure("Images", state="disabled") self.text_save_enabled = False self.image_save_enabled = False file_menu.add_separator() file_menu.add_command(label="Exit", command=self._exit) menu_bar.add_cascade(label="File", menu=file_menu) help_menu = Menu(menu_bar, tearoff=0) help_menu.add_command(label="About", command=self.about) help_menu.add_command(label="Manual", command=self.manual) help_menu.add_command(label="More info", command=self.open_github) menu_bar.add_cascade(label="Help", menu=help_menu) self.config(menu=menu_bar) self.bind_all("<Control-n>", self.new_network) self.bind_all("<Control-o>", self.open_network) self.bind_all("<Control-g>", lambda event: self.new_trees("Create rSPR graph")) self.bind_all("<Control-G>", lambda event: self.open_trees("Create rSPR graph")) self.bind_all("<Control-d>", lambda event: self.new_trees("Calculate drSPR")) self.bind_all("<Control-D>", lambda event: self.open_trees("Calculate drSPR")) self.bind_all("<Control-T>", self.save_text) self.bind_all("<Control-t>", self.save_trees_only_text) self.bind_all("<Control-I>", self.save_image) def _initialise_tool_bar(self): """For private use. Initialise the tool bar""" self.toolbar = Frame(self, bd=1, relief="raised") self.toolbar.pack(fill="x") self.select_leaves_button = HoverButton( self.toolbar, text="Select leaves", relief="raised", command=self._select_leaves, state="disabled", tooltip_text= "Enter set leaves to be displayed in trees. By default, all labelled leaves in network are selected" ) self.draw_button = HoverButton( self.toolbar, text="Draw trees/graph", relief="raised", command=self.generate_trees_graph, state="disabled", tooltip_text="Draw embedded/input trees or rSPR graph") self.graphics_enabled = IntVar() self.graphics = False graphics_check_box = Checkbutton(self.toolbar, text="Enable graphics", variable=self.graphics_enabled, command=self.set_graphics) graphics_check_box.pack(side="right", padx=(0, 10)) ToolTip( graphics_check_box, "Enable graph visualisation for the next entered network/trees") def _initialise_info_bar(self): """For private use. Info bar that displays file opened, number of reticulations and labelled leaves in network""" self.info_frame = Frame(self) self.info_frame.pack(side="bottom", fill="x") self.info_label = Label(self.info_frame, text="") self.info_label.pack(side="right", padx=(0, 10)) self.file_label = Label(self.info_frame, text="") self.file_label.pack(side="left", padx=(10, 0)) def _update_info_bar(self, filename=""): """ For private use. Update the network info when new network is opened. Parameters ---------- filename : str, optional Name of network/trees file opened """ if self.network: num_reticulations = self.network.num_reticulations num_labelled_leaves = self.network.num_labelled_leaves info_text = f"{num_reticulations} reticulations, {num_labelled_leaves} labelled leaves" self.info_label["text"] = info_text else: self.info_label["text"] = "" self.file_label["text"] = filename def _initialise_main_text_widget(self): """Setup the text widget in the main window""" self.main_text_widget = Text(self.main_frame, width=25) scroll = Scrollbar(self.main_text_widget, command=self.main_text_widget.yview) self.main_text_widget['yscrollcommand'] = scroll.set scroll.pack(side="right", fill="y") #Bind to make text selectable on Mac self.main_text_widget.bind( "<1>", lambda event: self.main_text_widget.focus_set()) def _enable_tree_tools(self): """For private use. Buttons involving trees are enabled when a network has successfully been processed and displayed.""" self.select_leaves_button.config(state="normal") self.draw_button.config(state="normal") def _enable_tree_display(self): """For private use. Draw button enabled when graphics enabled""" self.select_leaves_button.config(state="disabled") self.draw_button.config(state="normal") def _disable_tree_tools(self): """For private use. Disable tree buttons in toolbar""" self.select_leaves_button.config(state="disabled") self.draw_button.config(state="disabled") def _enable_select_leaves(self): """For private use. Enable select leaves button and disable draw button in toolbar""" self.select_leaves_button.config(state="normal") self.draw_button.config(state="disabled") def _enable_save(self): """For private use. Save functions are enabled when a network and trees has successfully been processed and displayed.""" self.text_save_enabled = True self.image_save_enabled = True self.save_sub_menu.entryconfigure("Text file", state="normal") self.save_sub_menu.entryconfigure("Images", state="normal") if self.network: self.save_sub_menu.entryconfigure("Text file (trees only)", state="normal") else: self.save_sub_menu.entryconfigure("Text file (trees only)", state="disabled") def _enable_text_save(self): """For private use. Used when visualisation is disabled. Only save as text enabled""" self.text_save_enabled = True self.image_save_enabled = False self.save_sub_menu.entryconfigure("Text file", state="normal") self.save_sub_menu.entryconfigure("Images", state="disabled") if self.network: self.save_sub_menu.entryconfigure("Text file (trees only)", state="normal") else: self.save_sub_menu.entryconfigure("Text file (trees only)", state="disabled") def _disable_save(self): self.text_save_enabled = False self.image_save_enabled = False self.save_sub_menu.entryconfigure("Text file", state="disabled") self.save_sub_menu.entryconfigure("Images", state="disabled") self.save_sub_menu.entryconfigure("Text file (trees only)", state="disabled") def _scale_window(self, window_length): """ For private use. Scales the window based on calculated scale Parameters ---------- window_length : int Window length dimension Returns ------- int Scaled window length dimension """ return round(window_length * self.scale) def _select_leaves(self): """For private use. Opens multiple choice dialog prompt to select the leaves that will be displayed in the generated trees.""" if self.select_leaves_prompt: self.select_leaves_prompt.update() self.select_leaves_prompt.deiconify() else: self.select_leaves_prompt = MultiChoicePrompt( self, "Select leaves", "Specify the leaves that will be included in the generated trees.", ["All"], customise_option=True, text_placeholder="e.g. 1, 2, 3, 4") def set_graphics(self, *_): """Set if the program draws the network and trees.""" if self.graphics_enabled.get() == 1: self.graphics = True else: self.graphics = False def about(self): """Display overview of program in window""" self.about_window = Window(title="About") path_file = path.resource_path("about.txt") print(f"\nOpened file at {path_file}\n") f = open(path_file, "r") about_text = f.read() text_widget = Text(self.about_window) text_widget.insert("1.0", about_text) text_widget.pack(expand=True, fill="both") text_widget.config(state="disabled") def manual(self): """Display program manual in window""" self.manual_window = Window(title="Manual", width=self.scaled_width, height=self.scaled_height // 2) path_file = path.resource_path("manual.txt") print(f"\nOpened file at {path_file}\n") f = open(path_file, "r") manual_text = f.read() text_widget = Text(self.manual_window, width=30) text_widget.insert("1.0", manual_text) scroll = Scrollbar(text_widget, command=text_widget.yview) text_widget['yscrollcommand'] = scroll.set scroll.pack(side="right", fill="y") text_widget.pack(expand=True, fill="both") text_widget.config(state="disabled") def open_github(self): """Open this program's Github page.""" webbrowser.open("https://github.com/jaunzo/summer-research", new=2) def new_network(self, *_): """Displays dialog and gets network in extended newick format inputted by the user.""" self.operation = "Network" if self.input_prompt: self.input_prompt.change_contents( "Enter network", "Enter network in extended newick format", "e.g. ((a,(b)#H1), (#H1,c));") self.input_prompt.update() self.input_prompt.deiconify() else: self.input_prompt = StringInputPrompt( self, "Enter network", "Enter network in extended newick format", "e.g. ((a,(b)#H1), (#H1,c));", self.operation) def open_network(self, *_): """Displays open file prompt and processes a text file that contains the network in extended newick format.""" self.operation = "Network" filename = tkinter.filedialog.askopenfilename( initialdir=self.net_directory, title="Open network...", filetypes=(("text files", "*.txt"), ("all files", "*.*"))) path = os.path.split(filename) self.net_directory = path[0] text_file = path[1] if filename != "": f = open(filename, "r") text = f.read().strip() if text != None: network_newick = text[:text.find(";") + 1] try: self.generate_network(network_newick, text_file) except MalformedNewickException: error_message = "Could not read network.\n\nNetwork requirements:\nNetwork must contain at least one labelled leaf and string must terminate with semicolon." tkinter.messagebox.showerror(title="Open network error", message=error_message) def generate_network(self, net_newick, filename=""): """ Generate the network object and display it depending on graphics mode Parameters ---------- net_newick : str Network in extended newick format filename : str, optional Filename of network opened (default="") """ self.network = Network(net_newick, self.net_fig, self.graphics) self._update_info_bar(filename) self.net_newick = net_newick self._disable_save() if self.graphics: self._enable_tree_tools() self.main_text_widget.pack_forget() self.net_canvas.get_tk_widget().pack(side="top", fill="both", expand=1) self.display_network() else: self._enable_select_leaves() self.net_canvas.get_tk_widget().pack_forget() self.main_text_widget.pack(expand=True, fill="both") self.print_network() def print_network(self): """Print out the network in main window. Hide tree window""" if self.graph_window: self.graph_window.destroy() self.graph_window = None self.main_text_widget.config(state="normal") self.main_text_widget.delete('1.0', "end") self.main_text_widget.insert("1.0", self.network.text) self.main_text_widget.insert( "end", "\n\nSelect leaves to generate embedded trees.") self.main_text_widget.config(state="disabled") def display_network(self): """Display input network in the main window.""" if self.graph_window: self.graph_window.withdraw() self.net_fig.gca().clear() try: self.network.draw() self.net_canvas.draw() except (ValueError, ImportError) as e: if self.net_fig: self.net_fig.clear() self.net_canvas.get_tk_widget().pack_forget() self.main_text_widget.pack(expand=True, fill="both") error_message = f"Error: {e}\n\nTo draw networks and trees, Graphviz must be installed and it's executables must be in the system's PATH." tkinter.messagebox.showerror(title="Open network error", message=error_message) self.graphics_enabled.set(0) self.graphics = False self._disable_tree_tools() self.main_text_widget.config(state="normal") self.main_text_widget.delete('1.0', "end") self.main_text_widget.insert( "1.0", "Error drawing network.\n\nGraphviz must be installed and it's executables must be in the system's PATH.\n" ) self.main_text_widget.insert( "end", "Check Github page for more information. \nGithub page can be accessed in Help -> More info.\n\n" ) self.main_text_widget.insert( "end", "Please enter network again with graphics disabled or install Graphviz if you \nwould like to proceed with graph visualisation." ) self.main_text_widget.config(state="disabled") def generate_trees_graph(self): """Generate the tree/graph object and display them depending on graphics mode.""" #Get Trees object if self.network: self.graph_trees = self.network.process() if not self.graphics: self.print_trees() if self.graphics: try: self.graph_trees.draw() self.display_trees_graph() except (ValueError, ImportError) as e: error_message = f"Error: {e}\n\nTo draw networks and trees, Graphviz must be installed and it's executables must be in the system's PATH." tkinter.messagebox.showerror(title="Draw error", message=error_message) print( " Draw error: Graphviz must be installed to be able to draw graphs\n" ) self.graphics_enabled.set(0) self.graphics = False self._disable_tree_tools() def print_trees(self): """Displays trees generated as text in the main window""" self.main_text_widget.config(state="normal") self.main_text_widget.delete('1.0', "end") self.main_text_widget.insert("1.0", self.network.text) self.main_text_widget.insert("end", self.graph_trees.text) self.main_text_widget.config(state="disabled") self._enable_text_save() def display_trees_graph(self, **kwargs): """ Displays trees in a window when user clicks "Draw graph/trees" or selects leaves. Only one trees window is displayed at a time """ #self.graph_trees.draw() if self.graph_window: self.graph_window.deiconify() self.graph_window.replace_graph(self.graph_trees, self.operation) else: #Create window if self.operation == "Network": title = "Embedded trees" elif self.operation == "Create rSPR graph": title = "rSPR Graph" else: title = "Trees" self.graph_window = GraphWindow(self, self.graph_trees, width=self.scaled_width, height=self.scaled_height, title=title, operation=self.operation, **kwargs) self._enable_save() def new_trees(self, operation): """ Displays dialog and gets at least 2 trees in newick format inputted by the user Parameters ---------- operation : str Specifies whether program is running rspr graph or drspr """ self.operation = operation if self.input_prompt: self.input_prompt.change_contents( f"{self.operation}: Enter trees", "Enter at least 2 trees in newick format", "e.g.\n(((1,2),3),4);\n(((1,4),2),3);") self.input_prompt.operation = operation self.input_prompt.update() self.input_prompt.deiconify() else: self.input_prompt = StringInputPrompt( self, f"{operation}: Enter trees", "Enter at least 2 trees in newick format", "e.g.\n(((1,2),3),4);\n(((1,4),2),3);", self.operation) def open_trees(self, operation): """ Opens file that contains at least 2 trees in newick format Parameters ---------- operation : str Specifies whether program is running rspr graph or drspr """ self.operation = operation filename = tkinter.filedialog.askopenfilename( initialdir=self.trees_directory, title=f"{self.operation}: Open trees...", filetypes=(("text files", "*.txt"), ("all files", "*.*"))) path = os.path.split(filename) self.trees_directory = path[0] text_file = path[1] if filename != "": f = open(filename, "r") text = f.read().strip() if text != None and self.operation == "Calculate drSPR": try: self.get_drspr(text, text_file) except MalformedNewickException: error_message = "Could not read trees.\n\nPlease enter at least 2 trees.\nTrees must contain at least one labelled leaf and trees must terminate with semicolon." tkinter.messagebox.showerror(title="Open trees error", message=error_message) elif text != None and self.operation == "Create rSPR graph": self.get_rspr_graph(text, text_file) def get_rspr_graph(self, input_trees_string, filename=""): """ Get rspr graph Parameters ---------- input_trees_string : str String containing at least 2 trees in newick format delimited by semicolon filename : str, optional Filename of trees text file opened (default="") """ if self.graph_window: self.graph_window.withdraw() self.network = None self.graph_trees = RsprGraph(input_trees_string) self.net_canvas.get_tk_widget().pack_forget() self.main_text_widget.pack(expand=True, fill="both") self._update_info_bar(filename) self.print_rspr_graph() self._enable_text_save() if self.graphics: self._enable_tree_display() else: self._disable_tree_tools() def print_rspr_graph(self): """Print all trees and adjacency list""" self.main_text_widget.config(state="normal") self.main_text_widget.delete('1.0', "end") self.main_text_widget.insert("end", self.graph_trees.text) self.main_text_widget.config(state="disabled") def get_drspr(self, input_trees, filename=""): """ Get the rspr distance Parameters ---------- input_trees : str String containing at least 2 trees in newick format delimited by semicolon filename : str, optional Filename of trees text file opened (default="") """ if self.graph_window: self.graph_window.withdraw() self.network = None input_trees = input_trees.translate(str.maketrans('', '', ' \n\t\r')) trees_array = input_trees.split(";") if not trees_array[-1]: trees_array.pop() (distances, clusters, self.graph_trees) = d.calculate_drspr(trees_array) self.net_canvas.get_tk_widget().pack_forget() self.main_text_widget.pack(expand=True, fill="both") self._update_info_bar(filename) self.print_drspr(self.graph_trees.trees, distances, clusters) self._enable_text_save() if self.graphics: self._enable_tree_display() else: self._disable_tree_tools() def print_drspr(self, trees_array, distances, clusters): """ Output rspr distance information in the main window Parameters ---------- trees_array : list[str, PhylogeneticNetwork] Array of str or PhylogeneticNetwork objects distances : list[str] Array of distances clusters : list[str] Array of clusters """ self.main_text_widget.config(state="normal") self.main_text_widget.delete('1.0', "end") self.main_text_widget.insert("end", "TREES:\n") text = "" for tree in trees_array: if type(tree) != str: text += f"{tree.text}\n" else: text += f"{tree}\n" self.main_text_widget.insert("end", text) length = len(distances) if length == 1: self.main_text_widget.insert("end", f"\ndrSPR = {distances[0]}\n") self.main_text_widget.insert("end", f"Clusters: {clusters[0]}\n") else: #Printing matrix self.main_text_widget.insert("end", "\nDISTANCE MATRIX:\n") for i in range(length): self.main_text_widget.insert("end", f"{', '.join(distances[i])}\n") #Printing cluster self.main_text_widget.insert("end", "\n\nCLUSTERS:") for i in range(length - 1): self.main_text_widget.insert( "end", f"\nClusters compared with t{i+1}:\n") for j in range(i + 1, len(clusters[i])): self.main_text_widget.insert( "end", f"t{j+1} (drSPR = {distances[i][j]}): {' '.join(clusters[i][j])}\n" ) self.main_text_widget.config(state="disabled") def save_trees_only_text(self, *_): """Saves just the tree newick strings in text file""" if self.text_save_enabled and self.network: file_contents = "" for tree in self.graph_trees.data.keys(): file_contents += f"{tree}\n" title = "Saving trees as text file" f = tkinter.filedialog.asksaveasfile( initialdir=self.save_directory, title=title, filetypes=[("Text file", "*.txt")], defaultextension=[("Text file", "*.txt")]) if f: #if dialog not closed with "cancel". print("\nSaving trees only in text file...") path = os.path.split(f.name) self.save_directory = path[0] f.write(file_contents) f.close() print(f" Text file saved at {f.name}\n") def save_text(self, *_): """Saves network and trees with any other information in newick format as a text file in the directory that the user specifies.""" if self.text_save_enabled: if self.network: file_contents = self.network.text file_contents += self.graph_trees.text title = "Saving network, trees and other info as text file" else: file_contents = self.main_text_widget.get("1.0", "end") title = "Saving trees and other info as text file" f = tkinter.filedialog.asksaveasfile( initialdir=self.save_directory, title=title, filetypes=[("Text file", "*.txt")], defaultextension=[("Text file", "*.txt")]) if f: #if dialog not closed with "cancel". print("\nSaving as text file...") path = os.path.split(f.name) self.save_directory = path[0] f.write(file_contents) f.close() print(f" Text file saved. at {f.name}\n") def save_image(self, *_): """Saves all figures as a series of images in the directory that the user specifies.""" if self.image_save_enabled: if self.operation == "Network": title = "Select folder to save network and tree images" elif self.operation == "Create rSPR graph": title = "Select folder to save graph" else: title = "Select folder to save tree images" directory = tkinter.filedialog.askdirectory( initialdir=self.save_directory, title=title) abs_path = os.path.dirname(__file__) export_path = abs_path + directory #Export network if directory: #if dialog not closed with "cancel". image_dpi = self.current_dpi * 2 print("\nSaving image(s)...") self.save_directory = export_path if self.network: self.net_fig.savefig(export_path + "/network.png", dpi=image_dpi, format='png', bbox_inches='tight') if self.operation == "Create rSPR graph": self.graph_trees.figures[0].savefig( f"{abs_path}{directory}/rspr_graph.png", dpi=image_dpi, format='png', bbox_inches='tight') else: #Export trees num_figures = len(self.graph_trees.figures) #count = 1 for i, tree_fig in enumerate(self.graph_trees.figures, start=1): tree_fig.savefig( f"{abs_path}{directory}/trees{str(i)}.png", dpi=image_dpi, format='png', bbox_inches='tight') #count += 1 print( f'\r {round(i / num_figures * 100)}% complete: Saved {i} / {num_figures} trees', end="\r", flush=True) print(f" 100% complete: Image(s) saved at {export_path}.\n") def _exit(self): """Display prompt dialog when user exits application.""" MsgBox = tk.messagebox.askquestion( "Exit Application", "Are you sure you want to exit the application?", icon="warning") if MsgBox == "yes": os._exit(0)
class MyMenu(): def __init__(self, tkObj, newcmd=None, opencmd=None, savecmd=None, exportcmd=None, logincmd=None): #Popupmenü Cut, Copy, Paste - gebunden an den rechten Mausbutton (press and release) tkObj.bind('<Button-3><ButtonRelease-3>', self._show_popup) self._make_popup_menu(tkObj) self.menubar = Menu(tkObj) # create a pulldown menu, and add it to the menu bar self.filemenu = Menu(self.menubar, tearoff=0) self.filemenu.add_command(label='Neu', command=newcmd) self.filemenu.add_command(label='Öffnen', command=opencmd) self.filemenu.add_command(label='Speichern', command=savecmd) self.filemenu.add_separator() self.filemenu.add_command(label='PDF-Export', command=exportcmd, background='dark red', foreground='white') self.filemenu.add_separator() self.filemenu.add_command(label='Exit', background='black', foreground='white', command=tkObj.destroy) self.menubar.add_cascade(label='Datei', menu=self.filemenu) self.editmenu = Menu(self.menubar, tearoff=0) #lambda definiert anonyme Funktionen #tkObj.focus_get() holt das Objekt, das den aktuellen Fokus hat. #In diesem Fokus wird der entsprechende event generiert. self.editmenu.add_command( label='Ausschneiden', accelerator="Ctrl+X", command=lambda: tkObj.focus_get().event_generate('<<Cut>>')) self.editmenu.add_command( label='Kopieren', accelerator="Ctrl+C", command=lambda: tkObj.focus_get().event_generate('<<Copy>>')) self.editmenu.add_command( label='Einfügen', accelerator="Ctrl+V", command=lambda: tkObj.focus_get().event_generate('<<Paste>>')) self.menubar.add_cascade(label='Bearbeiten', menu=self.editmenu) #self.loginmenu = Menu(self.menubar, tearoff=0) #self.loginmenu.add_command(label='Login Datenbank', command=logincmd) #self.menubar.add_cascade(label='Login', menu=self.loginmenu) self.helpmenu = Menu(self.menubar, tearoff=0) self.helpmenu.add_command(label='Readme', command=self._readme) self.helpmenu.add_separator() self.helpmenu.add_command(label='Über SOPGUI', command=self._about) self.menubar.add_cascade(label='Hilfe', menu=self.helpmenu) tkObj.config(menu=self.menubar) #------------------------------------ def _make_popup_menu(self, tkobj): #global popup_menu self.popup_menu = Menu(tkobj, tearoff=0) self.popup_menu.add_command(label='Cut') self.popup_menu.add_command(label='Copy') self.popup_menu.add_command(label='Paste') def _show_popup( self, e ): #e ist der event, der zurückgerufen wird, wenn der callback aufgerufen wird. w = e.widget #Abfrage des Objekts self.popup_menu.entryconfigure( 'Cut', command=lambda: w.event_generate('<<Cut>>')) self.popup_menu.entryconfigure( 'Copy', command=lambda: w.event_generate('<<Copy>>')) self.popup_menu.entryconfigure( 'Paste', command=lambda: w.event_generate('<<Paste>>')) self.popup_menu.tk.call( 'tk_popup', self.popup_menu, e.x_root, e.y_root) #ruft an der Mauszeigerposition das popup_menu auf #------------------------------------ def _readme(self): rm = messagebox.askokcancel( 'Bekanntmachung! ', 'Möchten Sie das Readme lesen?\nDie Datei öffnet sich im Webbrowser.' ) if rm == True: htmlfile = 'README.html' if os.name == 'nt': #windows os.startfile(htmlfile) elif os.name == 'posix': #unix/linux os.system('/usr/bin/xdg-open ' + htmlfile) else: pass #------------------------------------ def _about(self): about = 'Entwickelt unter:\n\nLinux (64-bit)\nPython (Version 3.6.7)\nThonny (Version 3.1.2)\nTk (8.6.8)' messagebox.showinfo('Über SOPGUI ', message=about)
class Sync(Tk): """FolderSync main window.""" def __init__(self): Tk.__init__(self, className='FolderSync') self.title("FolderSync") self.geometry("%ix%i" % (self.winfo_screenwidth(), self.winfo_screenheight())) self.protocol("WM_DELETE_WINDOW", self.quitter) self.icon = PhotoImage(master=self, file=IM_ICON) self.iconphoto(True, self.icon) self.rowconfigure(2, weight=1) self.columnconfigure(0, weight=1) # --- icons self.img_about = PhotoImage(master=self, file=IM_ABOUT) self.img_open = PhotoImage(master=self, file=IM_OPEN) self.img_plus = PhotoImage(master=self, file=IM_PLUS) self.img_moins = PhotoImage(master=self, file=IM_MOINS) self.img_sync = PhotoImage(master=self, file=IM_SYNC) self.img_prev = PhotoImage(master=self, file=IM_PREV) self.img_expand = PhotoImage(master=self, file=IM_EXPAND) self.img_collapse = PhotoImage(master=self, file=IM_COLLAPSE) self.original = "" self.sauvegarde = "" # list of files / folders to delete before starting the copy because # they are not of the same type on the original and the backup self.pb_chemins = [] self.err_copie = False self.err_supp = False # --- init log files l = [f for f in listdir(PATH) if match(r"foldersync[0-9]+.pid", f)] nbs = [] for f in l: with open(join(PATH, f)) as fich: old_pid = fich.read().strip() if exists("/proc/%s" % old_pid): nbs.append(int(search(r"[0-9]+", f).group())) else: remove(join(PATH, f)) if not nbs: i = 0 else: nbs.sort() i = 0 while i in nbs: i += 1 self.pidfile = PID_FILE % i open(self.pidfile, 'w').write(str(getpid())) self.log_copie = LOG_COPIE % i self.log_supp = LOG_SUPP % i self.logger_copie = setup_logger("copie", self.log_copie) self.logger_supp = setup_logger("supp", self.log_supp) date = datetime.now().strftime('%d/%m/%Y %H:%M') self.logger_copie.info("\n### %s ###\n" % date) self.logger_supp.info("\n### %s ###\n" % date) # --- filenames and extensions that will not be copied exclude_list = split(r'(?<!\\) ', CONFIG.get("Defaults", "exclude_copie")) self.exclude_names = [] self.exclude_ext = [] for elt in exclude_list: if elt: if elt[:2] == "*.": self.exclude_ext.append(elt[1:]) else: self.exclude_names.append(elt.replace("\ ", " ")) # --- paths that will not be deleted self.exclude_path_supp = [ ch.replace("\ ", " ") for ch in split( r'(?<!\\) ', CONFIG.get("Defaults", "exclude_supp")) if ch ] # while "" in self.exclude_path_supp: # self.exclude_path_supp.remove("") self.q_copie = Queue() self.q_supp = Queue() # True if a copy / deletion is running self.is_running_copie = False self.is_running_supp = False self.style = Style(self) self.style.theme_use("clam") self.style.configure("TProgressbar", troughcolor='lightgray', background='#387EF5', lightcolor="#5D95F5", darkcolor="#2758AB") self.style.map("TProgressbar", lightcolor=[("disabled", "white")], darkcolor=[("disabled", "gray")]) self.style.configure("folder.TButton", padding=0) # --- menu self.menu = Menu(self, tearoff=False) self.configure(menu=self.menu) # -------- recents self.menu_recent = Menu(self.menu, tearoff=False) if RECENT: for ch_o, ch_s in RECENT: self.menu_recent.add_command( label="%s -> %s" % (ch_o, ch_s), command=lambda o=ch_o, s=ch_s: self.open(o, s)) else: self.menu.entryconfigure(0, state="disabled") # -------- favorites self.menu_fav = Menu(self.menu, tearoff=False) self.menu_fav_del = Menu(self.menu_fav, tearoff=False) self.menu_fav.add_command(label=_("Add"), image=self.img_plus, compound="left", command=self.add_fav) self.menu_fav.add_cascade(label=_("Remove"), image=self.img_moins, compound="left", menu=self.menu_fav_del) for ch_o, ch_s in FAVORIS: label = "%s -> %s" % (ch_o, ch_s) self.menu_fav.add_command( label=label, command=lambda o=ch_o, s=ch_s: self.open(o, s)) self.menu_fav_del.add_command( label=label, command=lambda nom=label: self.del_fav(nom)) if not FAVORIS: self.menu_fav.entryconfigure(1, state="disabled") # -------- log files menu_log = Menu(self.menu, tearoff=False) menu_log.add_command(label=_("Copy"), command=self.open_log_copie) menu_log.add_command(label=_("Removal"), command=self.open_log_suppression) # -------- settings menu_params = Menu(self.menu, tearoff=False) self.copy_links = BooleanVar(self, value=CONFIG.getboolean( "Defaults", "copy_links")) self.show_size = BooleanVar(self, value=CONFIG.getboolean( "Defaults", "show_size")) menu_params.add_checkbutton(label=_("Copy links"), variable=self.copy_links, command=self.toggle_copy_links) menu_params.add_checkbutton(label=_("Show total size"), variable=self.show_size, command=self.toggle_show_size) self.langue = StringVar(self, CONFIG.get("Defaults", "language")) menu_lang = Menu(menu_params, tearoff=False) menu_lang.add_radiobutton(label="English", value="en", variable=self.langue, command=self.change_language) menu_lang.add_radiobutton(label="Français", value="fr", variable=self.langue, command=self.change_language) menu_params.add_cascade(label=_("Language"), menu=menu_lang) menu_params.add_command(label=_("Exclude from copy"), command=self.exclusion_copie) menu_params.add_command(label=_("Exclude from removal"), command=self.exclusion_supp) self.menu.add_cascade(label=_("Recents"), menu=self.menu_recent) self.menu.add_cascade(label=_("Favorites"), menu=self.menu_fav) self.menu.add_cascade(label=_("Log"), menu=menu_log) self.menu.add_cascade(label=_("Settings"), menu=menu_params) self.menu.add_command(image=self.img_prev, compound="center", command=self.list_files_to_sync) self.menu.add_command(image=self.img_sync, compound="center", state="disabled", command=self.synchronise) self.menu.add_command(image=self.img_about, compound="center", command=lambda: About(self)) # --- tooltips wrapper = TooltipMenuWrapper(self.menu) wrapper.add_tooltip(4, _('Preview')) wrapper.add_tooltip(5, _('Sync')) wrapper.add_tooltip(6, _('About')) # --- path selection frame_paths = Frame(self) frame_paths.grid(row=0, sticky="ew", pady=(10, 0)) frame_paths.columnconfigure(0, weight=1) frame_paths.columnconfigure(1, weight=1) f1 = Frame(frame_paths, height=26) f2 = Frame(frame_paths, height=26) f1.grid(row=0, column=0, sticky="ew") f2.grid(row=0, column=1, sticky="ew") f1.grid_propagate(False) f2.grid_propagate(False) f1.columnconfigure(1, weight=1) f2.columnconfigure(1, weight=1) # -------- path to original Label(f1, text=_("Original")).grid(row=0, column=0, padx=(10, 4)) f11 = Frame(f1) f11.grid(row=0, column=1, sticky="nsew", padx=(4, 0)) self.entry_orig = Entry(f11) self.entry_orig.place(x=1, y=0, bordermode='outside', relwidth=1) self.b_open_orig = Button(f1, image=self.img_open, style="folder.TButton", command=self.open_orig) self.b_open_orig.grid(row=0, column=2, padx=(0, 7)) # -------- path to backup Label(f2, text=_("Backup")).grid(row=0, column=0, padx=(8, 4)) f22 = Frame(f2) f22.grid(row=0, column=1, sticky="nsew", padx=(4, 0)) self.entry_sauve = Entry(f22) self.entry_sauve.place(x=1, y=0, bordermode='outside', relwidth=1) self.b_open_sauve = Button(f2, image=self.img_open, width=2, style="folder.TButton", command=self.open_sauve) self.b_open_sauve.grid(row=0, column=5, padx=(0, 10)) paned = PanedWindow(self, orient='horizontal') paned.grid(row=2, sticky="eswn") paned.rowconfigure(0, weight=1) paned.columnconfigure(1, weight=1) paned.columnconfigure(0, weight=1) # --- left side frame_left = Frame(paned) paned.add(frame_left, weight=1) frame_left.rowconfigure(3, weight=1) frame_left.columnconfigure(0, weight=1) # -------- files to copy f_left = Frame(frame_left) f_left.columnconfigure(2, weight=1) f_left.grid(row=2, columnspan=2, pady=(4, 2), padx=(10, 4), sticky="ew") Label(f_left, text=_("To copy")).grid(row=0, column=2) frame_copie = Frame(frame_left) frame_copie.rowconfigure(0, weight=1) frame_copie.columnconfigure(0, weight=1) frame_copie.grid(row=3, column=0, sticky="eswn", columnspan=2, pady=(2, 4), padx=(10, 4)) self.tree_copie = CheckboxTreeview(frame_copie, selectmode='none', show='tree') self.b_expand_copie = Button(f_left, image=self.img_expand, style="folder.TButton", command=self.tree_copie.expand_all) TooltipWrapper(self.b_expand_copie, text=_("Expand all")) self.b_expand_copie.grid(row=0, column=0) self.b_expand_copie.state(("disabled", )) self.b_collapse_copie = Button(f_left, image=self.img_collapse, style="folder.TButton", command=self.tree_copie.collapse_all) TooltipWrapper(self.b_collapse_copie, text=_("Collapse all")) self.b_collapse_copie.grid(row=0, column=1, padx=4) self.b_collapse_copie.state(("disabled", )) self.tree_copie.tag_configure("warning", foreground="red") self.tree_copie.tag_configure("link", font="tkDefaultFont 9 italic", foreground="blue") self.tree_copie.tag_bind("warning", "<Button-1>", self.show_warning) self.tree_copie.grid(row=0, column=0, sticky="eswn") self.scroll_y_copie = Scrollbar(frame_copie, orient="vertical", command=self.tree_copie.yview) self.scroll_y_copie.grid(row=0, column=1, sticky="ns") self.scroll_x_copie = Scrollbar(frame_copie, orient="horizontal", command=self.tree_copie.xview) self.scroll_x_copie.grid(row=1, column=0, sticky="ew") self.tree_copie.configure(yscrollcommand=self.scroll_y_copie.set, xscrollcommand=self.scroll_x_copie.set) self.pbar_copie = Progressbar(frame_left, orient="horizontal", mode="determinate") self.pbar_copie.grid(row=4, columnspan=2, sticky="ew", padx=(10, 4), pady=4) self.pbar_copie.state(("disabled", )) # --- right side frame_right = Frame(paned) paned.add(frame_right, weight=1) frame_right.rowconfigure(3, weight=1) frame_right.columnconfigure(0, weight=1) # -------- files to delete f_right = Frame(frame_right) f_right.columnconfigure(2, weight=1) f_right.grid(row=2, columnspan=2, pady=(4, 2), padx=(4, 10), sticky="ew") Label(f_right, text=_("To remove")).grid(row=0, column=2) frame_supp = Frame(frame_right) frame_supp.rowconfigure(0, weight=1) frame_supp.columnconfigure(0, weight=1) frame_supp.grid(row=3, columnspan=2, sticky="eswn", pady=(2, 4), padx=(4, 10)) self.tree_supp = CheckboxTreeview(frame_supp, selectmode='none', show='tree') self.b_expand_supp = Button(f_right, image=self.img_expand, style="folder.TButton", command=self.tree_supp.expand_all) TooltipWrapper(self.b_expand_supp, text=_("Expand all")) self.b_expand_supp.grid(row=0, column=0) self.b_expand_supp.state(("disabled", )) self.b_collapse_supp = Button(f_right, image=self.img_collapse, style="folder.TButton", command=self.tree_supp.collapse_all) TooltipWrapper(self.b_collapse_supp, text=_("Collapse all")) self.b_collapse_supp.grid(row=0, column=1, padx=4) self.b_collapse_supp.state(("disabled", )) self.tree_supp.grid(row=0, column=0, sticky="eswn") self.scroll_y_supp = Scrollbar(frame_supp, orient="vertical", command=self.tree_supp.yview) self.scroll_y_supp.grid(row=0, column=1, sticky="ns") self.scroll_x_supp = Scrollbar(frame_supp, orient="horizontal", command=self.tree_supp.xview) self.scroll_x_supp.grid(row=1, column=0, sticky="ew") self.tree_supp.configure(yscrollcommand=self.scroll_y_supp.set, xscrollcommand=self.scroll_x_supp.set) self.pbar_supp = Progressbar(frame_right, orient="horizontal", mode="determinate") self.pbar_supp.grid(row=4, columnspan=2, sticky="ew", padx=(4, 10), pady=4) self.pbar_supp.state(("disabled", )) # --- bindings self.entry_orig.bind("<Key-Return>", self.list_files_to_sync) self.entry_sauve.bind("<Key-Return>", self.list_files_to_sync) def exclusion_supp(self): excl = ExclusionsSupp(self) self.wait_window(excl) # paths that will not be deleted self.exclude_path_supp = [ ch.replace("\ ", " ") for ch in split( r'(?<!\\) ', CONFIG.get("Defaults", "exclude_supp")) if ch ] def exclusion_copie(self): excl = ExclusionsCopie(self) self.wait_window(excl) exclude_list = CONFIG.get("Defaults", "exclude_copie").split(" ") self.exclude_names = [] self.exclude_ext = [] for elt in exclude_list: if elt: if elt[:2] == "*.": self.exclude_ext.append(elt[2:]) else: self.exclude_names.append(elt) def toggle_copy_links(self): CONFIG.set("Defaults", "copy_links", str(self.copy_links.get())) def toggle_show_size(self): CONFIG.set("Defaults", "show_size", str(self.show_size.get())) def open_log_copie(self): open_file(self.log_copie) def open_log_suppression(self): open_file(self.log_supp) def quitter(self): rep = True if self.is_running_copie or self.is_running_supp: rep = askokcancel( _("Confirmation"), _("A synchronization is ongoing, do you really want to quit?"), parent=self) if rep: self.destroy() def del_fav(self, nom): self.menu_fav.delete(nom) self.menu_fav_del.delete(nom) FAVORIS.remove(tuple(nom.split(" -> "))) save_config() if not FAVORIS: self.menu_fav.entryconfigure(1, state="disabled") def add_fav(self): sauvegarde = self.entry_sauve.get() original = self.entry_orig.get() if original != sauvegarde and original and sauvegarde: if exists(original) and exists(sauvegarde): if not (original, sauvegarde) in FAVORIS: FAVORIS.append((original, sauvegarde)) save_config() label = "%s -> %s" % (original, sauvegarde) self.menu_fav.entryconfigure(1, state="normal") self.menu_fav.add_command(label=label, command=lambda o=original, s= sauvegarde: self.open(o, s)) self.menu_fav_del.add_command( label=label, command=lambda nom=label: self.del_fav(nom)) def open(self, ch_o, ch_s): self.entry_orig.delete(0, "end") self.entry_orig.insert(0, ch_o) self.entry_sauve.delete(0, "end") self.entry_sauve.insert(0, ch_s) self.list_files_to_sync() def open_sauve(self): sauvegarde = askdirectory(self.entry_sauve.get(), parent=self) if sauvegarde: self.entry_sauve.delete(0, "end") self.entry_sauve.insert(0, sauvegarde) def open_orig(self): original = askdirectory(self.entry_orig.get(), parent=self) if original: self.entry_orig.delete(0, "end") self.entry_orig.insert(0, original) def sync(self, original, sauvegarde): """ peuple tree_copie avec l'arborescence des fichiers d'original à copier vers sauvegarde et tree_supp avec celle des fichiers de sauvegarde à supprimer """ errors = [] copy_links = self.copy_links.get() excl_supp = [ path for path in self.exclude_path_supp if commonpath([path, sauvegarde]) == sauvegarde ] def get_name(elt): return elt.name.lower() def lower(char): return char.lower() def arbo(tree, parent, n): """ affiche l'arborescence complète de parent et renvoie la longueur maximale des items (pour gérer la scrollbar horizontale) """ m = 0 try: with scandir(parent) as content: l = sorted(content, key=get_name) for item in l: chemin = item.path nom = item.name if item.is_symlink(): if copy_links: tree.insert(parent, 'end', chemin, text=nom, tags=("whole", "link")) m = max(m, len(nom) * 9 + 20 * (n + 1)) elif ((nom not in self.exclude_names) and (splitext(nom)[-1] not in self.exclude_ext)): tree.insert(parent, 'end', chemin, text=nom, tags=("whole", )) m = max(m, len(nom) * 9 + 20 * (n + 1)) if item.is_dir(): m = max(m, arbo(tree, chemin, n + 1)) except NotADirectoryError: pass except Exception as e: errors.append(str(e)) return m def aux(orig, sauve, n, search_supp): m_copie = 0 m_supp = 0 try: lo = listdir(orig) ls = listdir(sauve) except Exception as e: errors.append(str(e)) lo = [] ls = [] lo.sort(key=lambda x: x.lower()) ls.sort(key=lambda x: x.lower()) supp = False copie = False if search_supp: for item in ls: chemin_s = join(sauve, item) if chemin_s not in excl_supp and item not in lo: supp = True self.tree_supp.insert(sauve, 'end', chemin_s, text=item, tags=("whole", )) m_supp = max(m_supp, int(len(item) * 9 + 20 * (n + 1)), arbo(self.tree_supp, chemin_s, n + 1)) for item in lo: chemin_o = join(orig, item) chemin_s = join(sauve, item) if ((item not in self.exclude_names) and (splitext(item)[-1] not in self.exclude_ext)): if item not in ls: # le dossier / fichier n'est pas dans la sauvegarde if islink(chemin_o): if copy_links: copie = True self.tree_copie.insert(orig, 'end', chemin_o, text=item, tags=("whole", "link")) m_copie = max( m_copie, (int(len(item) * 9 + 20 * (n + 1)))) else: copie = True self.tree_copie.insert(orig, 'end', chemin_o, text=item, tags=("whole", )) m_copie = max( m_copie, (int(len(item) * 9 + 20 * (n + 1))), arbo(self.tree_copie, chemin_o, n + 1)) elif islink(chemin_o) and exists(chemin_o): # checking the existence prevent from copying broken links if copy_links: if not islink(chemin_s): self.pb_chemins.append(chemin_o) tags = ("whole", "warning", "link") else: tags = ("whole", "link") self.tree_copie.insert(orig, 'end', chemin_o, text=item, tags=tags) m_copie = max(m_copie, int(len(item) * 9 + 20 * (n + 1))) copie = True elif isfile(chemin_o): # first check if chemin_s is also a file if isfile(chemin_s): if getmtime(chemin_o) // 60 > getmtime( chemin_s) // 60: # le fichier f a été modifié depuis la dernière sauvegarde copie = True self.tree_copie.insert(orig, 'end', chemin_o, text=item, tags=("whole", )) else: self.pb_chemins.append(chemin_o) self.tree_copie.insert(orig, 'end', chemin_o, text=item, tags=("whole", "warning")) elif isdir(chemin_o): # to avoid errors due to unrecognized item types (neither file nor folder nor link) if isdir(chemin_s): self.tree_copie.insert(orig, 'end', chemin_o, text=item) self.tree_supp.insert(sauve, 'end', chemin_s, text=item) c, s, mc, ms = aux( chemin_o, chemin_s, n + 1, search_supp and (chemin_s not in excl_supp)) supp = supp or s copie = copie or c if not c: # nothing to copy self.tree_copie.delete(chemin_o) else: m_copie = max( m_copie, mc, int(len(item) * 9 + 20 * (n + 1))) if not s: # nothing to delete self.tree_supp.delete(chemin_s) else: m_supp = max(m_supp, ms, int(len(item) * 9 + 20 * (n + 1))) else: copie = True self.pb_chemins.append(chemin_o) self.tree_copie.insert(orig, 'end', chemin_o, text=item, tags=("whole", "warning")) m_copie = max( m_copie, (int(len(item) * 9 + 20 * (n + 1))), arbo(self.tree_copie, chemin_o, n + 1)) return copie, supp, m_copie, m_supp self.tree_copie.insert("", 0, original, text=original, tags=("checked", ), open=True) self.tree_supp.insert("", 0, sauvegarde, text=sauvegarde, tags=("checked", ), open=True) c, s, mc, ms = aux(original, sauvegarde, 1, True) if not c: self.tree_copie.delete(original) self.tree_copie.column("#0", minwidth=0, width=0) else: mc = max(len(original) * 9 + 20, mc) self.tree_copie.column("#0", minwidth=mc, width=mc) if not s: self.tree_supp.delete(sauvegarde) self.tree_supp.column("#0", minwidth=0, width=0) else: ms = max(len(sauvegarde) * 9 + 20, mc) self.tree_supp.column("#0", minwidth=ms, width=ms) return errors def show_warning(self, event): if "disabled" not in self.b_open_orig.state(): x, y = event.x, event.y elem = event.widget.identify("element", x, y) if elem == "padding": orig = self.tree_copie.identify_row(y) sauve = orig.replace(self.original, self.sauvegarde) showwarning( _("Warning"), _("%(original)s and %(backup)s are not of the same kind (folder/file/link)" ) % { 'original': orig, 'backup': sauve }, master=self) def list_files_to_sync(self, event=None): """Display in a treeview the file to copy and the one to delete.""" self.pbar_copie.configure(value=0) self.pbar_supp.configure(value=0) self.sauvegarde = self.entry_sauve.get() self.original = self.entry_orig.get() if self.original != self.sauvegarde and self.original and self.sauvegarde: if exists(self.original) and exists(self.sauvegarde): o_s = (self.original, self.sauvegarde) if o_s in RECENT: RECENT.remove(o_s) self.menu_recent.delete("%s -> %s" % o_s) RECENT.insert(0, o_s) self.menu_recent.insert_command( 0, label="%s -> %s" % o_s, command=lambda o=self.original, s=self.sauvegarde: self. open(o, s)) if len(RECENT) == 10: del (RECENT[-1]) self.menu_recent.delete(9) save_config() self.menu.entryconfigure(0, state="normal") self.configure(cursor="watch") self.toggle_state_gui() self.update_idletasks() self.update() self.efface_tree() err = self.sync(self.original, self.sauvegarde) self.configure(cursor="") self.toggle_state_gui() c = self.tree_copie.get_children("") s = self.tree_supp.get_children("") if not (c or s): self.menu.entryconfigure(5, state="disabled") self.b_collapse_copie.state(("disabled", )) self.b_expand_copie.state(("disabled", )) self.b_collapse_supp.state(("disabled", )) self.b_expand_supp.state(("disabled", )) elif not c: self.b_collapse_copie.state(("disabled", )) self.b_expand_copie.state(("disabled", )) elif not s: self.b_collapse_supp.state(("disabled", )) self.b_expand_supp.state(("disabled", )) if err: showerror(_("Errors"), "\n".join(err), master=self) notification_send(_("Scan is finished.")) warnings = self.tree_copie.tag_has('warning') if warnings: showwarning( _("Warning"), _("Some elements to copy (in red) are not of the same kind on the original and the backup." ), master=self) else: showerror(_("Error"), _("Invalid path!"), master=self) def efface_tree(self): """Clear both trees.""" c = self.tree_copie.get_children("") for item in c: self.tree_copie.delete(item) s = self.tree_supp.get_children("") for item in s: self.tree_supp.delete(item) self.b_collapse_copie.state(("disabled", )) self.b_expand_copie.state(("disabled", )) self.b_collapse_supp.state(("disabled", )) self.b_expand_supp.state(("disabled", )) def toggle_state_gui(self): """Toggle the state (normal/disabled) of key elements of the GUI.""" if "disabled" in self.b_open_orig.state(): state = "!disabled" for i in range(7): self.menu.entryconfigure(i, state="normal") else: state = "disabled" for i in range(7): self.menu.entryconfigure(i, state="disabled") self.tree_copie.state((state, )) self.tree_supp.state((state, )) self.entry_orig.state((state, )) self.entry_sauve.state((state, )) self.b_expand_copie.state((state, )) self.b_collapse_copie.state((state, )) self.b_expand_supp.state((state, )) self.b_collapse_supp.state((state, )) self.b_open_orig.state((state, )) self.b_open_sauve.state((state, )) def update_pbar(self): """ Dislay the progress of the copy and deletion and put the GUI back in normal state once both processes are done. """ if not self.is_running_copie and not self.is_running_supp: notification_send(_("Sync is finished.")) self.toggle_state_gui() self.pbar_copie.configure(value=self.pbar_copie.cget("maximum")) self.pbar_supp.configure(value=self.pbar_supp.cget("maximum")) self.menu.entryconfigure(5, state="disabled") self.configure(cursor="") self.efface_tree() msg = "" if self.err_copie: msg += _( "There were errors during the copy, see %(file)s for more details.\n" ) % { 'file': self.log_copie } if self.err_supp: msg += _( "There were errors during the removal, see %(file)s for more details.\n" ) % { 'file': self.log_supp } if msg: showerror(_("Error"), msg, master=self) else: if not self.q_copie.empty(): self.pbar_copie.configure(value=self.q_copie.get()) if not self.q_supp.empty(): self.pbar_supp.configure(value=self.q_supp.get()) self.update() self.after(50, self.update_pbar) @staticmethod def get_list(tree): """Return the list of files/folders to copy/delete (depending on the tree).""" selected = [] def aux(item): tags = tree.item(item, "tags") if "checked" in tags and "whole" in tags: selected.append(item) elif "checked" in tags or "tristate" in tags: ch = tree.get_children(item) for c in ch: aux(c) ch = tree.get_children("") for c in ch: aux(c) return selected def synchronise(self): """ Display the list of files/folders that will be copied / deleted and launch the copy and deletion if the user validates the sync. """ # get files to delete and folder to delete if they are empty a_supp = self.get_list(self.tree_supp) # get files to copy a_copier = self.get_list(self.tree_copie) a_supp_avant_cp = [] for ch in self.pb_chemins: if ch in a_copier: a_supp_avant_cp.append( ch.replace(self.original, self.sauvegarde)) if a_supp or a_copier: Confirmation(self, a_copier, a_supp, a_supp_avant_cp, self.original, self.sauvegarde, self.show_size.get()) def copie_supp(self, a_copier, a_supp, a_supp_avant_cp): """Launch sync.""" self.toggle_state_gui() self.configure(cursor="watch") self.update() self.pbar_copie.state(("!disabled", )) self.pbar_supp.state(("!disabled", )) nbtot_copie = len(a_copier) + len(a_supp_avant_cp) self.pbar_copie.configure(maximum=nbtot_copie, value=0) nbtot_supp = len(a_supp) self.pbar_supp.configure(maximum=nbtot_supp, value=0) self.is_running_copie = True self.is_running_supp = True process_copie = Thread(target=self.copie, name="copie", daemon=True, args=(a_copier, a_supp_avant_cp)) process_supp = Thread(target=self.supp, daemon=True, name="suppression", args=(a_supp, )) process_copie.start() process_supp.start() self.pbar_copie.configure(value=0) self.pbar_supp.configure(value=0) self.update_pbar() def copie(self, a_copier, a_supp_avant_cp): """ Copie tous les fichiers/dossiers de a_copier de original vers sauvegarde en utilisant la commande système cp. Les erreurs rencontrées au cours du processus sont inscrites dans ~/.foldersync/copie.log """ self.err_copie = False orig = abspath(self.original) + "/" sauve = abspath(self.sauvegarde) + "/" chdir(orig) self.logger_copie.info( _("\n###### Copy: %(original)s -> %(backup)s\n") % { 'original': self.original, 'backup': self.sauvegarde }) n = len(a_supp_avant_cp) self.logger_copie.info(_("Removal before copy:")) for i, ch in zip(range(1, n + 1), a_supp_avant_cp): self.logger_copie.info(ch) p_copie = run(["rm", "-r", ch], stderr=PIPE) self.q_copie.put(i) err = p_copie.stderr.decode() if err: self.err_copie = True self.logger_copie.error(err.strip()) self.logger_copie.info(_("Copy:")) for i, ch in zip(range(n + 1, n + 1 + len(a_copier)), a_copier): ch_o = ch.replace(orig, "") self.logger_copie.info("%s -> %s" % (ch_o, sauve)) p_copie = run(["cp", "-ra", "--parents", ch_o, sauve], stderr=PIPE) self.q_copie.put(i) err = p_copie.stderr.decode() if err: self.err_copie = True self.logger_copie.error(err.strip()) self.is_running_copie = False def supp(self, a_supp): """ Supprime tous les fichiers/dossiers de a_supp de original vers sauvegarde en utilisant la commande système rm. Les erreurs rencontrées au cours du processus sont inscrites dans ~/.foldersync/suppression.log. """ self.err_supp = False self.logger_supp.info( _("\n###### Removal: %(original)s -> %(backup)s\n") % { 'original': self.original, 'backup': self.sauvegarde }) for i, ch in enumerate(a_supp): self.logger_supp.info(ch) p_supp = run(["rm", "-r", ch], stderr=PIPE) self.q_supp.put(i + 1) err = p_supp.stderr.decode() if err: self.logger_supp.error(err.strip()) self.err_supp = True self.is_running_supp = False def unlink(self): """Unlink pidfile.""" unlink(self.pidfile) def change_language(self): """Change app language.""" CONFIG.set("Defaults", "language", self.langue.get()) showinfo( _("Information"), _("The language setting will take effect after restarting the application" ))
class UserInterface(UIfunctions): def __init__(self, filehandler, databasehandler, path=None): self.title = "OpenKeynote (BETA)" self._filehandler = filehandler self._databasehandler = databasehandler self.path = path self.itemlist = [] self.root = Tk() self.previeweditem = "" self.editeditem = "" self.case_selected = IntVar() self.parentname = "" self.autosave = IntVar() self._html_path = None self._default_title = self.title self.main_window() self.tree_view() self.frame_vertical_bar() self.bindings_and_menu() self.frame_setup() self.update_status() self.root.mainloop() def main_window(self, *args): self.mainframe = Frame(self.root) self.mainframe.grid(column=0, row=0, sticky=E+W+N+S) self.bottomframe = Frame(self.root) self.bottomframe.grid(column=0, row=1, sticky=E+W) self.statusbar = Label( self.bottomframe, text=self._filehandler.statustext, anchor=W) self.statusbar.pack(fill=BOTH, padx=0, pady=0) self.root.columnconfigure(0, weight=1) self.root.rowconfigure(0, weight=1) self.root.rowconfigure(1, pad=10) self.pw = PanedWindow(self.mainframe, orient=HORIZONTAL) self.pw.pack(fill=BOTH, expand=1) self.pane_left = Frame(self.root) self.pw.add(self.pane_left) self.pane_right = Frame(self.root) self.pw.add(self.pane_right) self.frame_left = Frame(self.pane_left) self.frame_left.pack(fill=BOTH, expand=1, padx=3, pady=3) self.frame_center = Frame(self.pane_right) self.frame_center.grid(row=0, column=0, sticky=W+N, rowspan=4) self.frame_right = Frame(self.pane_right) self.frame_right.grid(row=0, column=1, sticky=W+E+N+S, padx=3, pady=3) self.pane_right.columnconfigure(1, weight=1) self.pane_right.rowconfigure(0, weight=1) self.sf1 = Text(self.frame_left, height=1, width=25, borderwidth=1, relief="solid", highlightthickness=0) self.sf1.insert(1.0, "TODO: Searchbar") self.sf1.grid(row=0, column=0, sticky=W+E+N+S, pady=5) self.sf1.config(state=DISABLED) self.cs = Button(self.frame_left, text="X", width=1) self.cs.grid(row=0, column=1) self.frame_left.columnconfigure(0, weight=1) def tree_view(self, *args): """ Tree view """ self.l1 = ttk.Treeview(self.frame_left, columns=["stuff"], show="tree") self.yscroll = Scrollbar(self.frame_left, orient=VERTICAL) self.yscroll.config(width=10) self.l1['yscrollcommand'] = self.yscroll.set self.yscroll['command'] = self.l1.yview self.l1.grid(row=1, column=0, columnspan=3, padx=30, pady=10, sticky=N+S+E+W) self.yscroll.grid(row=1, column=0, columnspan=3, sticky=N+S+E) self.l1.bind("<ButtonRelease-1>", self.change_selection) self.frame_left.rowconfigure(1, weight=1) def frame_vertical_bar(self, *args): self.vbs = [] middlebuttons = ("New Item", "New Subitem", "Delete", "Rename", "Change Parent","- Descriptions (BETA) -") middlefunctions = ( lambda: self.add_item(parent=self.parentname), lambda: self.add_item(parent=self.previeweditem), lambda: self.delete_item_dialog(), self.rename_item_dialog, self.change_parent_dialog, lambda: self.description_window(database_rows=self._databasehandler.view()) #lambda: self.save_keynote_to_database(title="title",keynote="KN10", entreprise="min entreprise", category="") #self.save_all_keynotes_to_database ) for a, button_text in enumerate(middlebuttons): self.vbs.append(ttk.Button(self.frame_center, text=button_text)) self.vbs[a].pack(fill=BOTH) self.vbs[a].config(command=middlefunctions[a], width=10) for x in [2, 3, 4]: self.vbs[x].config(state=DISABLED) self.tx1 = Label(self.frame_right, text="Preview", anchor=W) self.tx1.grid(row=0, column=0, columnspan=3, sticky=W+E) self.tx2 = Label(self.frame_right, text="Editing", anchor=W) self.tx2.grid(row=2, column=0, sticky=W+E) self.e1 = scrolledtext.ScrolledText(self.frame_right, fg="#555", font=("Courier", 13), padx=10, pady=10, highlightthickness=0, borderwidth=1, relief="solid") self.e1.grid(row=1, column=0, columnspan=3, sticky=N+S+E+W) # was Text before self.e2 = scrolledtext.ScrolledText(self.frame_right, font=("Courier", 13), borderwidth=1, relief="solid", padx=10, pady=10, highlightthickness=0) self.e2.grid(row=3, column=0, columnspan=3, sticky=E+W+S+N) # AUTOSAVE self.autosaveFrame = LabelFrame(self.frame_center, text=' Autosave ') self.autosaveFrame.pack(fill=BOTH) self.autosave.trace( 'w', lambda *args: print(f"Autosave: {self.autosave.get()}")) self.autosaveCheck = Checkbutton( self.autosaveFrame, text="Enabled", variable=self.autosave, anchor=W) self.autosaveCheck.select() self.autosaveCheck.pack(fill=BOTH) self.labelsFrame = LabelFrame(self.frame_center, text=' Change Case ') self.labelsFrame.pack(fill=BOTH) # CASE BUTTONS self.case_radiobuttons = [] self.case_selected.set(99) rbtns = ['No change', 'UPPERCASE', 'lowercase', 'First Letter'] for a, button_text in enumerate(rbtns): self.case_radiobuttons.append( Radiobutton(self.labelsFrame, text=button_text, variable=self.case_selected, value=a, command=self.change_case, width=10, anchor=W)) self.case_radiobuttons[a].grid(sticky="W", row=a) def change_case(self, *args): pass def bindings_and_menu(self, *args): """ Main key bindings """ if os.name == "nt": self.CTRL = "Control" self.MBTN = "3" else: self.CTRL = "Command" self.MBTN = "2" def bindings_key(event): if event.state == 8 or event.state == 12: return else: return("break") self.sf1.bind("<Tab>", lambda a: self.focus_on(target=self.l1)) self.sf1.bind("<Shift-Tab>", lambda a: self.focus_on(target=self.vb2)) self.e1.bind("<Key>", bindings_key) self.e1.bind("<Tab>", lambda a: self.focus_on(target=self.e2)) self.e1.bind( "<Shift-Tab>", lambda a: self.focus_on(target=self.vbs[-1])) self.e2.bind("<Tab>", lambda a: self.focus_on(target=self.vb1)) self.e2.bind("<Shift-Tab>", lambda a: self.focus_on(target=self.e1)) self.vb1 = ttk.Button(self.frame_right, text="Edit") self.vb1.grid(row=2, column=1) self.vb1.config(command=self.edit_item) self.vb2 = ttk.Button(self.frame_right, text="Save") self.vb2.grid(row=2, column=2) self.vb2.config(command=self.saveitem) self.frame_right.rowconfigure(1, weight=1) self.frame_right.rowconfigure(3, weight=1) self.frame_right.columnconfigure(0, weight=1) self.menu = Menu(self.root) self.root.config(menu=self.menu) file = Menu(self.menu, tearoff=0) # TODO is it a win thing? file.add_command(label='New File*', command=self.close_file) file.add_command( label='Open File...', accelerator=f"{self.CTRL}-o", command=self.open_file_dialog) file.add_command(label='Save File', accelerator=f"{self.CTRL}-s", command=self.save_file) file.add_command(label='Save File As...', command=self.save_file_dialog) file.add_command(label='Close file', command=self.close_file) file.add_command( label='Exit', accelerator=f"{self.CTRL}-q", command=self.client_exit) self.menu.add_cascade(label='File', menu=file) self.clickmenu = Menu(self.root, tearoff=0) self.clickmenu.add_command(label="Cut") self.clickmenu.add_command(label="Copy") self.clickmenu.add_command(label="Paste") self.root.bind_class( "Text", f"<Button-{self.MBTN}><ButtonRelease-{self.MBTN}>", lambda event=None: self.right_click_menu()) menu_edit = Menu(self.menu, tearoff=0) menu_edit.add_command(label='Select All', accelerator=f"{self.CTRL}-a", command=self.select_all) self.root.bind(f"<{self.CTRL}-a>", self.select_all) self.e1.bind(f"<{self.CTRL}-a>", self.select_all) self.e1.bind(f"<{self.CTRL}-c>", self.e1.event_generate("<<Copy>>")) self.e2.bind(f"<{self.CTRL}-a>", self.select_all) menu_edit.add_command(label='Cut', accelerator=f"{self.CTRL}-x", command=lambda: self.root.event_generate("<<Cut>>")) menu_edit.add_command(label='Copy', accelerator=f"{self.CTRL}-c", command=lambda: self.copy_text()) menu_edit.add_command(label='Paste', accelerator=f"{self.CTRL}-v", command=lambda: self.root.event_generate("<<Paste>>")) self.menu.add_cascade(label='Edit', menu=menu_edit) menu_help = Menu(self.menu, tearoff=0) menu_help.add_command(label='About', command=self.about) self.menu.add_cascade(label='Help', menu=menu_help) for i in "Up,Down,Right,Return,Left".split(","): self.root.bind("<"+i+">", self.change_selection) self.e1.bind("<F2>", self.rename_item_dialog) self.e1.bind(f"<{self.CTRL}-s>", None) self.e1.bind(f"<{self.CTRL}-o>", None) self.root.bind("<F2>", self.rename_item_dialog) self.root.bind(f"<{self.CTRL}-s>", self.save_file) self.root.bind(f"<{self.CTRL}-o>", self.open_file_dialog) def copy_text(self, event=None): w = self.root.focus_get() w.event_generate("<<Copy>>") def update_title(self, title=""): self.root.title(title) def frame_setup(self, *args): """ Misc UI functions """ # sharp fonts in high res (https://stackoverflow.com/questions/41315873/ # attempting-to-resolve-blurred-tkinter-text-scaling-on-windows-10-high-dpi-disp) if os.name == "nt": # TODO # self.root.protocol("WM_DELETE_WINDOW", self.client_exit) from ctypes import windll, pointer, wintypes try: windll.shcore.SetProcessDpiAwareness(1) except Exception: pass # this will fail on Windows Server and maybe early Windows # TODO: Put link to ico file on windows. try: iconpath = Path("icon.ico") self.root.iconbitmap(Path()) except: print("error with icon") else: # mac? self.root.createcommand('exit', self.client_exit) self.root.title(self.title) if self.path: self.open_file(path=self.path) """ TODO: ICON /Windows self.root.iconbitmap("/Users/msn/Dropbox/py/Git/OpenKeynote/images/ico.icns") img = Image( "photo", file="/Users/msn/Dropbox/py/Git/OpenKeynote/images/large.gif") self.root.iconphoto(True, img) # you may also want to try this. self.root.call('wm','iconphoto', self.root._w, img) """ self.width = min(int(self.root.winfo_screenwidth()-500), 1500) self.height = int(self.root.winfo_screenheight()-500) self.root.winfo_width() self.root.winfo_height() self.x = (self.root.winfo_screenwidth() // 2) - (self.width // 2) # self.x = 0 self.y = (self.root.winfo_screenheight() // 2) - (self.height // 2) # self.y = 50 self.root.geometry(f"{self.width}x{self.height}+{self.x}+{self.y}") self.root.update() self.root.after(0, self.fixUI) def right_click_menu(self, event=None): x, y = self.root.winfo_pointerxy() w = self.root.winfo_containing(x, y) # https://stackoverflow.com/a/8476726/11514850 # w = self.root self.clickmenu.entryconfigure("Cut", command=lambda: w.event_generate("<<Cut>>")) self.clickmenu.entryconfigure("Copy", command=lambda: w.event_generate("<<Copy>>")) self.clickmenu.entryconfigure("Paste", command=lambda: w.event_generate("<<Paste>>")) self.clickmenu.tk.call("tk_popup", self.clickmenu, w.winfo_pointerx(), w.winfo_pointery()) def update_status(self, event=None): """ Set statusbar in bottom of the window """ self.statusbar.config(text=self._filehandler.refresh_status()) self.root.after(100, self.update_status) def client_exit(self, *args): answer = messagebox.askyesnocancel('quit?', 'Save file first?') if answer == True: self.save_file() sys.exit() if answer == None: return if answer == False: exit()
class NewImageWindow(Toplevel): def __init__(self, master = None, pathToImage = None, name=None, image=None): super().__init__(master = master) if pathToImage is not None and image is None: self.pathToImage = pathToImage self.image = ImageSaved(pathToImage) elif image is not None: self.image = image self.manager = StateManager(self.image.cv2Image) self.set_images() self.set_geometry() self.set_basic(master, name) self.place_menu() self.manage_line_profile() self.bind_functions() # BASICS def create_another(self, master, pathToImage, name, image): NewImageWindow(master, pathToImage, name, image) def set_geometry(self): self.minsize(self.imageFromArray.width, self.imageFromArray.height) self.geometry('{}x{}'.format(self.imageFromArray.width, self.imageFromArray.height)) def set_images(self): self.imageFromArray = Image.fromarray(self.image.cv2Image) self.imageCopy = self.imageFromArray.copy() def set_basic(self, master, name): self.focus_force() self.master = master self.name = name self.title(name) self.imagePanel = Canvas(self) self.imagePanel.place(relwidth=1, relheight = 1, x=0, y=0) self.profileWindow = None self.histogramWindow = None self.lutWindow = None self.thresholdScaleWindow = None self.posterizeWindow = None self.neighborWindow = None NewTwoArgsWindow.images[self] = self.name for widget in self.master.winfo_children(): if(type(widget) == NewTwoArgsWindow): widget.update_list() break def manage_line_profile(self): self.lineCoords = {"x":0,"y":0,"x2":0,"y2":0} self.line = None def bind_functions(self): #Zmienić resize'owanie, aktualnie nie pasuje do linii profilu self.bind('<Configure>', self.resize_img) self.bind('<Control-d>', lambda event: self.duplicate_window()) self.bind('<Control-z>', lambda event: self.undo()) self.bind('<Control-y>', lambda event: self.redo()) self.bind('<Control-s>', lambda event: self.save()) self.bind("<ButtonPress-3>", self.click) self.bind("<B3-Motion>", self.drag) self.protocol("WM_DELETE_WINDOW", lambda:self.report_close_to_windows()) def undo(self): self.image.cv2Image = self.manager.undo() self.image.copy = self.image.cv2Image self.image.fill_histogram() self.update_visible_image() self.update_child_windows() def save(self): try: cv2.imwrite(self.pathToImage, self.image.cv2Image) except AttributeError: self.save_as() def save_as(self): try: ext = self.image.file_extension except AttributeError: ext = '.bmp' finally: dir = '' dir = filedialog.asksaveasfilename( initialfile='*'+ext, defaultextension=ext, filetypes=[ ('JPEG', '*.jpg'), ('PNG', '*.png'), ('BMP', '*.bmp'), ('GIF', '*.gif'), ] ) if dir != '': try: cv2.imwrite(dir, self.image.cv2Image) self.pathToImage = dir self.name = os.path.split(dir)[1] self.title(self.name) except cv2.error: messagebox.showerror("Błąd", "Nieoczekiwany błąd, sprawdź czy dobrze wprowadziłeś nazwę pliku") def redo(self): self.image.cv2Image = self.manager.redo() self.image.copy = self.image.cv2Image self.image.fill_histogram() self.update_visible_image() self.update_child_windows() def report_close_to_windows(self): NewTwoArgsWindow.images.pop(self) for widget in self.master.winfo_children(): if(type(widget) == NewTwoArgsWindow): widget.update_list() break self.destroy() # ------------------- # WINDOW def duplicate_window(self): NewImageWindow(self.master, None, self.name + '(Kopia)', ImageSaved(None,self.image.cv2Image)).focus_set() def place_menu(self): topMenu = Menu(self) self.dsc = Menu(topMenu, tearoff=False) histMan = Menu(topMenu, tearoff=False) imageMan = Menu(topMenu, tearoff=False) imageAnalize = Menu(topMenu, tearoff=False) pointOper = Menu(imageMan, tearoff=False) neighborOper = Menu(imageMan, tearoff=False) smooth = Menu(neighborOper, tearoff=False) detectEdges = Menu(neighborOper, tearoff=False) prewitt = Menu(neighborOper, tearoff=False) sharpen = Menu(neighborOper, tearoff=False) medianM = Menu(neighborOper, tearoff=False) imageMan.add_cascade(label="Jednoargumentowe", menu=pointOper) imageMan.add_command(label="Dwuargumentowe", compound=LEFT, command=self.create_two_args_window) imageMan.add_cascade(label="Sąsiedztwa", menu=neighborOper) imageMan.add_command(label="Morfologiczne", compound=LEFT, command=self.create_morph_window) imageMan.add_command(label="Morfologiczna ekstrakcja linii", compound=LEFT, command=self.create_morph_line_window) imageMan.add_command(label="Watershed", compound=LEFT, command=self.handle_watershed) # Opcje OPISU self.dsc.add_command(label='Cofnij', compound=LEFT, accelerator='Ctrl+Z', command=self.undo) self.dsc.add_command(label='Powtórz', compound=LEFT, accelerator='Ctrl+Y', command=self.redo) self.dsc.add_command(label='Zapisz', accelerator='Ctrl+S', compound=LEFT, command=self.save) self.dsc.add_command(label='Zapisz jako...', accelerator='Ctrl+Shift+S', compound=LEFT, command=self.save_as) self.dsc.add_command(label='Histogram', compound=LEFT, command=self.create_histogram_window) self.dsc.add_command(label='LUT', compound=LEFT, command=self.create_lut_window) self.dsc.add_command(label='Linia profilu', compound=LEFT, state=DISABLED, command=self.create_profile_window) # Opcje MANIPULACJI HISTOGRAMEM histMan.add_command(label="Rozciąganie", compound=LEFT, command=self.stretch_image) histMan.add_command(label="Rozciąganie przedziałami", compound=LEFT, command=self.create_stretching_window) histMan.add_command(label="Wyrównanie", compound=LEFT, command=self.equalize_image) # Opcje OPERACJI JEDNOARGUMENTOWYCH pointOper.add_command(label="Negacja", compound=LEFT, command=self.negate_image) pointOper.add_command(label="Progowanie", compound=LEFT, command=lambda:self.threshold_image("BASIC")) pointOper.add_command(label="Progowanie adaptacyjne", compound=LEFT, command=lambda:self.threshold_image("ADAPT")) pointOper.add_command(label="Progowanie Otsu", compound=LEFT, command=lambda:self.threshold_image("OTSU")) pointOper.add_command(label="Posteryzacja", compound=LEFT, command=self.posterize_image) # Opcje OPERACJI SĄSIEDZTWA neighborOper.add_cascade(label="Wygładzanie", menu=smooth) neighborOper.add_cascade(label="Detekcja krawędzi", menu=detectEdges) neighborOper.add_cascade(label="Wyostrzanie", menu=sharpen) neighborOper.add_cascade(label="Medianowe", menu=medianM) neighborOper.add_command(label="Własne", compound=LEFT, command=lambda:self.create_custom_mask_window(1)) neighborOper.add_command(label="Własne splot", compound=LEFT, command=lambda:self.create_custom_mask_window(0)) neighborOper.add_separator() neighborOper.add_command(label="OPCJE SKRAJNYCH PIKSELI", state='disabled', compound=LEFT) self.border = IntVar(self) self.border.set(2) neighborOper.add_radiobutton(label="Bez zmian (isolated)", variable=self.border, value=0) neighborOper.add_radiobutton(label="Odbicie lustrzane (reflect)", variable=self.border, value=1) neighborOper.add_radiobutton(label="Powielenie skrajnego piksela (replicate)", variable=self.border, value=2) smooth.add_command(label="Blur", compound=LEFT, command=lambda:self.handle_neighbor_operations("BLUR", self.border.get())) #POZMIENIAĆ COMMANDY smooth.add_command(label="Gaussian Blur", compound=LEFT, command=lambda:self.handle_neighbor_operations("GAUSSIAN", self.border.get())) detectEdges.add_command(label="Sobel", compound=LEFT, command=lambda:self.handle_neighbor_operations("SOBEL", self.border.get())) detectEdges.add_command(label="Laplasjan", compound=LEFT, command=lambda:self.handle_neighbor_operations("LAPLASJAN", self.border.get())) detectEdges.add_command(label="Canny", compound=LEFT, command=lambda:self.handle_neighbor_operations("CANNY", self.border.get())) detectEdges.add_cascade(label="Prewitt", menu=prewitt) prewitt.add_command(label="Prewitt N", compound=LEFT, command=lambda:self.handle_neighbor_operations("PRW N", self.border.get())) prewitt.add_command(label="Prewitt NE", compound=LEFT, command=lambda:self.handle_neighbor_operations("PRW NE", self.border.get())) prewitt.add_command(label="Prewitt E", compound=LEFT, command=lambda:self.handle_neighbor_operations("PRW E", self.border.get())) prewitt.add_command(label="Prewitt SE", compound=LEFT, command=lambda:self.handle_neighbor_operations("PRW SE", self.border.get())) prewitt.add_command(label="Prewitt S", compound=LEFT, command=lambda:self.handle_neighbor_operations("PRW S", self.border.get())) prewitt.add_command(label="Prewitt SW", compound=LEFT, command=lambda:self.handle_neighbor_operations("PRW SW", self.border.get())) prewitt.add_command(label="Prewitt W", compound=LEFT, command=lambda:self.handle_neighbor_operations("PRW W", self.border.get())) prewitt.add_command(label="Prewitt NW", compound=LEFT, command=lambda:self.handle_neighbor_operations("PRW NW", self.border.get())) sharpen.add_command(label="Laplasjan 0/-1/5", compound=LEFT, command=lambda:self.handle_neighbor_operations("LAPLASJAN 1", self.border.get())) sharpen.add_command(label="Laplasjan -1/-1/9", compound=LEFT, command=lambda:self.handle_neighbor_operations("LAPLASJAN 2", self.border.get())) sharpen.add_command(label="Laplasjan 1/-2/5", compound=LEFT, command=lambda:self.handle_neighbor_operations("LAPLASJAN 3", self.border.get())) medianM.add_command(label="Maska 3x3", compound=LEFT, command=lambda:self.handle_neighbor_operations("MEDIAN 3", self.border.get())) medianM.add_command(label="Maska 5x5", compound=LEFT, command=lambda:self.handle_neighbor_operations("MEDIAN 5", self.border.get())) medianM.add_command(label="Maska 7x7", compound=LEFT, command=lambda:self.handle_neighbor_operations("MEDIAN 7", self.border.get())) # OPECJE ANALIZY imageAnalize.add_command(label="Wyznaczenie wektora cech obiektów", compound=LEFT, command=self.handle_objects_vector) imageAnalize.add_command(label="Klasyfikacja obiektów fasoli, ryżu i grochu", compound=LEFT, command=self.handle_classify) # DODANIE GŁÓWNYCH ZAKŁADEK topMenu.add_cascade(label="Obraz", menu=self.dsc) topMenu.add_cascade(label="Manipulacja histogramem", menu=histMan) topMenu.add_cascade(label="Operacje", menu=imageMan) topMenu.add_cascade(label="Analiza", menu=imageAnalize) self.config(menu = topMenu) def update_visible_image(self): self.set_images() self.photoImage = ImageTk.PhotoImage(self.imageFromArray) self.imagePanel.delete("all") self.imagePanel.create_image(0, 0, anchor=NW, image=self.photoImage) # ------------------- # SET CHILD WINDOWS def create_moments_window(self, colours, moments, areas, lengths, aspectRatios, extents, solidities, equivalentDiameters): for widget in self.winfo_children(): if(type(widget) == NewMomentsWindow): widget.lift() widget.focus_set() return wg = NewMomentsWindow(self, colours, moments, areas, lengths, aspectRatios, extents, solidities, equivalentDiameters) wg.focus_set() def create_morph_window(self): if not self.image.check_if_binary(): messagebox.showerror("Błąd", "Obraz musi być binarny. Wykonaj posteryzację dla wartości 2 i spróbuj ponownie.") return for widget in self.winfo_children(): if(type(widget) == NewMorphWindow): widget.lift() widget.focus_set() return wg = NewMorphWindow(self) wg.focus_set() def create_morph_line_window(self): for widget in self.winfo_children(): if(type(widget) == NewMorphLineWindow): widget.lift() widget.focus_set() return wg = NewMorphLineWindow(self) wg.focus_set() def create_custom_mask_window(self, option): if option == 1: for widget in self.winfo_children(): if(type(widget) == NewCustomMaskWindow): widget.lift() widget.focus_set() return wg = NewCustomMaskWindow(self) wg.focus_set() elif option == 0: for widget in self.winfo_children(): if(type(widget) == NewCustomMaskWindowConv): widget.lift() widget.focus_set() return wg = NewCustomMaskWindowConv(self) wg.focus_set() def create_two_args_window(self): for widget in self.master.winfo_children(): if(type(widget) == NewTwoArgsWindow): widget.lift() widget.focus_set() return wg = NewTwoArgsWindow(self.master) wg.focus_set() def create_profile_window(self): if(self.profileWindow is not None): self.profileWindow.lift() self.profileWindow.focus_set() return self.profileWindow = NewLineProfileWindow(self.name, self.lineCoords, self) def create_histogram_window(self): if(self.histogramWindow is not None): self.histogramWindow.lift() self.histogramWindow.focus_set() return self.histogramWindow = NewHistogramWindow(self.name, self) def create_stretching_window(self): for widget in self.winfo_children(): if(type(widget) == NewCustomStretchWindow): widget.lift() widget.focus_set() return wg = NewCustomStretchWindow(self) wg.focus_set() def create_lut_window(self): if(self.lutWindow is not None): self.lutWindow.lift() self.lutWindow.focus_set() return self.lutWindow = NewLutWindow(self.name, self) def update_child_windows(self): if self.histogramWindow is not None: self.histogramWindow.update_histogram() if self.lutWindow is not None: self.lutWindow.display_lut_values() if self.profileWindow is not None: self.profileWindow.update_line(self.lineCoords) def resize_child_windows(self): offsetX = self.winfo_rootx()-self.winfo_x() offsetY = self.winfo_rooty()-self.winfo_y() if self.posterizeWindow is not None: self.posterizeWindow.geometry('%dx%d+%d+%d' % (self.winfo_width(), self.posterizeWindow.height, self.winfo_x()+offsetX, self.winfo_y()+self.winfo_height()+offsetY+2)) if self.thresholdScaleWindow is not None: self.thresholdScaleWindow.geometry('%dx%d+%d+%d' % (self.thresholdScaleWindow.width, self.winfo_height(),self.winfo_x()+self.winfo_width()+offsetX+2, self.winfo_y()+offsetY)) # ------------------- # ONE CLICK OPERATIONS def handle_classify(self): self.image.classify() self.manager.new_state(self.image.cv2Image) self.update_visible_image() self.update_child_windows() messagebox.showinfo("LEGENDA", "Zielony = ryz \n Niebieski = soczewica \n Czerwony = fasola") def handle_objects_vector(self): colours, moments, areas, lengths, aspectRatios, extents, solidities, equivalentDiameters = self.image.get_objects_vector() self.manager.new_state(self.image.cv2Image) self.update_visible_image() self.update_child_windows() self.create_moments_window(colours, moments, areas, lengths, aspectRatios, extents, solidities, equivalentDiameters) def handle_watershed(self): self.image.my_watershed() self.image.copy = self.image.cv2Image self.manager.new_state(self.image.cv2Image) self.update_visible_image() self.update_child_windows() def equalize_image(self): self.image.equalize() self.manager.new_state(self.image.cv2Image) self.update_visible_image() self.update_child_windows() def stretch_image(self, oldMin=None, oldMax=None, newMini=None, newMaxi=None): self.image.stretch(oldMin, oldMax, newMini, newMaxi) self.manager.new_state(self.image.cv2Image) self.update_visible_image() self.update_child_windows() def negate_image(self): self.image.negate() self.manager.new_state(self.image.cv2Image) self.update_visible_image() self.update_child_windows() def threshold_image(self, method): if method == "BASIC": if self.thresholdScaleWindow is None: self.thresholdScaleWindow = NewSliderWindow(self) else: self.thresholdScaleWindow.cancel() self.thresholdScaleWindow = NewSliderWindow(self) elif method == "ADAPT": if self.thresholdScaleWindow is None: self.thresholdScaleWindow = NewAdaptiveSliderWindow(self) else: self.thresholdScaleWindow.cancel() self.thresholdScaleWindow = NewAdaptiveSliderWindow(self) elif method == "OTSU": self.image.threshold_otsu() self.manager.new_state(self.image.cv2Image) self.update_visible_image() self.update_child_windows() def posterize_image(self): if self.posterizeWindow is None: self.posterizeWindow = NewPosterizeWindow(self.name, self) self.manager.new_state(self.image.cv2Image) self.update_visible_image() self.update_child_windows() def handle_neighbor_operations(self, operation, borderOption): self.image.neighbor_operations(operation, borderOption) self.manager.new_state(self.image.cv2Image) self.update_visible_image() self.update_child_windows() # ------------------- # ON EVENT def click(self, e): self.lineCoords["x"] = e.x self.lineCoords["y"] = e.y self.dsc.entryconfigure('Linia profilu', state=NORMAL) def drag(self, e): self.lineCoords["x2"] = e.x self.lineCoords["y2"] = e.y if self.line is not None: self.imagePanel.delete(self.line) self.line = self.imagePanel.create_line(self.lineCoords["x"], self.lineCoords["y"], self.lineCoords['x2'], self.lineCoords['y2'], fill='red', dash=(2, 2)) if self.profileWindow is not None: self.profileWindow.update_line(self.lineCoords) def resize_img(self, event): self.resize_child_windows() #self.newWidth = event.width #self.newHeight = event.height #self.imageFromArray = self.imageCopy.resize((self.newWidth, self.newHeight)) self.photoImage = ImageTk.PhotoImage(self.imageFromArray) self.imagePanel.create_image(0, 0, anchor=NW, image=self.photoImage) # -------------------
class Application: def __init__(self): self.root = Tk() VARIABLES.set_master(self.root) self.root.wm_title('asterios') self._init_menu() paned_window = PanedWindow(self.root, orient='vertical') nb = Notebook(paned_window) self.tips_text = Text(nb) nb.add(self.tips_text, text='tips') self.puzzle_text = PuzzleViewer(nb) nb.add(self.puzzle_text, text='puzzle') self.solver_text = CodeEditor(nb) nb.add(self.solver_text, text='solver') self.solver_text.insert( 'end', textwrap.dedent('''\ def solve(puzzle): """ Complete cette fonction """ puzzle_solved = '...' return puzzle_solved ''') ) self.notif_text = NotificationViewer(paned_window, relief='ridge', border=3) paned_window.add(nb, sticky='nsew') paned_window.add(self.notif_text, sticky='nsew', minsize=80) paned_window.pack(fill='both', expand=1) self.show_configuration_window() def _init_menu(self): self.menubar = Menu(self.root) self.root.config(menu=self.menubar) self.menubar.insert_command(1, label="config", command=self.show_configuration_window) self.menubar.insert_command(2, label="run", command=self.solve) def show_configuration_window(self): Configurator(self, start_command=self.start_game) def run_menu_swith(self, **cfg): self.menubar.entryconfigure(2, cfg) def run_menu_restart(self): self.menubar.entryconfigure(2, label='run', command=self.solve) def notify(self, message, severity=None): self.notif_text.notify(message, severity) def start(self): self.root.mainloop() def start_game(self): self.set_puzzle_and_tips_text() def set_puzzle_and_tips_text(self): url = '{host}/asterios/{team}/member/{member_id}'.format( host=VARIABLES.host.get(), team=VARIABLES.team.get(), member_id=VARIABLES.member_id.get() ) request = Request(url, method='GET') # headers=dict(headers) try: response = urlopen(request) # timeout=120 except HTTPError as error: msg = error.read().decode('utf-8') try: data = json.loads(msg) except: self.notify(msg) else: if data['exception'] == 'LevelSet.DoneException': GameOverToplevel(self.root, 'Victory') else: self.notify(msg) else: data = json.loads(response.read().decode('utf-8')) self.tips_text.delete('0.0', 'end') self.tips_text.insert('end', data['tip']) self.puzzle_text.update_text(data['puzzle']) VARIABLES.puzzle = data['puzzle'] def _display_solved_puzzle(self, solved_puzzle): """ Shows a Toplevel containing the `solved_puzzle`. """ top_level = Toplevel(self.root) viewer = PuzzleViewer(top_level) viewer.pack() viewer.update_text(solved_puzzle) top_level.wm_transient(self.root) def solve(self): def filter_traceback(tb): new_tb = tb[:1] iter_tb = iter(tb) expected_line = ' File "{}",'.format(VARIABLES.tmp_file) for line in iter_tb: if line.startswith(expected_line): new_tb.append(line) break new_tb.extend(iter_tb) return new_tb code = self.solver_text.get('0.0', 'end') with VARIABLES.tmp_file.open('w') as py_file: py_file.write(code) try: importlib.invalidate_caches() if hasattr(VARIABLES.module_solver, 'solve'): del VARIABLES.module_solver.solve if VARIABLES.module_solver is None: VARIABLES.module_solver = importlib.import_module( 'astrios_solver') else: VARIABLES.module_solver = importlib.reload( VARIABLES.module_solver) except Exception as error: tb = traceback.format_exception( type(error), error, error.__traceback__) tb = filter_traceback(tb) self.notify(''.join(tb), 'error') return if not (hasattr(VARIABLES.module_solver, 'solve') and callable(VARIABLES.module_solver.solve)): self.notify('Error: `solve` function not found', 'error') return def target(queue): """ Try to run solve function and push the result in the `queue` (True, result) or (False, error) """ try: solution = VARIABLES.module_solver.solve(VARIABLES.puzzle) queue.put((True, solution)) except Exception as error: tb = traceback.format_exception( type(error), error, error.__traceback__) tb = filter_traceback(tb) queue.put((False, tb)) queue = multiprocessing.Queue() process = multiprocessing.Process( target=target, args=(queue,)) def wait_for_solver(): try: success, solution_or_error = queue.get_nowait() except Empty: self.root.after(250, wait_for_solver) return None # except OSError: # if not success: self.notify(''.join(solution_or_error), 'error') else: self._display_solved_puzzle(solution_or_error) try: solution = json.dumps(solution_or_error) except (ValueError, TypeError) as error: self.notify('The solve function should return a' ' JSON serializable object ({})'.format(error), 'error') return url = '{host}/asterios/{team}/member/{member_id}'.format( host=VARIABLES.host.get(), team=VARIABLES.team.get(), member_id=VARIABLES.member_id.get() ) try: # headers=dict(headers) request = Request(url, method='POST') except ValueError: self.notify('Error: Wrong url: `{}`'.format(url), 'error') else: request.data = solution.encode('utf-8') try: response = urlopen(request) # timeout=120 except HTTPError as error: self.notify(json.loads(error.read().decode('utf-8'))) else: self.notify(json.loads( response.read().decode('utf-8')), 'success') self.set_puzzle_and_tips_text() self.run_menu_restart() process.start() def kill(): queue.put((False, 'killed')) process.terminate() self.run_menu_restart() self.run_menu_swith(label='kill', command=kill) self.root.after(0, wait_for_solver)
class Fenetre: def __init__(self, root, job): # Récupération de l'objet Jeu self.jeu = job self.saved = True # Création de la fenêtre principale self.root = root self.root.title('Rêve de Dragon') self.root.resizable(True, True) # Création des menus # On a 4 menu principaux : filemenu, cmdmenu, viewmenu et helpmenu self.menubar = Menu(root) self.root.config(menu=self.menubar) # filemenu: menu de manipulation des fichiers contenant les personnages self.filemenu = Menu(self.menubar, tearoff=0) self.menubar.add_cascade(label="Fichier", menu=self.filemenu) self.filemenu.add_command(label="Nouveau", command=self.nouveau) self.filemenu.add_command(label="Ouvrir", command=self.ouvrir) self.filemenu.add_separator() self.filemenu.add_command(label="Enregistrer", command=self.jeu.enregistrer) self.filemenu.add_command(label="Enregistrer sous...", command=self.jeu.enregistrer_sous) self.filemenu.add_separator() self.filemenu.add_command(label="Fermer", command=self.fermer) self.filemenu.add_separator() self.filemenu.add_command(label="Imprimer", command=self.void, state='disabled') self.filemenu.add_separator() self.filemenu.add_command(label="Quitter", command=self.quitter) # cmdmenu: menu des commandes sur les personnages self.cmdmenu = Menu(self.menubar, tearoff=0) self.menubar.add_cascade(label="Commande", menu=self.cmdmenu) self.cmdmenu.add_command(label="Nouvelle Partie", command=self.partie) self.cmdmenu.add_separator() self.cmdmenu.add_command(label="Nouveau Personnage", command=self.creer) self.cmdmenu.add_separator() self.cmdmenu.add_command(label="Valider le Personnage", command=self.valider) # viewmenu: menu de sélection du personnage à l'affichage # Ce menu est vide en l'absence de personnage # Il est rempli au chargement ou à la création d'un personnage self.viewmenu = Menu(self.menubar, tearoff=0) self.menubar.add_cascade(label="Personnage", menu=self.viewmenu) # helpmenu: menu d'aide self.helpmenu = Menu(self.menubar, tearoff=0) self.menubar.add_cascade(label="Aide", menu=self.helpmenu) self.helpmenu.add_command(label="Règles du Jeu", command=self.regles) self.helpmenu.add_command(label="Utilisation du Programme", command=self.utilise) self.helpmenu.add_command(label="A Propos...", command=self.a_propos) # frame1 : Fiche du personnage frame1 = Frame(root, borderwidth=0, relief='flat', height=200, width=600) frame1.grid(row=0, column=0, sticky='NW', padx="10", pady="5") # Nom self.Entry_Nom = StringVar() self.Old_Nom = "" Label(frame1, text='Nom:').grid(row=0, column=0, columnspan=2, sticky='E') Entry(frame1, textvariable=self.Entry_Nom, justify='left', width=34)\ .grid(row=0, column=2, columnspan=4, sticky='W', padx="5") # Age self.Entry_Age = IntVar() Label(frame1, text='Age:').grid(row=1, column=0, columnspan=2, sticky='E') Entry(frame1, textvariable=self.Entry_Age, justify='right', width=3)\ .grid(row=1, column=2, sticky='W', padx="5") # Heure de naissance (pour hauts-rêvants) self.Entry_Heure = IntVar() Label(frame1, text='Heure de Naissance:').grid(row=1, column=3, sticky='E') Entry(frame1, textvariable=self.Entry_Heure, justify='right', width=3) \ .grid(row=1, column=4, sticky='W', padx="5") # Taille self.Entry_Taille = IntVar() Label(frame1, text='Taille:').grid(row=1, column=5, sticky='E') Entry(frame1, textvariable=self.Entry_Taille, justify='right', width=3)\ .grid(row=1, column=6, sticky='W', padx="5") # Poids self.Entry_Poids = IntVar() Label(frame1, text='Poids:').grid(row=1, column=7, sticky='E') Entry(frame1, textvariable=self.Entry_Poids, justify='right', width=3)\ .grid(row=1, column=8, sticky='W', padx="5") # Beauté self.Entry_Beaute = IntVar() Label(frame1, text='Beauté:').grid(row=2, column=0, columnspan=2, sticky='E') Entry(frame1, textvariable=self.Entry_Beaute, justify='right', width=3) \ .grid(row=2, column=2, sticky='W', padx="5") # Cheveux self.Entry_Cheveux = StringVar() Label(frame1, text='Cheveux:').grid(row=2, column=3, sticky='E') Entry(frame1, textvariable=self.Entry_Cheveux, justify='left', width=8)\ .grid(row=2, column=4, sticky='W', padx="5") # Yeux self.Entry_Yeux = StringVar() Label(frame1, text='Yeux:').grid(row=2, column=5, sticky='E') Entry(frame1, textvariable=self.Entry_Yeux, justify='left', width=8)\ .grid(row=2, column=6, sticky='W', padx="5") # Haut rêvant self.Entry_HRevant = IntVar() Checkbutton(frame1, text="Haut-Rêvant", variable=self.Entry_HRevant, command=self.sel_revant) \ .grid(row=2, column=7, columnspan=2, sticky='W', padx="5") # Sexe self.Entry_Sexe = StringVar() Label(frame1, text='Sexe:').grid(row=3, column=0, columnspan=2, sticky='E') Entry(frame1, textvariable=self.Entry_Sexe, justify='left', width=2)\ .grid(row=3, column=2, sticky='W', padx="5") # Ambidextre self.Entry_Ambidextre = IntVar() Label(frame1, text='Ambidextre:').grid(row=3, column=3, sticky='E') Entry(frame1, textvariable=self.Entry_Ambidextre, justify='right', width=3)\ .grid(row=3, column=4, sticky='W', padx="5") # Signes Particuliers self.Entry_SignesP = StringVar() Label(frame1, text='Signes Particuliers:').grid(row=3, column=5, sticky='E') Entry(frame1, textvariable=self.Entry_SignesP, justify='left', width=37)\ .grid(row=3, column=6, columnspan=3, sticky='W', padx="5") # Frame 2 : Caractéristiques frame2 = LabelFrame(root, text=" Caractéristiques ", borderwidth=2, relief='ridge', height=200, width=600) frame2.grid(row=1, column=0, sticky='NW', padx="10", pady="5") frame20 = LabelFrame(frame2, text=' Physiques ', borderwidth=2, relief='ridge', height=200, width=200) frame20.grid(row=0, column=0, sticky='NW', padx="5", pady="5") frame21 = LabelFrame(frame2, text=' Mentales ', borderwidth=2, relief='ridge', height=200, width=200) frame21.grid(row=0, column=1, sticky='NW', padx="5", pady="5") frame22 = LabelFrame(frame2, text=' Pouvoirs ', borderwidth=2, relief='ridge', height=200, width=200) frame22.grid(row=0, column=2, sticky='NW', padx="5", pady="5") frame23 = LabelFrame(frame2, text=' Dérivées ', borderwidth=2, relief='ridge', height=200, width=200) frame23.grid(row=0, column=3, sticky='NW', padx="5", pady="5") self.Entry_C = [] # Colonne 0 de taille à Dextérité for i in range(0, 6): self.Entry_C.append(IntVar()) Label(frame20, text=" "+personnage.caracteristique(i, 1)+':')\ .grid(row=i, column=0, sticky='E') Entry(frame20, textvariable=self.Entry_C[i], justify='right', width=3)\ .grid(row=i, column=1, sticky='W', padx="5") Label(frame20, text=' ').grid(row=6, column=0, sticky='E') # Colonne 1 de Vue à Empathie for i in range(6, 12): self.Entry_C.append(IntVar()) Label(frame21, text=" "+personnage.caracteristique(i, 1) + ':')\ .grid(row=i-6, column=0, sticky='E') Entry(frame21, textvariable=self.Entry_C[i], justify='right', width=3)\ .grid(row=i-6, column=1, sticky='W', padx="5") Label(frame21, text=' ').grid(row=6, column=0, sticky='E') # Colonne 2 de Rêve à Chance for i in range(12, 14): self.Entry_C.append(IntVar()) Label(frame22, text=" "+personnage.caracteristique(i, 1) + ':')\ .grid(row=i-12, column=0, sticky='E') Entry(frame22, textvariable=self.Entry_C[i], justify='right', width=3)\ .grid(row=i-12, column=1, sticky='W', padx="5") for i in range(2, 7): Label(frame22, text=' ').grid(row=i, column=0, sticky='E') # Colonne 3 de Tir à Dérobée (ne peuvent être saisies) for i in range(14, 18): self.Entry_C.append(IntVar()) Label(frame23, text=" "+personnage.caracteristique(i, 1) + ':')\ .grid(row=i-14, column=0, sticky='E') Entry(frame23, textvariable=self.Entry_C[i], justify='right', width=3, state='disabled')\ .grid(row=i-14, column=1, sticky='W', padx="5") for i in range(4, 7): Label(frame23, text=' ').grid(row=i, column=0, sticky='E') # frame 3 : Points et Seuils (ne peuvent être saisis) frame3 = Frame(root, borderwidth=0, relief='flat', height=200, width=600, padx="5", pady="5") frame3.grid(row=2, column=0, sticky='NW', padx="10") self.Entry_P = [] # Vie - Endurance - Encombrement for i in range(0, 3): self.Entry_P.append(IntVar()) Label(frame3, text=personnage.point(i, 1) + ':').grid(row=0, column=2 * i, sticky='E') Entry(frame3, textvariable=self.Entry_P[i], justify='right', width=3, state='disabled')\ .grid(row=0, column=2*i+1, sticky='W', padx="5") # Bonus aux Dommages - Malus Armure - Seuil de Constitution - Seuil de Sustentation for i in range(3, 7): self.Entry_P.append(IntVar()) Label(frame3, text=personnage.point(i, 1) + ':').grid(row=1, column=2 * i - 6, sticky='E') Entry(frame3, textvariable=self.Entry_P[i], justify='right', width=3, state='disabled')\ .grid(row=1, column=2*i-5, sticky='W', padx="5") # frame 4 : Compétences frame4 = LabelFrame(root, text=" Compétences ", borderwidth=2, relief='ridge', height=200, width=800) frame4.grid(row=3, column=0, columnspan=2, sticky='NW', padx="10", pady="5") frame40 = LabelFrame(frame4, text=' Générales ', borderwidth=2, relief='ridge', height=200, width=300) frame40.grid(row=0, column=0, rowspan=2, sticky='NW', padx="5", pady="5") frame41 = LabelFrame(frame4, text=' Particulières ', borderwidth=2, relief='ridge', height=200, width=300) frame41.grid(row=0, column=1, rowspan=2, sticky='NW', padx="5", pady="5") frame42 = LabelFrame(frame4, text=' Spécialisées ', borderwidth=2, relief='ridge', height=200, width=300) frame42.grid(row=0, column=2, rowspan=2, sticky='NW', padx="5", pady="5") frame43 = LabelFrame(frame4, text=' Connaissances ', borderwidth=2, relief='ridge', height=200, width=300) frame43.grid(row=0, column=3, sticky='NW', padx="5", pady="5") frame44 = LabelFrame(frame4, text=' Draconic ', borderwidth=2, relief='ridge', height=200, width=300) frame44.grid(row=1, column=3, sticky='SW', padx="5", pady="5") frame45 = LabelFrame(frame4, text=' Combat Mélée ', borderwidth=2, relief='ridge', height=200, width=300) frame45.grid(row=0, column=4, rowspan=2, sticky='NW', padx="5", pady="5") frame46 = LabelFrame(frame4, text=' Combat Tir-Lancer ', borderwidth=2, relief='ridge', height=200, width=300) frame46.grid(row=0, column=5, rowspan=2, sticky='NW', padx="5", pady="5") self.Entry_A = [] # Colonne 0 : Générales for i in range(0, 11): self.Entry_A.append(IntVar()) Label(frame40, text=" " + personnage.competence(i, 2) + ':').grid( row=i, column=0, sticky='E') Entry(frame40, textvariable=self.Entry_A[i], justify='right', width=3)\ .grid(row=i, column=1, sticky='W', padx="5") for i in range(11, 15): Label(frame40, text=' ').grid(row=i, column=0, sticky='E') # Colonne 1 : Particulières for i in range(11, 26): self.Entry_A.append(IntVar()) Label(frame41, text=" " + personnage.competence(i, 2) + ':').grid(row=i - 11, column=0, sticky='E') Entry(frame41, textvariable=self.Entry_A[i], justify='right', width=3)\ .grid(row=i-11, column=1, sticky='W', padx="5") # Colonne 2 : Spécialisées for i in range(26, 36): self.Entry_A.append(IntVar()) Label(frame42, text=" "+personnage.competence(i, 2)+':')\ .grid(row=i-25, column=0, sticky='E') Entry(frame42, textvariable=self.Entry_A[i], justify='right', width=3)\ .grid(row=i-25, column=1, sticky='W', padx="5") for i in range(10, 15): Label(frame42, text=' ').grid(row=i + 1, column=0, sticky='E') # Colonne 3: Connaissances for i in range(36, 43): self.Entry_A.append(IntVar()) Label(frame43, text=" "+personnage.competence(i, 2)+':')\ .grid(row=i-35, column=0, sticky='E') Entry(frame43, textvariable=self.Entry_A[i], justify='right', width=3)\ .grid(row=i-35, column=1, sticky='W', padx="5") Label(frame43, text=' ').grid(row=8, column=0, sticky='E') # Colonne 3 : Draconic self.Draconic = [] for i in range(0, 4): self.Entry_A.append(IntVar()) Label(frame44, text=" "+personnage.competence(i+43, 2)+':')\ .grid(row=i, column=0, sticky='E') self.Draconic.append( Entry(frame44, textvariable=self.Entry_A[i + 43], justify='right', width=3)) self.Draconic[i].grid(row=i, column=1, sticky='W', padx="5") Label(frame44, text=' ').grid(row=4, column=0, sticky='E') # Colonne 4 : Combat Mélée for i in range(47, 60): self.Entry_A.append(IntVar()) Label(frame45, text=personnage.competence(i, 2) + ':') \ .grid(row=i - 46, column=0, sticky='E') Entry(frame45, textvariable=self.Entry_A[i], justify='right', width=3) \ .grid(row=i - 46, column=1, sticky='W', padx="5") for i in range(13, 15): Label(frame45, text=' ').grid(row=i + 1, column=0, sticky='E') # Colonne 5 : Combat Tir for i in range(60, 66): self.Entry_A.append(IntVar()) Label(frame46, text=" " + personnage.competence(i, 2) + ':') \ .grid(row=i - 59, column=0, sticky='E') Entry(frame46, textvariable=self.Entry_A[i], justify='right', width=3) \ .grid(row=i - 59, column=1, sticky='W', padx="5") for i in range(6, 15): Label(frame46, text=' ').grid(row=i + 1, column=0, sticky='E') # frame5 : table de résolution et lancer de dé frame5 = LabelFrame(root, text=" Résolution et Lancer de Dés ", borderwidth=2, relief='ridge', height=200, width=600) frame5.grid(row=0, column=1, rowspan=3, columnspan=2, sticky='NW', padx="10", pady="5") # Listbox caractéristiques Label(frame5, text=' Caractéristique:').grid(row=0, column=0, columnspan=2, padx="10", sticky='NW') self.liste1 = Listbox(frame5, height=13, width=18, relief='sunken') self.liste1.grid(row=1, column=1, sticky='NW', pady="5") for i in range(0, 18): self.liste1.insert(i, personnage.caracteristique(i, 1)) self.liste1.bind('<<ListboxSelect>>', self.sel_liste1) # Listbox compétences Label(frame5, text='Compétence:').grid(row=0, column=2, columnspan=2, padx="10", sticky='NW') self.liste2 = Listbox(frame5, height=13, width=18, relief='sunken') self.liste2.grid(row=1, column=3, sticky='NW', pady="5") for i in range(0, 66): self.liste2.insert(i, personnage.competence(i, 2)) self.liste2.bind('<<ListboxSelect>>', self.sel_liste2) # Zone de résulats self.Entry_R_C_Val = IntVar() Entry(frame5, textvariable=self.Entry_R_C_Val, justify='right', width=3,) \ .grid(row=16, column=0, sticky='E', padx="10") self.Entry_R_C_Name = StringVar() Entry(frame5, textvariable=self.Entry_R_C_Name, justify='left', width=18, state='disabled') \ .grid(row=16, column=1, sticky='W') self.Entry_R_A_Val = IntVar() Entry(frame5, textvariable=self.Entry_R_A_Val, justify='right', width=3,) \ .grid(row=16, column=2, sticky='E', padx="10") self.Entry_R_A_Name = StringVar() Entry(frame5, textvariable=self.Entry_R_A_Name, justify='left', width=18, state='disabled') \ .grid(row=16, column=3, sticky='W') Label(frame5, text=' Seuil de Réussite:').grid(row=17, column=0, sticky='NE') self.Entry_R_Seuil = IntVar() Entry(frame5, textvariable=self.Entry_R_Seuil, justify='right', width=3, state='disabled')\ .grid(row=17, column=1, sticky='W', padx="10") Label(frame5, text='Tirage:').grid(row=17, column=2, sticky='NE') self.Entry_R_Tirage = IntVar() Entry(frame5, textvariable=self.Entry_R_Tirage, justify='right', width=3, state='disabled') \ .grid(row=17, column=3, sticky='W', padx="10") Label(frame5, text='Résultat Spécial:').grid(row=18, column=0, sticky='NE') self.Entry_R_Special = StringVar() Entry(frame5, textvariable=self.Entry_R_Special, justify='left', width=30, state='disabled') \ .grid(row=18, column=1, columnspan=2, sticky='W', padx="10") Label(frame5, text=' ').grid(row=19, column=4, sticky='NE') # Bouton pour le lancer de Dés Button(frame5, text="Lancer les Dés", command=self.lancer) \ .grid(row=18, column=3, columnspan=3, sticky='W', padx="10") # La mascote # On la fait déborder sur le frame4 pour gagner en largeur totale self.dragon = PhotoImage(file='./dragon3.gif') logo = Canvas(root, width=200, height=181, bd=1, relief='ridge') logo.grid(row=3, column=1, columnspan=2, sticky='SE', padx="10", pady="3") logo.create_image(0, 0, image=self.dragon, anchor='nw') # L'ecran étant initialisé, on peut créér un premier personnage par défaut self.creer() return # Fonction de recopie de la sélection depuis la Listbox des caractéristiques # Met à jour les 2 champs points et nom de caractéristique pour le calcul de résolution def sel_liste1(self, event): if self.liste1.curselection() != (): index = self.liste1.curselection()[0] self.Entry_R_C_Name.set(self.liste1.get(index)) self.Entry_R_C_Val.set(self.Entry_C[index].get()) return # Fonction de recopie de la sélection depuis la Listbox des compétences # Met à jour les 2 champs points et nom de compétence pour le calcul de résolution def sel_liste2(self, event): if self.liste2.curselection() != (): index = self.liste2.curselection()[0] self.Entry_R_A_Name.set(self.liste2.get(index)) self.Entry_R_A_Val.set(self.Entry_A[index].get()) return # Fonction de changement d'etat haut-rêvant def sel_revant(self): if self.Entry_HRevant.get() != 1: for i in range(0, 4): self.Entry_A[i + 42].set(-11) self.Draconic[i].configure(state='disabled') else: for i in range(0, 4): self.Draconic[i].configure(state='normal') return # Nouveau jeu # Il faut préalablement fermer le jeu en cours def nouveau(self): if self.fermer(): self.jeu.nouveau() return # Ouvrir jeu # Il faut préalablement fermer le jeu en cours # On reçoit le nom du jeu suivi d'une liste de personnages ou None si rien d'ouvert par le jeu def ouvrir(self): if self.fermer(): names = self.jeu.ouvrir() if names != None: numero = -1 for person in names: # index 0 : nom du fichier jeu if numero < 0: self.root.title('Rêve de Dragon - ' + person) # autres index : personnages # index vaudra le nombre de personnages reçus else: self.viewmenu.add_command(label=person, command=lambda index=numero: self.selectionner(index)) numero += 1 # On affiche le premier personnage if numero > 0: self.selectionner(0) return # Fermer le jeu en cours # On efface tous les personnages # on cree un nouveau personnage vide pour obtenir un affichage vierge def fermer(self): if not self.saved: self.saved = askyesno( 'Fermer', 'Voulez-vous vraiment fermer ce Jeu ?\nLes données non enregistrées seront perdues' ) if self.saved: last = self.viewmenu.index("end") if last is not None: for i in range(last + 1): self.viewmenu.delete(0) self.root.title('Rêve de Dragon') self.jeu.fermer() self.creer() return self.saved # Quitter le programme # onh détruit la fenêtre et on quitte def quitter(self): if askyesno('Quitter', 'Voulez-vous vraiment quitter le programme ?'): self.root.destroy() self.root.quit() return # Fonction interne d'affichage des données d'un personnage # Copie toutes les données du dictionnaire local dans les variables associées aux champs de saisie def affiche(self): self.Entry_Nom.set(self.pod["Fiche"]["Nom"]) self.Entry_Age.set(self.pod["Fiche"]["Age"]) self.Entry_Heure.set(self.pod["Fiche"]["Heure_Naissance"]) self.Entry_Taille.set(self.pod["Fiche"]["Taille"]) self.Entry_Poids.set(self.pod["Fiche"]["Poids"]) self.Entry_Sexe.set(self.pod["Fiche"]["Sexe"]) self.Entry_Cheveux.set(self.pod["Fiche"]["Cheveux"]) self.Entry_Yeux.set(self.pod["Fiche"]["Yeux"]) self.Entry_Beaute.set(self.pod["Fiche"]["Beaute"]) self.Entry_Ambidextre.set(self.pod["Fiche"]["Ambidextre"]) self.Entry_HRevant.set(self.pod["Fiche"]["Haut_Revant"]) self.Entry_SignesP.set(self.pod["Fiche"]["Signes_Particulier"]) for i in range(0, 18): self.Entry_C[i].set( self.pod["Caracteristique"][personnage.caracteristique(i, 0)]) for i in range(0, 7): self.Entry_P[i].set(self.pod["Point"][personnage.point(i, 0)]) for i in range(0, 66): self.Entry_A[i].set(self.pod["Competence"][personnage.competence( i, 0)][personnage.competence(i, 1)]) if self.Entry_HRevant.get() != 1: for i in range(0, 4): self.Draconic[i].configure(state='disabled') return # Création d'un nouveau personnage # On demande au jeu de créer un nouveau personnage dans la liste # On initialise toutes les variables de saisie aux valeur reçues def creer(self): self.pod = self.jeu.creer() self.affiche() return # Validation des données du personnage # On reconstitue le dictionnaire qui est envoyé au jeu pour vérification # Le jeu répond avec un dictionnaire contenant # - l'index du personnage # - Le nom de personnage # - Un message d'erreur ou d'acceptation def valider(self): if len(self.Entry_Nom.get()) < 1: return self.pod["Fiche"]["Nom"] = self.Entry_Nom.get() self.pod["Fiche"]["Age"] = self.Entry_Age.get() self.pod["Fiche"]["Heure_Naissance"] = self.Entry_Heure.get() self.pod["Fiche"]["Taille"] = self.Entry_Taille.get() self.pod["Fiche"]["Poids"] = self.Entry_Poids.get() self.pod["Fiche"]["Sexe"] = self.Entry_Sexe.get() self.pod["Fiche"]["Cheveux"] = self.Entry_Cheveux.get() self.pod["Fiche"]["Yeux"] = self.Entry_Yeux.get() self.pod["Fiche"]["Beaute"] = self.Entry_Beaute.get() self.pod["Fiche"]["Ambidextre"] = self.Entry_Ambidextre.get() self.pod["Fiche"]["Haut_Revant"] = self.Entry_HRevant.get() self.pod["Fiche"]["Signes_Particulier"] = self.Entry_SignesP.get() for i in range(0, 18): self.pod["Caracteristique"][personnage.caracteristique( i, 0)] = self.Entry_C[i].get() for i in range(0, 7): self.pod["Point"][personnage.point(i, 0)] = self.Entry_P[i].get() for i in range(0, 65): self.pod["Competence"][personnage.competence( i, 0)][personnage.competence(i, 1)] = self.Entry_A[i].get() retour = self.jeu.valider(self.pod) index = retour["index"] # On a bien un index valide : alors on met à jour le menu et on va chercher les données du personnage if index is not None: if self.viewmenu.entrycget(index, 'label') != self.Old_Nom: self.viewmenu.entryconfigure(index, label=retour["nom"]) elif self.viewmenu.entrycget(index, 'label') != retour["nom"]: self.viewmenu.add_command( label=retour["nom"], command=lambda index=index: self.selectionner(index)) self.pod = self.jeu.selectionner(index) self.affiche() # En cas d'erreur ou retour ok, il y a un message du jeu if len(retour["message"]): showerror("Validation", retour["message"]) self.saved = False return # Sélection d'un personnage depuis le menu # On envoie au jeu l'index du menu qui correspond à l'index de la liste du jeu # Les données du personnage reçu sont ensuite affichées def selectionner(self, code): self.pod = self.jeu.selectionner(code) self.affiche() return # Nouvelle partie # On demande au Jeu de changer de partie # Le jeu renvoie le contenu du personnage courant def partie(self): if askyesno( 'Nouvelle Partie', 'Voulez-vous vraiment terminer cette partie ?\nLes données non enregistrées seront perdues' ): self.pod = self.jeu.partie() self.affiche() return # Lancer de dé # Calcul de résolution puis lancer des dés # On passe au jeu les valeurs de caractéristiques et compétences sélectionnées # Le résultat sera récupéré en retour et affiché def lancer(self): resultat = self.jeu.lancer(self.Entry_R_C_Val.get(), self.Entry_R_A_Val.get()) self.Entry_R_Seuil.set(resultat["seuil"]) self.Entry_R_Tirage.set(resultat["tirage"]) self.Entry_R_Special.set(resultat["special"]) self.saved = False return # Affichage de la boite de dialogue A propos # Le texte est dans le fichier A_PROPOS.TXT def a_propos(self): fp = open("A_PROPOS.TXT", "r") texte_a_propos = fp.read() fp.close() showinfo("A Propos de...", texte_a_propos) return # Dialogue d'aide pour connaitre les règles du jeu # Le texte est dans le fichier REGLES.TXT def regles(self): file = "REGLE.TXT" titre = "Règles du Jeu Rêve de Dragon." self.aide(file, titre) return # Le texte est dans le fichier AIDE.TXT def utilise(self): file = "AIDE.TXT" titre = "Utilisation de Rêve de Dragon." self.aide(file, titre) return # Aide du jeu # Affiche une boite de dialogue avec un widget texte contenant l'aide def aide(self, file, titre): # On ouvre une fenêtre fille de celle du jeu self.wdw = Toplevel() self.wdw.geometry('+400+100') self.wdw.title(titre) # Le texte de l'aide est stocké dans un fichier Atexte fp = open(file, "r") texte_aide = fp.read() fp.close() # On l'affiche dans un widget Text avec une barre de défilement # Ne fonctionne que si on utilise grid pour placer les Widgets # Il faut mettre le widget en état disabled pour éviter que l'on y entre du texte self.S = Scrollbar(self.wdw, orient='vertical') self.T = Text(self.wdw, height=50, width=100, font=('TkDefaultFont', 10)) self.T.grid(row=0, column=0, sticky='NW') self.S.configure(command=self.T.yview) self.T.configure(yscrollcommand=self.S.set) self.S.grid(row=0, column=1, sticky='SN') self.T.configure(state='normal') self.T.insert('end', texte_aide) self.T.configure(state='disabled') # La nouvelle fenêtre est ouverte en modal # Il faudra la fermer pour reprendre le contrôle de la fenêtre principale self.wdw.transient(self.root) self.wdw.grab_set() self.root.wait_window(self.wdw) return # fonction qui ne fait rien (pour les tests) def void(self): return
text_menu.add_command(label="粘贴", command=callback) text_menu.add_command(label="撤销", command=callback) text_menu.add_separator() text_menu.add_command(label="重置", command=callback) text_menu.add_command(label="新建窗口", command=createWindow) # 一级菜单,直接放在菜单栏上,窗口菜单栏上不会生效 menuBar.add_separator() menuBar.add_command(label="重置2", command=callback) # 窗口菜单栏不现实 menuBar.add_command(label="新建窗口2", command=createWindow) # 窗口菜单栏不现实 # 没有置顶menu参数的情况下,在顶部菜单栏是无效的 menuBar.add_cascade(label="顶部菜单栏是无效的") # 禁用某个菜单 menuBar.entryconfigure(2, state='disabled') frame = Frame(window, width=512, height=512) frame.pack() def popup(event): # print(event.x_root + 10, event.y_root + 10) menuBar.post(event.x_root + 1, event.y_root) # frame绑定事件popup frame.bind("<Button-2>", popup) # or # frame.bind("<ButtonPress-2>", popup)
class CostumEntry(Entry): def __init__(self, *args, **kwargs): Entry.__init__(self, *args, **kwargs) self.config(highlightthickness=1, relief='flat') self.config(highlightbackground="#AAA", highlightcolor="#AAA") self.changes = [""] self.steps = int() self.context_menu = Menu(self, tearoff=0) self.context_menu.add_command(label="Cut") self.context_menu.add_command(label="Copy") self.context_menu.add_command(label="Paste") self.bind("<KeyRelease>", self.on_change) self.bind("<Control-z>", self.undo) self.bind("<Control-Shift-Z>", self.redo) self.bind("<Button-3>", self.popup) # self.focus_set() def flash(self): self.config(highlightbackground="red", highlightcolor="red") self.update_idletasks() time.sleep(0.06) self.config(highlightbackground="white", highlightcolor="white") self.update_idletasks() time.sleep(0.05) self.config(highlightbackground="red", highlightcolor="red") self.update_idletasks() time.sleep(0.04) self.config(highlightbackground="white", highlightcolor="white") self.update_idletasks() time.sleep(0.03) self.config(highlightbackground="red", highlightcolor="red") self.update_idletasks() time.sleep(0.02) self.config(highlightbackground="white", highlightcolor="white") self.update_idletasks() self.config(highlightbackground="#DDD", highlightcolor="#DDD") def on_change(self, event=None): if self.get() != self.changes[-1]: self.changes.append(self.get()) self.steps += 1 def popup(self, event): self.context_menu.post(event.x_root, event.y_root) self.context_menu.entryconfigure( "Cut", command=lambda: self.event_generate("<<Cut>>")) self.context_menu.entryconfigure( "Copy", command=lambda: self.event_generate("<<Copy>>")) self.context_menu.entryconfigure( "Paste", command=lambda: self.event_generate("<<Paste>>")) def undo(self, event=None): if self.steps != 0: self.steps -= 1 self.delete(0, 'end') self.insert('end', self.changes[self.steps]) def redo(self, event=None): if self.steps < len(self.changes): self.delete(0, 'end') self.insert('end', self.changes[self.steps]) self.steps += 1
class GUIDatasetClasificacion(): def __init__(self, root): self.root = root #TODO Esta información se lee desde .cfg con ConfigParser self.ruta_datasets = '' self.ruta_resultados = '' self.rutas_relativas = BooleanVar(root, True) self.usa_sha1 = BooleanVar(root, True) self._tamanyo_muestra = 5 self.clase_al_final = BooleanVar(root, True) self.mostrar_proceso = BooleanVar(root, False) self.lee_configuracion() estilo_bien = Style() estilo_bien.configure('G.TLabel', foreground='green') estilo_mal = Style() estilo_mal.configure('R.TLabel', foreground='red') self.crea_GUI() self.root.title(APP_NAME) # self.root.update() # self.root.minsize(self.root.winfo_width(), self.root.winfo_height()) self.root.minsize(800, 500) # self.lee_configuracion() self.root.protocol('WM_DELETE_WINDOW', self.cerrar_aplicacion) def crea_GUI(self): root = self.root #Menús self.menubar = Menu(root, tearoff=0) m_archivo = Menu(self.menubar, tearoff=0) m_archivo.add_command(label='Abrir', command=self.abrir_dataset, accelerator='Ctrl+O') m_archivo.add_separator() m_archivo.add_command(label='Salir', command=self.cerrar_aplicacion) self.menubar.add_cascade(label='Archivo', menu=m_archivo) m_proyecto = Menu(self.menubar, tearoff=0) m_proyecto.add_command(label='Abrir', command=self.abrir_proyecto, state="disabled") m_proyecto.add_separator() m_proyecto.add_checkbutton(label='Clase al final', onvalue=True, offvalue=False, variable=self.clase_al_final) self.menubar.add_cascade(label='Proyecto', menu=m_proyecto) self.m_configuracion = Menu(self.menubar, tearoff=0) self.m_configuracion.add_command( label='Ruta datasets', command=lambda: self.rutas('datasets')) self.m_configuracion.add_command( label='Ruta resultados', command=lambda: self.rutas('resultados')) self.m_configuracion.add_checkbutton(label='Rutas relativas', onvalue=True, offvalue=False, variable=self.rutas_relativas, command=self.cambia_rutas) self.m_configuracion.add_separator() #TODO Revisar self.v_tamanyo_muestra, no la uso # self.v_tamanyo_muestra = StringVar(root, 'Tamaño muestra ({:,})'.\ # format(self._tamanyo_muestra)) self.m_cfg_tamanyo_muestra = \ self.m_configuracion.add_command(label='Tamaño muestra ({:,})'.\ format(self._tamanyo_muestra), command=lambda: self.tamanyo_muestra(\ self._tamanyo_muestra)) self.m_configuracion.add_separator() self.m_configuracion.add_checkbutton(label='Utiliza sha1', onvalue=True, offvalue=False, variable=self.usa_sha1) self.menubar.add_cascade(label='Configuración', menu=self.m_configuracion) m_ver = Menu(self.menubar, tearoff=0) self.v_tipo_dataset = StringVar(self.root, 'Dataset original') m_ver.add_radiobutton(label='Dataset original', value='Dataset original', variable=self.v_tipo_dataset, command=self.muestra_atributos_y_clase) m_ver.add_radiobutton(label='Dataset sin evidencias incompletas', value='Dataset sin evidencias incompletas', variable=self.v_tipo_dataset, command=self.muestra_atributos_y_clase) m_ver.add_radiobutton(label='Dataset sin atributos constantes', value='Dataset sin atributos constantes', variable=self.v_tipo_dataset, command=self.muestra_atributos_y_clase) m_ver.add_radiobutton(label='Catálogo', value='Catálogo', variable=self.v_tipo_dataset, command=self.muestra_atributos_y_clase) m_ver.add_radiobutton(label='Catálogo Robusto', value='Catálogo Robusto', variable=self.v_tipo_dataset, command=self.muestra_atributos_y_clase) m_ver.add_separator() m_ver.add_checkbutton(label='Log del proceso', onvalue=True, offvalue=False, variable=self.mostrar_proceso, state='disabled') self.menubar.add_cascade(label='Ver', menu=m_ver) root.config(menu=self.menubar) #Dataset de clasificación lf_dataset = LabelFrame(root, text='Dataset de Clasificación') lf_dataset.pack(fill='both', expand=True, padx=5, pady=5) Label(lf_dataset, text='Nombre:').grid(row=0, column=0, sticky='e') self.v_nombre_dataset = StringVar(root, '-------') self.l_nombre_dataset = Label(lf_dataset, textvariable=self.v_nombre_dataset) self.l_nombre_dataset.grid(row=0, column=1, sticky='w') Label(lf_dataset, text='Tamaño:').grid(row=0, column=2, sticky='e') self.v_tamanyo_dataset = StringVar(root, '-------') Label(lf_dataset, textvariable=self.v_tamanyo_dataset).grid(row=0, column=3, sticky='w') Label(lf_dataset, text='Ubicación:').grid(row=1, column=0, sticky='e') self.v_ruta_dataset = StringVar(root, '-------------------------') #TODO Expandir en columnas 1-3, puede ser muy larga Label(lf_dataset, textvariable=self.v_ruta_dataset).grid(row=1, column=1, sticky='w', columnspan=3) #Dataset de clasificación / Muestra lf_dataset_muestra = LabelFrame(lf_dataset, text='Muestra') lf_dataset_muestra.grid(row=2, column=0, sticky='nsew', columnspan=4, padx=5, pady=5) self.sb_v_t_muestra = Scrollbar(lf_dataset_muestra) self.sb_v_t_muestra.grid(row=0, column=1, sticky='sn') self.sb_h_t_muestra = Scrollbar(lf_dataset_muestra, orient='horizontal') self.sb_h_t_muestra.grid(row=1, column=0, sticky='ew') self.t_muestra = Text(lf_dataset_muestra, yscrollcommand=self.sb_v_t_muestra.set, xscrollcommand=self.sb_h_t_muestra.set, bd=0, wrap='none', state='disabled', height=8) self.t_muestra.grid(row=0, column=0, sticky='nswe') self.sb_v_t_muestra.config(command=self.t_muestra.yview) self.sb_h_t_muestra.config(command=self.t_muestra.xview) lf_dataset_muestra.rowconfigure(0, weight=1) lf_dataset_muestra.columnconfigure(0, weight=1) lf_dataset.rowconfigure(2, weight=3) lf_dataset.columnconfigure(1, weight=1) lf_dataset.columnconfigure(3, weight=1) #Dataset de clasificación / Evidencias lf_dataset_evidencias = LabelFrame(lf_dataset, text='Evidencias') lf_dataset_evidencias.grid(row=3, column=0, sticky='nsew', padx=5, pady=5) Label(lf_dataset_evidencias, text='Total:').grid(row=0, column=0, sticky='e') self.v_evidencias_total = StringVar(root, '-------') Label(lf_dataset_evidencias, textvariable=self.v_evidencias_total).\ grid(row=0, column=1, sticky='w') Label(lf_dataset_evidencias, text='Completas:').grid(row=1, column=0, sticky='e') self.v_evidencias_completas = StringVar(root, '-------') Label(lf_dataset_evidencias, textvariable=self.v_evidencias_completas).\ grid(row=1, column=1, sticky='w') Label(lf_dataset_evidencias, text='Únicas:').grid(row=2, column=0, sticky='e') self.v_evidencias_catalogo = StringVar(root, '-------') Label(lf_dataset_evidencias, textvariable=self.v_evidencias_catalogo).\ grid(row=2, column=1, sticky='w') Label(lf_dataset_evidencias, text='Robustas:').grid(row=3, column=0, sticky='e') self.v_evidencias_robustas = StringVar(root, '-------') Label(lf_dataset_evidencias, textvariable=self.v_evidencias_robustas).\ grid(row=3, column=1, sticky='w') #Dataset de clasificación / Atributos lf_dataset_clase_y_atributos = LabelFrame(lf_dataset, text='Clase y atributos') lf_dataset_clase_y_atributos.grid(row=3, column=1, sticky='nsew', columnspan=3, padx=5, pady=5) PROPIEDADES_ATRIBUTOS = ('Nombre', 'count', 'unique', 'top', 'freq', 'mean', 'std', 'min', '25%', '50%', '75%', 'max') self.sb_h_tv_clase = Scrollbar(lf_dataset_clase_y_atributos, orient='horizontal') self.sb_h_tv_clase.grid(row=1, column=0, sticky='ew') self.tv_clase = Treeview(lf_dataset_clase_y_atributos, columns=PROPIEDADES_ATRIBUTOS, height=1, xscrollcommand=self.sb_h_tv_clase.set) self.tv_clase.grid(row=0, column=0, sticky='ew') self.sb_h_tv_clase.config(command=self.tv_clase.xview) self.tv_clase.heading("#0", text="#") self.tv_clase.column("#0", minwidth=30, width=40, stretch=False) self.sb_v_tv_atributos = Scrollbar(lf_dataset_clase_y_atributos) self.sb_v_tv_atributos.grid(row=2, column=1, sticky='sn') self.sb_h_tv_atributos = Scrollbar(lf_dataset_clase_y_atributos, orient='horizontal') self.sb_h_tv_atributos.grid(row=3, column=0, sticky='ew') self.tv_atributos = Treeview(lf_dataset_clase_y_atributos, columns=PROPIEDADES_ATRIBUTOS, yscrollcommand=self.sb_v_tv_atributos.set, xscrollcommand=self.sb_h_tv_atributos.set) self.tv_atributos.grid(row=2, column=0, sticky='nsew') self.tv_atributos.bind('<ButtonRelease-1>', self.selectItem) self.sb_v_tv_atributos.config(command=self.tv_atributos.yview) self.sb_h_tv_atributos.config(command=self.tv_atributos.xview) self.tv_atributos.heading("#0", text="#") self.tv_atributos.column("#0", minwidth=30, width=40, stretch=False) for i in PROPIEDADES_ATRIBUTOS: self.tv_clase.heading(i, text=i) self.tv_clase.column(i, minwidth=50, width=50, stretch=False) self.tv_atributos.heading(i, text=i) self.tv_atributos.column(i, minwidth=50, width=50, stretch=False) lf_dataset_clase_y_atributos.rowconfigure(2, weight=1) lf_dataset_clase_y_atributos.columnconfigure(0, weight=1) lf_dataset.rowconfigure(3, weight=1) def abrir_dataset(self): inicio = time.time() #TODO Las constantes se podrán modificar a través de .cfg nombre = askopenfilename( initialdir=self.ruta_datasets, filetypes=(('Archivos de valores separado por comas', '*.csv'), ('Todos los archivos', '*.*')), title='Selecciona un Dataset de Clasificación') self.root.focus_force() if not nombre: return self.v_nombre_dataset.set( os.path.splitext(os.path.basename(nombre))[0]) #TODO Esto debería hacerlo en otro sitio, no cuando lo elijo con # filedialog, y tener en cuenta self.usa_sha1 if os.path.exists(os.path.dirname(nombre)): self.l_nombre_dataset.configure(style='G.TLabel') else: self.l_nombre_dataset.configure(style='R.TLabel') self.v_tamanyo_dataset.set(tamanyo_legible(os.path.getsize(nombre))) self.v_ruta_dataset.set(os.path.relpath(os.path.dirname(nombre)) \ if self.rutas_relativas.get() else \ os.path.dirname(nombre)) self.limpia_muestra() self.limpia_atributos_y_clase() self.root.update() #TODO Usar hilos o procesos para leer grandes datasets sin problemas # self.progreso = Toplevel(self.root) # self.progreso.title("Leyendo Dataset de Clasificación") # barra = Progressbar(self.progreso, length=200, mode="indeterminate") # barra.pack() # self.q = Queue() # hilo_lectura = Process(target=self.get_dc) # hilo_lectura.start() ## self.dc = self.q.get() ## hilo_lectura.join() # self.root.after(50, self.check_if_running, hilo_lectura, self.progreso) self.dc = DC(self.v_ruta_dataset.get(), self.v_nombre_dataset.get(), self.ruta_resultados, guardar_resultados=False, clase_al_final=self.clase_al_final.get(), mostrar_proceso=self.mostrar_proceso, num_filas_a_leer=None, obtener_catalogo_robusto=False, guardar_datos_proyecto=False, mostrar_uso_ram=False) self.escribe_datos() def escribe_datos(self): self.escribe_muestra() self.v_evidencias_total.set('{:,}'.\ format(self.dc.info_dataset_original.num_evidencias())) self.v_evidencias_completas.set('{:,}'.\ format(self.dc.info_dataset_sin_datos_desconocidos.num_evidencias())) self.v_evidencias_catalogo.set('{:,}'.\ format(self.dc.info_catalogo.num_evidencias())) self.v_evidencias_robustas.set('{:,}'.\ format(self.dc.info_catalogo_robusto.num_evidencias())) self.muestra_atributos_y_clase() self.root.title('{} - {}'.format(APP_NAME, self.v_nombre_dataset.get())) def get_dc(self): #TODO No puedo crear self.dc en un try mientras depure DC # try: self.dc = DC(self.v_ruta_dataset.get(), self.v_nombre_dataset.get(), self.ruta_resultados, guardar_resultados=False, clase_al_final=self.clase_al_final, mostrar_proceso=self.mostrar_proceso, num_filas_a_leer=None, obtener_catalogo_robusto=False, guardar_datos_proyecto=False, mostrar_uso_ram=False) self.q.put(self.dc) # except Exception as e: # self.t_muestra.delete(1.0, 'end') # self.t_muestra.insert('end', e) def check_if_running(self, hilo, ventana): """Check every second if the function is finished.""" if hilo.is_alive(): self.root.after(50, self.check_if_running, hilo, ventana) else: ventana.destroy() self.escribe_datos() def abrir_proyecto(self): inicio = time.time() #TODO Diseñar estrategia para que el usuario sepa si ha de cambiarlo. # Podría bastar con mostrarle los atributos y sus características. # clase_al_final = self._clase_al_final #TODO Las constantes se podrán modificar a través de .cfg nombre = askopenfilename(initialdir=self.ruta_resultados, filetypes=(('Proyectos ACDC', '*.prjACDC'), ('Todos los archivos', '*.*')), title='Selecciona un proyecto ACDC') if nombre is None: return #TODO ¿Mostrar sólo el nombre del dataset original? self.root.title('{} - {}'.format( APP_NAME, os.path.splitext(os.path.basename(nombre))[0])) def rutas(self, r=None): ruta = askdirectory(title='Directorio de {}'.format(r), initialdir=eval('self.ruta_{}'.format(r)), mustexist=True) if ruta != '': if self.rutas_relativas.get(): if r == 'datasets': self.ruta_datasets = os.path.relpath(ruta) else: self.ruta_resultados = os.path.relpath(ruta) else: if r == 'datasets': self.ruta_datasets = ruta else: self.ruta_resultados = ruta def cambia_rutas(self): if self.rutas_relativas.get(): self.ruta_datasets = os.path.relpath(self.ruta_datasets) self.ruta_resultados = os.path.relpath(self.ruta_resultados) else: self.ruta_datasets = os.path.abspath(self.ruta_datasets) self.ruta_resultados = os.path.abspath(self.ruta_resultados) def limpia_atributos_y_clase(self): for i in self.tv_clase.get_children(): self.tv_clase.delete(i) for i in self.tv_atributos.get_children(): self.tv_atributos.delete(i) def limpia_muestra(self): self.t_muestra['state'] = 'normal' self.t_muestra.delete(1.0, 'end') self.t_muestra['state'] = 'disabled' def tamanyo_muestra(self, tamanyo): nuevo_tamanyo = askinteger('Muestra', '¿Cuántas evidencias quieres ver?', parent=self.root, minvalue=1, initialvalue=self._tamanyo_muestra) # minvalue=0, maxvalue=1000) if nuevo_tamanyo: self._tamanyo_muestra = nuevo_tamanyo #TODO Debería averiguar el índice del menú que quiero modificar self.m_configuracion.entryconfigure(4, label='Tamaño muestra ({:,})'.\ format(self._tamanyo_muestra)) self.escribe_muestra() #TODO Tratar excepciones para que no se quede habilitada la escritura def escribe_muestra(self): self.t_muestra['state'] = 'normal' self.t_muestra.delete(1.0, 'end') self.t_muestra.insert('end', '#######################################'\ '########################\n') self.t_muestra.insert('end', ' PRIMERAS {:,} LÍNEAS DEL DATASET DE '\ 'CLASIFICACIÓN ORIGINAL\n'.\ format(self._tamanyo_muestra)) self.t_muestra.insert('end', '#######################################'\ '########################\n') self.t_muestra.insert('end', self.dc.muestra(self._tamanyo_muestra)) self.t_muestra.insert('end', '\n\n') self.t_muestra['state'] = 'disabled' def muestra_atributos_y_clase(self): self.limpia_atributos_y_clase() if self.v_tipo_dataset.get() == 'Dataset original': df = self.dc.info_dataset_original.columnas elif self.v_tipo_dataset.get() == 'Dataset sin evidencias incompletas': df = self.dc.info_dataset_sin_datos_desconocidos.columnas elif self.v_tipo_dataset.get() == 'Dataset sin atributos constantes': df = self.dc.info_dataset_sin_atributos_constantes.columnas elif self.v_tipo_dataset.get() == 'Catálogo': df = self.dc.info_catalogo.columnas elif self.v_tipo_dataset.get() == 'Catálogo Robusto': df = self.dc.info_catalogo_robusto.columnas self.tv_clase["columns"] = list(df.index).insert(0, 'Nombre') self.tv_atributos["columns"] = list(df.index).insert(0, 'Nombre') self.tv_clase.heading('Nombre', text='Nombre') self.tv_atributos.heading('Nombre', text='Nombre') for i in df.index: self.tv_clase.heading(i, text=i) self.tv_clase.column(i, minwidth=50, width=50, stretch=False) self.tv_atributos.heading(i, text=i) self.tv_atributos.column(i, minwidth=50, width=50, stretch=False) for i, atributo in enumerate(df.columns): valores = [valor if not pd.isnull(valor) else '-' for valor \ in df[atributo]] valores.insert(0, atributo) if atributo == self.dc.info_dataset_original.clase: self.tv_clase.insert('', 'end', text=(0), values=valores) else: self.tv_atributos.insert('', 'end', text=(i + 1), values=valores) #TODO Modificar para que muestre información relevante de la celda. def selectItem(self, event): curItem = self.tv_atributos.item(self.tv_atributos.focus()) col = self.tv_atributos.identify_column(event.x) print('curItem = ', curItem) print('col = ', col) if col == '#0': cell_value = curItem['text'] else: cell_value = curItem['values'][int(col[1:]) - 1] print('cell_value = ', cell_value) def lee_configuracion(self): archivo_cfg = ConfigParser() archivo_cfg.optionxform = lambda option: option archivo_cfg.read('app.cfg') self.root.geometry( archivo_cfg.get('Ventana principal', 'Dimensiones y posición')) self.clase_al_final.set( archivo_cfg.get('Proyecto', 'Clase al final', fallback=True)) self.rutas_relativas.set( archivo_cfg.get('Proyecto', 'Rutas relativas', fallback=True)) self.ruta_datasets = archivo_cfg.get('Proyecto', 'Ruta datasets', fallback='../datos/ACDC/') self.ruta_resultados = archivo_cfg.get('Proyecto', 'Ruta resultados', fallback='../datos/catalogos/') def guarda_configuracion(self): archivo_cfg = ConfigParser() archivo_cfg.optionxform = lambda option: option #Dimensiones y posición de la ventana self.root.update() ancho, alto = self.root.winfo_width(), self.root.winfo_height() x, y = self.root.winfo_x(), self.root.winfo_y() archivo_cfg['Ventana principal'] = {\ 'Dimensiones y posición': '{}x{}+{}+{}'.format(ancho, alto, x, y)} archivo_cfg['Proyecto'] = {\ 'Clase al final': self.clase_al_final.get(), 'Ruta datasets': self.ruta_datasets, 'Ruta resultados': self.ruta_resultados, 'Rutas relativas': self.rutas_relativas.get()} with open('app.cfg', 'w') as archivo: archivo_cfg.write(archivo) def cerrar_aplicacion(self): self.guarda_configuracion() self.root.destroy()
class QueryBrowserUI(Tk): appName = "PySQLiTk3 - query browser" initSize = "900x600" configFile = 'conf.ini' sqlSqliteUrl = "http://www.sqlite.org/lang.html" tablesAtLeft = True selectedTable = '' def __init__(self, app): self.app = app super().__init__() self.configureLocale() self.title(self.appName) self.geometry(self.initSize) self.createMenu() self.createContent() self.loadLastDataBaseFile() def configureLocale(self): if sys.platform.startswith('win'): import locale if os.getenv('LANG') is None: lang, enc = locale.getdefaultlocale() os.environ['LANG'] = lang textdomain("app") bindtextdomain("app","./locale") def start(self): self.mainloop() def createMenu(self): menu = Menu(self) self.config(menu=menu) #File mnFile = Menu(menu, tearoff=0) menu.add_cascade(label=_("File"), underline=0, menu=mnFile) mnFile.add_command(label=_("New..."), underline=0, command=lambda cmd='NEW': self.onCommand(cmd)) mnFile.add_command(label=_("Open..."), underline=0, command=lambda cmd='OPEN': self.onCommand(cmd)) mnFile.add_separator() mnFile.add_command(label=_("Quit"), underline=0, command=lambda cmd='EXIT': self.onCommand(cmd)) #View self.mnView = Menu(menu, tearoff=0) menu.add_cascade(label=_("View"), underline=0, menu=self.mnView) self.mnView.add_command(label=_("Tables at %s") % _('right'), underline=0, command=lambda cmd='TABLES_RL': self.onCommand(cmd)) #Help mnHelp = Menu(menu, tearoff=0) menu.add_cascade(label=_("Help"), underline=0, menu=mnHelp) mnHelp.add_command(label=_("SQL by SQLite..."), underline=0, command=lambda: webbrowser.open(self.sqlSqliteUrl)) mnHelp.add_command(label=_("About..."), underline=0, command=lambda cmd='ABOUT': self.onCommand(cmd)) def createContent(self): # Top Panel: --------------------------------- pnlTop = Frame(self) pnlTop.pack(side="top", fill="both", pady=10, padx=10) self.btnDB = Button(pnlTop, text=_("Open Data Base..."), command=lambda cmd='OPEN': self.onCommand(cmd)) self.btnDB.grid(row=1, column=2, padx=5) # Query Panel: ----------------------------------- self.pnlQuery = Frame(self) self.pnlQuery.pack(side="right", fill="both", expand=True, padx=10) #-- SQL Panel: pnlSQL = Frame(self.pnlQuery) pnlSQL.pack(side="top", fill="both") Label(pnlSQL, text = "SQL:").grid(row=1, column=1, padx=5) self.txtSQL = TextSQL(pnlSQL, height=10, width = 60) self.txtSQL.grid(row=1, column=2, padx=5, pady=10) Button(pnlSQL, text = _("Run"), command=lambda cmd='RUN': self.onCommand(cmd)).grid(row=1, column=3, padx=5) #-- Buttons Panel pnlBtns = Frame(pnlSQL) pnlBtns.grid(row=2, column=2) Button(pnlBtns, text='INSERT', width = 12, command=lambda cmd='QUERY',query='INSERT': self.onCommand(cmd,query=query)).grid(row=1, column=1) Button(pnlBtns, text='SELECT', width = 12, command=lambda cmd='QUERY',query='SELECT': self.onCommand(cmd,query=query)).grid(row=1, column=2) Button(pnlBtns, text='UPDATE', width = 12, command=lambda cmd='QUERY',query='UPDATE': self.onCommand(cmd,query=query)).grid(row=1, column=3) Button(pnlBtns, text='DELETE', width = 12, command=lambda cmd='QUERY',query='DELETE': self.onCommand(cmd,query=query)).grid(row=1, column=4) #-- Result Panel: self.pnlResult = Frame(self.pnlQuery) self.pnlResult.pack(side="top", fill="both", expand=True, pady=10, padx=10) self.resultGrid = TextResult(self.pnlResult) self.resultGrid.pack(side="top", fill="both", expand=True) #Table List Panel: --------------------------------------- self.pnlTables = Frame(self) self.pnlTables.pack(side="left", fill="both", pady=10, padx=10) #---Tables Buttons Panel: self.pnlTableList = Frame(self.pnlTables) self.pnlTableList.pack(side="top", fill="both", pady=10) #---Panel Nueva: Button pnlNewTable = Frame(self.pnlTables) pnlNewTable.pack(side="bottom", pady=10, padx=10) Button(pnlNewTable, text=_("New Table"), command=lambda cmd='QUERY',query='CREATE': self.onCommand(cmd,query=query,table='<table>')).grid(row=1, column=2, padx=5) def tableContextMenu(self, event, table=''): popup = Menu(self, tearoff=0) popup.add_command(label=_("Add Column"), command=lambda cmd='QUERY',query='ADD', table=table: self.onCommand(cmd,query=query,table=table)) popup.add_command(label=_("Rename Table"), command=lambda cmd='QUERY',query='RENAME', table=table: self.onCommand(cmd,query=query,table=table)) popup.add_command(label=_("Drop Table"), command=lambda cmd='QUERY',query='DROP', table=table: self.onCommand(cmd,query=query,table=table)) popup.post(event.x_root, event.y_root) def showTables(self): tables = self.app.getTables() self.pnlTableList.destroy() self.pnlTableList = Frame(self.pnlTables) self.pnlTableList.pack(side="top", fill="both", pady=10) self.btnTables = {} Label(self.pnlTableList, text=_('Tables')).grid(row=0, column=1) for n,table in enumerate(tables): self.btnTables[table] = Button(self.pnlTableList, text=table,width=20, command=lambda cmd='QUERY',query='SELECT',table=table: self.onCommand(cmd,query=query,table=table)) self.btnTables[table].grid(row=n+1, column=1) self.btnTables[table].bind("<Button-3>", lambda event, table=table:self.tableContextMenu(event, table)) if table == self.selectedTable: self.markTable(table) def onCommand(self, comando, **args): comandos = { 'NEW':self.newDataBaseFile, 'OPEN':self.openDataBaseFile, 'EXIT':self.exitApp, 'ABOUT':self.about, 'QUERY':self.showQuery, 'RUN':self.run, 'TABLES_RL': self.showTablesAtRightOrLeft } try: comandos[comando](**args) except KeyError: showerror(_('Error'), _('Unknown command: ')+ comando) def exitApp(self): self.destroy() def about(self): showinfo(_('About...'), self.appName+'\n'+_('Desarrollado por:')+'\n'+_('Martín Nicolás Carbone')+'\n'+_('Agosto 2014')) def showTablesAtRightOrLeft(self): self.tablesAtLeft = not self.tablesAtLeft querySide, tablesSide = ("right", "left") if self.tablesAtLeft else ("left", "right") self.pnlTables.pack(side=tablesSide, fill="both", pady=10, padx=10) self.pnlQuery.pack(side=querySide, fill="both", expand=True, padx=10) self.mnView.entryconfigure(0, label=_("Tables at %s") % _(querySide)) def selectTable(self, table): self.unMarkTable(self.selectedTable) self.selectedTable = table if table != '' else self.selectedTable self.markTable(self.selectedTable) return self.selectedTable def unMarkTable(self, table): try: self.btnTables[table].config(relief='raised') except KeyError: pass def markTable(self, table): try: self.btnTables[table].config(relief='sunken') except KeyError: pass def newDataBaseFile(self): path = asksaveasfilename(defaultextension=".db") if path != '': self.openDataBase(path) def openDataBaseFile(self): path = askopenfilename(filetypes=((_("Data base files"), "*.db;*.dat;*.sqlite;*.sqlite3;*.sql;"),(_("All files"), "*.*") )) if path != '': self.openDataBase(path) def loadLastDataBaseFile(self): path = self.readPath() if path != '': self.openDataBase(path) def savePath(self, path): with open(self.configFile, 'w') as f: f.write(str(path)) def readPath(self): try: with open(self.configFile, 'r') as f: return str(f.read()) except IOError: return '' def openDataBase(self, path): self.basePath = path if path != '' else self.basePath try: self.app.openDataBase(self.basePath) self.savePath(self.basePath) self.btnDB.config(text=self.basePath) self.selectedTable = '' self.showTables() except IOError: showerror(_('Error'),_('Error')+' '+_("Open Data Base...")) def showQuery(self, query, table=''): table = self.selectTable(table) sql=self.app.createQuery(query, table) self.txtSQL.setText(sql) def run(self): query = self.txtSQL.getText() result = self.app.runQuery(query) self.resultGrid.setText(result) if result == '': self.showTables()
class Main(Tk): """ Just edit Main() """ open_file: object def __init__(self): super().__init__() self.fish = () self.base_memory = [1024, "+"] self._create_window() def _create_window(self): # Root configure self.wm_resizable(False, False) self.wm_geometry("780x800") self.configure(bg="#3C3F41") # Frame self.top_control = Frame(self) self.left_hand_frame = Frame(self) self.right_hand_frame = Frame(self) self.top_control.pack(side=TOP) self.left_hand_frame.pack(side=LEFT) self.right_hand_frame.pack(side=RIGHT) # Scrollbar self.scrollbar = Scrollbar(self.left_hand_frame, orient=VERTICAL, command=self.on_scrollbar) self.scrollbar_d = Scrollbar(self.right_hand_frame, orient=VERTICAL, command=self.on_scrollbar) self.scrollbar.pack(side=RIGHT, fill=Y) self.scrollbar_d.pack(side=RIGHT, fill=Y) # Menu self.menu = Menu(self) self.configure(menu=self.menu) self.file_menu = Menu(self, tearoff=0) self.file_menu.add_command(label="Open", command=self.menu_open) self.file_menu.add_command(label="Save", command=self.menu_save, state=DISABLED) self.file_menu.add_command(label="Save as") self.menu.add_cascade(label="File", menu=self.file_menu) # Text lines self.text = Text(self.left_hand_frame, bg="#2B2B2B", width=47, fg="white", height=32, relief="flat", yscrollcommand=self.scroll_func) self.text.insert(END, "12 34 56 78 90 as df gh jk ls 12 gh 5t e4 gt ui"+"\n") self.text.insert(END,"as df gh jk lpn1 v2 h4 6j o9 u7 ff 34 " "9s 99") self.text_hex = Text(self.right_hand_frame, bg="#2B2B2B", fg="white", width=16, height=32, relief="flat", yscrollcommand=self.scroll_func) self.text_hex.insert(END, "12345\n12345\n12345\n12345") self.text.pack(side=LEFT) self.text_hex.pack(side=RIGHT) # Control panel self.svar = StringVar() self.ivar = IntVar() self.ivar.set(1024) self.fr = Radiobutton(self.top_control, text="512", variable=self.ivar, value=512, command=self.change_increment) self.fr2 = Radiobutton(self.top_control, text="1024", variable=self.ivar, value=1024, command=self.change_increment) self.fr.pack(side=RIGHT) self.fr2.pack(side=RIGHT, padx=5) self.spin = Spinbox(self.top_control, from_=0, textvariable=self.svar, increment=1024, to=10000, wrap=True, command=self.draw, state=DISABLED) # readonly self.spin.set(1024) self.spin_encoding = Spinbox(self.top_control, value=ENCODINGS, command=self.set_encoding) self.spin_encoding.set("utf-8") self.spin_encoding.pack() self.spin.pack() # Func self.bind_all("q", lambda event=None: print(self.text.get(0.0, END))) self._copy() self._opposite_copy() def set_encoding(self): self.file.encoding = self.spin_encoding.get() def menu_open(self): """we should use repr, not decode""" self.text.delete(0.0, END) self.text_hex.delete(0.0, END) self.open_file = askopenfilename(filetypes=(("eHex File", "*.eHex"), ("All files", "*.*"))) if self.open_file == '': pass else: self.spin.configure(state="readonly") self.file_menu.entryconfigure("Save", state=NORMAL) openfile = self.open_file print(os.path.getsize(self.open_file) if os.path.getsize(self.open_file) > 1024 else 1024) self.spin["to"] = os.path.getsize(self.open_file) if os.path.getsize(self.open_file) > 1024 else 1024 self.file = FileEngine(self.open_file, "rb", encoding=self.spin_encoding.get()) try: insert = self.file.read_sth("+", 1024) except AttributeError: insert = self.file.read_as_new("+", 1024) print("-start", insert) for i in insert[0]: print(type(i),i, " ".join(i)+"!", bs.b16decode("".join(i).encode())) self.text.insert(END, " ".join(i)+"\n") for i in insert[1]: print(type(i), i) self.text_hex.insert(END,i+"\n") print("hi") def menu_save(self): if savefilename is None: self.save_file = asksaveasfile(filetypes=(("eHex File", "*.eHex"), ("All files", "*.*"))) self.ob_file = FileEngine(self.save_file, "rb", openfile, encoding=self.spin_encoding.get()) self.ob_file.write_sth() self.ob_file.close() def change_increment(self): self.spin["increment"] = self.ivar.get() def draw(self): self.text.delete(0.0, END) self.text_hex.delete(0.0, END) var = 0 if int(self.spin.get()) < self.base_memory[0]: self.base_memory[0] = int(self.spin.get()) self.base_memory[1] = "-" var = 0 try: self.pre_draw = self.file.read_sth("-", int(self.spin.get())) except AttributeError: self.pre_draw = self.file.read_as_new("-", int(self.spin.get())) for i in self.pre_draw[0]: self.text.insert(END, i+'\n') var += 1 var = 0 for i in self.pre_draw[1]: self.text_hex.insert(END, i+'\n') var += 1 elif int(self.spin.get()) > self.base_memory[0]: self.base_memory[0] = int(self.spin.get()) self.base_memory[1] = "+" var = 0 try: self.pre_draw = self.file.read_sth("+", int(self.spin.get())) except AttributeError: self.pre_draw = self.file.read_as_new("+", int(self.spin.get())) for i in self.pre_draw[0]: self.text.insert(END, " ".join(i)+'\n') var += 1 var = 0 for i in self.pre_draw[1]: self.text_hex.insert(END, i+'\n') var += 1 print(self.base_memory, self.pre_draw) def scroll_func(self, *args): self.scrollbar.set(*args) self.scrollbar_d.set(*args) self.on_scrollbar('moveto', args[0]) def on_scrollbar(self, *args): """Scrolls both text widgets when the scrollbar is moved""" self.text.yview(*args) self.text_hex.yview(*args) def _copy(self): self.after(20, self._copy) try: try: self.text_hex.tag_configure("selle", background="white", foreground="black") self.text_hex.tag_delete("selle") except TclError: pass self.first = self.text.index(SEL_FIRST).split(".") self.second = self.text.index(SEL_LAST).split(".") # print(self.first, self.second) self.first = (self.first[0], str(self._pround( (int( self.first[1]) + ( 1 if (int(self.first[1]) + 1) % 3 == 0 else 0) ) / 3))) self.second = (self.second[0], str(self._pround( (int( self.second[1]) - ( 1 if (int(self.second[1]) - 1) % 3 == 0 else 0) ) / 3))) # print("after", self.first, self.second) self.marks = (".".join(self.first), ".".join(self.second)) # print(self.marks) self.text_hex.tag_add("selle", self.marks[0], self.marks[1]) self.text_hex.tag_configure("selle", background=rgb(162, 8, 229), foreground="#ffffff") self.update_idletasks() except TclError: pass @staticmethod def _pround(r): if r > int(r): # print(r, int(r+1)) return int(r) + 1 else: # print(r, int(r), "else") return int(r) def _opposite_copy(self): self.after(20, self._opposite_copy) try: try: self.text.tag_configure("selle-hex", background="white", foreground="black") self.text.tag_delete("selle-hex") except TclError: pass self.hex_first, self.hex_second = self.text_hex.index(SEL_FIRST).split(".")[0] + "." + \ str(int(self.text_hex.index(SEL_FIRST).split(".")[1]) * 3), \ self.text_hex.index(SEL_LAST).split(".")[0] + "." + \ str(int(self.text_hex.index(SEL_LAST).split(".")[1]) * 3 - 1) self.text.tag_add("selle-hex", self.hex_first, self.hex_second) self.text.tag_configure("selle-hex", background=rgb(229, 139, 8), foreground="#ffffff") self.update_idletasks() except TclError: pass
class PlayerMenu(Menu): root: "GUI" movemenu: Menu axis: str startX: int startY: int cX: int cY: int tw: Toplevel def __init__(self, root: "GUI"): self.root = root Menu.__init__(self, master=root, tearoff=0) # create 'move' menu self.movemenu = Menu(master=self, tearoff=0) self.movemenu.add_command(label="Move (Horizontal)", command=lambda: self.movePlayer("X")) self.movemenu.add_command(label="Move (Vertical)", command=lambda: self.movePlayer("Y")) self.movemenu.add_separator() self.movemenu.add_command(label="Save Position", command=self.savePos, state='disabled') self.movemenu.add_command(label="Reset Position", command=self.resetPos, state='disabled') # create 'main' menu self.add_cascade(label="Move Player", menu=self.movemenu) self.add_separator() self.add_command(label="Exit Spotify", command=lambda: root.spotify.win.close()) self.add_command(label="Close Player", command=root.closePlayer) def show(self, event: "Event") -> None: self.post(event.x_root, self.root.winfo_rooty()) def movePlayer(self, axis: str) -> None: self.root.event_generate(sequence='<Motion>', warp=True, x=(self.root.screen.appW // 2), y=(self.root.screen.appH // 2)) self.axis = axis cur = f"sb_{'h' if axis == 'X' else 'v'}_double_arrow" self.root.configure(cursor=cur) self.startX = self.root.winfo_rootx() self.startY = self.root.winfo_rooty() for btn in self.root.btns.values(): btn.unbind(sequence='<ButtonRelease-1>') self.root.unbind_all(sequence='<ButtonRelease-3>') self.root.bind(sequence='<Escape>', func=self.stopMoving) self.root.bind(sequence='<ButtonRelease-3>', func=self.stopMoving) self.root.bind(sequence='<Button-1>', func=self.on_click) self.root.bind(sequence='<B1-Motion>', func=self.on_move) self.root.bind(sequence='<ButtonRelease-1>', func=self.on_release) def on_click(self, event: "Event") -> None: self.cX = event.x self.cY = event.y self.tw = Toplevel(master=self.root, bg='white', bd=3, relief='groove') self.tw.attributes('-transparentcolor', 'white', '-topmost', 1) self.tw.overrideredirect(1) self.tw.geometry(self.root.geometry()) def on_move(self, event: "Event") -> None: if self.axis == "X": pos = (event.x - self.cX) dX = pos dY = 0 else: pos = (event.y - self.cY) dX = 0 dY = pos self.tw.geometry(f'+{self.startX + dX}' f'+{self.startY + dY}') def on_release(self, *_) -> None: if self.tw.geometry() != self.root.geometry(): self.root.geometry(self.tw.geometry()) self.movemenu.entryconfigure(3, state='normal') self.movemenu.entryconfigure(4, state='normal') self.tw.destroy() self.stopMoving() def stopMoving(self, *_) -> None: self.root.bind_all(sequence='<ButtonRelease-3>', func=self.show) self.root.unbind(sequence='<Button-1>') self.root.unbind(sequence='<B1-Motion>') self.root.unbind(sequence='<ButtonRelease-1>') self.root.unbind(sequence='<Escape>') self.root.configure(cursor="") for id, btn in enumerate(self.root.btns.values()): btn.bind(sequence='<ButtonRelease-1>', func=lambda e, n=id: self.root.sendInput(e, n)) def savePos(self) -> None: global OFFSET_X, OFFSET_Y self.movemenu.entryconfigure(3, state='disabled') self.movemenu.entryconfigure(4, state='disabled') curX = self.root.winfo_rootx() curY = self.root.winfo_rooty() OFFSET_X += (self.root.screen.X - curX) OFFSET_Y += (self.root.screen.Y - curY) cfg['Sizes']['horizontal_offset'] = OFFSET_X cfg['Sizes']['vertical_offset'] = OFFSET_Y with cfgfile.open('w') as f: cfg.write(f) self.root.screen = Screen() def resetPos(self) -> None: self.movemenu.entryconfigure(3, state='disabled') self.movemenu.entryconfigure(4, state='disabled') self.root.focus_set() self.root.geometry(f'+{self.root.screen.X}' f'+{self.root.screen.Y}')
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