From c0915ae13ee54e7c79d3c88c945e57319821b37e Mon Sep 17 00:00:00 2001 From: Faraphel Date: Mon, 15 Aug 2022 14:14:53 +0200 Subject: [PATCH] added a 2nd progress bar --- source/gui/install.py | 70 +++++++++++++----------------- source/mkw/ExtractedGame.py | 57 ++++++++++++------------ source/mkw/Game.py | 32 ++++++-------- source/mkw/ModConfig.py | 12 +++-- source/mkw/Patch/Patch.py | 6 ++- source/mkw/Patch/PatchDirectory.py | 5 ++- source/mkw/Patch/PatchFile.py | 7 +-- source/mkw/Patch/PatchObject.py | 4 +- source/progress.py | 21 +++++++++ source/wt/wit.py | 4 +- 10 files changed, 118 insertions(+), 100 deletions(-) create mode 100644 source/progress.py diff --git a/source/gui/install.py b/source/gui/install.py index 2001bb0..e9fd01d 100644 --- a/source/gui/install.py +++ b/source/gui/install.py @@ -13,6 +13,7 @@ from source.gui import better_gui_error, mystuff, mod_settings from source.mkw.Game import Game from source.mkw.ModConfig import ModConfig from source.option import Option +from source.progress import Progress from source.translation import translate as _ from source import plugins from source import * @@ -95,18 +96,23 @@ class Window(tkinter.Tk): for child in self.winfo_children(): getattr(child, "set_state", lambda *_: "pass")(state) - def progress_function(self, func_gen: Generator) -> None: + def progress_function(self, func_gen: Generator[Progress, None, None]) -> 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 "description" in step_data: self.progress_bar.set_description(step_data["description"]) - if "maximum" in step_data: self.progress_bar.set_maximum(step_data["maximum"]) - if "step" in step_data: self.progress_bar.step(step_data["step"]) - if "value" in step_data: self.progress_bar.set_value(step_data["value"]) - if "determinate" in step_data: self.progress_bar.set_determinate(step_data["determinate"]) + for progress in func_gen: + if progress.title is not None: self.progress_bar.set_title(progress.title) + if progress.part is not None: self.progress_bar.part(progress.part) + if progress.set_part is not None: self.progress_bar.set_part(progress.set_part) + if progress.max_part is not None: self.progress_bar.set_max_part(progress.max_part) + + if progress.description is not None: self.progress_bar.set_description(progress.description) + if progress.step is not None: self.progress_bar.step(progress.step) + if progress.set_step is not None: self.progress_bar.set_step(progress.set_step) + if progress.max_step is not None: self.progress_bar.set_max_step(progress.max_step) + if progress.determinate is not None: self.progress_bar.set_determinate(progress.determinate) def get_mod_config(self) -> ModConfig: """ @@ -450,11 +456,17 @@ class ProgressBar(ttk.LabelFrame): # make the element fill the whole frame self.columnconfigure(1, weight=1) - self.progress_bar = ttk.Progressbar(self, orient="horizontal") - self.progress_bar.grid(row=1, column=1, sticky="nsew") + self.progress_bar_part = ttk.Progressbar(self, orient="horizontal") + self.progress_bar_part.grid(row=1, column=1, sticky="nsew") + + self.title = ttk.Label(self, text="", anchor="center", font=("TkDefaultFont", 10), wraplength=350) + self.title.grid(row=2, column=1, sticky="nsew") + + self.progress_bar_step = ttk.Progressbar(self, orient="horizontal") + self.progress_bar_step.grid(row=3, column=1, sticky="nsew") self.description = ttk.Label(self, text="", anchor="center", font=("TkDefaultFont", 10), wraplength=350) - self.description.grid(row=2, column=1, sticky="nsew") + self.description.grid(row=4, column=1, sticky="nsew") def set_state(self, state: InstallerState) -> None: """ @@ -466,37 +478,15 @@ class ProgressBar(ttk.LabelFrame): 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) + def set_title(self, title: str): self.title.config(text=title) + def set_max_part(self, maximum: int): self.progress_bar_part.configure(maximum=maximum) + def set_part(self, value: int): self.progress_bar_part.configure(value=value) + def part(self, value: int = 1): self.progress_bar_part.step(value) - def set_maximum(self, maximum: int) -> None: - """ - Set the progress bar maximum value - :param maximum: the maximum value - :return: - """ - self.progress_bar.configure(maximum=maximum) - - def set_value(self, value: int) -> None: - """ - Set the progress bar value - :param value: the value - :return: - """ - self.progress_bar.configure(value=value) - - def step(self, value: int = 1) -> None: - """ - Set the progress bar by the value - :param value: the step - :return: - """ - self.progress_bar.step(value) + def set_description(self, desc: str) -> None: self.description.config(text=desc) + def set_max_step(self, maximum: int) -> None: self.progress_bar_step.configure(maximum=maximum) + def set_step(self, value: int) -> None: self.progress_bar_step.configure(value=value) + def step(self, value: int = 1) -> None: self.progress_bar_step.step(value) def set_determinate(self, value: bool) -> None: """ diff --git a/source/mkw/ExtractedGame.py b/source/mkw/ExtractedGame.py index 6867645..d0b4c77 100644 --- a/source/mkw/ExtractedGame.py +++ b/source/mkw/ExtractedGame.py @@ -5,6 +5,7 @@ from typing import Generator, IO, TYPE_CHECKING from source.mkw.ModConfig import ModConfig from source.mkw.Patch.Patch import Patch +from source.progress import Progress from source.wt import szs, lec, wit from source.wt.wstrt import StrPath @@ -27,35 +28,35 @@ class ExtractedGame: self.original_game = original_game self._special_file: dict[str, IO] = {} - def extract_autoadd(self, destination_path: "Path | str") -> Generator[dict, None, None]: + def extract_autoadd(self, destination_path: "Path | str") -> Generator[Progress, None, None]: """ Extract all the autoadd files from the game to destination_path :param destination_path: directory where the autoadd files will be extracted """ - yield {"description": "Extracting autoadd files...", "determinate": False} + yield Progress(description="Extracting autoadd files...", determinate=False) szs.autoadd(self.path / "files/Race/Course/", destination_path) - def extract_original_tracks(self, destination_path: "Path | str") -> Generator[dict, None, None]: + def extract_original_tracks(self, destination_path: "Path | str") -> Generator[Progress, None, None]: """ Move all the original tracks to the destination path :param destination_path: destination of the track """ destination_path = Path(destination_path) destination_path.mkdir(parents=True, exist_ok=True) - yield {"description": "Extracting original tracks...", "determinate": False} + yield Progress(description="Extracting original tracks...", determinate=False) for track_file in (self.path / "files/Race/Course/").glob("*.szs"): - yield {"description": f"Extracting original tracks ({track_file.name})...", "determinate": False} + yield Progress(description=f"Extracting original tracks ({track_file.name})...", determinate=False) if not (destination_path / track_file.name).exists(): track_file.rename(destination_path / track_file.name) else: track_file.unlink() - def install_mystuff(self, mystuff_path: "Path | str") -> Generator[dict, None, None]: + def install_mystuff(self, mystuff_path: "Path | str") -> Generator[Progress, None, None]: """ Install mystuff directory. If any files of the game have the same name as a file at the root of the MyStuff Patch, then it is copied. :mystuff_path: path to the MyStuff directory :return: """ - yield {"description": f"Installing MyStuff '{mystuff_path}'...", "determinate": False} + yield Progress(description=f"Installing MyStuff '{mystuff_path}'...", determinate=False) mystuff_path = Path(mystuff_path) mystuff_rootfiles: dict[str, Path] = {} @@ -67,49 +68,49 @@ class ExtractedGame: if (mystuff_file := mystuff_rootfiles.get(game_file.name)) is None: continue shutil.copy(mystuff_file, game_file) - def install_multiple_mystuff(self, mystuff_paths: list["Path | str"]) -> Generator[dict, None, None]: + def install_multiple_mystuff(self, mystuff_paths: list["Path | str"]) -> Generator[Progress, None, None]: """ Install multiple mystuff patch :param mystuff_paths: paths to all the mystuff patch """ - yield {"description": "Installing all the mystuff patchs"} + yield Progress(description="Installing all the mystuff patchs") for mystuff_path in mystuff_paths: yield from self.install_mystuff(mystuff_path) - def prepare_special_file(self, mod_config: ModConfig) -> Generator[dict, None, None]: + def prepare_special_file(self, mod_config: ModConfig) -> Generator[Progress, None, None]: """ Prepare special files for the patch :return: the special files dict """ - yield {"description": "Preparing ct_icon special file...", "determinate": False} + yield Progress(description="Preparing ct_icon special file...", determinate=False) ct_icons = BytesIO() mod_config.get_full_cticon().save(ct_icons, format="PNG") ct_icons.seek(0) self._special_file["ct_icons"] = ct_icons - def prepare_dol(self) -> Generator[dict, None, None]: + def prepare_dol(self) -> Generator[Progress, None, None]: """ Prepare main.dol and StaticR.rel files (clean them and add lecode) """ - yield {"description": "Preparing main.dol...", "determinate": False} + yield Progress(description="Preparing main.dol...", determinate=False) StrPath(self.path / "sys/main.dol").patch(clean_dol=True, add_lecode=True) - def recreate_all_szs(self) -> Generator[dict, None, None]: + def recreate_all_szs(self) -> Generator[Progress, None, None]: """ Repack all the .d directory into .szs files. """ - yield {"description": f"Repacking all szs", "determinate": False} + yield Progress(description=f"Repacking all szs", determinate=False) for extracted_szs in filter(lambda path: path.is_dir(), self.path.rglob("*.d")): # for every directory that end with a .d in the extracted game, recreate the szs - yield {"description": f"Repacking {extracted_szs} to szs", "determinate": False} + yield Progress(description=f"Repacking {extracted_szs} to szs", determinate=False) szs.create(extracted_szs, extracted_szs.with_suffix(".szs"), overwrite=True) shutil.rmtree(str(extracted_szs.resolve())) def patch_lecode(self, mod_config: ModConfig, cache_directory: Path | str, - cttracks_directory: Path | str, ogtracks_directory: Path | str) -> Generator[dict, None, None]: + cttracks_directory: Path | str, ogtracks_directory: Path | str) -> Generator[Progress, None, None]: """ install lecode on the mod :param cttracks_directory: directory to the customs tracks @@ -117,7 +118,7 @@ class ExtractedGame: :param cache_directory: Path to the cache :param mod_config: mod configuration """ - yield {"description": "Patching LECODE.bin"} + yield Progress(description="Patching LECODE.bin") cache_directory = Path(cache_directory) cttracks_directory = Path(cttracks_directory) ogtracks_directory = Path(ogtracks_directory) @@ -139,42 +140,44 @@ class ExtractedGame: copy_tracks_directories=[ogtracks_directory, cttracks_directory] ) - def _install_all_patch(self, mod_config: ModConfig, patch_directory_name: str) -> Generator[dict, None, None]: + def _install_all_patch(self, mod_config: ModConfig, patch_directory_name: str) -> Generator[Progress, None, None]: """ for all directory that are in the root of the mod, and don't start with an underscore, for all the subdirectory named by the patch_directory_name, apply the patch :param mod_config: the mod to install """ - yield {} # yield an empty dict so that if nothing is yielded by the Patch, still is considered a generator + # yield an empty dict so that if nothing is yielded by the Patch, still is considered a generator + yield Progress() + for part_directory in mod_config.get_mod_directory().glob("[!_]*"): for patch_directory in part_directory.glob(patch_directory_name): yield from Patch(patch_directory, mod_config, self._special_file).install(self) - def install_all_prepatch(self, mod_config: ModConfig) -> Generator[dict, None, None]: + def install_all_prepatch(self, mod_config: ModConfig) -> Generator[Progress, None, None]: """ Install all patchs of the mod_config into the game. Used before the lecode patch is applied :param mod_config: the mod to install """ - yield {"description": "Installing all Pre-Patch...", "determinate": False} + yield Progress(description="Installing all Pre-Patch...", determinate=False) yield from self._install_all_patch(mod_config, "_PREPATCH/") - def install_all_patch(self, mod_config: ModConfig) -> Generator[dict, None, None]: + def install_all_patch(self, mod_config: ModConfig) -> Generator[Progress, None, None]: """ Install all patchs of the mod_config into the game. Used after the lecode patch is applied :param mod_config: the mod to install """ - yield {"description": "Installing all Patch...", "determinate": False} + yield Progress(description="Installing all Patch...", determinate=False) yield from self._install_all_patch(mod_config, "_PATCH/") - def convert_to(self, output_type: wit.Extension) -> Generator[dict, None, wit.WITPath | None]: + def convert_to(self, output_type: wit.Extension) -> Generator[Progress, None, wit.WITPath | None]: """ Convert the extracted game to another format :param output_type: path to the destination of the game :output_type: format of the destination game """ - yield {"description": f"Converting game to {output_type}", "determinate": False} + yield Progress(description=f"Converting game to {output_type}", determinate=False) if output_type == wit.Extension.FST: return destination_file = self.path.with_suffix(self.path.suffix + output_type.value) @@ -190,7 +193,7 @@ class ExtractedGame: destination_file=destination_file, ) - yield {"description": "Deleting the extracted game...", "determinate": False} + yield Progress(description="Deleting the extracted game...", determinate=False) shutil.rmtree(self.path) return converted_game diff --git a/source/mkw/Game.py b/source/mkw/Game.py index 4df4808..1aed2e1 100644 --- a/source/mkw/Game.py +++ b/source/mkw/Game.py @@ -4,6 +4,7 @@ from typing import Generator from source.mkw.ExtractedGame import ExtractedGame from source.mkw.ModConfig import ModConfig from source.option import Option +from source.progress import Progress from source.wt.wit import WITPath, Region, Extension @@ -37,7 +38,7 @@ class Game: """ return not any(self.wit_path[f"./files/rel/lecode-{region.value}.bin"].exists() for region in Region) - def extract(self, dest: "Path | str") -> Generator[dict, None, Path]: + def extract(self, dest: "Path | str") -> Generator[Progress, None, Path]: """ Extract the game to the destination directory. If the game is a FST, just copy to the destination :param dest: destination directory @@ -45,30 +46,25 @@ class Game: gen = self.wit_path.progress_extract_all(dest) if self.wit_path.extension == Extension.FST: - for gen_data in gen: - yield { - "description": "Copying Game...", - "determinate": False - } + for _ in gen: yield Progress(description="Copying Game...", determinate=False) try: next(gen) - except StopIteration as e: - return e.value + except StopIteration as e: return e.value else: for gen_data in gen: - yield { - "description": f'Extracting - {gen_data["percentage"]}% - (estimated time remaining: ' - f'{gen_data["estimation"] if gen_data["estimation"] is not None else "-:--"})', - "maximum": 100, - "value": gen_data["percentage"], - "determinate": True - } + yield Progress( + description=f'Extracting - {gen_data["percentage"]}% - (estimated time remaining: ' + f'{gen_data["estimation"] if gen_data["estimation"] is not None else "-:--"})', + max_step=100, + set_step=gen_data["percentage"], + determinate=True + ) try: next(gen) except StopIteration as e: return e.value - def edit(self, mod_config: ModConfig) -> Generator[dict, None, None]: - yield {"description": "Changing game metadata...", "determinate": False} + def edit(self, mod_config: ModConfig) -> Generator[Progress, None, None]: + yield Progress(description="Changing game metadata...", determinate=False) self.wit_path.edit( name=mod_config.name, game_id=self.wit_path.id[:4] + mod_config.variant @@ -96,7 +92,7 @@ class Game: return extracted_game def install_mod(self, dest: Path, mod_config: ModConfig, options: "Option", output_type: Extension - ) -> Generator[dict, None, None]: + ) -> Generator[Progress, None, None]: """ Patch the game with the mod :dest: destination directory diff --git a/source/mkw/ModConfig.py b/source/mkw/ModConfig.py index 1813bb1..3a45b2a 100644 --- a/source/mkw/ModConfig.py +++ b/source/mkw/ModConfig.py @@ -13,6 +13,7 @@ from source.mkw.Track import CustomTrack, DefaultTrack, Arena import json from source.mkw.OriginalTrack import OriginalTrack +from source.progress import Progress from source.safe_eval import safe_eval, multiple_safe_eval from source.wt.szs import SZSPath @@ -404,7 +405,8 @@ class ModConfig: return full_cticon def normalize_all_tracks(self, autoadd_path: "Path | str", destination_path: "Path | str", - original_tracks_path: "Path | str", thread_amount: int = 8) -> Generator[dict, None, None]: + original_tracks_path: "Path | str", + thread_amount: int = 8) -> Generator[Progress, None, None]: """ Convert all tracks of the mod to szs into the destination_path :param original_tracks_path: path to the originals tracks (if a track is disabled for multiplayer) @@ -412,21 +414,23 @@ class ModConfig: :param autoadd_path: autoadd directory :param destination_path: destination where the files are converted """ - yield {"description": "Normalizing track..."} + yield Progress(description="Normalizing track...") destination_path = Path(destination_path) original_tracks_path = Path(original_tracks_path) destination_path.mkdir(parents=True, exist_ok=True) normalize_threads: list[dict] = [] - def remove_finished_threads() -> Generator[dict, None, None]: + def remove_finished_threads() -> Generator[Progress, None, None]: """ Remove all the thread that stopped in a thread list :return: the list without the stopped thread """ nonlocal normalize_threads - yield {"description": f"Normalizing tracks :\n" + "\n".join(thread['name'] for thread in normalize_threads)} + yield Progress( + description=f"Normalizing tracks :\n" + "\n".join(thread['name'] for thread in normalize_threads) + ) normalize_threads = list(filter(lambda thread: thread["thread"].is_alive(), normalize_threads)) track_directory = self.path.parent / "_TRACKS" diff --git a/source/mkw/Patch/Patch.py b/source/mkw/Patch/Patch.py index 65a3db0..4b3685f 100644 --- a/source/mkw/Patch/Patch.py +++ b/source/mkw/Patch/Patch.py @@ -1,6 +1,8 @@ from pathlib import Path from typing import Generator, IO, TYPE_CHECKING +from source.progress import Progress + if TYPE_CHECKING: from source.mkw.ModConfig import ModConfig from source.mkw.ExtractedGame import ExtractedGame @@ -19,13 +21,13 @@ class Patch: def __repr__(self) -> str: return f"<{self.__class__.__name__} {self.path}>" - def install(self, extracted_game: "ExtractedGame") -> Generator[dict, None, None]: + def install(self, extracted_game: "ExtractedGame") -> Generator[Progress, None, None]: """ patch a game with this Patch :param extracted_game: the extracted game """ from source.mkw.Patch.PatchDirectory import PatchDirectory - yield {"description": f"Installing the patch", "determinate": False} + yield Progress(description=f"Installing the patch", determinate=False) # take all the files in the root directory, and patch them into the game. # Patch is not directly applied to the root to avoid custom configuration diff --git a/source/mkw/Patch/PatchDirectory.py b/source/mkw/Patch/PatchDirectory.py index 6a07353..bea88da 100644 --- a/source/mkw/Patch/PatchDirectory.py +++ b/source/mkw/Patch/PatchDirectory.py @@ -3,6 +3,7 @@ from typing import Generator, TYPE_CHECKING from source.mkw.Patch import PathOutsidePatch, InvalidPatchMode from source.mkw.Patch.PatchObject import PatchObject +from source.progress import Progress if TYPE_CHECKING: from source.mkw.ExtractedGame import ExtractedGame @@ -22,11 +23,11 @@ class PatchDirectory(PatchObject): if subpath.suffix == ".json": continue yield self.subfile_from_path(subpath) - def install(self, extracted_game: "ExtractedGame", game_subpath: Path) -> Generator[dict, None, None]: + def install(self, extracted_game: "ExtractedGame", game_subpath: Path) -> Generator[Progress, None, None]: """ patch a subdirectory of the game with the PatchDirectory """ - yield {"description": f"Patching {game_subpath}"} + yield Progress(description=f"Patching {game_subpath}") # check if the directory should be patched if not self.is_enabled(extracted_game): return diff --git a/source/mkw/Patch/PatchFile.py b/source/mkw/Patch/PatchFile.py index d2dc524..a6d7e26 100644 --- a/source/mkw/Patch/PatchFile.py +++ b/source/mkw/Patch/PatchFile.py @@ -5,6 +5,7 @@ from typing import Generator, IO, TYPE_CHECKING from source.mkw.Patch import PathOutsidePatch, InvalidPatchMode, InvalidSourceMode from source.mkw.Patch.PatchOperation import AbstractPatchOperation from source.mkw.Patch.PatchObject import PatchObject +from source.progress import Progress from source.wt.szs import SZSPath if TYPE_CHECKING: @@ -76,11 +77,11 @@ class PatchFile(PatchObject): if not szs_path.exists() and szs_path.with_suffix(".szs").exists(): SZSPath(szs_path.with_suffix(".szs")).extract_all(szs_path) - def install(self, extracted_game: "ExtractedGame", game_subpath: Path) -> Generator[dict, None, None]: + def install(self, extracted_game: "ExtractedGame", game_subpath: Path) -> Generator[Progress, None, None]: """ patch a subfile of the game with the PatchFile """ - yield {"description": f"Patching {game_subpath}"} + yield Progress(description=f"Patching {game_subpath}") # check if the file should be patched if not self.is_enabled(extracted_game): return @@ -118,7 +119,7 @@ class PatchFile(PatchObject): if not game_subfile.relative_to(extracted_game.path): raise PathOutsidePatch(game_subfile, extracted_game.path) - yield {"description": f"Patching {game_subfile}"} + yield Progress(description=f"Patching {game_subfile}") # if the source is the game, then recalculate the content for every game subfile if self.configuration["source"] == "game": diff --git a/source/mkw/Patch/PatchObject.py b/source/mkw/Patch/PatchObject.py index e484b62..7f621bc 100644 --- a/source/mkw/Patch/PatchObject.py +++ b/source/mkw/Patch/PatchObject.py @@ -3,6 +3,8 @@ from abc import abstractmethod, ABC from pathlib import Path from typing import Generator, TYPE_CHECKING +from source.progress import Progress + if TYPE_CHECKING: from source.mkw.Patch import Patch from source.mkw.ExtractedGame import ExtractedGame @@ -64,7 +66,7 @@ class PatchObject(ABC): return obj(self.patch, str(path.relative_to(self.patch.path))) @abstractmethod - def install(self, extracted_game: "ExtractedGame", game_subpath: Path) -> Generator[dict, None, None]: + def install(self, extracted_game: "ExtractedGame", game_subpath: Path) -> Generator[Progress, None, None]: """ install the PatchObject into the game yield the step of the process diff --git a/source/progress.py b/source/progress.py new file mode 100644 index 0000000..3137a56 --- /dev/null +++ b/source/progress.py @@ -0,0 +1,21 @@ +from dataclasses import dataclass + + +@dataclass +class Progress: + """ + Represent the level of progression of the installer. Used for progress bar. + """ + + # this represents the first progress bar, showing every big part in the process + title: str = None + part: int = None + set_part: int = None + max_part: int = None + + # this represents the second progress bar, showing every step of the current part of the process + description: str = None + step: int = None + set_step: int = None + max_step: int = None + determinate: bool = None diff --git a/source/wt/wit.py b/source/wt/wit.py index ed45a43..9dd11d1 100644 --- a/source/wt/wit.py +++ b/source/wt/wit.py @@ -148,9 +148,7 @@ class WITPath: :return: the extracted file path """ if self.extension == Extension.FST: - yield { - "determinate": False - } + yield {} shutil.copytree(self._get_fst_root(), dest) else: