def runtime_select(self, button: ttk.Button): logging.debug("selecting runtime button: %s", button) self.runtime_select_button.state(["!pressed"]) self.stop_button.state(["!pressed"]) self.runtime_marker_button.state(["!pressed"]) self.run_command_button.state(["!pressed"]) button.state(["pressed"])
def design_select(self, button: ttk.Button): logging.debug("selecting design button: %s", button) self.select_button.state(["!pressed"]) self.link_button.state(["!pressed"]) self.node_button.state(["!pressed"]) self.network_button.state(["!pressed"]) self.annotation_button.state(["!pressed"]) button.state(["pressed"])
class AutoCorrectConfig(Frame): """Configuration window for autocorrect.""" def __init__(self, master, app, **kwargs): Frame.__init__(self, master, padding=4, **kwargs) self.rowconfigure(2, weight=1) self.columnconfigure(0, weight=1) self.columnconfigure(1, weight=1) self.tree = Treeview(self, columns=('replace', 'by'), show='', selectmode='browse') scroll_x = AutoScrollbar(self, orient='horizontal', command=self.tree.xview) scroll_y = AutoScrollbar(self, orient='vertical', command=self.tree.yview) self.tree.configure(xscrollcommand=scroll_x.set, yscrollcommand=scroll_y.set) self.reset() self.replace = StringVar(self) self.by = StringVar(self) add_trace(self.replace, 'write', self._trace_replace) add_trace(self.by, 'write', self._trace_by) b_frame = Frame(self) self.b_add = Button(b_frame, text=_('New'), command=self.add) self.b_rem = Button(b_frame, text=_('Delete'), command=self.remove) self.b_add.state(('disabled', )) self.b_rem.state(('disabled', )) self.b_add.pack(pady=4, fill='x') self.b_rem.pack(pady=4, fill='x') Button(b_frame, text=_('Reset'), command=self.reset).pack(pady=8, fill='x') Label(self, text=_('Replace')).grid(row=0, column=0, sticky='w', pady=4) Label(self, text=_('By')).grid(row=0, column=1, sticky='w', pady=4) Entry(self, textvariable=self.replace).grid(row=1, column=0, sticky='ew', pady=4, padx=(0, 4)) Entry(self, textvariable=self.by).grid(row=1, column=1, sticky='ew', pady=4) self.tree.grid(row=2, columnspan=2, sticky='ewsn', pady=(4, 0)) scroll_x.grid(row=3, columnspan=2, sticky='ew', pady=(0, 4)) scroll_y.grid(row=2, column=2, sticky='ns', pady=(4, 0)) b_frame.grid(row=1, rowspan=2, padx=(4, 0), sticky='nw', column=3) self.tree.bind('<<TreeviewSelect>>', self._on_treeview_select) def _trace_by(self, *args): key = self.replace.get().strip() val = self.by.get().strip() self.by.set(val) if key in self.tree.get_children(''): if val != self.tree.set(key, 'by'): self.b_add.state(('!disabled', )) else: self.b_add.state(('disabled', )) else: self.b_add.state(('!disabled', )) if not val: self.b_add.state(('disabled', )) def _trace_replace(self, *args): key = self.replace.get().strip() val = self.by.get().strip() self.replace.set(key) if not key: self.b_add.state(('disabled', )) self.b_rem.state(('disabled', )) else: self.b_add.state(('!disabled', )) sel = self.tree.selection() if key in self.tree.get_children(''): if key not in sel: self.tree.selection_set(key) self.b_add.configure(text=_('Replace')) self.b_rem.state(('!disabled', )) if val != self.tree.set(key, 'by'): self.b_add.state(('!disabled', )) else: self.b_add.state(('disabled', )) else: self.b_rem.state(('disabled', )) self.b_add.configure(text=_('New')) if sel: self.tree.selection_remove(*sel) if not val: self.b_add.state(('disabled', )) def _on_treeview_select(self, event): sel = self.tree.selection() if sel: key, val = self.tree.item(sel[0], 'values') self.replace.set(key) self.by.set(val) def reset(self): self.tree.delete(*self.tree.get_children('')) keys = list(AUTOCORRECT.keys()) keys.sort() for key in keys: self.tree.insert('', 'end', key, values=(key, AUTOCORRECT[key])) def add(self): key = self.replace.get().strip() val = self.by.get().strip() if key in self.tree.get_children(''): self.tree.item(key, values=(key, val)) elif key and val: self.tree.insert('', 'end', key, values=(key, val)) def remove(self): key = self.replace.get() if key in self.tree.get_children(''): self.tree.delete(key) def ok(self): keys = self.tree.get_children('') AUTOCORRECT.clear() for key in keys: AUTOCORRECT[key] = self.tree.set(key, 'by')
def select_radio(self, selected: ttk.Button) -> None: for button in self.radio_buttons: button.state(["!pressed"]) selected.state(["pressed"])
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 GUI: x0 = y0 = x1 = y1 = x_start = y_start = rect_x0 = rect_y0 = rect_x2 = rect_y2 = -1 polygone = [] # polygonesPointsCollection = [] polygone_id_collection = {} polygoneCollection = {} colorCollection = [ 'black', 'red', 'green', 'blue', 'cyan', 'yellow', 'magenta' ] randomColor = random.choice(colorCollection) # fontText = tkFont.Font(family='Helvetica') textHeight = 30 line_tmp = None # format de text in the text-area text_format = "txt" check_mark = u"\u2713" rect = None canvasLigneCollection = [] the_last_draw = None def __init__(self, master): self.master = master self.master.title("Points indicator") # Mode de draw self.draw_mode = "point" # Image self.origin_image = None self.scale = 1.0 self.img = None self.img_id = None # Menu self.menu_bar = Menu(self.master) self.create_menus() # main frame self.frame = Frame(self.master, relief=SUNKEN, bg="red", width=self.master.winfo_width()) self.frame.grid_rowconfigure(0, weight=1) self.frame.grid_columnconfigure(0, weight=1) self.frame.pack(fill=None, expand=False) # scroll bars self.xscrollbar = Scrollbar(self.frame, orient=HORIZONTAL) self.xscrollbar.grid(row=2, column=0, columnspan=2, sticky=EW) self.yscrollbar = Scrollbar(self.frame) self.yscrollbar.grid(row=1, column=1, sticky=NS) # canvas to put image self.canvas = Canvas(self.frame, bg="black", bd=0, height=(self.master.winfo_height() - 250), width=(self.master.winfo_width() - 100), xscrollcommand=self.xscrollbar.set, yscrollcommand=self.yscrollbar.set) self.canvas.grid(row=1, column=0, sticky=NSEW) self.canvas.config(state=DISABLED) self.xscrollbar.config(command=self.canvas.xview) self.yscrollbar.config(command=self.canvas.yview) # zoom utility buttons self.zoom_in_button = Button(self.frame, text="+", command=lambda: self.zoom(self.scale * 2), width=3) self.zoom_out_button = Button( self.frame, text="-", command=lambda: self.zoom(self.scale * 0.5), width=3) self.zoom_scale = Scale(self.frame, from_=1, to=3, length=80, command=self.on_scale, orient=VERTICAL) self.zoom_in_button.place(relx=0.93, rely=0.03) self.zoom_scale.place(relx=0.93, rely=0.07) self.zoom_out_button.place(relx=0.93, rely=0.18) self.change_zoom_state(0) # Frame to put text self.textFrame = Frame(self.frame, relief=SUNKEN, bg=self.master.cget('bg'), width=(self.master.winfo_width()), height=(self.master.winfo_height() - int(self.canvas['height']) - 50)) self.textFrame.grid_rowconfigure(0, weight=1) self.textFrame.grid_columnconfigure(0, weight=1) self.textFrame.grid(row=3, column=0, columnspan=2, sticky=NSEW) self.textFrame.grid_propagate(False) # text area widget self.textContent = Text(self.textFrame, font='Arial') self.textContent.grid(row=4, column=0, padx=25, pady=25, sticky=NSEW) self.set_text_color_tags() # popup entry value self.roomLabel = StringVar() self.open_file() # bind mouse-click event self.go_to_point_mode() # point mode by default self.canvas.bind("<MouseWheel>", self.on_mouse_wheel) def create_menus(self): """ create pull-down menus, and add it to the menu bar """ # menu File file_menu = Menu(self.menu_bar, tearoff=0) file_menu.add_command(label="New Image", command=self.open_file) file_menu.add_command(label="Save Data", command=self.save_to) file_menu.add_command(label="Load From File", command=self.load_work_from_file) file_menu.add_separator() file_menu.add_command(label="Exit", command=self.master.quit) self.menu_bar.add_cascade(label="File", menu=file_menu) self.master.config(menu=self.menu_bar) # menu Format convert_menu = Menu(self.menu_bar, tearoff=0) convert_menu.add_command(label="Json", command=lambda: self.generate_text("json")) convert_menu.add_command(label="Html", command=lambda: self.generate_text("html")) convert_menu.add_command(label="Text", command=lambda: self.generate_text("txt")) self.menu_bar.add_cascade(label="Format", menu=convert_menu) self.master.config(menu=self.menu_bar) # menu Tools tools_menu = Menu(self.menu_bar, tearoff=0) tools_menu.add_command(label="Copy text to clipboard", command=self.copy_text_to_clipboard) self.menu_bar.add_cascade(label="Tools", menu=tools_menu) self.master.config(menu=self.menu_bar) """ menu Mode (draw mode): [Point mode]: dessiner des polygones par des points [Drag mode]: dessiner des polygones par le tire d'une aire de polygone sur l'interface """ draw_mode_menu = Menu(self.menu_bar, tearoff=0) draw_mode_menu.add_command( label=self.check_mark + " Point", command=lambda: self.change_draw_mode(draw_mode_menu, 'point')) draw_mode_menu.add_command( label=" Drag", command=lambda: self.change_draw_mode(draw_mode_menu, 'drag')) self.menu_bar.add_cascade(label="Mode", menu=draw_mode_menu) self.master.config(menu=self.menu_bar) # menu Undo menu_undo = Menu(self.menu_bar, tearoff=0) menu_undo.add_command(label="Remove the last polygone", command=self.undo_last_polygone) self.menu_bar.add_cascade(label="Undo", menu=menu_undo) self.master.config(menu=self.menu_bar) def end_draw_cycle(self, popup): if self.roomLabel.get() in self.polygoneCollection: self.popup("Id already exist and they are unique.", "#ff3838") self.roomLabel.set('') elif self.roomLabel.get() == '': self.popup("The id can't be empty.", "#ff3838") else: popup.destroy() polygone_str = self.get_formatted_coordinates(self.polygone) # self.textContent.insert('end', self.roomLabel.get() + ': ' + polygone_str+'\n', self.randomColor) self.polygoneCollection[self.roomLabel.get()] = polygone_str self.roomLabel.set('') self.polygone = [] self.remove_polygone_lignes() self.randomColor = self.get_no_repeat_color() self.generate_text(self.text_format) print('restart!') print(self.polygoneCollection) def popup_entry(self): popup = Toplevel() popup.wm_title("Entry an id") popup.wm_geometry("%dx%d%+d%+d" % (220, 80, 450, 300)) popup_label = Label(popup, text="Id : ") popup_label.grid(row=0, padx=10, pady=10) entry = Entry(popup, textvariable=self.roomLabel) entry.grid(row=0, column=1, padx=10, pady=10) entry.focus_set() entry.bind("<Return>", (lambda event: self.end_draw_cycle(popup))) button = Button(popup, text="Ok", command=lambda: self.end_draw_cycle(popup)) button.grid(row=1, columnspan=2, padx=10, pady=10) popup.protocol("WM_DELETE_WINDOW", lambda: self.close_entry_popup_window(popup)) def get_formatted_coordinates(self, the_polygone): """ Retourne un String de coordonées de polygone sous forme: "x0, y0 x1, y1 ... " """ formatted_coordinates = '' for i in range(len(self.polygone)): if i % 2 == 0: # indice paire (x) formatted_coordinates += (str(the_polygone[i]) + ',') else: # impaire (y) formatted_coordinates += (str(the_polygone[i]) + ' ') return formatted_coordinates def get_no_repeat_color(self): """ Retourne une couleur dans la colorCollection autre que la variable 'randomColor' Pour éviter d'avoir 2 fois la suite la meme couleur """ color_collection_copie = self.colorCollection.copy() color_collection_copie.remove(self.randomColor) return random.choice(color_collection_copie) def add_line(self, event): """ MouseClick event pour tracer les polygones """ if self.x0 == -1 and self.y0 == -1: # start drawing (start point: x0, y0) self.change_zoom_state(0) self.x0 = event.x self.y0 = event.y self.x_start = self.x0 self.y_start = self.y0 """ self.x_start = self.mutate_point(self.x0) if self.x_start != self.x0: self.y_start = self.mutate_point(self.y0) else: self.y_start = self.y0 print("after: " + str(self.x_start) + ", " + str(self.y_start)) """ self.polygone.append( self.canvas.canvasx(self.x_start) / self.scale) self.polygone.append( self.canvas.canvasy(self.y_start) / self.scale) else: # in drawing self.x1 = event.x self.y1 = event.y canvas_ligne = self.canvas.create_line( self.canvas.canvasx(self.x0), self.canvas.canvasy(self.y0), self.canvas.canvasx(self.x1), self.canvas.canvasy(self.y1), fill=self.randomColor) self.canvasLigneCollection.append(canvas_ligne) if ((self.x_start - 5) <= self.x1 <= (self.x_start + 5)) and ((self.y_start - 5) <= self.y1 <= (self.y_start + 5)): # endPoint ~ start point (in a range of 5 pixels ): end 1 cycle draw self.x0 = -1 self.y0 = -1 self.the_last_draw = self.canvas.create_polygon( ' '.join( str(points * self.scale) for points in self.polygone), fill=self.randomColor) self.polygone_id_collection[ self.the_last_draw] = self.randomColor # self.polygonesPointsCollection.append(self.polygone) self.popup_entry() self.change_zoom_state(1) else: self.x0 = self.x1 self.y0 = self.y1 """ print(str(self.x1) + ', ' + str(self.y1)) self.x0 = self.mutate_point(self.x1) self.y0 = self.mutate_point(self.y1) print("after: " + str(self.x0) + ", " + str(self.y0)) """ self.polygone.append( self.canvas.canvasx(self.x0, 0.5) / self.scale) self.polygone.append( self.canvas.canvasy(self.y0, 0.5) / self.scale) """ [TODO]: draw with a polygone near by def mutate_point(self, point): if self.polygonesPointsCollection: for polygonPoints in self.polygonesPointsCollection: for pt in polygonPoints: if (pt-10) <= point <= (pt+10): return pt return point """ def cancel_draw(self, e): self.x0 = self.y0 = self.x_start = self.y_start = -1 self.polygone = [] self.popup("Draw cancelled.") self.remove_polygone_lignes() self.change_zoom_state(1) def popup(self, msg, text_color=None): popup = Toplevel(self.master) # popup window par rapport a l'écran TODO: center to root window popup.wm_title("Info") popup.wm_geometry("%dx%d%+d%+d" % (220, 80, 450, 300)) if text_color: popup_label = Label(popup, text=msg, fg=text_color) else: popup_label = Label(popup, text=msg) popup_label.grid(row=0, padx=10, pady=10) button = Button(popup, text="Ok", command=popup.destroy) button.grid(row=1, padx=65, pady=10) def on_mouse_wheel(self, event): self.canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") def generate_text(self, text_format): formatted_text = '' if text_format == "json": formatted_text = '{\n\t"value": "", "label": "", \n\t"zones": [\n\t' for key, value in self.polygoneCollection.items(): formatted_text += '\t{"id": "' + key + '", "points": "' + value + '"},\n\t' formatted_text = self.r_replace(formatted_text, ',', '', 1) formatted_text += ']\n}' self.text_format = "json" elif text_format == 'html': for key, value in self.polygoneCollection.items(): formatted_text += '<polygon id="' + key + '" class="st0" points="' + value + '"/>\n' self.text_format = "html" else: for key, value in self.polygoneCollection.items(): formatted_text += (key + ': ' + value + '\n') self.text_format = "txt" self.reload_text_content(formatted_text) def reload_text_content(self, text): self.textContent.delete('1.0', END) self.textContent.insert('1.0', text) def copy_text_to_clipboard(self): text_area_values = self.textContent.get('1.0', END) self.master.clipboard_clear() self.master.clipboard_append(text_area_values) self.popup("Text copied to clipboard, [Ctrl]+[V] to paste.") def set_text_color_tags(self): """ Créer un textTag pour chaque couleur dans 'colorCollection' """ for color in self.colorCollection: self.textContent.tag_config(color, foreground=color) def r_replace(self, s, old, new, occurrence): """ Inversement remplacer le 'old' character 'occurrence' fois par 'new' caracter dans le string 's' """ li = s.rsplit(old, occurrence) return new.join(li) def preview_line(self, event): if self.line_tmp: self.canvas.delete(self.line_tmp) self.line_tmp = None if self.x0 != -1 and self.y0 != -1: x_start = self.x0 y_start = self.y0 self.line_tmp = self.canvas.create_line( self.canvas.canvasx(x_start), self.canvas.canvasy(y_start), self.canvas.canvasx(event.x), self.canvas.canvasy(event.y), fill=self.randomColor) def zoom(self, zoom_scale): if 1 <= zoom_scale <= 4: self.scale = zoom_scale self.zoom_scale.set(self.scale) self.load_image() self.redraw_all_polygone() def load_image(self): self.canvas.delete("all") image_width, image_height = self.origin_image.size size = int(image_width * self.scale), int(image_height * self.scale) self.img = ImageTk.PhotoImage(self.origin_image.resize(size)) self.img_id = self.canvas.create_image(0, 0, image=self.img, anchor=NW) # tell the canvas to scale up/down the vector objects self.canvas.scale(ALL, 0, 0, self.scale, self.scale) self.canvas.configure(scrollregion=self.canvas.bbox("all")) def open_file(self): """ [menu][File][Open] changer l'image sur la quelle qu'on travail """ """ # [DEV] Chemin seulement pour faciliter le développement: à changer en prod file = Image.open("C:/Users/gs63vr/Documents/Grener/app/src/assets/img/confort/N4.png") """ file = askopenfilename(parent=self.master, initialdir="C:/", title='Choose a file to open', filetypes=FileHandler.IMAGE_FILE_TYPES) if file: try: self.origin_image = Image.open(file) self.polygoneCollection = {} self.reload_text_content('') self.load_image() self.change_zoom_state(1) except IOError: self.popup("Can't open the image file!", "#ff3838") def on_button_press(self, event): """ Set x0, y0 du rectangle :param event: on click event """ self.rect = None self.rect_x0 = self.canvas.canvasx(event.x) / self.scale self.rect_y0 = self.canvas.canvasy(event.y) / self.scale def on_move_press(self, event): if self.rect: self.canvas.delete(self.rect) self.rect_x2 = self.canvas.canvasx(event.x) self.rect_y2 = self.canvas.canvasy(event.y) # expand rectangle as you drag the mouse self.rect = self.canvas.create_rectangle(self.rect_x0 * self.scale, self.rect_y0 * self.scale, self.rect_x2, self.rect_y2, fill=self.randomColor) def on_button_release(self, event): self.polygone.extend([ self.rect_x0, self.rect_y0, self.rect_x2 / self.scale, self.rect_y0, self.rect_x2 / self.scale, self.rect_y2 / self.scale, self.rect_x0, self.rect_y2 / self.scale ]) self.the_last_draw = self.rect self.polygone_id_collection[self.the_last_draw] = self.randomColor self.popup_entry() def save_to(self): """ [menu][File][Save] export de fichier """ my_file_handler = FileHandler(self.master) my_file_handler.export_file(self.textContent.get(1.0, END), self.text_format) def load_work_from_file(self): """ [menu][File][Load_from_file] export de fichier """ my_file_handler = FileHandler(self.master) content = my_file_handler.load_file() if my_file_handler.file_extension: self.reinit_variables_from_content(content, my_file_handler.file_extension) self.text_format = my_file_handler.file_extension.replace('.', '') self.reload_text_content(content) def reinit_variables_from_content(self, content_text, extension): # clean global variables self.clean_global_variables() self.polygone_id_collection = {} self.polygoneCollection = {} # Initialisation datas quand reload if extension == '.json': json_obj = json.loads(content_text) for zone in json_obj['zones']: points = zone['points'].replace(',', ' ') polygone = self.canvas.create_polygon(points, fill=self.randomColor) self.polygone_id_collection[polygone] = '' self.polygoneCollection[zone['id']] = zone['points'] elif extension == '.html': my_html_parser = MyHTMLParser() my_html_parser.feed(content_text) for polygone in my_html_parser.polygone_collection: points = polygone[2][1].replace(',', ' ') polygone = self.canvas.create_polygon(points, fill=self.randomColor) self.polygone_id_collection[polygone] = '' self.polygoneCollection[polygone[0][1]] = polygone[2][1] elif extension == '.txt': structured_data = content_text.rstrip('\n') structured_data = [ s.split(': ') for s in structured_data.splitlines() ] for polygon in structured_data: points = polygon[1].replace(',', ' ') polygone = self.canvas.create_polygon(points, fill=self.randomColor) self.polygone_id_collection[polygone] = '' self.polygoneCollection[polygon[0]] = polygon[1] self.randomColor = self.get_no_repeat_color() def on_scale(self, event): value = self.zoom_scale.get() if int(value) != value: self.zoom_scale.set(round(value)) self.scale = round(value) self.zoom(round(value)) def undo_last_polygone(self): """ clean the last drawn """ if self.polygone_id_collection: self.canvas.delete( list(self.polygone_id_collection.items())[-1][0]) self.polygone_id_collection.popitem() # self.canvas.delete(self.the_last_draw) self.clean_global_variables() self.polygoneCollection.popitem() self.generate_text(self.text_format) def change_draw_mode(self, menu, mode_to_change): """ Fonction qui change le mode de draw entre "point mode" et "drag mode" """ if mode_to_change != self.draw_mode: # TODO: [clean] separer le switch de menu dans 2 fonction differents if mode_to_change == 'point': self.go_to_point_mode() menu.entryconfig(0, label=self.check_mark + ' Point') menu.entryconfig(2, label=' Drag') elif mode_to_change == 'drag': self.go_to_drag_mode() menu.entryconfig(0, label=' Point') menu.entryconfig(2, label=self.check_mark + ' Drag') self.draw_mode = mode_to_change def go_to_point_mode(self): self.clean_global_variables() self.canvas.unbind("<ButtonRelease 1>") self.canvas.unbind("<B1-Motion>") self.canvas.bind("<Button 1>", self.add_line) self.canvas.bind("<Motion>", self.preview_line) self.canvas.bind("<Button 3>", self.cancel_draw) def go_to_drag_mode(self): """ [Drag Mode] """ self.clean_global_variables() self.canvas.unbind("<Button 3>") self.canvas.bind("<Button 1>", self.on_button_press) self.canvas.bind("<B1-Motion>", self.on_move_press) self.canvas.bind("<ButtonRelease 1>", self.on_button_release) def clean_global_variables(self): """ Réinitialiser les variables globales sauf la collection de polygone :polygoneCollection """ self.x0 = self.y0 = self.x1 = self.y1 = self.x_start = self.y_start = self.rect_x0 = self.rect_y0 = self.rect_x2 = self.rect_y2 = -1 self.polygone = [] self.randomColor = self.get_no_repeat_color() self.line_tmp = None self.rect = None def close_entry_popup_window(self, popup, event=None): popup.destroy() self.canvas.delete(self.the_last_draw) self.remove_polygone_lignes() self.clean_global_variables() def remove_polygone_lignes(self): for canvasLigne in self.canvasLigneCollection: self.canvas.delete(canvasLigne) self.canvasLigneCollection = [] def redraw_all_polygone(self): for polygone_coordinates in self.polygoneCollection.values(): points = polygone_coordinates.replace(',', ' ').rstrip().split(' ') points = [float(point) * self.scale for point in points] canvas_coord = ' '.join(str(s) for s in points) polygone = self.canvas.create_polygon(canvas_coord, fill=self.randomColor) self.polygone_id_collection[polygone] = '' def change_zoom_state(self, state): if state == 0: self.zoom_in_button.state(['disabled']) self.zoom_out_button.state(['disabled']) self.zoom_scale.state(['disabled']) else: self.zoom_in_button.state(['!disabled']) self.zoom_out_button.state(['!disabled']) self.zoom_scale.state(['!disabled'])
class ClientGUI: def __init__(self): self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.client_socket.settimeout(5.0) self.port = 2345 self.client_running = False self.client_connected = False self.root = tk.Tk() self.root.resizable(width=False, height=False) self.root.title('Chat Client') self.createGUI() self.root.protocol('WM_DELETE_WINDOW', self.onShuttingDownClient) self.root.mainloop() return def createGUI(self): tk.ttk.Style().configure('TLabel', font=('Times New Roman', 16)) tk.ttk.Style().configure('TButton', font=('Times New Roman', 16)) self.frame = Frame(self.root) self.user_name_label = Label(self.frame, text='User Name') self.user_name_label.grid(row=0, column=0, sticky='W') self.user_name_box = Entry(self.frame, font=('Times New Roman', 16), width=20) self.user_name_box.grid(row=0, column=1) self.ip_label = Label(self.frame, text='Server IP Address') self.ip_label.grid(row=1, column=0, sticky='W') self.ip_addr_box = Entry(self.frame, font=('Times New Roman', 16)) self.ip_addr_box.grid(row=1, column=1) self.connect_btn = Button(self.frame, text='Connect', command=self.connectBtnClick) self.connect_btn.grid(row=2, column=0, sticky='W') self.chat_area = scrolledtext.ScrolledText(self.frame, height=10, width=42, undo=True, font=('Times New Roman', 16)) self.chat_area.grid(row=3, column=0, sticky='NEWS') self.chat_area.configure(state='disabled') self.client_list = Listbox(self.frame, font=('Times New Roman', 16), selectmode=tk.MULTIPLE) self.client_list.grid(row=3, column=1, sticky='NEWS') self.msg_box = scrolledtext.ScrolledText(self.frame, font=('Times New Roman', 16), height=1, width=42, undo=True) self.msg_box.grid(row=4, column=0, sticky='W') self.send_btn = Button(self.frame, text='Send', command=self.sendBtnClick) self.send_btn.grid(row=4, column=1, sticky='W') self.send_btn.state(['disabled']) self.frame.grid(row=0, column=0) return def connectBtnClick(self): self.client_name = self.user_name_box.get() if not (len(self.client_name) == 0): try: self.client_socket.sendto(('1111||'+ self.client_name).encode('ascii'), (self.ip_addr_box.get(), self.port)) self.message, self.address = self.client_socket.recvfrom(2048) self.chat_area.config(state='normal') self.chat_area.insert(tk.END, self.message.decode('ascii') + '\n') self.chat_area.config(state='disabled') self.client_running = True threading.Thread(target=self.updateClientList).start() self.connect_btn.state(['disabled']) self.user_name_box.state(['disabled']) self.ip_addr_box.state(['disabled']) self.send_btn.state(['!disabled']) self.client_connected = True except socket.timeout as tout: messagebox.showerror("Message From Client", tout) except socket.gaierror as gerror: messagebox.showerror("Message From Client", gerror) pass pass else: messagebox.showerror('Message From Client', 'User Name cannot be empty') def sendBtnClick(self): recv_clients_list = self.client_list.curselection() msg_type = '' if len(recv_clients_list) > 1: msg_type = ' [MC]' recv_clients = '' for k in recv_clients_list: recv_clients = recv_clients + '||' + self.client_list.get(k) self.message_to_sent = self.client_name + '||' + self.msg_box.get(1.0, tk.END) + msg_type + recv_clients self.chat_area.config(state='normal') self.chat_area.insert(tk.END, 'You>>' + self.msg_box.get(1.0, tk.END) + '\n') self.chat_area.config(state='disabled') self.client_socket.sendto(self.message_to_sent.encode('ascii'), (self.ip_addr_box.get(), self.port)) self.msg_box.delete(1.0, tk.END) pass def onShuttingDownClient(self): if self.client_connected: self.client_socket.sendto(('0000' + '||' + self.client_name).encode('ascii'), (self.ip_addr_box.get(), self.port)) self.client_running = False self.root.destroy() pass def updateClientList(self): self.client_socket.setblocking(0) while self.client_running: try: self.message, self.address = self.client_socket.recvfrom(2048) self.message = self.message.decode('ascii') i = 0 j = 0 if self.isContain(self.message, '1111'): temp_client_list = self.message.split('||') self.client_list.delete(0, tk.END) while i < len(temp_client_list): if not (self.client_name == temp_client_list[i]) and not (temp_client_list[i] == '1111'): self.client_list.insert(j, temp_client_list[i]) j = j + 1 i = i+1 elif 'Empty List 0000' in self.message: self.client_list.delete(0, tk.END) elif 'Server Offline 0101' in self.message: self.client_connected = False self.chat_area.config(state='normal') self.chat_area.insert(tk.END, 'Server is Offline. Chat discontinues!!!\n') self.chat_area.config(state='disabled') self.send_btn.state(['disabled']) self.connect_btn.state(['!disabled']) self.ip_addr_box.state(['!disabled']) self.user_name_box.state(['!disabled']) self.client_socket.settimeout(1) self.client_running = False else: self.chat_area.config(state='normal') self.chat_area.insert(tk.END, self.message + '\n') self.chat_area.config(state='disabled') except socket.error: pass pass def isContain(self, str, sub_str): index = str.find(sub_str) if index == -1: return False else: return True
class App(Tk): def __init__(self, **kwargs): super(App, self).__init__(**kwargs) tags = list(self.bindtags()) tags.insert(2, 'App') self.bindtags(tags) themes = ThemeEngine() self.option_readfile(themes.options_file) themes.set_ttk_style() self.new_reader_icon = get_icon('ic_new_reader') self.new_writer_icon = get_icon('ic_new_writer') self.close_panel_icon = get_icon('ic_close') self.save_icon = get_icon('ic_save') self.edit_icon = get_icon('ic_mode_edit') self.link_icon = get_icon('ic_insert_link') self.clear_content_icon = get_icon('ic_delete_sweep') self.delete_icon = get_icon('ic_delete_forever') img = Image.open('.resources/wm_icon.png') app_icon = ImageTk.PhotoImage(image=img) if backup_enabled(): check = check_backup() if check != 1: # print(check) pass self.title('Meta-Jurnl') self.iconphoto(True, app_icon) self.withdraw() menu_bar = Menu(master=self) self.config(menu=menu_bar) # menu_bar.pack(fill='x') file_menu = Menu(master=menu_bar, tearoff=0, relief='flat', borderwidth=1) file_menu.add_command(label='New Entry', command=self.add_writer) file_menu.add_command(label='New Linked Entry') page_menu = Menu(master=file_menu, tearoff=0) page_menu.add_command(label='New Reader', command=self.add_reader) page_menu.add_command(label='New Writer', command=self.add_writer) file_menu.add_cascade(label='New', menu=page_menu, underline=0) file_menu.add_command(label='Save') file_menu.add_command(label='Quit', command=self.destroy) edit_menu = Menu(master=menu_bar, tearoff=0) edit_menu.add_command(label='Preferences') help_menu = Menu(master=menu_bar, tearoff=0) help_menu.add_command(label='Keyboard Shortcuts') help_menu.add_command(label='About') menu_bar.add_cascade(label='File', menu=file_menu, underline=0) menu_bar.add_cascade(label='Edit', menu=edit_menu, underline=0) menu_bar.add_cascade(label='Help', menu=help_menu, underline=0) toolbar = Frame(master=self, relief='flat', borderwidth=0, padding=5) toolbar.pack(fill='x') self.journal = Journal(master=self) self.journal.pack(fill='both', expand=True) self.autoimport() self.new_reader = Button(master=toolbar, image=self.new_reader_icon, command=self.add_reader) new_writer = Button(master=toolbar, image=self.new_writer_icon, command=self.add_writer) self.close_panel = Button(master=toolbar, text='Close Tab', image=self.close_panel_icon, command=self.journal.remove_page) self.clear_button = Button(master=toolbar, image=self.clear_content_icon, command=self.clear_fields) self.save_button = Button(master=toolbar, image=self.save_icon, command=self.save_entry) self.edit_button = Button(master=toolbar, image=self.edit_icon, command=self.edit_entry) self.link_button = Button(master=toolbar, image=self.link_icon, command=self.link_entry) self.delete_button = Button(master=toolbar, image=self.delete_icon) self.new_reader.pack(side='left', padx=(0, 5)) new_writer.pack(side='left', padx=(0, 5)) self.close_panel.pack(side='left', padx=(0, 5)) self.clear_button.pack(side='left', padx=(0, 5)) self.close_panel.state(['disabled']) self.clear_button.state(['disabled']) self.journal.update_idletasks() s_width = self.winfo_screenwidth() / 2 s_height = self.winfo_screenheight() / 2 dims = dimensions() try: w_width = self.winfo_reqwidth() if not dims[0] else dims[0] w_height = self.winfo_reqheight() if not dims[1] else dims[1] except IndexError: w_width = self.winfo_reqwidth() w_height = self.winfo_reqheight() pos = w_width, w_height, s_width - floor( w_width / 2), s_height - floor(w_height / 2) self.geometry('{}x{}+{}+{}'.format(int(pos[0]), int(pos[1]), int(pos[2]), int(pos[3]))) self.bind('<Configure>', self.update_dimensions) self.bind_all('<<Check Save Button>>', self.check_writer_buttons) self.bind_all('<<Id Selected>>', self.check_reader_buttons) self.bind('<<NotebookTabChanged>>', self.change_buttons) self.change_buttons() self.after(1000, self.deiconify) self.mainloop() def update_dimensions(self, event): # s_width = self.winfo_screenwidth() / 2 # s_height = self.winfo_screenheight() / 2 w_width = max(self.winfo_width(), 1500) w_height = max(self.winfo_height(), 600) # pos = w_width, w_height, self.winfo_rootx(), self.winfo_rooty() # self.geometry('{}x{}+{}+{}'.format( # int(pos[0]), int(pos[1]), int(pos[2]), int(pos[3]) # )) dimensions((w_width, w_height)) def add_reader(self): if not database_is_empty(): self.journal.add_page('Reader') def add_writer(self): self.journal.add_page('Writer') # def add_tagged_writer(self): # self.journal.add_page('Writer', tags=True) def change_buttons(self, event=None): self.new_reader.state( ['disabled' if database_is_empty() else '!disabled']) if self.journal.mode_ == 'Writer': self.close_panel.state(['!disabled']) self.clear_button.state(['!disabled']) self.save_button.pack_forget() self.edit_button.pack_forget() self.link_button.pack_forget() self.delete_button.pack_forget() self.save_button.pack(side='left', padx=(0, 5)) self.link_button.pack(side='left', padx=(0, 5)) self.check_writer_buttons() elif self.journal.mode_ == 'Reader': self.close_panel.state(['!disabled']) self.clear_button.state(['!disabled']) self.save_button.pack_forget() self.edit_button.pack_forget() self.link_button.pack_forget() self.delete_button.pack_forget() self.edit_button.pack(side='left', padx=(0, 5)) self.link_button.pack(side='left', padx=(0, 5)) self.delete_button.pack(side='left', padx=(0, 5)) self.check_reader_buttons() else: self.close_panel.state(['disabled']) self.clear_button.state(['disabled']) self.save_button.pack_forget() self.edit_button.pack_forget() self.link_button.pack_forget() self.delete_button.pack_forget() def check_writer_buttons(self, event=None): saved = self.journal.check_saved(event) self.save_button.state(['disabled' if saved else '!disabled']) self.link_button.state( ['disabled' if not self.journal.id_ else '!disabled']) def check_reader_buttons(self, event=None): entry = True if self.journal.id_ else False self.edit_button.state(['disabled' if not entry else '!disabled']) self.link_button.state(['disabled' if not entry else '!disabled']) self.delete_button.state(['disabled' if not entry else '!disabled']) def save_entry(self): self.journal.save() self.change_buttons() def edit_entry(self): self.journal.add_page(mode='Writer', entry_id=self.journal.id_) def link_entry(self): self.journal.add_page(mode='Writer', parent=self.journal.id_) def delete_entry(self): # TODO Implement pass def clear_fields(self): if self.journal.mode_ == 'Writer': saved = self.journal.check_saved() if not saved: if askquestion( 'Clear Window Contents?', 'There are unsaved edits in this tab.\nSave before continuing?' ): self.save_entry() self.journal.clear() def autoimport(self): import_entries() self.journal.refresh_readers() if autodelete_imports(): delete_imports()
class Application(Frame): """The main Tk application, a simple dialog.""" def __init__(self, master=None): super().__init__(master) self.badge = None self.grid() self.columnconfigure(0, minsize=200) self.columnconfigure(1, minsize=200) self.rowconfigure(0, minsize=300) self.rowconfigure(3, minsize=30) self.create_widgets() # self.output.insert("1.0", "This is a test\n") # self.sentto.insert("end", b"this is an error\n", "error") self.connect() self.after(SERIAL_POLL_INTERVAL, self.poll_serial) def create_widgets(self): """Sets up dialog elements.""" da_row = 0 "counter so I don't have to update every grid() call when I add/remove row" self.select = tix.FileSelectBox(self, browsecmd=self.on_file_selected, pattern="*.fs", directory="forth") # self.select["textVariable"] = self.forth_file self.select.grid(row=da_row, columnspan=2, sticky='nwes', pady=10) da_row += 1 self.output_label = Label(self, text="Badge Output") self.output_label.grid(row=da_row, column=1, sticky='w', padx=10, pady=3) self.sentto_label = Label(self, text="Sent To Badge") self.sentto_label.grid(row=da_row, column=0, sticky='w', padx=10, pady=3) da_row += 1 # height is in lines, width in characters self.output = Text(self, height=16, width=40) self.output.grid(row=da_row, column=1, padx=10) self.sentto = Text(self, height=16, width=40) self.sentto.tag_configure("error", background="red") self.sentto.grid(row=da_row, column=0, padx=10) da_row += 1 self.connect_btn = Button(self, text="Connect", command=self.toggle_connect) self.connect_btn.grid(row=da_row, column=0, columnspan=2) da_row += 1 self.exec_btn = Button(self, text="Execute", command=self.send_file) self.exec_btn.state(["disabled"]) self.exec_btn.grid(row=da_row, column=0, sticky='w' + 'e', padx=10, pady=3) self.quit = Button(self, text="QUIT", command=self.master.destroy) self.quit.grid(row=da_row, column=1, sticky='w' + 'e', padx=10, pady=3) da_row += 1 self.status_panel = Frame(self, relief="sunken", borderwidth=3) self.status_panel.grid(row=da_row, columnspan=2, sticky='nwse') self.connect_status = Label(self.status_panel, text="Not Connected") self.connect_status.grid(row=0, padx=10, pady=5, sticky="w") if self.badge is not None: self.connect_btn.state(["disabled"]) self.connect_status.config(text="Connected: " + self.badge.os_device) def send_file(self, _retry=False): """Send the selected file to the badge.""" if self.badge: try: # oddly, very first set LED seems to not set correct color self.badge.led(0, 0, 128) self.badge.led(0, 0, 128) with open(self.select.cget("value"), 'r') as forthin: self.badge.forth_run(forthin.read()) time.sleep(1) # because forth_run() may be too fast self.badge.led(0, 128, 0) except IOError: if not _retry: self.connect() self.send_file(True) else: raise def poll_serial(self): "Checks serial port for incoming bytes, reads and displays them." if self.badge is not None: bytes_in = self.badge.read_from() if bytes_in: self.output.insert("end", bytes_in + b'\n') self.after(SERIAL_POLL_INTERVAL, self.poll_serial) def toggle_connect(self): "If connected, disconnect, otherwise connect." if self.connect_btn.cget("text") == "Connect": self.connect() else: self.disconnect() def disconnect(self): "Disconnect from current badge." isinstance(self.badge, Badge) self.badge.close() self.badge = None self.connect_btn.config(text="Connect") self.connect_status.config(text="Not connected.") self.exec_btn.state(["disabled"]) def connect(self): """Attempt to connect to a badge; toggle Connect button if successful.""" try: self.badge = Badge(on_serial_write=self.on_bytes_sent, min_write_dt=0.01) self.connect_status.config(text="Connected: " + self.badge.os_device) self.connect_btn.config(text="Disconnect") # enable "Execute" if file is selected self.on_file_selected(self.select.cget("value")) except BadgeSerialException: self.connect_status.config(text="Not connected") def on_file_selected(self, selected_file): """Respond to user selection of file by enabling the Execute button.""" if Path(selected_file).is_file: self.exec_btn.state(["!disabled"]) else: self.exec_btn.state(["disabled"]) def on_bytes_sent(self, data, count): self.sentto.insert("end", data[:count]) if count < len(data): self.sentto.insert("end", data[count:], "error") self.sentto.insert("end", "\n")
class App(Tk): def __init__(self, master=None): super().__init__(master) self.style = ThemedStyle(self) self.style.set_theme('elegance') self.iconbitmap(r'data\app.ico') self.minsize(450, 300) self.title('WoT Battle Counter') self.menu_bar = Menu(self) self.content = Frame(self) self.entry = Entry(self.content) self.player_list = Listbox(self.content) self.count_button = Button(self) self.scrollbar = Scrollbar(self.content) self.buttons_frame = Frame(self) self.sort_button = Checkbutton(self.buttons_frame) self.progressbar = Progressbar(self.buttons_frame) self.sort_variable = IntVar(self) self.PlayerObjects = [] self.replays = [] self.player_names = [] self.offset = 0 self.skirmish_value = 1 self.advance_value = 1 self.clan_war_value = 3 def create_app(self): # Config menu entries and attach them self.menu_bar.add_command(label='Config', command=self.open_config_window) self.menu_bar.add_command(label='Open replay files', command=self.open_skirmish_files) self.menu_bar.add_command(label='Open list', command=self.load_list) self.menu_bar.add_command(label='Save list', command=self.save_list) self.menu_bar.add_command(label='Export to file', command=self.export_to_file) self.menu_bar.add_command(label='About', command=about) self.config(menu=self.menu_bar) # Config main content window self.content.pack(fill='both', expand=1) # Config Text Entry + bind enter key self.entry.config(exportselection=0) self.entry.pack(fill='x') self.entry.bind('<Return>', self.add_player) # Config Listbox + bind delete key self.player_list.config(yscrollcommand=self.scrollbar.set) self.scrollbar.config(command=self.player_list.yview, orient='vertical') self.scrollbar.pack(side='right', fill='y') self.player_list.pack(side='left', fill='both', expand=1) self.player_list.bind('<Delete>', self.remove_player) # Count button at the bottom self.count_button.config( text='Count!', command=Thread(target=self.decode_replays).start) self.count_button.pack(side='right', padx=10, pady=10) self.count_button.state(['disabled']) # Config button frame + button + progressbar self.buttons_frame.pack(side='left', fill='both', pady=5, padx=5, expand=1) self.sort_button.config(text="Sort the list", variable=self.sort_variable) self.sort_button.pack(anchor='nw', pady=3, padx=3) self.progressbar.config(length=360, mode='indeterminate', orient=HORIZONTAL, maximum=10) self.progressbar.pack(anchor='e', pady=3, padx=3) # Config the style Style().configure('TEntry', background='white') Style().configure('TButton', font=('Roboto', 12)) Style().configure('OK.TButton', font=('Roboto', 12, 'bold')) # Loading configuration self.load_config() # Start the app self.mainloop() def add_player(self, event): name = self.entry.get() self.entry.delete(0, 'end') player_obj = Player(name) self.PlayerObjects.append(player_obj) if self.sort_variable.get() == 1: self.PlayerObjects.sort(key=lambda player: player.name.lower()) self.player_list.delete(0, 'end') for player in self.PlayerObjects: self.player_list.insert('end', (self.PlayerObjects.index(player) + self.offset + 1, player.name)) else: self.player_list.delete(0, 'end') for player in self.PlayerObjects: self.player_list.insert('end', (self.PlayerObjects.index(player) + self.offset + 1, player.name)) def remove_player(self, event): select = self.player_list.curselection() name = self.player_list.get(select) self.player_list.delete(select) for player in self.PlayerObjects: if name.split()[1] == player.name: self.PlayerObjects.remove(player) self.player_list.delete(0, 'end') for player in self.PlayerObjects: self.player_list.insert('end', (self.PlayerObjects.index(player) + self.offset + 1, player.name)) def open_skirmish_files(self): directory_path = filedialog.askdirectory() if not path.exists(directory_path): return self.replays = self.list_dir(directory_path) self.count_button.state(['!disabled']) def save_list(self): file_path = filedialog.asksaveasfilename(defaultextension='.json') if not path.exists(file_path): return players = list() for player in self.PlayerObjects: players.append(player.name) if path.isfile(file_path): f = open(file_path, 'w') else: f = open(file_path, 'x') f.seek(0) f.write(dumps(players)) def load_list(self): file_path = filedialog.askopenfilename( filetypes=[('json-file', '*.json'), ('all files', '*.*')]) if path.isfile(file_path): self.player_list.delete(0, 'end') f = open(file_path, 'r') players = loads(f.read()) for name in players: player_obj = Player(name) self.PlayerObjects.append(player_obj) for player in self.PlayerObjects: self.player_list.insert('end', (self.PlayerObjects.index(player) + self.offset + 1, player.name)) def export_to_file(self): file_path = filedialog.asksaveasfilename(defaultextension='.txt') if path.isfile(file_path): f = open(file_path, 'w') elif path.exists(file_path): f = open(file_path, 'x') else: return data = str() for player in self.PlayerObjects: if player.battles >= 100: data += f'{player.battles} {player.name} \n' elif player.battles >= 10: data += f'{player.battles} {player.name} \n' elif player.battles > 0: data += f'{player.battles} {player.name} \n' f.seek(0) f.write(data) def list_dir(self, path): entries = listdir(path) re_replay = compile('\.wotreplay') re_file = compile('\.') replays = [] # recursive function for searching in subdirectories for .wotreplay files and putting them into a list for entry in entries: if not search(re_file, entry): new_path = path + "/" + entry new_replays = self.list_dir(new_path) for replay in new_replays: replays.append(replay) elif search(re_replay, entry): replays.append((path + '/' + entry)) elif not search(re_replay, entry) and search(re_file, entry): continue return replays def decode_replays(self): self.progressbar.start() thread_queue = Queue() replay_list_1 = [ self.replays[x] for x in range(0, round(len(self.replays) / 4)) ] replay_list_2 = [ self.replays[x] for x in range(round(len(self.replays) / 4), round(len(self.replays) / 4) * 2) ] replay_list_3 = [ self.replays[x] for x in range( round(len(self.replays) / 4) * 2, round(len(self.replays) / 4) * 3) ] replay_list_4 = [ self.replays[x] for x in range( round(len(self.replays) / 4) * 3, len(self.replays)) ] thread_1 = Thread(target=self.convert_binary_data, args=(replay_list_1, thread_queue)) thread_2 = Thread(target=self.convert_binary_data, args=(replay_list_2, thread_queue)) thread_3 = Thread(target=self.convert_binary_data, args=(replay_list_3, thread_queue)) thread_4 = Thread(target=self.convert_binary_data, args=(replay_list_4, thread_queue)) threads = (thread_1, thread_2, thread_3, thread_4) for thread in threads: thread.start() sleep(1) if self.listen_for_result(threads): self.player_names = thread_queue.get() for name in thread_queue.get(): self.player_names.append(name) for name in thread_queue.get(): self.player_names.append(name) for name in thread_queue.get(): self.player_names.append(name) # COUNTING TIME! for name in self.player_names: for player in self.PlayerObjects: if name[0] == player.name: player.battles += name[1] # Insert names together with battle count back into the list self.player_list.delete(0, 'end') for player in self.PlayerObjects: if player.battles > 0: self.player_list.insert( 'end', (self.PlayerObjects.index(player) + self.offset + 1, player.name, player.battles)) else: continue self.progressbar.stop() def listen_for_result(self, threads): # Check if all replay results have come in alive_threads = 0 for thread in threads: thread.join(0.1) for thread in threads: if thread.is_alive(): print("thread not ded") alive_threads += 1 if alive_threads > 0: if self.listen_for_result(threads): return True return True def convert_binary_data(self, replays, queue): player_names = list() for replay in range(len(replays)): filename_source = replays[replay] f = open(filename_source, 'rb') f.seek(8) size = f.read(4) data_block_size = unpack('I', size)[0] f.seek(12) my_block = f.read(int(data_block_size)) # Convert binary data into a json and then into an iterable tuple json_replay = loads(my_block) players_dict = [(v, k) for (k, v) in dict.items(json_replay['vehicles'])] # Extract names and append to a list for player_id in players_dict: player_name = player_id[0]['name'] if json_replay['battleType'] == 20: player_names.append((player_name, self.skirmish_value)) elif json_replay['battleType'] == 13: player_names.append((player_name, self.clan_war_value)) else: player_names.append((player_name, 1)) queue.put(player_names) def open_config_window(self): config_window = Toplevel(self) config_window.iconbitmap(r'data\app.ico') config_window.minsize(500, 350) config_frame = Labelframe(config_window) config_frame.config(text="App Configuration", relief='groove', borderwidth=5) config_frame.pack(expand=1, fill='both', padx=5, pady=5) offset_title = Label(config_frame) offset_title.config(text='Numbering offset (Default 0)') offset_title.pack(anchor='nw', padx=5, pady=5) offset_entry = Entry(config_frame) offset_entry.config( width=10, exportselection=0, validate='key', validatecommand=(offset_entry.register(validate_config_entry), '%P')) offset_entry.pack(anchor='nw', padx=5, pady=5) battle_value_frame = Labelframe(config_frame) battle_value_frame.config(text='Battle weighting', relief='groove', borderwidth=5) battle_value_frame.pack(anchor='nw', fill='both', expand=1, padx=5, pady=5) descriptor_frame = Frame(battle_value_frame) descriptor_frame.pack(side='left', fill='both', expand=1) entry_frame = Frame(battle_value_frame) entry_frame.pack(side='left', fill='both', expand=1) skirmish_title = Label(descriptor_frame) skirmish_title.config(text='Skirmish weighting (Default = 1):') skirmish_title.pack(anchor='nw', padx=5, pady=7) skirmish_entry = Entry(entry_frame) skirmish_entry.config( width=10, exportselection=0, validate='key', validatecommand=(skirmish_entry.register(validate_config_entry), '%P')) skirmish_entry.pack(anchor='nw', padx=5, pady=5) advance_title = Label(descriptor_frame) advance_title.config(text='Advance weighting (Default = 1):') advance_title.pack(anchor='nw', padx=5, pady=10) advance_entry = Entry(entry_frame) advance_entry.config( width=10, exportselection=0, validate='key', validatecommand=(advance_entry.register(validate_config_entry), '%P')) advance_entry.pack(anchor='nw', padx=5, pady=5) clan_war_title = Label(descriptor_frame) clan_war_title.config(text='Clan War weighting (Default = 3):') clan_war_title.pack(anchor='nw', padx=5, pady=6) clan_war_entry = Entry(entry_frame) clan_war_entry.config( width=10, exportselection=0, validate='key', validatecommand=(clan_war_entry.register(validate_config_entry), '%P')) clan_war_entry.pack(anchor='nw', padx=5, pady=5) buttons_frame = Frame(config_frame) buttons_frame.pack(anchor='sw', fill='both', expand=0) apply_button = Button(buttons_frame) apply_button.config(text='Apply', command=partial(self.config_apply, offset_entry, skirmish_entry, advance_entry, clan_war_entry)) apply_button.pack(side='right', padx=5, pady=5) cancel_button = Button(buttons_frame) cancel_button.config(text='Cancel', command=lambda: config_window.destroy()) cancel_button.pack(side='right', padx=5, pady=5) ok_button = Button(buttons_frame) ok_button.config(text='OK', style='OK.TButton', command=partial(self.config_ok, offset_entry, skirmish_entry, advance_entry, clan_war_entry, config_window)) ok_button.pack(side='right', padx=5, pady=5) offset_entry.insert('end', self.offset) skirmish_entry.insert('end', self.skirmish_value) advance_entry.insert('end', self.advance_value) clan_war_entry.insert('end', self.clan_war_value) def config_ok(self, offset, skirmish, advance, clan_war, window): self.offset = int(offset.get()) self.skirmish_value = int(skirmish.get()) self.advance_value = int(advance.get()) self.clan_war_value = int(clan_war.get()) data = { 'offset': offset.get(), 'skirmish_value': skirmish.get(), 'advance_value': advance.get(), 'clan_war_value': clan_war.get() } if path.isfile(r'config.json'): f = open(r'config.json', 'w') else: f = open(r'config.json', 'x') f.seek(0) f.write(dumps(data)) window.destroy() def config_apply(self, offset, skirmish, advance, clan_war): self.offset = int(offset.get()) self.skirmish_value = int(skirmish.get()) self.advance_value = int(advance.get()) self.clan_war_value = int(clan_war.get()) data = { 'offset': offset.get(), 'skirmish_value': skirmish.get(), 'advance_value': advance.get(), 'clan_war_value': clan_war.get() } if path.isfile(r'config.json'): f = open(r'config.json', 'w') else: f = open(r'config.json', 'x') f.seek(0) f.write(dumps(data)) def load_config(self): if path.isfile(r'config.json'): f = open(r'config.json', 'r') data = loads(f.read()) print(data) self.offset = int(data['offset']) self.skirmish_value = int(data['skirmish_value']) self.advance_value = int(data['advance_value']) self.clan_war_value = int(data['clan_war_value']) else: pass
class Application(Frame): """The main Tk application, a simple dialog.""" def __init__(self, master=None): super().__init__(master) self.badge = None self.grid() self.columnconfigure(0, minsize=200) self.columnconfigure(1, minsize=200) self.rowconfigure(0, minsize=300) self.rowconfigure(3, minsize=30) self.create_widgets() self.connect() def create_widgets(self): """Sets up dialog elements.""" self.select = tix.FileSelectBox(self, browsecmd=self.on_file_selected, pattern="*.fs", directory="forth") # self.select["textVariable"] = self.forth_file self.select.grid(row=0, columnspan=2, sticky='n'+'w'+'e') self.connect_btn = Button(self, text="Connect", command=self.toggle_connect) self.connect_btn.grid(row=1, column=0, columnspan=2) self.exec_btn = Button(self, text="Execute", command=self.send_file) self.exec_btn.state(["disabled"]) self.exec_btn.grid(row=2, column=0, sticky='w' + 'e', padx=5, pady=3) self.quit = Button(self, text="QUIT", command=self.master.destroy) self.quit.grid(row=2, column=1, sticky='w' + 'e', padx=5, pady=3) self.status_panel = Frame(self, relief="groove", borderwidth=3) self.status_panel.grid(row=3, columnspan=2, sticky='nwse') self.connect_status = Label(self.status_panel, text="Not Connected") self.connect_status.grid(row=0, padx=5, pady=5, sticky="w") if self.badge is not None: self.connect_btn.state(["disabled"]) self.connect_status.config(text="Connected: " + self.badge.os_device) def send_file(self, _retry=False): """Send the selected file to the badge.""" if self.badge: try: # oddly, very first set LED seems to not set correct color self.badge.led(0, 0, 128) self.badge.led(0, 0, 128) with open(self.select.cget("value"), 'r') as forthin: self.badge.forth_run(forthin.read()) time.sleep(1) # because forth_run() may be too fast self.badge.led(0, 128, 0) except IOError: if not _retry: self.connect() self.send_file(True) else: raise def toggle_connect(self): "If connected, disconnect, otherwise connect." if self.connect_btn.cget("text") == "Connect": self.connect() else: self.disconnect() def disconnect(self): "Disconnect from current badge." isinstance(self.badge, Badge) self.badge.close() self.badge = None self.connect_btn.config(text="Connect") self.connect_status.config(text="Not connected.") self.exec_btn.state(["disabled"]) def connect(self): """Attempt to connect to a badge; toggle Connect button if successful.""" try: self.badge = Badge() self.connect_status.config(text="Connected: " + self.badge.os_device) self.connect_btn.config(text="Disconnect") # enable "Execute" if file is selected self.on_file_selected(self.select.cget("value")) except BadgeSerialException: self.connect_status.config(text="Not connected") def on_file_selected(self, selected_file): """Respond to user selection of file by enabling the Execute button.""" if Path(selected_file).is_file: self.exec_btn.state(["!disabled"]) else: self.exec_btn.state(["disabled"])
class Application(Frame): """The main Tk application, a simple dialog.""" def __init__(self, master=None): super().__init__(master) self.badge = None self.grid() self.columnconfigure(0, minsize=200) self.columnconfigure(1, minsize=200) self.rowconfigure(0, minsize=300) self.rowconfigure(3, minsize=30) self.create_widgets() self.connect() def create_widgets(self): """Sets up dialog elements.""" self.select = tix.FileSelectBox(self, browsecmd=self.on_file_selected, pattern="*.fs", directory="forth") # self.select["textVariable"] = self.forth_file self.select.grid(row=0, columnspan=2, sticky='n' + 'w' + 'e') self.connect_btn = Button(self, text="Connect", command=self.toggle_connect) self.connect_btn.grid(row=1, column=0, columnspan=2) self.exec_btn = Button(self, text="Execute", command=self.send_file) self.exec_btn.state(["disabled"]) self.exec_btn.grid(row=2, column=0, sticky='w' + 'e', padx=5, pady=3) self.quit = Button(self, text="QUIT", command=self.master.destroy) self.quit.grid(row=2, column=1, sticky='w' + 'e', padx=5, pady=3) self.status_panel = Frame(self, relief="groove", borderwidth=3) self.status_panel.grid(row=3, columnspan=2, sticky='nwse') self.connect_status = Label(self.status_panel, text="Not Connected") self.connect_status.grid(row=0, padx=5, pady=5, sticky="w") if self.badge is not None: self.connect_btn.state(["disabled"]) self.connect_status.config(text="Connected: " + self.badge.os_device) def send_file(self, _retry=False): """Send the selected file to the badge.""" if self.badge: try: # oddly, very first set LED seems to not set correct color self.badge.led(0, 0, 128) self.badge.led(0, 0, 128) with open(self.select.cget("value"), 'r') as forthin: self.badge.forth_run(forthin.read()) time.sleep(1) # because forth_run() may be too fast self.badge.led(0, 128, 0) except IOError: if not _retry: self.connect() self.send_file(True) else: raise def toggle_connect(self): "If connected, disconnect, otherwise connect." if self.connect_btn.cget("text") == "Connect": self.connect() else: self.disconnect() def disconnect(self): "Disconnect from current badge." isinstance(self.badge, Badge) self.badge.close() self.badge = None self.connect_btn.config(text="Connect") self.connect_status.config(text="Not connected.") self.exec_btn.state(["disabled"]) def connect(self): """Attempt to connect to a badge; toggle Connect button if successful.""" try: self.badge = Badge() self.connect_status.config(text="Connected: " + self.badge.os_device) self.connect_btn.config(text="Disconnect") # enable "Execute" if file is selected self.on_file_selected(self.select.cget("value")) except BadgeSerialException: self.connect_status.config(text="Not connected") def on_file_selected(self, selected_file): """Respond to user selection of file by enabling the Execute button.""" if Path(selected_file).is_file: self.exec_btn.state(["!disabled"]) else: self.exec_btn.state(["disabled"])
class Timer(BaseWidget): def __init__(self, master): BaseWidget.__init__(self, 'Timer', master) def create_content(self, **kw): self.minsize(50, 120) self._time = [0, 0, 0] self._on = False self._after_id = '' self.img_play = PhotoImage(master=self, file=IM_START) self.img_pause = PhotoImage(master=self, file=IM_PAUSE) self.img_stop = PhotoImage(master=self, file=IM_STOP) self.rowconfigure(2, weight=1) self.columnconfigure(0, weight=1) self.columnconfigure(1, weight=1) # --- GUI elements self.display = Label(self, text='%i:%.2i:%.2i' % tuple(self._time), anchor='center', style='timer.TLabel') self.intervals = Text(self, highlightthickness=0, relief='flat', height=3, width=1, inactiveselectbackground=self.style.lookup('TEntry', 'selectbackground')) self.intervals.tag_configure('center', justify='center') self.intervals.configure(state='disabled') self.b_interv = Button(self, text=_('Interval'), style='timer.TButton', command=self.add_interval) self.b_interv.state(('disabled',)) self.b_launch = Button(self, image=self.img_play, padding=2, command=self.launch, style='timer.TButton') self.b_stop = Button(self, image=self.img_stop, padding=2, command=self.stop, style='timer.TButton') # --- placement self.display.grid(row=0, columnspan=2, sticky='ew', padx=8, pady=(4, 0)) Label(self, text=_('Intervals:'), style='timer.TLabel').grid(row=1, columnspan=2, sticky='w', padx=4) self.intervals.grid(row=2, columnspan=2, sticky='eswn') self.b_interv.grid(row=3, columnspan=2, sticky='ew') self.b_launch.grid(row=4, column=0, sticky='ew') self.b_stop.grid(row=4, column=1, sticky='ew') self._corner = Sizegrip(self, style="timer.TSizegrip") self._corner.place(relx=1, rely=1, anchor='se') # --- bindings self.intervals.bind("<1>", lambda event: self.intervals.focus_set()) self.bind('<3>', lambda e: self.menu.tk_popup(e.x_root, e.y_root)) self.display.bind('<ButtonPress-1>', self._start_move) self.display.bind('<ButtonRelease-1>', self._stop_move) self.display.bind('<B1-Motion>', self._move) self.b_stop.bind('<Enter>', self._on_enter) self.b_stop.bind('<Leave>', self._on_leave) def update_style(self): self.attributes('-alpha', CONFIG.get(self.name, 'alpha', fallback=0.85)) bg = CONFIG.get('Timer', 'background') fg = CONFIG.get('Timer', 'foreground') active_bg = active_color(*self.winfo_rgb(bg)) self.configure(bg=bg) self.menu.configure(bg=bg, fg=fg, selectcolor=fg, activeforeground=fg, activebackground=active_bg) self.menu_pos.configure(bg=bg, fg=fg, selectcolor=fg, activeforeground=fg, activebackground=active_bg) self.display.configure(font=CONFIG.get('Timer', 'font_time')) self.intervals.configure(bg=bg, fg=fg, font=CONFIG.get('Timer', 'font_intervals')) self.style.configure('timer.TButton', background=bg, relief='flat', foreground=fg, borderwidth=0) self.style.configure('timer.TLabel', background=bg, foreground=fg) self.style.configure('timer.TSizegrip', background=bg) self.style.map('timer.TSizegrip', background=[('active', active_bg)]) self.style.map('timer.TButton', background=[('disabled', bg), ('!disabled', 'active', active_bg)]) def _on_enter(self, event=None): self._corner.state(('active',)) def _on_leave(self, event=None): self._corner.state(('!active',)) def show(self): self.deiconify() self.update_idletasks() self.withdraw() if self._position.get() == 'above': self.overrideredirect(True) else: self.overrideredirect(False) BaseWidget.show(self) self.update_idletasks() self.withdraw() self.deiconify() def _run(self): if self._on: self._time[2] += 1 if self._time[2] == 60: self._time[2] = 0 self._time[1] += 1 if self._time[1] == 60: self._time[1] = 0 self._time[0] += 1 self.display.configure(text='%i:%.2i:%.2i' % tuple(self._time)) self._after_id = self.after(1000, self._run) def launch(self): if self._on: self._on = False self.b_launch.configure(image=self.img_play) self.b_interv.state(('disabled',)) else: self._on = True self.b_interv.state(('!disabled',)) self.b_launch.configure(image=self.img_pause) self.after(1000, self._run) def add_interval(self): tps = '\n%i:%.2i:%.2i' % tuple(self._time) if self.intervals.get('1.0', 'end') == '\n': tps = tps[1:] self.intervals.configure(state='normal') self.intervals.insert('end', tps, 'center') self.intervals.configure(state='disabled') def stop(self): self._on = False self.b_interv.state(('disabled',)) self.b_launch.configure(image=self.img_play) self._time = [0, 0, 0] self.intervals.configure(state='normal') self.intervals.delete('1.0', 'end') self.intervals.configure(state='disabled') self.display.configure(text='%i:%.2i:%.2i' % tuple(self._time))
class Home(TkPage): Name = 'Home' #name of the class (attributes) Font = lambda Size: ('Courier', Size) #font of the page def __init__(self, Parent, *args, **kwargs): super().__init__(Parent, *args, **kwargs) #constructor of super class self.Songs = [Song.replace('.mid', '') for Song in DirList('Songs') if Song.endswith('.mid')] #mappable songs self.MappedSongs = [Song for Song in DirList('MappedSongs') if Song.endswith('.cmid')] #mapped and compiled song self.Playing = ThreadEvent() #pause state self.Stop = ThreadEvent() #playing state self.Stop.set() TopLabel = Label(self, text = 'Genshin Lyre Player', font= Home.Font(24), bd = 10) #top label with a title for the page TopLabel.place(anchor= 'n', relx= 0.5, rely = 0.015, relwidth = 1, relheight=0.15) #placing the label self.ItemList = ListBox(self) #item list of the song for Index,Item in enumerate(self.MappedSongs): #for loop for every compiled comiled song self.ItemList.insert(Index, Item) #indexing the song in the list self.ItemList.itemconfig(Index, {'bg' : '#C2C2C2'}) #background of itemlist self.ItemList.place(anchor= 'n', relx= 0.5, rely = 0.19, relwidth = 1, relheight = 0.46) #placing the item list #RefreshLogo = Photo(file = 'Res/Refresh.png') #logo of refresh button (not showing at the moment) self.RefreshButton = Button\ ( self, text = 'Refresh', command = lambda : self.Refresh() ) #button to refresh the song list #self.RefreshButton.image = RefreshLogo ######## self.RefreshButton.place(anchor= 'nw', relx = 0.01, rely = 0.7, relwidth = 0.18, relheight = 0.2) #placing the button self.StopButton = Button\ ( self, text = 'Stop', command = lambda : self.StopSong() ) self.StopButton.place(anchor= 'nw', relx= 0.21, rely = 0.7, relwidth = 0.18, relheight = 0.2) #PlayLogo = Photo(file = 'Res/Play.png') #logo of play button (not showing at the moment) self.PlayButton = Button\ ( self, text = 'Play', )#button to play the song selected self.PlayButton.config\ ( command =\ lambda: [ Thread(target = self.PlayTrack, args = (Track,), name = f'{self.ItemList.get("active")}[{i}]', daemon = True).start() for i, Track in enumerate(self.LoadSong()) ]#lambda: self.Play() ) #self.PlayButton.image = PlayLogo ########## self.PlayButton.place(anchor= 'nw', relx= 0.41, rely = 0.7, relwidth = 0.18, relheight = 0.2) #placing the button #PauseLogo = Photo(file = '') #logo of the pause button self.PauseButton = Button\ ( self, text = 'Pause', command = lambda : self.PauseSong() ) #button to pause a song self.PauseButton.place(anchor= 'nw', relx= 0.61, rely = 0.7, relwidth = 0.18, relheight = 0.2) #placing the button self.CompileButton = Button\ ( self, text = 'Compilation\n Screen', command = lambda : self.Compile(), ) self.CompileButton.place(anchor = 'nw', relx= 0.81, rely = 0.7, relwidth = 0.18, relheight = 0.2) #placing the button def Refresh(self): #this function refresh the song list self.MappedSongs = [Song for Song in DirList('MappedSongs') if Song.endswith('.cmid')] #check the folder for the songs self.ItemList.delete('0','end') #delete every item of the list for Index, Item in enumerate(self.MappedSongs): #loop for every song in the folder self.ItemList.insert(Index, Item) #index the song in the item list self.ItemList.itemconfig(Index, {'bg' : '#C2C2C2'}) #background of the itemlist def Countdown(self): #this function create an initial countdown for i in range(3): #3...2...1 print(3-i) self.after(1000) def LoadSong(self): self.PlayButton.state(['disabled']) #disable the play button (might cause some unexpected behaviours) Song = self.ItemList.get('active') #getting the selected song from the list self.Stop.clear() #reset the stop state self.Playing.set() with open('MappedSongs/' + Song, 'rb') as InputFile: #opening the compiled midi file Music = Load(InputFile) #load the searialized object self.Countdown() #initial countdown to give user time to switch to genshin return Music def PlayTrack(self, Part = None): #this (THREADED) function play a single part (track) of the selected song if Part == None: raise ValueError('Part cannot be None') else: print(f'{Identity()} ready') global Notes global DXCodes global NotesFlags Elements = len(Part) #keystrokes to execute Actual = 0 #element counter def PlayNote(Sound, Duration): #this function play a single note of the part NotesFlags[Sound] = False #make the resource unavailable for other threads (to avoid deadlock) PressKey(DXCodes[Notes[Sound]]) #press note-corresponding key Sleep(abs(Duration)) #wait the duration of the note ReleaseKey(DXCodes[Notes[Sound]]) #release note-corresponding key NotesFlags[Sound] = True #make the resource available for other threads def PlayChord(Sounds, Duration): # function play a single chord of the part #print(Duration) for Sound in Sounds: #for loop to make every note of the chord unavailable for other threads (to avoid deadlock) NotesFlags[Sound] = False #lock single resource for Sound in Sounds: #for loop to press chord-corresponding keys PressKey(DXCodes[Notes[Sound]]) #press the note-corresponding key of the chord Sleep(abs(Duration)) #wait the duration of the notes for Sound in Sounds: #for loop to release chord-corresponding keys ReleaseKey(DXCodes[Notes[Sound]]) #release the note-corresponding key of the chord for Sound in Sounds:#for loop to make every note of the chord available for other threads NotesFlags[Sound] = True #unlock single resource while not self.Stop.is_set() and Actual < Elements: if IsPressed('ctrl+space'): if not PauseFunction.locked(): PauseFunction.acquire() self.StopSong() PauseFunction.release() break if IsPressed('shift'): print('resume trigger') Sleep(1) self.Playing.set() while self.Playing.is_set() and Actual < Elements: if IsPressed('ctrl+space'): if not PauseFunction.locked(): PauseFunction.acquire() self.StopSong() PauseFunction.release() break if IsPressed('shift'): print('pause trigger') Sleep(1) self.Playing.clear() break Counter = 0 if Part[Actual]['Type'] == 'Note': #check if the element is a note Duration = float(Part[Actual]['Duration']) PartialDuration = Duration / 10 #duration splitted to check multiple times Note = f'{Part[Actual]["Sound"]}{Part[Actual]["Octave"]}' #extract the note if NotesFlags[Note] and IsMainAlive(): #check if the resource is available PlayNote(Note, Duration) #if the reseourse plays the note at full duration else: #if the resource is not available at the moment while not NotesFlags[Note] or Counter < 10: #check if the note is still playable Sleep(PartialDuration) #waiting the partial duration to check availability Counter += 1 #increasing wastd times if NotesFlags[Note] and Counter < 10: #check if the resource are available and the note is still playable RemainingDuration = Duration - (PartialDuration * Counter) #calculate remaining duration PlayNote(Note, RemainingDuration) #play the note at partial duration elif Part[Actual]['Type'] == 'Chord': # check if the element is a chord (multiple notes together) NotesList = Part[Actual]['Sound'] #extract notes of the chord Octaves = Part[Actual]['Octave'] #extract respective octaves of the notes Chord = [f'{Note}{Octave}' for Note, Octave in zip(NotesList, Octaves)] # combine notes and octaves together Duration = float(Part[Actual]['Duration']) PartialDuration = Duration / 10 #duration splitted to check multiple times if all([NotesFlags[Note] for Note in Chord]) and IsMainAlive(): #check if all the notes in the chord are available (otherwise the cord wouldn't make sense) PlayChord(Chord, Duration) #play the chord at full duration else: while not all([NotesFlags[Note] for Note in Chord]): #check if the chord is stil playable Sleep(PartialDuration) #waiting the partial duration to check availability Counter += 1 #increasing wasted times if all([NotesFlags[Note] for Note in Chord]) and IsMainAlive(): #check if the resources are available and the chors is still playable RemainingDuration = Duration - (PartialDuration * Counter) #calculate remaining duration PlayChord(Chord, RemainingDuration) #play the chord at partial duration elif Part[Actual]['Type'] == 'Rest': #check if the element is a rest Duration = float(Part[Actual]['Duration']) Sleep(abs(Duration)) #wait the rest Actual += 1 #increase the played notes Sleep(5) print(f'{Identity()} ended') def PauseSong(self): #this fucntion pause the song playing Sleep(2) #delay to get rid of function call besides the first if self.Playing.is_set(): #check if the song is playing print(f'{Identity()} is pausing') self.Playing.clear() #clear the playing state if not self.Playing.is_set(): #check if the song is not playing print(f'{Identity()} is resuming') self.Playing.set() #set the song as playing def StopSong(self): #this fucntion stop the song if not StopFunction.locked(): #check if the fucntion has called by multiple thread print('Stop called') Sleep(1) self.Stop.set() #set the stop state self.Playing.clear() #clear the playing state self.PlayButton.state(['!disabled']) #enable the play button self.PlayButton.state(['!pressed']) #reset the play button to the default state ReleaseResources() #release possible hanging resources def Compile(self): #this function switch to compilation screen GenshinLyrePlayer.Raise(CompilationScreen.Name)