class Ufd: """ Universal File Dialog - "UFD" Unopinionated, minimalist, reusable, slightly configurable, general-purpose file-dialog. """ def __init__(self, title: str = "Universal File Dialog", icon: str = "", show_hidden: bool = False, include_files: bool = True, multiselect: bool = True, select_dirs: bool = True, select_files: bool = True, unix_delimiter: bool = True, stdout: bool = False): """ Init kwargs as object attributes, save references to Tk PhotoImages, & define the widgets + layout """ if not isinstance(title, str): raise TypeError("Argument title must be type string.") self.title = title if icon: if not isinstance(icon, str): raise TypeError("Argument icon must be type string.") if not isfile(icon): raise FileNotFoundError(f"File not found: {icon}") self.icon = icon else: self.icon = "" if show_hidden: self.show_hidden = True else: self.show_hidden = False if include_files: self.include_files = True else: self.include_files = False if multiselect: self.multiselect = True else: self.multiselect = False if select_dirs: self.select_dirs = True else: self.select_dirs = False if select_files: self.select_files = True else: self.select_files = False if unix_delimiter: self.unix_delimiter = True else: self.unix_delimiter = False if stdout: self.stdout = True else: self.stdout = False # Tkinter: self.dialog = Tk() self.dialog.withdraw() self.dialog.title(self.title) self.dialog.minsize(width=300, height=200) self.dialog.geometry("500x300") self.dialog.update_idletasks() self.file_icon = PhotoImage(file=f"{dirname(__file__)}/file.gif", master=self.dialog).subsample(50) self.folder_icon = PhotoImage(file=f"{dirname(__file__)}/folder.gif", master=self.dialog).subsample(15) self.disk_icon = PhotoImage(file=f"{dirname(__file__)}/disk.gif", master=self.dialog).subsample(15) if self.icon: self.dialog.iconbitmap(self.icon) else: self.dialog.iconbitmap(f"{dirname(__file__)}/icon.ico") # Widgets: self.paneview = PanedWindow( self.dialog, sashwidth=7, bg="#cccccc", bd=0, ) self.left_pane = PanedWindow(self.paneview) self.right_pane = PanedWindow(self.paneview) self.paneview.add(self.left_pane) self.paneview.add(self.right_pane) self.treeview_x_scrollbar = Scrollbar(self.left_pane, orient="horizontal") self.treeview_y_scrollbar = Scrollbar(self.left_pane, orient="vertical") self.list_box_x_scrollbar = Scrollbar(self.right_pane, orient="horizontal") self.list_box_y_scrollbar = Scrollbar(self.right_pane, orient="vertical") # tstyle = Style().configure(".", ) self.treeview = Treeview( self.left_pane, xscrollcommand=self.treeview_x_scrollbar.set, yscrollcommand=self.treeview_y_scrollbar.set, show="tree", selectmode="browse", # style=tstyle ) self.list_box = Listbox(self.right_pane, xscrollcommand=self.list_box_x_scrollbar.set, yscrollcommand=self.list_box_y_scrollbar.set, width=34, highlightthickness=0, bd=2, relief="ridge") if self.multiselect: self.list_box.config(selectmode="extended") else: self.list_box.config(selectmode="browse") self.cancel_button = Button(self.left_pane, text="Cancel", command=self.cancel) self.submit_button = Button(self.right_pane, text="Submit", command=self.submit) self.treeview_x_scrollbar.config(command=self.treeview.xview) self.treeview_y_scrollbar.config(command=self.treeview.yview) self.list_box_x_scrollbar.config(command=self.list_box.xview) self.list_box_y_scrollbar.config(command=self.list_box.yview) #Layout: self.dialog.rowconfigure(0, weight=1) self.dialog.columnconfigure(0, weight=1) self.left_pane.grid_rowconfigure(0, weight=1) self.left_pane.grid_columnconfigure(0, weight=1) self.right_pane.grid_rowconfigure(0, weight=1) self.right_pane.grid_columnconfigure(0, weight=1) self.paneview.paneconfigure( self.left_pane, minsize=100, #Start off w/ the sash centered in the GUI: width=(self.dialog.winfo_width() / 2) - ceil( (self.paneview.cget("sashwidth") * 1.5)), ) self.paneview.paneconfigure(self.right_pane, minsize=100) self.paneview.grid(row=0, column=0, sticky="nsew") self.treeview.grid(row=0, column=0, sticky="nsew") self.treeview_y_scrollbar.grid(row=0, column=1, sticky="ns") self.treeview_x_scrollbar.grid(row=1, column=0, columnspan=2, sticky="ew") self.list_box.grid(row=0, column=0, sticky="nsew") self.list_box_y_scrollbar.grid(row=0, column=1, sticky="ns") self.list_box_x_scrollbar.grid(row=1, column=0, columnspan=2, sticky="ew") self.cancel_button.grid(row=2, column=0, sticky="w", padx=10, pady=10) self.submit_button.grid(row=2, column=0, columnspan=2, sticky="e", padx=10, pady=10) #Bindings, Protocols, & Misc: self.dialog.bind("<Control-w>", self.cancel) self.treeview.bind("<<TreeviewSelect>>", self.treeview_select) self.treeview.bind("<Double-Button-1>", self.dialog_populate) self.treeview.bind("<Return>", self.dialog_populate) self.treeview.bind("<Right>", self.dialog_populate) self.list_box.bind("<<ListboxSelect>>", self.list_box_select) self.list_box.bind("<Return>", self.submit) self.dialog.protocol("WM_DELETE_WINDOW", self.cancel) self.dialog_selection = deque() self.selection_paths = deque() for disk in self.get_disks(): self.treeview.insert( "", index="end", text=disk, image=self.disk_icon, ) self.dialog.focus() def __call__(self): """ Display dialog & return selection """ (width_offset, height_offset) = self.get_offset(self.dialog) self.dialog.geometry(f"+{width_offset}+{height_offset}") self.dialog.update_idletasks() self.dialog.deiconify() self.dialog.wait_window() for i, path in enumerate(self.dialog_selection): if self.unix_delimiter: self.dialog_selection[i] = sub("\\\\", "/", path) else: self.dialog_selection[i] = sub("/", "\\\\", path) if self.stdout: [print(item) for item in self.dialog_selection] return list(self.dialog_selection) def __str__(self): """ Return own address """ return "Universal File Dialog"\ f" @ {hex(id(self))}" def __repr__(self): """ Return full string representation of constructor signature """ return f"Ufd("\ f"title=\"{self.title}\","\ f" icon=\"{self.icon}\","\ f" show_hidden={self.show_hidden},"\ f" include_files={self.include_files},"\ f" multiselect={self.multiselect},"\ f" select_dirs={self.select_dirs},"\ f" select_files={self.select_files},"\ f" unix_delimiter={self.unix_delimiter})"\ f" stdout={self.stdout})"\ f" @ {hex(id(self))}" @staticmethod def get_offset(tk_window): """ Returns an appropriate offset for a given tkinter toplevel, such that it always is created center screen on the primary display. """ width_offset = int((tk_window.winfo_screenwidth() / 2) - (tk_window.winfo_width() / 2)) height_offset = int((tk_window.winfo_screenheight() / 2) - (tk_window.winfo_height() / 2)) return (width_offset, height_offset) @staticmethod def get_disks(): """ Returns all mounted disks (for Windows) >> ["A:", "B:", "C:"] """ if system() != "Windows": raise OSError("For use with Windows platforms.") logicaldisks = run(["wmic", "logicaldisk", "get", "name"], capture_output=True) return findall("[A-Z]:", str(logicaldisks.stdout)) @staticmethod def list_dir(path, force=False): """ Reads a directory with a shell call to dir. Truthiness of bool force determines whether hidden items are returned or not. (For Windows) """ path = sub("/", "\\\\", path) if force: dir_listing = run(["dir", path, "/b", "/a"], shell=True, capture_output=True) else: dir_listing = run(["dir", path, "/b"], shell=True, capture_output=True) output = dir_listing.stdout err = dir_listing.stderr if not output: return [] if err: err = err.decode("utf-8") raise Exception(err) str_output = output.decode("utf-8") list_output = re_split("\r\n", str_output) return sorted([item for item in list_output if item]) def climb(self, item): """ Builds & returns a complete path to root directory, including the item name itself as the path tail. An extra delimiter is appeneded for the subsequent child node, which is normalized in dialog_populate() """ item_text = self.treeview.item(item)["text"] parent = self.treeview.parent(item) path = "" parents = deque() while parent: parents.append(self.treeview.item(parent)["text"] + "/") parent = self.treeview.parent(parent) for parent in reversed(parents): path += parent path += item_text + "/" return path def dialog_populate(self, event=None): """ Dynamically populates & updates the treeview, listbox, and keeps track of the full paths corresponding to each item in the listbox """ if not self.treeview.focus(): return self.treeview.column("#0", width=1000) existing_children = self.treeview.get_children(self.treeview.focus()) [self.treeview.delete(child) for child in existing_children] self.list_box.delete(0, "end") self.selection_paths.clear() focus_item = self.treeview.focus() path = self.climb(focus_item) if self.show_hidden: children = self.list_dir(path, force=True) else: children = self.list_dir(path) for child in children: if isdir(path + child): self.treeview.insert(focus_item, index="end", text=child, image=self.folder_icon) if self.select_dirs: self.list_box.insert("end", child) self.selection_paths.append(path + child) elif isfile(path + child): if self.include_files: self.treeview.insert(focus_item, index="end", text=child, image=self.file_icon) if self.select_files: self.list_box.insert("end", child) self.list_box.itemconfig("end", {"bg": "#EAEAEA"}) self.selection_paths.append(path + child) if isfile(normpath(path)): (head, tail) = path_split(normpath(path)) head = sub("\\\\", "/", head) self.list_box.insert("end", tail) self.selection_paths.append(head + "/" + tail) self.list_box.itemconfig("end", {"bg": "#EAEAEA"}) def list_box_select(self, event=None): """ Dynamically refresh the dialog selection with what's selected in the listbox (Callback for <<ListboxSelect>>). """ self.dialog_selection.clear() for i in self.list_box.curselection(): self.dialog_selection.append(self.selection_paths[i]) def treeview_select(self, event=None): """ Dynamically refresh the dialog selection with what's selected in the treeview (Callback for <<TreeviewSelect>>). """ for i in self.list_box.curselection(): self.list_box.selection_clear(i) self.dialog_selection.clear() item = normpath(self.climb(self.treeview.focus())) self.dialog_selection.append(item) def submit(self, event=None): """ Satisfies wait_window() in self.__call__() and validates selection (Callback for <Return>, <Button-1> on file_list, submit_button) """ if self.select_dirs == False: for item in self.dialog_selection: if isdir(item): messagebox.showwarning( "Error - Invalid Selection", "Unable to select directory. Please select a file(s).") return if self.select_files == False: for item in self.dialog_selection: if isfile(item): messagebox.showwarning( "Error - Invalid Selection", "Unable to select file. Please select a folder(s)") return self.dialog.destroy() def cancel(self, event=None): """ Satisfies wait_window() in self.__call__() (Callback for <Button-1> on cancel_button) (Callback for protocol "WM_DELETE_WINDOW" on self.dialog) """ self.dialog_selection.clear() self.dialog.destroy()
class PyLatency: """Ping tool visualization with tkinter""" def __init__(self, root): """Setup window geometry & widgets + layout, init counters""" self.master = root self.master.title("pyLatency") self.appdata_dir = getenv("APPDATA") + "/pyLatency" self.options_path = self.appdata_dir + "/options.json" self.log_dir = self.appdata_dir + "/logs" self.logfile = None self.options_logging = BooleanVar() self.options_geometry = "" self.options = self.init_options() if self.options: self.options_geometry = self.options["geometry"] self.options_logging.set(self.options["logging"]) if self.options_geometry: self.master.geometry(self.options_geometry) else: self.master.geometry("400x200") self.master.minsize(width=400, height=200) self.master.update() self.running = False self.hostname = None self.RECT_SCALE_FACTOR = 2 self.TIMEOUT = 5000 self.minimum = self.TIMEOUT self.maximum = 0 self.average = 0 self.SAMPLE_SIZE = 1000 self.sample = deque(maxlen=self.SAMPLE_SIZE) self.pcount = 0 self.max_bar = None self.min_bar = None # Widgets: self.frame = Frame(self.master) self.lbl_entry = Label(self.frame, text="Host:") self.lbl_status_1 = Label(self.frame, text="Ready") self.lbl_status_2 = Label(self.frame, fg="red") self.entry = Entry(self.frame) self.btn_start = Button(self.frame, text="Start", command=self.start) self.btn_stop = Button(self.frame, text="Stop", command=self.stop) self.chk_log = Checkbutton(self.frame, text="Enable log", variable=self.options_logging) self.delay_scale = Scale( self.frame, label="Interval (ms)", orient="horizontal", from_=100, to=self.TIMEOUT, resolution=100, ) self.delay_scale.set(1000) self.paneview = PanedWindow(self.master, sashwidth=5, bg="#cccccc") self.left_pane = PanedWindow(self.paneview) self.right_pane = PanedWindow(self.paneview) self.paneview.add(self.left_pane) self.paneview.add(self.right_pane) self.canvas_scroll_y = Scrollbar(self.left_pane) self.canvas = Canvas(self.left_pane, bg="#FFFFFF", yscrollcommand=self.canvas_scroll_y.set) self.canvas_scroll_y.config(command=self.canvas.yview) self.left_pane.add(self.canvas_scroll_y) self.ping_list_scroll = Scrollbar(self.master) self.ping_list = Listbox(self.right_pane, highlightthickness=0, font=14, selectmode="disabled", yscrollcommand=self.ping_list_scroll.set) self.ping_list_scroll.config(command=self.ping_list.yview) self.right_pane.add(self.ping_list_scroll) self.left_pane.add(self.canvas) self.right_pane.add(self.ping_list) # Layout: self.master.columnconfigure(0, weight=1) self.master.rowconfigure(1, weight=1) self.frame.columnconfigure(1, weight=1) self.frame.grid(row=0, column=0, sticky="nsew") self.lbl_entry.grid(row=0, column=0) self.lbl_status_1.grid(row=1, column=0, columnspan=4) self.lbl_status_2.grid(row=2, column=0, columnspan=4) self.entry.grid(row=0, column=1, sticky="ew") self.btn_start.grid(row=0, column=2) self.btn_stop.grid(row=0, column=3) self.chk_log.grid(row=1, column=2, columnspan=2) self.delay_scale.grid(row=0, column=4, rowspan=2) # self.canvas_scroll_y.grid(row=1, column=2, sticky="ns") self.paneview.grid(row=1, column=0, sticky="nsew") # self.ping_list_scroll.grid(row=1, column=1, sticky="ns") self.paneview.paneconfigure( self.left_pane, width=(self.master.winfo_width() - self.delay_scale.winfo_reqwidth()), ) #Bindings: self.canvas.bind("<MouseWheel>", self.scroll_canvas) self.master.bind("<Return>", self.start) self.master.bind("<Escape>", self.stop) self.master.bind("<Control-w>", lambda event: self.master.destroy()) self.master.bind( "<Up>", lambda event: self.delay_scale.set(self.delay_scale.get() + 100)) self.master.bind( "<Down>", lambda event: self.delay_scale.set(self.delay_scale.get() - 100)) self.master.protocol("WM_DELETE_WINDOW", self.master_close) def __str__(self): """Return own address""" return f"pyLatency GUI @ {hex(id(self))}" def start(self, event=None): """ Reset the GUI, create & start a thread so we don't block the mainloop during each poll. """ if not self.running: self.hostname = self.entry.get() if self.hostname: self.ping_list.delete(0, "end") self.canvas.delete("all") self.lbl_status_1.config(text="Running", fg="green") self.lbl_status_2.config(text="") self.sample.clear() (self.minimum, self.maximum, self.average, self.pcount) = self.TIMEOUT, 0, 0, 0 self.running = True self.thread = Thread(target=self.run, daemon=True) self.thread.start() else: self.lbl_status_2.config(text="Missing Hostname") def logged(fn): """ decorates self.run(), create a log directory if one doesn't exist, create a filename with a date & timestamp, call self.run() with logging enabled or disabled """ @wraps(fn) def inner(self): if self.options_logging.get(): if not exists(self.log_dir): mkdir(self.log_dir) timestamp = datetime.now() fname = timestamp.strftime("%a %b %d @ %H-%M-%S") with open(self.log_dir + f"/{fname}.txt", "w+") as self.logfile: self.logfile.write(f"pyLatency {fname}\n") self.logfile.write(f"Host: {self.hostname}\n") self.logfile.write("-" * 40 + "\n") start = default_timer() fn(self) end = default_timer() elapsed = end - start self.logfile.write("-" * 40 + "\n") self.logfile.write( f"Logged {self.pcount} pings over {int(elapsed)} seconds" ) else: fn(self) return inner @logged def run(self): """ Continuously shell out to ping, get an integer result, update the GUI, and wait. """ while self.running: latency = self.ping(self.hostname) self.pcount += 1 if latency is None: self.stop() self.lbl_status_2.config(text="Unable to ping host") return if latency > self.maximum: self.maximum = latency if latency < self.minimum: self.minimum = latency self.sample.append(latency) self.average = sum(self.sample) / len(self.sample) if self.logfile: self.logfile.write(str(latency) + "\n") self.update_gui(latency) sleep(self.delay_scale.get() / 1000) def update_gui(self, latency): """ Update the listbox, shift all existing rectangles, draw the latest result from self.ping(), cleanup unused rectangles, update the mainloop """ if self.ping_list.size() >= self.SAMPLE_SIZE: self.ping_list.delete(self.SAMPLE_SIZE - 1, "end") self.ping_list.insert(0, str(latency) + "ms") self.canvas.move("rect", 10, 0) self.canvas.create_rectangle(0, 0, 10, int(latency * self.RECT_SCALE_FACTOR), fill="#333333", tags="rect", width=0) self.canvas.delete(self.max_bar) self.max_bar = self.canvas.create_line( 0, self.maximum * self.RECT_SCALE_FACTOR, self.canvas.winfo_width(), self.maximum * self.RECT_SCALE_FACTOR, fill="red", ) self.canvas.delete(self.min_bar) self.min_bar = self.canvas.create_line( 0, self.minimum * self.RECT_SCALE_FACTOR, self.canvas.winfo_width(), self.minimum * self.RECT_SCALE_FACTOR, fill="green", ) # canvas scrollable region is not updated automatically self.canvas.configure(scrollregion=self.canvas.bbox("all")) self.lbl_status_2.config(fg="#000000", text=f"Min: {self.minimum} " f"Max: {self.maximum} " f"Avg: {round(self.average,2):.2f}") self.cleanup_rects() self.master.update() def scroll_canvas(self, event): """ Bound to <MouseWheel> tkinter event on self.canvas. Respond to Linux or Windows mousewheel event, and scroll the canvas accordingly """ count = None if event.num == 5 or event.delta == -120: count = 1 if event.num == 4 or event.delta == 120: count = -1 self.canvas.yview_scroll(count, "units") def cleanup_rects(self): """Delete rectangles that are outside the bbox of the canvas""" for rect in self.canvas.find_withtag("rect"): if self.canvas.coords(rect)[0] > self.canvas.winfo_width(): self.canvas.delete(rect) def stop(self, event=None): """Satisfy the condition in which self.thread exits""" if self.running: self.running = False self.lbl_status_1.config(text="Stopped", fg="red") def master_close(self, event=None): """Writes window geometry/options to the disk.""" options = dumps({ "geometry": self.master.geometry(), "logging": self.options_logging.get() }) if not exists(self.appdata_dir): mkdir(self.appdata_dir) with open(self.options_path, "w+") as options_file: options_file.write(options) self.master.destroy() def init_options(self): """Called on startup, loads, parses, and returns options from json.""" if exists(self.options_path): with open(self.options_path, "r") as options_file: options_json = options_file.read() return loads(options_json) else: return None @staticmethod def ping(url): """ Shell out to ping and return an integer result. Returns None if ping fails for any reason: timeout, bad hostname, etc. """ flag = "-n" if platform == "win32" else "-c" result = run(["ping", flag, "1", "-w", "5000", url], capture_output=True, creationflags=DETACHED_PROCESS) output = result.stdout.decode("utf-8") try: duration = findall("\\d+ms", output)[0] return int(duration[:-2]) except IndexError: return None
def __init__(self, config: Config = None): """Constructs the GUI element of the Validation Tool""" self.task = None self.config = config or Config() self._root = Tk() self._root.title(self.config.app_name) self._root.protocol("WM_DELETE_WINDOW", self.shutdown) if self.config.terms_link_text: menubar = Menu(self._root) menubar.add_command( label=self.config.terms_link_text, command=lambda: webbrowser.open(self.config.terms_link_url), ) self._root.config(menu=menubar) parent_frame = Frame(self._root) main_window = PanedWindow(parent_frame) main_window.pack(fill=BOTH, expand=1) control_panel = PanedWindow( main_window, orient=HORIZONTAL, sashpad=4, sashrelief=RAISED ) actions = Frame(control_panel) control_panel.add(actions) control_panel.paneconfigure(actions, minsize=250) if self.config.disclaimer_text or self.config.requirement_link_text: self.footer = self.create_footer(parent_frame) parent_frame.pack(fill=BOTH, expand=True) # profile start number_of_categories = len(self.config.category_names) category_frame = LabelFrame(actions, text="Additional Validation Categories:") category_frame.grid(row=1, column=1, columnspan=3, pady=5, sticky="we") self.categories = [] for x in range(0, number_of_categories): category_name = self.config.category_names[x] category_value = IntVar(value=0) category_value._name = "category_{}".format(category_name.replace(" ", "_")) # noinspection PyProtectedMember category_value.set(self.config.get_category_value(category_value._name)) self.categories.append(category_value) category_checkbox = Checkbutton( category_frame, text=category_name, variable=self.categories[x] ) ToolTip(category_checkbox, self.config.get_description(category_name)) category_checkbox.grid(row=x + 1, column=1, columnspan=2, sticky="w") settings_frame = LabelFrame(actions, text="Settings") settings_frame.grid(row=3, column=1, columnspan=3, pady=10, sticky="we") verbosity_label = Label(settings_frame, text="Verbosity:") verbosity_label.grid(row=1, column=1, sticky=W) self.verbosity = StringVar(self._root, name="verbosity") self.verbosity.set(self.config.default_verbosity(self.VERBOSITY_LEVELS)) verbosity_menu = OptionMenu( settings_frame, self.verbosity, *tuple(self.VERBOSITY_LEVELS.keys()) ) verbosity_menu.config(width=25) verbosity_menu.grid(row=1, column=2, columnspan=3, sticky=E, pady=5) report_format_label = Label(settings_frame, text="Report Format:") report_format_label.grid(row=2, column=1, sticky=W) self.report_format = StringVar(self._root, name="report_format") self.report_format.set(self.config.default_report_format) report_format_menu = OptionMenu( settings_frame, self.report_format, *self.config.report_formats ) report_format_menu.config(width=25) report_format_menu.grid(row=2, column=2, columnspan=3, sticky=E, pady=5) input_format_label = Label(settings_frame, text="Input Format:") input_format_label.grid(row=3, column=1, sticky=W) self.input_format = StringVar(self._root, name="input_format") self.input_format.set(self.config.default_input_format) input_format_menu = OptionMenu( settings_frame, self.input_format, *self.config.input_formats ) input_format_menu.config(width=25) input_format_menu.grid(row=3, column=2, columnspan=3, sticky=E, pady=5) self.halt_on_failure = BooleanVar(self._root, name="halt_on_failure") self.halt_on_failure.set(self.config.default_halt_on_failure) halt_on_failure_label = Label(settings_frame, text="Halt on Basic Failures:") halt_on_failure_label.grid(row=4, column=1, sticky=E, pady=5) halt_checkbox = Checkbutton( settings_frame, offvalue=False, onvalue=True, variable=self.halt_on_failure ) halt_checkbox.grid(row=4, column=2, columnspan=2, sticky=W, pady=5) directory_label = Label(actions, text="Template Location:") directory_label.grid(row=4, column=1, pady=5, sticky=W) self.template_source = StringVar(self._root, name="template_source") directory_entry = Entry(actions, width=40, textvariable=self.template_source) directory_entry.grid(row=4, column=2, pady=5, sticky=W) directory_browse = Button(actions, text="...", command=self.ask_template_source) directory_browse.grid(row=4, column=3, pady=5, sticky=W) validate_button = Button( actions, text="Validate Templates", command=self.validate ) validate_button.grid(row=5, column=1, columnspan=2, pady=5) self.result_panel = Frame(actions) # We'll add these labels now, and then make them visible when the run completes self.completion_label = Label(self.result_panel, text="Validation Complete!") self.result_label = Label( self.result_panel, text="View Report", fg="blue", cursor="hand2" ) self.underline(self.result_label) self.result_label.bind("<Button-1>", self.open_report) self.result_panel.grid(row=6, column=1, columnspan=2) control_panel.pack(fill=BOTH, expand=1) main_window.add(control_panel) self.log_panel = ScrolledText(main_window, wrap=WORD, width=120, height=20) self.log_panel.configure(font=font.Font(family="Courier New", size="11")) self.log_panel.pack(fill=BOTH, expand=1) main_window.add(self.log_panel) # Briefly add the completion and result labels so the window size includes # room for them self.completion_label.pack() self.result_label.pack() # Show report link self._root.after_idle( lambda: ( self.completion_label.pack_forget(), self.result_label.pack_forget(), ) ) self.config.watch( *self.categories, self.verbosity, self.input_format, self.report_format, self.halt_on_failure, ) self.schedule(self.execute_pollers) if self.config.terms_link_text and not self.config.are_terms_accepted: TermsAndConditionsDialog(parent_frame, self.config) if not self.config.are_terms_accepted: self.shutdown()
def __init__(self, controller): ttk.Frame.__init__(self) self.controller = controller self.window = self._nametowidget(self.winfo_parent()) self.window.bind("<Escape>", self.exit) self.window.title("LyfePixel") self.window.protocol("WM_DELETE_WINDOW", self.exit) self.controller.configure_toplevel(self.window) self.window.overrideredirect(0) self.place(relwidth=1, relheight=1) self.state = TOOLCONST.DRAW self.window.resizable(True, True) self.project = PixelProject(8, 8) self.pallet = self.project.selected_frame.selected_layer self.pallet.selection = [ f"{self.pallet.width - 1}x{self.pallet.height - 1}" ] self.alpha = 255 menubar = Menu(self.window) menubar.add_command(label="New", command=self.open_start_window) menubar.add_separator() menubar.add_command(label="Help", command=lambda: HelpWindow(self.controller)) menubar.add_separator() # display the menu self.window.config(menu=menubar) panes = PanedWindow(self, orient="vertical", sashpad=3, sashrelief="sunken") panes.pack(fill="both", expand=True) panes.config(borderwidth=0) canvas_frame = Frame(panes) canvas_frame.pack(fill="both", expand=True, side="top", anchor="n") panes.add(canvas_frame) panes.paneconfigure(canvas_frame, height=150) self.pallet_box = LyfeCanvas(self.project, canvas_frame) force_aspect(self.pallet_box, canvas_frame, 1.0) colors = get_gradient(8) colors.extend(get_rainbow(56)) for id in self.pallet_box.itterate_canvas(): self.pallet.set_pixel_color(id, hex_to_rgba(colors.pop())) self.pallet_box.bind_left(self.select_color) self.pallet_box.bind_double_left(self.change_color) self.pallet_box.bind("<Configure>", self.on_configure) self.pallet_box.configure() outer_frame = Frame(panes) outer_frame.pack(fill="both", expand=True, padx=4) panes.add(outer_frame) panes.paneconfigure(outer_frame, height=250) color_label_frame = Frame(outer_frame) self.color_label_text_var = StringVar() self.color_label_text_var.set(ToolController.get_color()) color_label = Label(color_label_frame, textvariable=self.color_label_text_var) color_label.pack(side="bottom", fill="x", expand=True) color_label_frame.pack(fill="x", expand=True) self.alpha_scale = Scale(outer_frame, orient="horizontal", from_=0, to=255, command=self.set_alpha) self.alpha_scale.set(self.alpha) self.alpha_scale.pack(fill="x", expand=True, pady=2) div_1 = ttk.Separator(outer_frame) div_1.pack(fill="x", expand=True, pady=2) self.toolbox = ToolBox(outer_frame) self.toolbox.pack(fill="both", expand=True, pady=2) self.grip = ttk.Sizegrip(self) self.grip.place(relx=1.0, rely=1.0, anchor="se") self.grip.bind("<ButtonPress-1>", self.on_press) self.grip.bind("<B1-Motion>", self.on_resize) self.grip.bind("<ButtonRelease-1>", self.on_release) self.clipboard_box = ClipBoardBox(self.controller, panes) self.clipboard_box.pack(fill="both", expand=True) panes.add(self.clipboard_box) self.window.minsize(250, 400) self.window.geometry(f"250x720") self.pallet_box.after_idle(self.refresh) color = self.pallet.array[self.pallet.width - 1][self.pallet.height - 1] ToolController.set_color(color) self.alpha = color[3] self.alpha_scale.set(self.alpha) self.pallet.start_selection = id self.pallet.end_selection = id self.update()
class CopyToMoveTo: """ Minimalist file manager intended to be used independently or alongside Windows Explorer """ def __init__(self, root): """ Setup window geometry, init settings, define widgets + layout """ self.master = root self.master.title("CopyTo-MoveTo") self.master.iconbitmap(f"{dirname(__file__)}/icon.ico") if system() != "Windows": self.master.withdraw() messagebox.showwarning( "Incompatible platform", "CopyTo-MoveTo currently supports Windows platforms only.") raise SystemExit #Settings: self.settings_show_hidden = BooleanVar() self.settings_include_files = BooleanVar(value=True) self.settings_ask_overwrite = BooleanVar() self.settings_ask_overwrite.trace("w", self.settings_exclusives) self.settings_rename_dupes = BooleanVar(value=True) self.settings_rename_dupes.trace("w", self.settings_exclusives) self.settings_multiselect = BooleanVar(value=True) self.settings_select_dirs = BooleanVar(value=True) self.settings_select_dirs.trace("w", self.settings_mutuals) self.settings_select_files = BooleanVar(value=True) self.settings_select_files.trace("w", self.settings_mutuals) self.settings_geometry = None self.appdata_dir = getenv("APPDATA") + "/CopyTo-MoveTo" self.appdata_path = self.appdata_dir + "/settings.json" self.settings = self.init_settings() if self.settings: self.settings_geometry = self.settings["geometry"] self.settings_show_hidden.set(self.settings["show_hidden"]) self.settings_include_files.set(self.settings["include_files"]) self.settings_ask_overwrite.set(self.settings["ask_overwrite"]) self.settings_rename_dupes.set(self.settings["rename_dupes"]) self.settings_multiselect.set(self.settings["multiselect"]) self.settings_select_dirs.set(self.settings["select_dirs"]) self.settings_select_files.set(self.settings["select_files"]) self.dialog_showing = BooleanVar() self.help_showing = BooleanVar() self.about_showing = BooleanVar() self.master.protocol("WM_DELETE_WINDOW", self.master_close) self.master.bind("<Control-w>", self.master_close) #Geometry: self.master.minsize(width=450, height=200) if self.settings_geometry: self.master.geometry(self.settings_geometry) self.master.update() else: self.master.geometry("600x400") self.master.update_idletasks() (width_offset, height_offset) = Ufd.get_offset(self.master) self.master.geometry(f"+{width_offset}+{height_offset}") self.master.update_idletasks() # Menu: self.main_menu = Menu(self.master) self.master.config(menu=self.main_menu) self.file_menu = Menu(self.main_menu, tearoff=0) self.settings_menu = Menu(self.main_menu, tearoff=0) self.main_menu.add_cascade(label="File", menu=self.file_menu) self.main_menu.add_cascade(label="Settings", menu=self.settings_menu) self.file_menu.add_command(label="Open Source(s)", accelerator="Ctrl+O", command=lambda: self.show_ufd(source=True)) self.master.bind("<Control-o>", lambda event: self.show_ufd(source=True)) self.file_menu.add_command(label="Open Destination(s)", accelerator="Ctrl+K+O", command=lambda: self.show_ufd(source=False)) self.master.bind("<Control-k>o", lambda event: self.show_ufd(source=False)) self.file_menu.add_separator() self.file_menu.add_command(label="Help / Commands", command=self.show_help) self.file_menu.add_command(label="About", command=self.show_about) #Settings menu: self.settings_menu.add_checkbutton(label="Show Hidden Files & Folders", variable=self.settings_show_hidden, onvalue=True, offvalue=False) self.settings_menu.add_checkbutton( label="Include Files in Tree", variable=self.settings_include_files, onvalue=True, offvalue=False) self.settings_menu.add_separator() self.settings_menu.add_checkbutton( label="Ask Overwrite", variable=self.settings_ask_overwrite, onvalue=True, offvalue=False) self.settings_menu.add_checkbutton(label="Rename Duplicates", variable=self.settings_rename_dupes, onvalue=True, offvalue=False) self.settings_menu.add_separator() self.settings_menu.add_checkbutton(label="Multiselect", variable=self.settings_multiselect, onvalue=True, offvalue=False) self.settings_menu.add_checkbutton(label="Select Folders", variable=self.settings_select_dirs, onvalue=True, offvalue=False) self.settings_menu.add_checkbutton(label="Select Files", variable=self.settings_select_files, onvalue=True, offvalue=False) self.main_menu.add_separator() #Menu commands: self.main_menu.add_command(label="Swap Selected", command=self.swap_selected) self.master.bind("<Control-s>", lambda event: self.swap_selected()) self.main_menu.add_command(label="Clear Selected", command=self.clear_selected) self.master.bind("<Control-x>", lambda event: self.clear_selected()) self.main_menu.add_command(label="Clear All", command=self.clear_all) self.master.bind("<Control-Shift-X>", lambda event: self.clear_all()) self.main_menu.add_separator() self.main_menu.add_command(label="COPY", command=lambda: self._submit(copy=True)) self.master.bind("<Control-Shift-Return>", lambda event: self._submit(copy=True)) self.main_menu.add_command(label="MOVE", command=lambda: self._submit(copy=False)) self.master.bind("<Control-Return>", lambda event: self._submit(copy=False)) # Body: self.paneview = PanedWindow(self.master, sashwidth=7, bg="#cccccc", bd=0, orient="vertical") self.top_pane = PanedWindow(self.paneview) self.bottom_pane = PanedWindow(self.paneview) self.paneview.add(self.top_pane) self.paneview.add(self.bottom_pane) self.label_source = Label(self.top_pane, text="Source(s):") self.label_dest = Label(self.bottom_pane, text="Destination(s):") self.y_scrollbar_source = Scrollbar(self.top_pane, orient="vertical") self.x_scrollbar_source = Scrollbar(self.top_pane, orient="horizontal") self.y_scrollbar_dest = Scrollbar(self.bottom_pane, orient="vertical") self.x_scrollbar_dest = Scrollbar(self.bottom_pane, orient="horizontal") self.list_box_source = Listbox( self.top_pane, selectmode="extended", yscrollcommand=self.y_scrollbar_source.set, xscrollcommand=self.x_scrollbar_source.set) self.list_box_dest = Listbox(self.bottom_pane, selectmode="extended", yscrollcommand=self.y_scrollbar_dest.set, xscrollcommand=self.x_scrollbar_dest.set) self.x_scrollbar_source.config(command=self.list_box_source.xview) self.y_scrollbar_source.config(command=self.list_box_source.yview) self.x_scrollbar_dest.config(command=self.list_box_dest.xview) self.y_scrollbar_dest.config(command=self.list_box_dest.yview) # Layout: self.master.rowconfigure(0, weight=1) self.master.columnconfigure(0, weight=1) self.top_pane.rowconfigure(1, weight=1) self.top_pane.columnconfigure(0, weight=1) self.bottom_pane.rowconfigure(1, weight=1) self.bottom_pane.columnconfigure(0, weight=1) self.paneview.paneconfigure(self.top_pane, minsize=100) self.paneview.paneconfigure(self.bottom_pane, minsize=100) self.paneview.grid(row=0, column=0, sticky="nsew") self.label_source.grid(row=0, column=0, sticky="w") self.list_box_source.grid(row=1, column=0, sticky="nsew") self.y_scrollbar_source.grid(row=1, column=1, sticky="ns") self.x_scrollbar_source.grid(row=2, column=0, sticky="ew") self.label_dest.grid(row=0, column=0, sticky="w", columnspan=2) self.list_box_dest.grid(row=1, column=0, sticky="nsew") self.y_scrollbar_dest.grid(row=1, column=1, sticky="ns") self.x_scrollbar_dest.grid(row=2, column=0, sticky="ew") def __str__(self): """Return own address""" return f"CopyTo-MoveTo @ {hex(id(self))}" def init_settings(self): """Called on startup, loads, parses, and returns json settings.""" if exists(self.appdata_path): with open(self.appdata_path, "r") as settings_file: settings_json = settings_file.read() settings = loads(settings_json) return settings else: return None def settings_exclusives(self, *args): """ Callback assigned to settings that are mutually exclusive, to prevent logical/runtime errors or unexpected behavior. """ if args[0] == "PY_VAR2": if self.settings_ask_overwrite.get() == 1: self.settings_rename_dupes.set(0) return elif args[0] == "PY_VAR3": if self.settings_rename_dupes.get() == 1: self.settings_ask_overwrite.set(0) return def settings_mutuals(self, *args): """ Prevent select folders & select files from being disabled concurrently If both are unselected, reselect the one we didn't just deselect on. """ if self.settings_select_dirs.get() == 0 \ and self.settings_select_files.get() == 0: if args[0] == "PY_VAR5": self.settings_select_files.set(1) elif args[0] == "PY_VAR6": self.settings_select_dirs.set(1) def master_close(self, event=None): """ Similar to utils.toplevel_close(). writes settings to the disk as json. """ settings = { "geometry": self.master.geometry(), "show_hidden": self.settings_show_hidden.get(), "include_files": self.settings_include_files.get(), "ask_overwrite": self.settings_ask_overwrite.get(), "rename_dupes": self.settings_rename_dupes.get(), "multiselect": self.settings_multiselect.get(), "select_dirs": self.settings_select_dirs.get(), "select_files": self.settings_select_files.get(), } settings_json = dumps(settings) if not exists(self.appdata_dir): mkdir(self.appdata_dir) with open(self.appdata_path, "w+") as settings_file: settings_file.write(settings_json) if self.dialog_showing.get() == 1: self.ufd.cancel() self.master.destroy() def toplevel_close(self, dialog, boolean): """ This callback flips the value for a given toplevel_showing boolean to false, before disposing of the toplevel. """ boolean.set(0) dialog.destroy() def swap_selected(self): """Swap list entries between source & destination""" source_selection = list(self.list_box_source.curselection()) dest_selection = list(self.list_box_dest.curselection()) for i in reversed(source_selection): item = self.list_box_source.get(i) self.list_box_source.delete(i) self.list_box_dest.insert("0", item) for i in reversed(dest_selection): item = self.list_box_dest.get(i) self.list_box_dest.delete(i) self.list_box_source.insert("0", item) def clear_selected(self): """Removes selected (highlighted) item(s) from a given listbox""" source_selection = list(self.list_box_source.curselection()) dest_selection = list(self.list_box_dest.curselection()) if source_selection: for i in reversed(source_selection): self.list_box_source.delete(i) self.list_box_source.selection_set(source_selection[0]) if dest_selection: for i in reversed(dest_selection): self.list_box_dest.delete(i) self.list_box_dest.selection_set(dest_selection[0]) def clear_all(self): """Clears both listboxes in the main UI, resetting the form.""" self.list_box_source.delete(0, "end") self.list_box_dest.delete(0, "end") def handled(fn): """Filesystem operations are wrapped here for error handling""" @wraps(fn) def inner(self, *args, **kwargs): try: fn(self, *args, **kwargs) return True except (PermissionError, FileNotFoundError) as err: self.skipped_err.append(f"{err.args[1]}:\n" + (" => ".join(args))) return False return inner @handled def _copy(self, path, destination): """Wrapper for shutil.copy2() || shutil.copytree()""" if isfile(path): copy2(path, destination) else: copytree(path, destination) @handled def _move(self, path, destination): """Wrapper for shutil.move()""" move(path, destination) @handled def _delete(self, path): """Wrapper for os.remove() || shutil.rmtree()""" if isfile(path): remove(path) elif isdir(path): rmtree(path) def disabled_ui(fn): """Menubar is disabled during operations""" @wraps(fn) def inner(self, *args, **kwargs): self.main_menu.entryconfig("File", state="disabled") self.main_menu.entryconfig("Settings", state="disabled") self.main_menu.entryconfig("Clear Selected", state="disabled") self.main_menu.entryconfig("Clear All", state="disabled") self.main_menu.entryconfig("COPY", state="disabled") self.main_menu.entryconfig("MOVE", state="disabled") fn(self, *args, **kwargs) self.main_menu.entryconfig("File", state="normal") self.main_menu.entryconfig("Settings", state="normal") self.main_menu.entryconfig("Clear Selected", state="normal") self.main_menu.entryconfig("Clear All", state="normal") self.main_menu.entryconfig("COPY", state="normal") self.main_menu.entryconfig("MOVE", state="normal") return inner def _submit(self, copy): """Thread/wrapper for submit() so we don't block the UI during operations""" self.thread = Thread(target=self.submit, args=(copy, ), daemon=True) self.thread.start() @disabled_ui def submit(self, copy): """ Move or copy each item in the origin list to the path in the destination list. Supports no more than one destination directory where copy == False. Ask Overwrite and Rename Dupes will alter the way we handle existing data standing in the way. By default, duplicates are renamed with an index. A messagebox can complain to the user if shutil raises a PermissionError, and the operation is skipped. """ if (self.list_box_dest.size() > 1) and not copy: messagebox.showwarning( "Invalid Operation", "Move operation only supports a single destination directory.") return sources = self.list_box_source.get(0, "end") destinations = self.list_box_dest.get(0, "end") self.skipped_err = [] for j, destination in enumerate(destinations): if isfile(destination): self.skipped_err.append(f"Invalid destination: {destination}") continue for i, source in enumerate(sources): self.progress(i, j) (_, filename) = split(source) future_destination = join(destination + sep, filename) if exists(future_destination): if not self.settings_ask_overwrite.get() \ and not self.settings_rename_dupes.get(): if not self._delete(future_destination): continue if self.settings_ask_overwrite.get(): if self.ask_overwrite(future_destination): if not self._delete(future_destination): continue else: continue if self.settings_rename_dupes.get(): future_destination = self.name_dupe(future_destination) if copy: if not self._copy(source, future_destination): continue else: if not self._move(source, future_destination): continue self.list_box_source.delete(0, "end") self.list_box_dest.delete(0, "end") if self.skipped_err: messagebox.showerror(title="Error(s)", message="\n\n".join(self.skipped_err)) @staticmethod def name_dupe(path): """ Renames the file or directory until it doesn't exist in the destination with that name anymore, by appending the filename with an index wrapped in parenthesis. (Windows platforms) file.txt => file (1).txt => file (2).txt """ if system() != "Windows": raise OSError("For use with Windows filesystems.") path_ = path (root, filename) = split(path_) if isdir(path_): title = filename ext = None else: (title, ext) = splitext(filename) filecount = 0 while exists(path_): filecount += 1 new_title = title + " (" + str(filecount) + ")" if ext: new_title = new_title + ext path_ = join(root, new_title) return path_ def ask_overwrite(self, future_destination): """Messagebox result returned as truth value""" return messagebox.askyesno( title="Path Conflict", message=f"Overwrite:\n\n{future_destination}?\n\n" \ f"YES - Overwrite\nNO - Skip" ) def progress(self, i, j): """ Visualize operands in GUI during operations i = current source operand index j = current destination operand index """ for y, _ in enumerate(self.list_box_source.get(0, "end")): if y != i: self.list_box_source.itemconfigure(y, bg="#FFFFFF", fg="#000000") else: self.list_box_source.itemconfigure(y, bg="#cccccc", fg="#000000") for x, _ in enumerate(self.list_box_dest.get(0, "end")): if x != j: self.list_box_dest.itemconfigure(x, bg="#FFFFFF", fg="#000000") else: self.list_box_dest.itemconfigure(x, bg="#cccccc", fg="#000000") self.master.update() #Toplevels: def show_about(self): """ Displays a static dialog that doesn't allow additional instances of itself to be created while showing. """ if self.about_showing.get() == 0: self.about_showing.set(1) try: with open(f"{dirname(__file__)}/about.txt", "r") as aboutfile: about_info = aboutfile.read() except FileNotFoundError: messagebox.showerror("Error", "File not found") self.about_showing.set(0) return else: self.about = Toplevel() self.about.title("About") self.about.iconbitmap(f"{dirname(__file__)}/icon.ico") self.about.geometry("600x400") self.about.resizable(0, 0) self.about.update_idletasks() (width_offset, height_offset) = Ufd.get_offset(self.about) self.about.geometry(f"+{width_offset-75}+{height_offset-75}") self.about.update_idletasks() self.about_message = Label( self.about, text=about_info, justify="left", wraplength=(self.about.winfo_width() - 25)) self.about_message.grid(sticky="nsew") self.about.protocol( "WM_DELETE_WINDOW", lambda: self.toplevel_close( self.about, self.about_showing)) def show_help(self): """ Displays a scrollable dialog that doesn't allow additional instances of itself to be created while showing. """ if self.help_showing.get() == 0: self.help_showing.set(1) try: with open(f"{dirname(__file__)}/help.txt", "r") as helpfile: help_info = helpfile.read() except FileNotFoundError: messagebox.showerror("Error", "File not found") self.help_showing.set(0) return else: self.help_window = Toplevel() self.help_window.title("Help") self.help_window.iconbitmap(f"{dirname(__file__)}/icon.ico") self.help_window.geometry("500x300") self.help_window.update_idletasks() (width_offset, height_offset) = Ufd.get_offset(self.help_window) self.help_window.geometry( f"+{width_offset+75}+{height_offset-75}") self.help_window.update_idletasks() self.message_y_scrollbar = Scrollbar(self.help_window, orient="vertical") self.help_text = Text( self.help_window, wrap="word", yscrollcommand=self.message_y_scrollbar.set) self.help_window.rowconfigure(0, weight=1) self.help_window.columnconfigure(0, weight=1) self.help_text.grid(row=0, column=0, sticky="nsew") self.message_y_scrollbar.grid(row=0, column=1, sticky="nse") self.message_y_scrollbar.config(command=self.help_text.yview) self.help_text.insert("end", help_info) self.help_text.config(state="disabled") self.help_window.protocol( "WM_DELETE_WINDOW", lambda: self.toplevel_close( self.help_window, self.help_showing)) def show_ufd(self, source=True): """ Display Ufd w/ appropriate kwargs => Populate GUI w/ result""" if self.dialog_showing.get() == 0: self.dialog_showing.set(1) self.ufd = Ufd(title="Add Items", show_hidden=self.settings_show_hidden.get(), include_files=self.settings_include_files.get(), multiselect=self.settings_multiselect.get(), select_dirs=self.settings_select_dirs.get(), select_files=self.settings_select_files.get(), unix_delimiter=False, stdout=False) for result in self.ufd(): if source: self.list_box_source.insert("end", result) else: self.list_box_dest.insert("end", result) self.dialog_showing.set(0)