From 753be7df0dbce685c030203068875bec57278c45 Mon Sep 17 00:00:00 2001 From: Faraphel Date: Fri, 10 Jun 2022 21:40:03 +0200 Subject: [PATCH] when pressing the install button, everything but the help menu is disabled. Added progress_function to start a function that yield data about the installation to show it on the progress bar --- source/__init__.py | 18 +++++ source/gui/install.py | 170 +++++++++++++++++++++++++++++++++++++++--- source/mkw/Game.py | 12 +++ 3 files changed, 190 insertions(+), 10 deletions(-) diff --git a/source/__init__.py b/source/__init__.py index 30d928d..fca8e91 100644 --- a/source/__init__.py +++ b/source/__init__.py @@ -1,3 +1,7 @@ +from threading import Thread +from typing import Callable + + __version__ = (0, 12, 0) __author__ = 'Faraphel' @@ -10,3 +14,17 @@ Mo = 1_000 * Ko Go = 1_000 * Mo minimum_space_available = 15*Go + + +def threaded(func: Callable) -> Callable: + """ + Decorate a function to run in a separate thread + :param func: a function + :return: the decorated function + """ + + def wrapper(*args, **kwargs): + # run the function in a Daemon, so it will stop when the main thread stops + Thread(target=func, args=args, kwargs=kwargs, daemon=True).start() + + return wrapper diff --git a/source/gui/install.py b/source/gui/install.py index a66d240..fa2d93c 100644 --- a/source/gui/install.py +++ b/source/gui/install.py @@ -1,3 +1,4 @@ +import enum import shutil import tkinter from pathlib import Path @@ -6,13 +7,30 @@ from tkinter import ttk from tkinter import filedialog from tkinter import messagebox import webbrowser +from typing import Generator +from source.mkw.Game import Game from source.translation import translate as _ from source import event from source import * import os +class SourceGameError(Exception): + def __init__(self, path: Path | str): + super().__init__(f"Invalid path for source game : {path}") + + +class DestinationGameError(Exception): + def __init__(self, path: Path | str): + super().__init__(f"Invalid path for destination game : {path}") + + +class InstallerState(enum.Enum): + IDLE = 0 + INSTALLING = 1 + + # Main window for the installer class Window(tkinter.Tk): def __init__(self): @@ -42,6 +60,8 @@ class Window(tkinter.Tk): self.progress_bar = ProgressBar(self) self.progress_bar.grid(row=5, column=1, sticky="nsew") + self.set_state(InstallerState.IDLE) + def run(self) -> None: """ Run the installer @@ -58,6 +78,24 @@ class Window(tkinter.Tk): """ return None + def set_state(self, state: InstallerState) -> None: + """ + Set the progress bar state when the installer change state + :param state: state of the installer + :return: + """ + for child in self.winfo_children(): + getattr(child, "set_state", lambda *_: "pass")(state) + + def progress_function(self, func_gen: Generator) -> None: + """ + Run a generator function that yield status for the progress bar + :return: + """ + # get the generator data yield by the generator function + for step_data in func_gen: + if "desc" in step_data: self.progress_bar.set_description(step_data["desc"]) + # Menu bar class Menu(tkinter.Menu): @@ -101,14 +139,39 @@ class Menu(tkinter.Menu): super().__init__(master, tearoff=False) master.add_cascade(label="Help", menu=self) + self.menu_id = self.master.index(tkinter.END) + self.add_command(label="Discord", command=lambda: webbrowser.open(discord_url)) self.add_command(label="Github Wiki", command=lambda: webbrowser.open(github_wiki_url)) + def set_installation_state(self, state: InstallerState) -> bool: + """ + Set the installation state of the installer + :param state: The state to set the installer to + :return: True if the state was set, False if not + """ + + def set_state(self, state: InstallerState) -> None: + """ + Set the progress bar state when the installer change state + :param state: state of the installer + :return: + """ + # get the last child id of the menu + + for child_id in range(1, self.index(tkinter.END) + 1): + # don't modify the state of the help menu + if child_id == self.help.menu_id: continue + + match state: + case state.IDLE: self.entryconfigure(child_id, state=tkinter.NORMAL) + case state.INSTALLING: self.entryconfigure(child_id, state=tkinter.DISABLED) + # Select game frame class SourceGame(ttk.LabelFrame): def __init__(self, master: tkinter.Tk): - super().__init__(master, text="Original Game") + super().__init__(master, text="Original Game File") self.entry = ttk.Entry(self, width=50) self.entry.grid(row=1, column=1, sticky="nsew") @@ -141,13 +204,34 @@ class SourceGame(ttk.LabelFrame): self.entry.delete(0, tkinter.END) self.entry.insert(0, str(path.absolute())) - self.master.destination_game.set_path(path.parent / "MKWF.iso") + self.master.destination_game.set_path(path.parent) + + def get_path(self) -> Path: + """ + Get the source game path + :return: the game path + """ + path = Path(self.entry.get()) + if not path.exists(): raise SourceGameError(path) + return path + + + def set_state(self, state: InstallerState) -> None: + """ + Set the progress bar state when the installer change state + :param state: state of the installer + :return: + """ + for child in self.winfo_children(): + match state: + case InstallerState.IDLE: child.config(state="normal") + case InstallerState.INSTALLING: child.config(state="disabled") # Select game destination frame class DestinationGame(ttk.LabelFrame): def __init__(self, master: tkinter.Tk): - super().__init__(master, text="Game Destination") + super().__init__(master, text="Game Directory Destination") self.entry = ttk.Entry(self, width=50) self.entry.grid(row=1, column=1, sticky="nsew") @@ -160,32 +244,70 @@ class DestinationGame(ttk.LabelFrame): Select the source game :return: """ - path = Path(tkinter.filedialog.asksaveasfilename( + path = Path(tkinter.filedialog.askdirectory( title=_("SELECT_DESTINATION_GAME"), - filetypes=[(_("WII GAMES"), "*.iso *.wbfs *.dol")], )) - path.parent.mkdir(mode=0o777, parents=True, exist_ok=True) + path.mkdir(mode=0o777, parents=True, exist_ok=True) self.set_path(path) def set_path(self, path: Path): - if not os.access(path.parent, os.W_OK): + if not os.access(path, os.W_OK): messagebox.showwarning(_("WARNING"), _("WARNING_DESTINATION_GAME_NOT_WRITABLE")) self.entry.delete(0, tkinter.END) self.entry.insert(0, str(path.absolute())) + def get_path(self) -> Path: + """ + Get the destination game path + :return: the game path + """ + path = Path(self.entry.get()) + if not path.exists(): raise DestinationGameError(path) + return path + + def set_state(self, state: InstallerState) -> None: + """ + Set the progress bar state when the installer change state + :param state: state of the installer + :return: + """ + for child in self.winfo_children(): + match state: + case InstallerState.IDLE: child.config(state="normal") + case InstallerState.INSTALLING: child.config(state="disabled") + # Install button class ButtonInstall(ttk.Button): def __init__(self, master: tkinter.Tk): super().__init__(master, text="Install", command=self.install) + @threaded def install(self): - # get space remaining on the C: drive - if shutil.disk_usage(".").free < minimum_space_available: - if not messagebox.askokcancel(_("WARNING"), _("WARNING_NOT_ENOUGH_SPACE_CONTINUE")): return + try: + self.master.set_state(InstallerState.INSTALLING) + # get space remaining on the C: drive + if shutil.disk_usage(".").free < minimum_space_available: + if not messagebox.askokcancel(_("WARNING"), _("WARNING_NOT_ENOUGH_SPACE_CONTINUE")): return + + game = Game(self.master.source_game.get_path()) + self.master.progress_function(game.install_mod()) + + finally: + self.master.set_state(InstallerState.IDLE) + + def set_state(self, state: InstallerState) -> None: + """ + Set the progress bar state when the installer change state + :param state: state of the installer + :return: + """ + match state: + case InstallerState.IDLE: self.config(state="normal") + case InstallerState.INSTALLING: self.config(state="disabled") # Progress bar @@ -202,6 +324,24 @@ class ProgressBar(ttk.LabelFrame): self.description = ttk.Label(self, text="no process running", anchor="center", font=("TkDefaultFont", 10)) self.description.grid(row=2, column=1, sticky="nsew") + def set_state(self, state: InstallerState) -> None: + """ + Set the progress bar state when the installer change state + :param state: state of the installer + :return: + """ + match state: + case InstallerState.IDLE: self.grid_remove() + case InstallerState.INSTALLING: self.grid() + + def set_description(self, desc: str) -> None: + """ + Set the progress bar description + :param desc: description + :return: + """ + self.description.config(text=desc) + # Combobox to select the pack class SelectPack(ttk.Combobox): @@ -210,3 +350,13 @@ class SelectPack(ttk.Combobox): for pack in Path("./Pack/").iterdir(): self.insert(tkinter.END, pack.name) + + def set_state(self, state: InstallerState) -> None: + """ + Set the progress bar state when the installer change state + :param state: state of the installer + :return: + """ + match state: + case InstallerState.IDLE: self.config(state="readonly") + case InstallerState.INSTALLING: self.config(state="disabled") diff --git a/source/mkw/Game.py b/source/mkw/Game.py index 5820c51..7ed464f 100644 --- a/source/mkw/Game.py +++ b/source/mkw/Game.py @@ -1,4 +1,6 @@ +import time from pathlib import Path +from typing import Generator from source.wt.wit import WITPath, Region @@ -27,3 +29,13 @@ class Game: :param dest: destination directory """ return self.wit_path.extract_all(dest) + + def install_mod(self) -> Generator[str, None, None]: + """ + Patch the game with the mod + """ + i = 0 + while True: + time.sleep(1) + yield {"desc": f"step {i}"} + i += 1 \ No newline at end of file