예제 #1
0
class Window:
    def __init__(self, workstation: WorkstationSettings,
                 wallpaper: Image.Image):
        self._root = Tk()
        self._root.title("wallcrop")
        self._root.minsize(*_MINSIZE)

        self._show_monitor_labels = BooleanVar(self._root, value=False)
        self._show_unselected_area = BooleanVar(self._root, value=True)

        frame = Frame(self._root, padding=_PADDING)
        frame.grid(column=0, row=0, sticky="n w s e")
        self._root.columnconfigure(0, weight=1)  # type: ignore
        self._root.rowconfigure(0, weight=1)  # type: ignore

        frame_top = Frame(frame)
        frame_top.grid(column=0, row=0, sticky="n w s e")

        frame_bot = Frame(frame)
        frame_bot.grid(column=0, row=2, sticky="n w s e")

        self._selection = Selection(aspect_ratio=np.divide(*wallpaper.size))
        self._selection.register_onchange_handler(self._reset_spinbox_values)

        self._selection_widget = SelectionWidget(
            parent=frame,
            workstation=workstation,
            wallpaper=wallpaper,
            selection=self._selection,
        )
        self._selection_widget.set_show_monitor_labels(
            self._show_monitor_labels.get())
        self._selection_widget.set_show_unselected_area(
            self._show_unselected_area.get())
        self._selection_widget.grid(column=0,
                                    row=1,
                                    sticky="n w s e",
                                    pady=_PADDING)
        frame.columnconfigure(0, weight=1)  # type: ignore
        frame.rowconfigure(1, weight=1)  # type: ignore

        label_wallpaper = Label(frame_top, text=workstation.name)
        label_wallpaper.grid(column=0, row=0)

        # Center columns 1-6 on frame_bot.
        frame_bot.columnconfigure(0, weight=1)  # type: ignore
        frame_bot.columnconfigure(7, weight=1)  # type: ignore

        # TODO: Figure out how to not have spinbox show zero when using
        #  increment/decrement buttons.
        label_selection_position_x = Label(frame_bot, text="X: ")
        label_selection_position_x.grid(column=1, row=0)
        self._spinbox_selection_position_x = Spinbox(frame_bot,
                                                     width=5,
                                                     validate="focusout")
        self._spinbox_selection_position_x.grid(column=2, row=0)

        label_selection_position_y = Label(frame_bot, text="  Y: ")
        label_selection_position_y.grid(column=3, row=0)
        self._spinbox_selection_position_y = Spinbox(frame_bot,
                                                     width=5,
                                                     validate="focusout")
        self._spinbox_selection_position_y.grid(column=4, row=0)

        label_selection_zoom = Label(frame_bot, text="  Zoom: ")
        label_selection_zoom.grid(column=5, row=0)
        self._spinbox_selection_zoom = Spinbox(frame_bot,
                                               width=5,
                                               validate="focusout")
        self._spinbox_selection_zoom.grid(column=6, row=0)

        self._bind_actions()
        self._set_up_menubar()
        self._reset_spinbox_values()

    def _bind_actions(self) -> None:
        self._root.bind("<Escape>", lambda _event: self.quit())
        self._root.bind("<q>", lambda _event: self.quit())

        self._root.bind(
            "<m>",
            lambda _event: self._show_monitor_labels.set(not (
                self._show_monitor_labels.get())),
        )
        self._root.bind(
            "<n>",
            lambda _event: self._show_unselected_area.set(not (
                self._show_unselected_area.get())),
        )

        self._root.bind("<i>", lambda _event: self._selection.zoom_increase())
        self._root.bind(
            "<I>", lambda _event: self._selection.zoom_increase(precise=True))

        self._root.bind("<o>", lambda _event: self._selection.zoom_decrease())
        self._root.bind(
            "<O>", lambda _event: self._selection.zoom_decrease(precise=True))

        self._root.bind("<h>", lambda _event: self._selection.move_left())
        self._root.bind("<H>",
                        lambda _event: self._selection.move_left(precise=True))
        self._root.bind("<Left>", lambda _event: self._selection.move_left())
        self._root.bind("<Shift-Left>",
                        lambda _event: self._selection.move_left(precise=True))

        self._root.bind("<l>", lambda _event: self._selection.move_right())
        self._root.bind(
            "<L>", lambda _event: self._selection.move_right(precise=True))
        self._root.bind("<Right>", lambda _event: self._selection.move_right())
        self._root.bind(
            "<Shift-Right>",
            lambda _event: self._selection.move_right(precise=True))

        self._root.bind("<k>", lambda _event: self._selection.move_up())
        self._root.bind("<K>",
                        lambda _event: self._selection.move_up(precise=True))
        self._root.bind("<Up>", lambda _event: self._selection.move_up())
        self._root.bind("<Shift-Up>",
                        lambda _event: self._selection.move_up(precise=True))

        self._root.bind("<j>", lambda _event: self._selection.move_down())
        self._root.bind("<J>",
                        lambda _event: self._selection.move_down(precise=True))
        self._root.bind("<Down>", lambda _event: self._selection.move_down())
        self._root.bind("<Shift-Down>",
                        lambda _event: self._selection.move_down(precise=True))

        self._spinbox_selection_position_x.configure(
            validatecommand=lambda *_args: self._set_selection_position_x())
        self._spinbox_selection_position_x.bind(
            "<Return>",
            lambda _event: self._set_selection_position_x()  # type: ignore
        )
        self._spinbox_selection_position_x.bind(
            "<<Decrement>>", lambda _event: self._selection.move_left())
        self._spinbox_selection_position_x.bind(
            "<<Increment>>", lambda _event: self._selection.move_right())

        self._spinbox_selection_position_y.configure(
            validatecommand=lambda *_args: self._set_selection_position_y())
        self._spinbox_selection_position_y.bind(
            "<Return>",
            lambda _event: self._set_selection_position_y()  # type: ignore
        )
        self._spinbox_selection_position_y.bind(
            "<<Decrement>>", lambda _event: self._selection.move_up())
        self._spinbox_selection_position_y.bind(
            "<<Increment>>", lambda _event: self._selection.move_down())

        self._spinbox_selection_zoom.configure(
            validatecommand=lambda *_args: self._set_selection_zoom())
        self._spinbox_selection_zoom.bind(
            "<Return>",
            lambda _event: self._set_selection_zoom()  # type: ignore
        )
        self._spinbox_selection_zoom.bind(
            "<<Decrement>>", lambda _event: self._selection.zoom_decrease())
        self._spinbox_selection_zoom.bind(
            "<<Increment>>", lambda _event: self._selection.zoom_increase())

        self._show_monitor_labels.trace_add(
            "write",
            lambda *_args: self._selection_widget.set_show_monitor_labels(
                self._show_monitor_labels.get()),
        )
        self._show_unselected_area.trace_add(
            "write",
            lambda *_args: self._selection_widget.set_show_unselected_area(
                self._show_unselected_area.get()),
        )

    def _set_up_menubar(self) -> None:
        # TODO: check that this look good on macOS, as described here:
        #   https://tkdocs.com/tutorial/menus.html#platformmenus

        self._root.option_add("*tearOff", False)

        menu = Menu(self._root)

        menu_file = Menu(menu)
        menu_file.add_command(  # type: ignore
            label="Quit",
            underline=0,
            accelerator="Q, Escape",
            command=self.quit)
        menu.add_cascade(menu=menu_file, label="File",
                         underline=0)  # type: ignore

        menu_edit = Menu(menu)
        menu_edit.add_command(  # type: ignore
            label="Move Left",
            underline=5,
            accelerator="H, Left",
            command=self._selection.move_left,
        )
        menu_edit.add_command(  # type: ignore
            label="Move Right",
            underline=5,
            accelerator="L, Right",
            command=self._selection.move_right,
        )
        menu_edit.add_command(  # type: ignore
            label="Move Up",
            underline=5,
            accelerator="K, Up",
            command=self._selection.move_up,
        )
        menu_edit.add_command(  # type: ignore
            label="Move Down",
            underline=5,
            accelerator="J, Down",
            command=self._selection.move_down,
        )
        menu_edit.add_separator()  # type: ignore
        menu_edit.add_command(  # type: ignore
            label="Increase Zoom",
            underline=0,
            accelerator="I",
            command=self._selection.zoom_increase,
        )
        menu_edit.add_command(  # type: ignore
            label="Decrease Zoom",
            underline=10,
            accelerator="O",
            command=self._selection.zoom_decrease,
        )
        menu.add_cascade(menu=menu_edit, label="Edit",
                         underline=0)  # type: ignore

        menu_view = Menu(menu)
        menu_view.add_checkbutton(  # type: ignore
            label="Label Monitors",
            variable=self._show_monitor_labels,
            underline=6,
            accelerator="M",
        )
        menu_view.add_checkbutton(  # type: ignore
            label="Show Unselected",
            variable=self._show_unselected_area,
            underline=6,
            accelerator="N",
        )
        menu.add_cascade(menu=menu_view, label="View",
                         underline=0)  # type: ignore

        menu_help = Menu(menu, name="help")
        menu_help.add_command(  # type: ignore
            label="About Wallcrop",
            underline=0,
            command=lambda: messagebox.showinfo(
                parent=self._root,
                title="About Wallcrop",
                message=f"Wallcrop {wallcrop.__version__}",
                detail=("Copyright 2021 Lukas Schmelzeisen.\n"
                        "Licensed under the Apache License, Version 2.0.\n"
                        "https://github.com/lschmelzeisen/wallcrop/"),
            ),
        )
        menu.add_cascade(menu=menu_help, label="Help",
                         underline=3)  # type: ignore

        self._root["menu"] = menu

    def mainloop(self) -> None:
        self._root.mainloop()

    def quit(self) -> None:
        self._root.destroy()

    def _set_selection_position_x(self) -> bool:
        try:
            value = float(
                self._spinbox_selection_position_x.get())  # type: ignore
        except ValueError:
            self._spinbox_selection_position_x.set(self._selection.position[0])
            return False
        self._selection.set_position(
            np.array((value, self._selection.position[1])))
        return True

    def _set_selection_position_y(self) -> bool:
        try:
            value = float(
                self._spinbox_selection_position_y.get())  # type: ignore
        except ValueError:
            self._spinbox_selection_position_y.set(self._selection.position[1])
            return False
        self._selection.set_position(
            np.array((self._selection.position[0], value)))
        return True

    def _set_selection_zoom(self) -> bool:
        try:
            value = float(self._spinbox_selection_zoom.get())  # type: ignore
        except ValueError:
            self._spinbox_selection_zoom.set(self._selection.zoom)
            return False
        self._selection.set_zoom(value)
        return True

    def _reset_spinbox_values(self) -> None:
        self._spinbox_selection_position_x.set(
            f"{self._selection.position[0]:.3f}")
        self._spinbox_selection_position_y.set(
            f"{self._selection.position[1]:.3f}")
        self._spinbox_selection_zoom.set(f"{self._selection.zoom:.3f}")
예제 #2
0
class TkApp(Tk):
    """
    The main Tk class for the gui of simplebackup
    """
    def __init__(self, **kwargs):
        super().__init__()
        title = "Simple Backup | V" + __version__
        self.wm_title(title)
        self.protocol("WM_DELETE_WINDOW", self.on_closing)

        self.__thread = None
        self.__files_found = 0
        self.__files_copied = 0

        config_fn = kwargs.get("config_fn", user_config_filepath())
        self.__app_config = Config_Handler(config_fn)
        self.__curr_config = self.__app_config.default_config_i

        self.__menu = Menu(self)
        self.__menu_file = Menu(self.__menu, tearoff=0)
        self.__menu_file.add_command(label="Quit", command=self.quit)
        self.__menu_config = Menu(self.__menu, tearoff=0)
        self.__menu_config.add_command(label="New", command=self.new_config)
        self.__menu_config.add_command(label="Load",
                                       command=self.switch_config)
        self.__menu_config.add_command(label="Change Default",
                                       command=self.change_default_config)
        self.__menu_config.add_command(label="Rename Current",
                                       command=self.rename_curr_conf)
        self.__menu_config.add_separator()
        self.__menu_config.add_command(label="Delete Current",
                                       command=self.delete_current_config)
        self.__menu_config.add_command(label="Delete All",
                                       command=self.reset_config)
        self.__menu_help = Menu(self.__menu, tearoff=0)
        self.__menu_help.add_command(label="Check for Updates",
                                     command=self.show_update_popup)
        self.__menu_help.add_command(label="About",
                                     command=self.show_about_popup)
        self.__menu.add_cascade(label="File", menu=self.__menu_file)
        self.__menu.add_cascade(label="Config", menu=self.__menu_config)
        self.__menu.add_cascade(label="Help", menu=self.__menu_help)

        self.__title_l = Label(self, text=title, font=(16))
        self.__curr_config_name_l = Label(self)
        self.__last_backup_l = Label(self)
        self.__set_versions_to_keep = Button(
            self,
            text="Set Versions To Keep",
            command=self.update_versions_to_keep)
        self.__versions_to_keep_l = Label(self)
        self.__inc_folder_bnt = Button(self,
                                       text="Include Another Folder",
                                       command=self.add_included_folder)
        self.__included_folders_lb = Listbox(self, height=4)
        self.__included_folders_lb.bind("<<ListboxSelect>>",
                                        self.remove_selected_included_folder)
        self.__included_folders_lb.bind('<FocusOut>',
                                        self.deselect_included_folder)
        self.__excl_folder_bnt = Button(self,
                                        text="Exclude Another Folder",
                                        command=self.add_excluded_folder)
        self.__excluded_folders_lb = Listbox(self, height=4)
        self.__excluded_folders_lb.bind("<<ListboxSelect>>",
                                        self.remove_selected_excluded_folder)
        self.__excluded_folders_lb.bind('<FocusOut>',
                                        self.deselect_excluded_folder)
        self.__backup_to_bnt = Button(self,
                                      text="Backup Folder",
                                      command=self.set_backup_folder)
        self.__backup_folder_l = Label(self)
        self.__use_tar_l = Label(self, text="Use Tar")
        self.__use_tar_var = BooleanVar(self)
        self.__use_tar_var.trace_add("write", self.use_tar_changed)
        self.__use_tar = Checkbutton(self, variable=self.__use_tar_var)
        self.__backup_start_bnt = Button(self,
                                         text="Start Backup",
                                         command=self.start_backup)
        self.__progress = Progressbar(self)
        self.__statusbar = Label(self, text="ok", relief=SUNKEN, anchor=W)
        self._load_display()
        self._layout()

        if self.__app_config.show_help:
            self.show_help_popup()

    def on_closing(self):
        """
        called on window close
        """
        if self.__files_found != self.__files_copied:
            if messagebox.askyesno("Backup Running",
                                   "Do you want to stop the backup?"):
                self.destroy()
        else:
            self.destroy()

    def _load_display(self):
        """
        load the widgets with data from the current backup config,
        should be run after loading a config from file and at app launch
        """
        self.__versions_to_keep = self.__app_config.get_versions_to_keep(
            self.__curr_config)
        self.__included_folders = self.__app_config.get_included_folders(
            self.__curr_config)
        self.__excluded_folders = self.__app_config.get_excluded_folders(
            self.__curr_config)
        self.__backup_location = self.__app_config.get_backup_path(
            self.__curr_config)

        curr_conf_name = self.__app_config.get_config_name(self.__curr_config)
        self.__curr_config_name_l.config(text=f"Config Name: {curr_conf_name}")
        self.__last_backup_l.config(
            text=
            f"Last Known Backup: {self.__app_config.get_human_last_backup(self.__curr_config)}"
        )
        self.__versions_to_keep_l.config(text=self.__versions_to_keep)
        self.__included_folders_lb.delete(0, END)
        self.__included_folders_lb.insert(0, *self.__included_folders)
        self.__excluded_folders_lb.delete(0, END)
        self.__excluded_folders_lb.insert(0, *self.__excluded_folders)
        self.__backup_folder_l.config(text=str(self.__backup_location))
        self.__use_tar_var.set(
            self.__app_config.get_use_tar(self.__curr_config))

    def switch_config(self):
        """
        switches what config to use for backup,
        asks the user for a config to load,
        then loads the display
        """
        next_combo = ask_combobox("Load Config", "Config Name",
                                  self.__app_config.get_config_names())
        if next_combo != None:
            self.__curr_config = next_combo
            self._load_display()

    def change_default_config(self):
        """
        switches what config to use for the default backup,
        asks the user for a config to load
        """
        next_combo = ask_combobox("Default Config", "Config Name",
                                  self.__app_config.get_config_names())
        if next_combo != None:
            self.__app_config.default_config_i = next_combo

    def rename_curr_conf(self):
        """
        rename a existing config,
        will ask the user in a popup string input
        """
        new_name = simpledialog.askstring("Rename Config", "New Name")
        if new_name:
            self.__app_config.rename_config(self.__curr_config, new_name)
            self._load_display()

    def new_config(self):
        """
        creates a new empty backup config,
        asks the user for config name
        """
        name = simpledialog.askstring("New Config", "Config Name")
        if name:
            self.__app_config.create_config(name)

    def delete_current_config(self):
        """
        deletes the current selected config, asks the user to confirm
        """
        if messagebox.askyesno(
                "Confirm Delete",
                "Are you sure you want to delete the current config?"):
            self.__app_config.remove_config(self.__curr_config)
            self.__curr_config = self.__app_config.default_config_i
            self._load_display()

    def reset_config(self):
        """
        resets all the user configs, asks the user to confirm
        """
        if messagebox.askyesno(
                "Confirm Reset",
                "Are you sure you want to reset the all configurations?"):
            self.__app_config.reset_config()
            self.__curr_config = self.__app_config.default_config_i
            self._load_display()

    def use_tar_changed(self, *args):
        """
        called each time the __use_tar_var is called
        """
        self.__app_config.set_use_tar(self.__curr_config,
                                      self.__use_tar_var.get())

    def update_versions_to_keep(self):
        """
        update the number of versions to keep,
        asks the user for a integer
        """
        new_val = simpledialog.askinteger(
            "Versions To Keep",
            "How many backups do you want to keep",
            minvalue=0)
        if new_val != self.__versions_to_keep and new_val != None:
            self.__versions_to_keep = new_val
            self.__app_config.set_versions_to_keep(self.__curr_config,
                                                   self.__versions_to_keep)
            self.__versions_to_keep_l.config(text=self.__versions_to_keep)

    def deselect_included_folder(self, *args):
        """
        deselects the selected element in included folder
        """
        self.__included_folders_lb.selection_clear(0, END)

    def deselect_excluded_folder(self, *args):
        """
        deselects the selected element in excluded folder
        """
        self.__excluded_folders_lb.selection_clear(0, END)

    def add_included_folder(self):
        """
        add a folder to include in the backup,
        will ask user for a directory
        """
        folder = filedialog.askdirectory(initialdir="/",
                                         title="Select Folder To Backup")
        if folder:
            folder_path = Path(folder)
            if folder_path != self.__backup_location:
                self.__included_folders.append(folder_path)
                self.__included_folders_lb.insert(END, folder_path)
                self.__app_config.set_included_folders(self.__curr_config,
                                                       self.__included_folders)
            else:
                messagebox.showwarning(
                    title="Folder Same As Backup Path",
                    message=
                    "You selected a folder that was the same as the backup path!"
                )

    def remove_selected_included_folder(self, *args):
        """
        remove the currently selected
        item in the included folders ListBox,
        will ask the user to confirm
        """
        curr_selection = self.__included_folders_lb.curselection()
        # check if there is a selection
        if curr_selection:
            if messagebox.askyesno("Confirm Delete",
                                   "Are you want to delete this folder?"):
                index_to_del = curr_selection[0]
                self.__included_folders.pop(index_to_del)
                self.__app_config.set_included_folders(self.__curr_config,
                                                       self.__included_folders)
                self.__included_folders_lb.delete(index_to_del)
            self.deselect_included_folder()

    def add_excluded_folder(self):
        """
        add a folder to exclude in the backup,
        will ask user for a directory
        """
        folder = filedialog.askdirectory(initialdir="/",
                                         title="Select Folder To Exclude")
        if folder:
            folder_path = Path(folder)
            self.__excluded_folders.append(folder_path)
            self.__excluded_folders_lb.insert(END, folder_path)
            self.__app_config.set_excluded_folders(self.__curr_config,
                                                   self.__excluded_folders)

    def remove_selected_excluded_folder(self, *args):
        """
        remove the currently selected
        item in the excluded folders ListBox,
        will ask the user to confirm
        """

        curr_selection = self.__excluded_folders_lb.curselection()
        # check if there is a selection
        if curr_selection:
            if messagebox.askyesno("Confirm Delete",
                                   "Are you want to delete this folder?"):
                index_to_del = curr_selection[0]
                self.__excluded_folders.pop(index_to_del)
                self.__app_config.set_excluded_folders(self.__curr_config,
                                                       self.__excluded_folders)
                self.__excluded_folders_lb.delete(index_to_del)
            self.deselect_excluded_folder()

    def set_backup_folder(self):
        """
        sets the backup folder by asking the user for a base directory
        """
        folder = filedialog.askdirectory(initialdir="/",
                                         title="Select Where To Backup To")
        if folder:
            self.__backup_location = Path(folder)
            self.__backup_folder_l.config(text=folder)
            self.__app_config.set_backup_path(self.__curr_config,
                                              self.__backup_location)

    def enable_gui(self):
        """
        enable the gui buttons, run when a backup has completed
        """
        self.__set_versions_to_keep.config(state=NORMAL)
        self.__inc_folder_bnt.config(state=NORMAL)
        self.__included_folders_lb.config(state=NORMAL)
        self.__excl_folder_bnt.config(state=NORMAL)
        self.__excluded_folders_lb.config(state=NORMAL)
        self.__backup_to_bnt.config(state=NORMAL)
        self.__use_tar.config(state=NORMAL)
        self.__backup_start_bnt.config(state=NORMAL)

    def disable_gui(self):
        """
        disable the gui buttons, run when a backup is started
        """
        self.__set_versions_to_keep.config(state=DISABLED)
        self.__inc_folder_bnt.config(state=DISABLED)
        self.__included_folders_lb.config(state=DISABLED)
        self.__excl_folder_bnt.config(state=DISABLED)
        self.__excluded_folders_lb.config(state=DISABLED)
        self.__backup_to_bnt.config(state=DISABLED)
        self.__use_tar.config(state=DISABLED)
        self.__backup_start_bnt.config(state=DISABLED)

    def progress_find_incr(self, finished=False):
        """
        increment the progress bar for finding
        files by 1 or mark as finished

            :param finished: mark the progressbar as finished
        """
        if finished:
            self.__progress.config(mode="determinate")
            self.__progress.config(value=0, maximum=self.__files_found)
            self.__statusbar.config(text=f"Found {self.__files_found} Files")
        else:
            self.__files_found += 1
            self.__progress.config(value=self.__files_found)
            self.__statusbar.config(
                text=f"Searching For Files, Found {self.__files_found} Files")

    def progress_copy_incr(self):
        """
        increment the progress bar for copying
        files by 1 or mark as finished
        """
        self.__files_copied += 1
        self.__progress.config(value=self.__files_copied)
        self.__statusbar.config(
            text=f"Copying Files {self.__files_copied} of {self.__files_found}"
        )
        if self.__files_copied == self.__files_found:
            self.__app_config.set_last_backup(self.__curr_config,
                                              datetime.utcnow())
            self.__last_backup_l.config(
                text=
                f"Last Known Backup: {self.__app_config.get_human_last_backup(self.__curr_config)}"
            )
            self.__statusbar.config(text=f"Finished Copying Files")
            messagebox.showinfo(title="Finished Copying Files",
                                message="Finished copying all found files")
            # reset counters
            self.__files_found = 0
            self.__files_copied = 0
            self.__progress.config(value=0, maximum=100)
            self.enable_gui()

    def start_backup(self):
        """
        starts the backup
        """
        if not self.__backup_location:
            # no backup location was selected
            messagebox.showwarning(
                title="Backup Location Not Selected",
                message="You did not select a backup location!")
        elif not self.__included_folders:
            # no folders where found to backup
            messagebox.showwarning(
                title="No Folders To Backup",
                message="You did not add any folders to backup!")
        else:
            # basic checks passed
            self.disable_gui()
            # prep for search of files
            self.__progress.config(mode="indeterminate")
            self.__statusbar.config(text=f"Searching For Files")

            self.__thread = BackupThread(
                self.__included_folders, self.__excluded_folders,
                self.__backup_location, self.__versions_to_keep,
                self.progress_find_incr, self.progress_copy_incr,
                self.handle_error_message, self.__use_tar_var.get())
            # start the background backup thread so GUI wont appear frozen
            self.__thread.start()

    def show_about_popup(self):
        """
        show the about popup
        """
        messagebox.showinfo(
            "About", "simplebackup V" + __version__ +
            """ is cross-platform backup program written in python.
This app was made by enchant97/Leo Spratt.
It is licenced under GPL-3.0""")

    def show_update_popup(self):
        """
        open the default webbrowser to the update url
        """
        webbrowser.open(UPDATE_URL)

    def show_help_popup(self):
        messagebox.showinfo(
            "Welcome",
            """Welcome to simplebackup, here is some help to get you started:
\nIncluding a folder to backup
    - Press the 'Include Folder' button to add a folder to backup
    - Remove a entry by clicking on the list below
\nExcluding a folder from the backup
    - Press the 'Exclude Folder' button to skip a folder to backup
    - Remove a entry by clicking on the list below
\nSetting where backups are stored
    - Click the 'Backup Folder' button to set where backups should be placed
\nMultiple backup configs
    Use the 'Config' button in the titlebar to change varius settings like creating a new config
\nVersions to keep
    This will be the number of backup to keep in the backup folder
""")
        self.__app_config.show_help = False

    def handle_error_message(self, error_type: ERROR_TYPES):
        self.__statusbar.config(text="Failed")
        if error_type is ERROR_TYPES.NO_BACKUP_WRITE_PERMISION:
            messagebox.showerror("No Write Permission",
                                 ERROR_TYPES.NO_BACKUP_WRITE_PERMISION.value)
        elif error_type is ERROR_TYPES.NO_BACKUP_READ_PERMISION:
            messagebox.showerror("No Read Permission",
                                 ERROR_TYPES.NO_BACKUP_READ_PERMISION.value)
        elif error_type is ERROR_TYPES.NO_FILES_FOUND_TO_BACKUP:
            messagebox.showerror("No Files Found",
                                 ERROR_TYPES.NO_FILES_FOUND_TO_BACKUP.value)
        elif error_type is ERROR_TYPES.NO_BACKUP_PATH_FOUND:
            messagebox.showerror("No Backup Path Found",
                                 ERROR_TYPES.NO_BACKUP_PATH_FOUND.value)
        self.__progress.config(mode="determinate")
        self.enable_gui()

    def _layout(self):
        self.config(menu=self.__menu)
        self.__title_l.pack(fill=X, pady=10, padx=5)
        self.__curr_config_name_l.pack(fill=X, padx=5)
        self.__last_backup_l.pack(fill=X, padx=5)
        self.__set_versions_to_keep.pack(fill=X, padx=5)
        self.__versions_to_keep_l.pack(fill=X, padx=5)
        self.__inc_folder_bnt.pack(fill=X, padx=5)
        self.__included_folders_lb.pack(fill=X, padx=5)
        self.__excl_folder_bnt.pack(fill=X, padx=5)
        self.__excluded_folders_lb.pack(fill=X, padx=5)
        self.__backup_to_bnt.pack(fill=X, padx=5)
        self.__backup_folder_l.pack(fill=X, padx=5)
        self.__use_tar_l.pack(fill=X, padx=5)
        self.__use_tar.pack(fill=X, padx=5)
        self.__backup_start_bnt.pack(fill=X, padx=5)
        self.__progress.pack(fill=X)
        self.__statusbar.pack(side=BOTTOM, fill=X)
        self.wm_minsize(300, self.winfo_height())
        self.wm_resizable(True, False)