示例#1
0
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()
示例#2
0
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
示例#3
0
    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()
示例#4
0
    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()
示例#5
0
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)