Options class have been rewritten to make saving and getting more explicit

This commit is contained in:
Faraphel 2022-08-21 11:30:25 +02:00
parent e88fb57143
commit c511f7bb86
6 changed files with 116 additions and 74 deletions

View file

@ -1,13 +1,13 @@
from source.gui import install from source.gui import install
from source.option import Option from source.option import Options
from source.translation import load_language from source.translation import load_language
# this allows every variable to be accessible from other files, useful for the plugins # this allows every variable to be accessible from other files, useful for the plugins
self = __import__(__name__) self = __import__(__name__)
options = Option.from_file("./option.json") options = Options.from_file("./option.json")
translater = load_language(options["language"]) translater = load_language(options.language.get())
self.window = install.Window(options) self.window = install.Window(options)
self.window.run() self.window.run()

View file

@ -161,12 +161,15 @@ class Menu(tkinter.Menu):
master.add_cascade(label=_("LANGUAGE_SELECTION"), menu=self) 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(): for file in Path("./assets/language/").iterdir():
lang_json = json.loads(file.read_text(encoding="utf8")) lang_json = json.loads(file.read_text(encoding="utf8"))
self.add_radiobutton( self.add_radiobutton(
label=lang_json["name"], label=lang_json["name"],
value=file.stem, 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 # Advanced menu
@ -187,11 +190,14 @@ class Menu(tkinter.Menu):
master.add_cascade(label=_("THREADS_USAGE"), menu=self) 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]: for i in [1, 2, 4, 8, 12, 16]:
self.add_radiobutton( self.add_radiobutton(
label=_("USE", f" {i} ", "THREADS"), label=_("USE", f" {i} ", "THREADS"),
value=i, value=i,
variable=self.root.options.threads, variable=self.variable,
command=(lambda amount: (lambda: self.root.options.threads.set(amount)))(i),
) )
# Help menu # Help menu
@ -310,8 +316,9 @@ class DestinationGame(ttk.LabelFrame):
self.entry = ttk.Entry(self) self.entry = ttk.Entry(self)
self.entry.grid(row=1, column=1, sticky="nsew") self.entry.grid(row=1, column=1, sticky="nsew")
self.output_type = ttk.Combobox(self, width=5, values=[extension.name for extension in Extension], self.output_type = ttk.Combobox(self, width=5, values=[extension.name for extension in Extension])
textvariable=self.root.options.extension) self.output_type.set(self.root.options.extension.get())
self.output_type.bind("<<ComboboxSelected>>", lambda _: self.root.options.extension.set(self.output_type.get()))
self.output_type.grid(row=1, column=2, sticky="nsew") self.output_type.grid(row=1, column=2, sticky="nsew")
self.button = ttk.Button(self, text="...", width=2, command=self.select) self.button = ttk.Button(self, text="...", width=2, command=self.select)
@ -420,7 +427,7 @@ class ButtonInstall(ttk.Button):
message: str = translate_external( message: str = translate_external(
mod_config, mod_config,
self.root.options.language.value, self.root.options.language.get(),
mod_config.messages.get("installation_completed", {}).get("text", {}) mod_config.messages.get("installation_completed", {}).get("text", {})
) )

View file

@ -66,7 +66,7 @@ class FrameSettings(ttk.Frame):
for index, (settings_name, settings_data) in enumerate(settings.items()): for index, (settings_name, settings_data) in enumerate(settings.items()):
text = translate_external( text = translate_external(
self.master.master.mod_config, self.master.master.mod_config,
self.root.options["language"], self.root.options.language.get(),
settings_data.text, 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. # add at the end a message from the mod creator where he can put some additional note about the settings.
if text := translate_external( if text := translate_external(
self.master.master.mod_config, 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.master.master.mod_config.messages.get("settings_description", {}).get("text", {})
): ):
self.label_description = ttk.Label(self, text="\n"+text, foreground="gray") self.label_description = ttk.Label(self, text="\n"+text, foreground="gray")

View file

@ -20,7 +20,6 @@ class Window(tkinter.Toplevel):
self.grab_set() # the others window will be disabled, keeping only this one activated self.grab_set() # the others window will be disabled, keeping only this one activated
self.disabled_text: str = _("<", "DISABLED", ">") 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 = ttk.Frame(self)
self.frame_profile.grid(row=1, column=1, sticky="NEWS") self.frame_profile.grid(row=1, column=1, sticky="NEWS")
@ -80,33 +79,38 @@ class Window(tkinter.Toplevel):
""" """
Refresh all the profile 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.configure(values=combobox_values)
self.combobox_profile.current(combobox_values.index( self.combobox_profile.current(combobox_values.index(
self.root.options["mystuff_pack_selected"] selected_mystuff_pack if selected_mystuff_pack in mystuff_packs else self.disabled_text
if self.root.options["mystuff_pack_selected"] in self.root.options["mystuff_packs"] else
self.disabled_text
)) ))
def select_profile(self, event: tkinter.Event = None, profile_name: str = None) -> None: def select_profile(self, event: tkinter.Event = None, profile_name: str = None) -> None:
""" """
Select another profile 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 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.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) self.listbox_mystuff_paths.delete(0, tkinter.END)
is_disabled: bool = (profile_name == self.disabled_text) 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(): 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 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"]: for path in profile_data["paths"]:
self.listbox_mystuff_paths.insert(tkinter.END, path) self.listbox_mystuff_paths.insert(tkinter.END, path)
@ -115,9 +119,10 @@ class Window(tkinter.Toplevel):
""" """
Save the new profile Save the new profile
""" """
mystuff_packs = self.root.options.mystuff_packs.get()
profile_name: str = self.combobox_profile.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")) messagebox.showerror(_("ERROR"), _("MYSTUFF_PROFILE_ALREADY_EXIST"))
return return
@ -126,7 +131,8 @@ class Window(tkinter.Toplevel):
messagebox.showerror(_("ERROR"), _("MYSTUFF_PROFILE_FORBIDDEN_NAME")) messagebox.showerror(_("ERROR"), _("MYSTUFF_PROFILE_FORBIDDEN_NAME"))
return 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.refresh_profiles()
self.select_profile(profile_name=profile_name) self.select_profile(profile_name=profile_name)
@ -134,8 +140,10 @@ class Window(tkinter.Toplevel):
""" """
Delete the currently selected profile Delete the currently selected profile
""" """
mystuff_packs = self.root.options.mystuff_packs.get()
self.root.options["mystuff_packs"].pop(self.root.options["mystuff_pack_selected"]) mystuff_packs.pop(self.root.options.mystuff_pack_selected.get())
self.root.options.mystuff_packs.set(mystuff_packs)
self.refresh_profiles() self.refresh_profiles()
self.select_profile() self.select_profile()
@ -147,9 +155,9 @@ class Window(tkinter.Toplevel):
if (mystuff_path := filedialog.askdirectory(title=_("SELECT_MYSTUFF"), mustexist=True)) is None: return if (mystuff_path := filedialog.askdirectory(title=_("SELECT_MYSTUFF"), mustexist=True)) is None: return
mystuff_path = Path(mystuff_path) mystuff_path = Path(mystuff_path)
self.root.options["mystuff_packs"][self.root.options["mystuff_pack_selected"]]["paths"].append( mystuff_packs = self.root.options.mystuff_packs.get()
str(mystuff_path.resolve()) 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() self.select_profile()
@ -161,7 +169,9 @@ class Window(tkinter.Toplevel):
selections = self.listbox_mystuff_paths.curselection() selections = self.listbox_mystuff_paths.curselection()
if not selections: return if not selections: return
mystuff_packs = self.root.options.mystuff_packs.get()
for selection in selections: 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() self.select_profile()

View file

@ -5,7 +5,7 @@ from source.mkw.ExtractedGame import ExtractedGame
from source.mkw.ModConfig import ModConfig from source.mkw.ModConfig import ModConfig
from source.mkw.collection.Extension import Extension from source.mkw.collection.Extension import Extension
from source.mkw.collection.Region import Region from source.mkw.collection.Region import Region
from source.option import Option from source.option import Options
from source.progress import Progress from source.progress import Progress
from source.wt.wit import WITPath from source.wt.wit import WITPath
from source.translation import translate as _ from source.translation import translate as _
@ -94,7 +94,7 @@ class Game:
return extracted_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]: ) -> Generator[Progress, None, None]:
""" """
Patch the game with the mod Patch the game with the mod
@ -125,7 +125,8 @@ class Game:
# install mystuff # install mystuff
yield Progress(title=_("MYSTUFF"), set_part=2) 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"]) if mystuff_data is not None: yield from extracted_game.install_multiple_mystuff(mystuff_data["paths"])
# prepare the cache # prepare the cache
@ -136,7 +137,7 @@ class Game:
cache_autoadd_directory, cache_autoadd_directory,
cache_cttracks_directory, cache_cttracks_directory,
cache_ogtracks_directory, cache_ogtracks_directory,
options["threads"], options.threads.get(),
) )
yield from extracted_game.prepare_dol() yield from extracted_game.prepare_dol()
yield from extracted_game.prepare_special_file(mod_config) yield from extracted_game.prepare_special_file(mod_config)

View file

@ -4,29 +4,62 @@ from pathlib import Path
from source import restart_program 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 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") __slots__ = ("_path", "_options")
reboot_on_change: list[str] = [ def __init__(self, path, **options):
"language", self._path: Path = Path(path)
]
default_options: dict[str, any] = { self._options: dict[str, Option] = {
"language": "en", "language": Option(self, value="en", reboot_on_change=True),
"threads": 8, "threads": Option(self, value=8),
"mystuff_pack_selected": None, "mystuff_pack_selected": Option(self, value=None),
"mystuff_packs": {}, "mystuff_packs": Option(self, value={}),
"extension": "WBFS", "extension": Option(self, value="WBFS"),
} }
def __init__(self, **options):
self._path: Path | None = None
self._options: dict[str, any] = self.default_options.copy()
for option_name, option_value in options.items(): 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 get an options value from its key
:param key: the option name :param key: the option name
@ -34,49 +67,40 @@ class Option:
""" """
return self._options[key] return self._options[key]
def __setitem__(self, key: str, value: any) -> None: def save(self) -> 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:
""" """
save the options to the file save the options to the file
:return: None :return: None
""" """
if option_file is None: option_file = self._path with self._path.open("w") as file:
option_file = Path(option_file) json.dump(self.to_dict(), file, indent=4, ensure_ascii=False)
with option_file.open("w") as file: def to_dict(self) -> dict[str, any]:
json.dump(self._options, file, indent=4, ensure_ascii=False) """
Return the dictionary form of the options
:return:
"""
return {key: option.get() for key, option in self._options.items()}
@classmethod @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 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 :param option_dict: dict containing the configuration
:return: Option :return: Option
""" """
return cls(**option_dict) return cls(path, **option_dict)
@classmethod @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 Loads the option from a file. If the option file does not exist, only load default configuration
:param option_file: the option file :param option_file: the option file
:return: Option :return: Option
""" """
option_file = Path(option_file) 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