diff --git a/main.pyw b/main.pyw index 0be98ac..2146446 100644 --- a/main.pyw +++ b/main.pyw @@ -1,13 +1,13 @@ from source.gui import install -from source.option import Option +from source.option import Options from source.translation import load_language # this allows every variable to be accessible from other files, useful for the plugins self = __import__(__name__) -options = Option.from_file("./option.json") -translater = load_language(options["language"]) +options = Options.from_file("./option.json") +translater = load_language(options.language.get()) self.window = install.Window(options) self.window.run() diff --git a/source/gui/install.py b/source/gui/install.py index 4fc9203..78fd56b 100644 --- a/source/gui/install.py +++ b/source/gui/install.py @@ -161,12 +161,15 @@ class Menu(tkinter.Menu): master.add_cascade(label=_("LANGUAGE_SELECTION"), menu=self) + self.lang_variable = tkinter.StringVar(value=self.root.options.language.get()) + for file in Path("./assets/language/").iterdir(): lang_json = json.loads(file.read_text(encoding="utf8")) self.add_radiobutton( label=lang_json["name"], value=file.stem, - variable=self.root.options.language + variable=self.lang_variable, + command=(lambda value: (lambda: self.root.options.language.set(value)))(file.stem), ) # Advanced menu @@ -187,11 +190,14 @@ class Menu(tkinter.Menu): master.add_cascade(label=_("THREADS_USAGE"), menu=self) + self.variable = tkinter.IntVar(value=self.root.options.threads.get()) + for i in [1, 2, 4, 8, 12, 16]: self.add_radiobutton( label=_("USE", f" {i} ", "THREADS"), value=i, - variable=self.root.options.threads, + variable=self.variable, + command=(lambda amount: (lambda: self.root.options.threads.set(amount)))(i), ) # Help menu @@ -310,8 +316,9 @@ class DestinationGame(ttk.LabelFrame): self.entry = ttk.Entry(self) self.entry.grid(row=1, column=1, sticky="nsew") - self.output_type = ttk.Combobox(self, width=5, values=[extension.name for extension in Extension], - textvariable=self.root.options.extension) + self.output_type = ttk.Combobox(self, width=5, values=[extension.name for extension in Extension]) + self.output_type.set(self.root.options.extension.get()) + self.output_type.bind("<>", lambda _: self.root.options.extension.set(self.output_type.get())) self.output_type.grid(row=1, column=2, sticky="nsew") self.button = ttk.Button(self, text="...", width=2, command=self.select) @@ -420,7 +427,7 @@ class ButtonInstall(ttk.Button): message: str = translate_external( mod_config, - self.root.options.language.value, + self.root.options.language.get(), mod_config.messages.get("installation_completed", {}).get("text", {}) ) diff --git a/source/gui/mod_settings.py b/source/gui/mod_settings.py index e0b843f..7fef9c7 100644 --- a/source/gui/mod_settings.py +++ b/source/gui/mod_settings.py @@ -66,7 +66,7 @@ class FrameSettings(ttk.Frame): for index, (settings_name, settings_data) in enumerate(settings.items()): text = translate_external( self.master.master.mod_config, - self.root.options["language"], + self.root.options.language.get(), settings_data.text, ) @@ -84,7 +84,7 @@ class FrameSettings(ttk.Frame): # add at the end a message from the mod creator where he can put some additional note about the settings. if text := translate_external( self.master.master.mod_config, - self.root.options["language"], + self.root.options.language.get(), self.master.master.mod_config.messages.get("settings_description", {}).get("text", {}) ): self.label_description = ttk.Label(self, text="\n"+text, foreground="gray") diff --git a/source/gui/mystuff.py b/source/gui/mystuff.py index fdae581..6591b82 100644 --- a/source/gui/mystuff.py +++ b/source/gui/mystuff.py @@ -20,7 +20,6 @@ class Window(tkinter.Toplevel): self.grab_set() # the others window will be disabled, keeping only this one activated self.disabled_text: str = _("<", "DISABLED", ">") - self.root.options["mystuff_pack_selected"] = self.root.options["mystuff_pack_selected"] self.frame_profile = ttk.Frame(self) self.frame_profile.grid(row=1, column=1, sticky="NEWS") @@ -80,33 +79,38 @@ class Window(tkinter.Toplevel): """ Refresh all the profile """ + mystuff_packs = self.root.options.mystuff_packs.get() + selected_mystuff_pack = self.root.options.mystuff_pack_selected.get() - combobox_values = [self.disabled_text, *self.root.options["mystuff_packs"]] + combobox_values = [self.disabled_text, *self.root.options.mystuff_packs.get()] self.combobox_profile.configure(values=combobox_values) self.combobox_profile.current(combobox_values.index( - self.root.options["mystuff_pack_selected"] - if self.root.options["mystuff_pack_selected"] in self.root.options["mystuff_packs"] else - self.disabled_text + selected_mystuff_pack if selected_mystuff_pack in mystuff_packs else self.disabled_text )) def select_profile(self, event: tkinter.Event = None, profile_name: str = None) -> None: """ Select another profile """ - + mystuff_packs = self.root.options.mystuff_packs.get() + profile_name = self.combobox_profile.get() if profile_name is None else profile_name - if not profile_name in self.root.options["mystuff_packs"]: profile_name = self.disabled_text + if not profile_name in mystuff_packs: profile_name = self.disabled_text self.combobox_profile.set(profile_name) - self.root.options["mystuff_pack_selected"] = profile_name + self.root.options.mystuff_pack_selected.set(profile_name) self.listbox_mystuff_paths.delete(0, tkinter.END) is_disabled: bool = (profile_name == self.disabled_text) + state = tkinter.DISABLED if is_disabled else tkinter.NORMAL + + self.button_delete_profile.configure(state=state) for children in self.frame_mystuff_paths_action.children.values(): - children.configure(state=tkinter.DISABLED if is_disabled else tkinter.NORMAL) + children.configure(state=state) + if is_disabled: return - profile_data = self.root.options["mystuff_packs"][profile_name] + profile_data = mystuff_packs[profile_name] for path in profile_data["paths"]: self.listbox_mystuff_paths.insert(tkinter.END, path) @@ -115,9 +119,10 @@ class Window(tkinter.Toplevel): """ Save the new profile """ - + mystuff_packs = self.root.options.mystuff_packs.get() + profile_name: str = self.combobox_profile.get() - if profile_name in self.root.options["mystuff_packs"]: + if profile_name in mystuff_packs: messagebox.showerror(_("ERROR"), _("MYSTUFF_PROFILE_ALREADY_EXIST")) return @@ -126,7 +131,8 @@ class Window(tkinter.Toplevel): messagebox.showerror(_("ERROR"), _("MYSTUFF_PROFILE_FORBIDDEN_NAME")) return - self.root.options["mystuff_packs"][profile_name] = {"paths": []} + mystuff_packs[profile_name] = {"paths": []} + self.root.options.mystuff_packs.set(mystuff_packs) self.refresh_profiles() self.select_profile(profile_name=profile_name) @@ -134,8 +140,10 @@ class Window(tkinter.Toplevel): """ Delete the currently selected profile """ - - self.root.options["mystuff_packs"].pop(self.root.options["mystuff_pack_selected"]) + mystuff_packs = self.root.options.mystuff_packs.get() + mystuff_packs.pop(self.root.options.mystuff_pack_selected.get()) + self.root.options.mystuff_packs.set(mystuff_packs) + self.refresh_profiles() self.select_profile() @@ -147,9 +155,9 @@ class Window(tkinter.Toplevel): if (mystuff_path := filedialog.askdirectory(title=_("SELECT_MYSTUFF"), mustexist=True)) is None: return mystuff_path = Path(mystuff_path) - self.root.options["mystuff_packs"][self.root.options["mystuff_pack_selected"]]["paths"].append( - str(mystuff_path.resolve()) - ) + mystuff_packs = self.root.options.mystuff_packs.get() + mystuff_packs[self.root.options.mystuff_pack_selected.get()]["paths"].append(str(mystuff_path.resolve())) + self.root.options.mystuff_packs.set(mystuff_packs) self.select_profile() @@ -161,7 +169,9 @@ class Window(tkinter.Toplevel): selections = self.listbox_mystuff_paths.curselection() if not selections: return + mystuff_packs = self.root.options.mystuff_packs.get() for selection in selections: - self.root.options["mystuff_packs"][self.root.options["mystuff_pack_selected"]]["paths"].pop(selection) + mystuff_packs[self.root.options.mystuff_pack_selected.get()]["paths"].pop(selection) + self.root.options.mystuff_packs.set(mystuff_packs) self.select_profile() diff --git a/source/mkw/Game.py b/source/mkw/Game.py index 885fe91..6083cc8 100644 --- a/source/mkw/Game.py +++ b/source/mkw/Game.py @@ -5,7 +5,7 @@ from source.mkw.ExtractedGame import ExtractedGame from source.mkw.ModConfig import ModConfig from source.mkw.collection.Extension import Extension from source.mkw.collection.Region import Region -from source.option import Option +from source.option import Options from source.progress import Progress from source.wt.wit import WITPath from source.translation import translate as _ @@ -94,7 +94,7 @@ class Game: return extracted_game - def install_mod(self, dest: Path, mod_config: ModConfig, options: "Option", output_type: Extension + def install_mod(self, dest: Path, mod_config: ModConfig, options: "Options", output_type: Extension ) -> Generator[Progress, None, None]: """ Patch the game with the mod @@ -125,7 +125,8 @@ class Game: # install mystuff yield Progress(title=_("MYSTUFF"), set_part=2) - mystuff_data = options["mystuff_packs"].get(options["mystuff_pack_selected"]) + mystuff_packs = options.mystuff_packs.get() + mystuff_data = mystuff_packs.get(options.mystuff_pack_selected.get()) if mystuff_data is not None: yield from extracted_game.install_multiple_mystuff(mystuff_data["paths"]) # prepare the cache @@ -136,7 +137,7 @@ class Game: cache_autoadd_directory, cache_cttracks_directory, cache_ogtracks_directory, - options["threads"], + options.threads.get(), ) yield from extracted_game.prepare_dol() yield from extracted_game.prepare_special_file(mod_config) diff --git a/source/option.py b/source/option.py index 781f2d9..3741fc6 100644 --- a/source/option.py +++ b/source/option.py @@ -4,29 +4,62 @@ from pathlib import Path from source import restart_program +class OptionLoadingError(Exception): + def __init__(self): + super().__init__(f"An error occured while loading options. Try deleting the option.json file.") + + class Option: + """ + Class representing a single option. It mimic a TkinterVar to make binding easier + """ + + __slots__ = ("options", "_value", "reboot_on_change") + + def __init__(self, options: "Options", value: any, reboot_on_change: bool = False): + self.options = options + self._value = value + self.reboot_on_change = reboot_on_change + + def get(self) -> any: + """ + :return: the value of the option + """ + return self._value + + def set(self, value, ignore_reboot: bool = False) -> None: + """ + Set the value of the option and save the settings. + :param value: the new value of the option + :param ignore_reboot: should the installer ignore the reboot if the settings need it ? + """ + self._value = value + self.options.save() + if self.reboot_on_change and not ignore_reboot: restart_program() + + +class Options: + """ + Class representing a group of Options + """ + __slots__ = ("_path", "_options") - reboot_on_change: list[str] = [ - "language", - ] + def __init__(self, path, **options): + self._path: Path = Path(path) - default_options: dict[str, any] = { - "language": "en", - "threads": 8, - "mystuff_pack_selected": None, - "mystuff_packs": {}, - "extension": "WBFS", - } - - def __init__(self, **options): - self._path: Path | None = None - self._options: dict[str, any] = self.default_options.copy() + self._options: dict[str, Option] = { + "language": Option(self, value="en", reboot_on_change=True), + "threads": Option(self, value=8), + "mystuff_pack_selected": Option(self, value=None), + "mystuff_packs": Option(self, value={}), + "extension": Option(self, value="WBFS"), + } for option_name, option_value in options.items(): - self._options[option_name] = option_value + self._options[option_name].set(option_value, ignore_reboot=True) - def __getitem__(self, key: str) -> any: + def __getattr__(self, key: str) -> any: """ get an options value from its key :param key: the option name @@ -34,49 +67,40 @@ class Option: """ return self._options[key] - def __setitem__(self, key: str, value: any) -> None: - """ - change the value of an options for a key, if the options have been loaded from a file, save it inside - if the option is in the reboot_on_change list, reboot the program - :param key: the name of the option to edit - :param value: the value of the option - :return: - """ - self._options[key] = value - if self._path: self.save() - if key in self.reboot_on_change: restart_program() - - def save(self, option_file: Path | str = None) -> None: + def save(self) -> None: """ save the options to the file :return: None """ - if option_file is None: option_file = self._path - option_file = Path(option_file) + with self._path.open("w") as file: + json.dump(self.to_dict(), file, indent=4, ensure_ascii=False) - with option_file.open("w") as file: - json.dump(self._options, file, indent=4, ensure_ascii=False) + def to_dict(self) -> dict[str, any]: + """ + Return the dictionary form of the options + :return: + """ + return {key: option.get() for key, option in self._options.items()} @classmethod - def from_dict(cls, option_dict: dict) -> "Option": + def from_dict(cls, path: Path, option_dict: dict) -> "Options": """ Create a Option from a dict if the parameters are in the default_options + :param path: path to the option file :param option_dict: dict containing the configuration :return: Option """ - return cls(**option_dict) + return cls(path, **option_dict) @classmethod - def from_file(cls, option_file: str | Path) -> "Option": + def from_file(cls, option_file: str | Path) -> "Options": """ Loads the option from a file. If the option file does not exist, only load default configuration :param option_file: the option file :return: Option """ option_file = Path(option_file) + try: data = json.loads(option_file.read_text(encoding="utf8")) if option_file.exists() else {} + except Exception as exc: raise OptionLoadingError() from exc + return cls.from_dict(option_file, data) - if not option_file.exists(): obj = cls() - else: obj = cls.from_dict(json.loads(option_file.read_text(encoding="utf8"))) - - obj._path = option_file - return obj