added a 2nd progress bar

This commit is contained in:
Faraphel 2022-08-15 14:14:53 +02:00
parent 917f491ad1
commit c0915ae13e
10 changed files with 118 additions and 100 deletions

View file

@ -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:
"""

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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":

View file

@ -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

21
source/progress.py Normal file
View file

@ -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

View file

@ -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: