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

This commit is contained in:
Faraphel 2022-06-10 21:40:03 +02:00
parent f9db2e96ea
commit 753be7df0d
3 changed files with 190 additions and 10 deletions

View file

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

View file

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

View file

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