|
| 1 | +import json |
| 2 | +import os |
| 3 | +from collections import defaultdict, namedtuple |
| 4 | +from tkinter import Tk, StringVar, IntVar, BooleanVar, Menu, filedialog, messagebox |
| 5 | +from tkinter.ttk import Frame, Label, Entry, Button, Scale, Checkbutton, LabelFrame |
| 6 | +from typing import Any, Dict |
| 7 | + |
| 8 | +from utils import icons, conversions |
| 9 | + |
| 10 | + |
| 11 | +class ConfigFile: |
| 12 | + def __init__(self, file_path: str, load: bool = True): |
| 13 | + self._config: Dict[str, Any] = {} |
| 14 | + self.file_path = file_path |
| 15 | + if load: |
| 16 | + self.load() |
| 17 | + |
| 18 | + def get(self, *args, **kwargs) -> Any: |
| 19 | + return self._config.get(*args, **kwargs) |
| 20 | + |
| 21 | + def __getitem__(self, item: str) -> Any: |
| 22 | + return self._config[item] |
| 23 | + |
| 24 | + def __setitem__(self, key: str, value: Any) -> None: |
| 25 | + self._config[key] = value |
| 26 | + |
| 27 | + def save(self) -> None: |
| 28 | + with open(self.file_path, "w") as f: |
| 29 | + f.write(json.dumps(self._config)) |
| 30 | + |
| 31 | + def load_defaults(self) -> None: |
| 32 | + self._config = {"archive_tool_path": ""} |
| 33 | + |
| 34 | + def load(self) -> None: |
| 35 | + if not os.path.isfile(self.file_path): |
| 36 | + self.load_defaults() |
| 37 | + self.save() |
| 38 | + else: |
| 39 | + with open(self.file_path, "r") as f: |
| 40 | + self._config = json.loads(f.read()) |
| 41 | + |
| 42 | + |
| 43 | +class BrowseFrame(Frame): |
| 44 | + def __init__(self, master, variable, **kwargs): |
| 45 | + super(BrowseFrame, self).__init__(master, **kwargs) |
| 46 | + self.variable = variable |
| 47 | + self.master = master |
| 48 | + self.grid_columnconfigure(0, weight=1) |
| 49 | + self.entry = Entry(self, textvariable=self.variable) |
| 50 | + self.entry.grid(row=0, column=0, sticky="we") |
| 51 | + self.button = Button( |
| 52 | + self, image=icons.get_icon("folder.png"), |
| 53 | + command=lambda: self.variable.set(filedialog.askdirectory().replace("/", "\\")) |
| 54 | + ).grid( |
| 55 | + row=0, column=1, sticky="we" |
| 56 | + ) |
| 57 | + |
| 58 | + |
| 59 | +SubfolderEntry = namedtuple("FolderEntry", "var frame status_label") |
| 60 | + |
| 61 | + |
| 62 | +class SubfoldersListFrame(Frame): |
| 63 | + """ |
| 64 | + Frame con Entry degli autori |
| 65 | + """ |
| 66 | + |
| 67 | + def __init__(self, master, data_path_var, add_empty_author_entry=True, **kwargs): |
| 68 | + """ |
| 69 | + :param master: |
| 70 | + :param add_empty_author_entry: se `True`, aggiungi una Entry vuota in cima, altrimenti parti senza nessuna Entry |
| 71 | + :param kwargs: |
| 72 | + """ |
| 73 | + super(SubfoldersListFrame, self).__init__(master, **kwargs) |
| 74 | + self.master = master |
| 75 | + |
| 76 | + self._subfolder_entries = [] |
| 77 | + |
| 78 | + self.add_button = None |
| 79 | + |
| 80 | + self.data_path_var = data_path_var |
| 81 | + |
| 82 | + # Aggiungi entry vuota se richiesto |
| 83 | + if add_empty_author_entry: |
| 84 | + self.add_subfolder() |
| 85 | + |
| 86 | + # Pulsante 'Aggiungi autore' |
| 87 | + self.add_button = Button(self, image=icons.get_icon("add.png"), text="Add subfolder", compound="left", |
| 88 | + command=self.add_subfolder) |
| 89 | + self.add_button.pack(fill="x", pady=2) |
| 90 | + |
| 91 | + def add_subfolder(self, value=""): |
| 92 | + """ |
| 93 | + Aggiungi un autore |
| 94 | +
|
| 95 | + :param value: nome dell'autore |
| 96 | + :return: |
| 97 | + """ |
| 98 | + # Rimuovi pulsante 'Aggiungi autore', se è stato posizionato |
| 99 | + if self.add_button is not None: |
| 100 | + self.add_button.pack_forget() |
| 101 | + |
| 102 | + # Crea frame con Entry e pulsante rimozione e posizionali |
| 103 | + f = Frame(self) |
| 104 | + status_label = Label(f, image=icons.get_icon("warning.png")) |
| 105 | + ae = SubfolderEntry(StringVar(value=value), f, status_label) |
| 106 | + self._subfolder_entries.append(ae) |
| 107 | + f.grid_columnconfigure(1, weight=1) |
| 108 | + ae.status_label.grid(row=0, column=0, sticky="we") |
| 109 | + ae.var.trace("w", lambda *_: self.update_entry_status_label(ae)) |
| 110 | + Entry(f, textvariable=self._subfolder_entries[-1].var).grid(row=0, column=1, sticky="we") |
| 111 | + Button( |
| 112 | + f, image=icons.get_icon("search.png"), |
| 113 | + command=lambda: ae.var.set(f"{self.data_path_var.get().strip()}\\{ae.var.get().strip()}") |
| 114 | + ).grid(row=0, column=2, sticky="e") |
| 115 | + Button( |
| 116 | + f, image=icons.get_icon("folder.png"), |
| 117 | + command=lambda: ae.var.set(filedialog.askdirectory().replace("/", "\\").strip()) |
| 118 | + ).grid(row=0, column=3, sticky="e") |
| 119 | + Button( |
| 120 | + f, image=icons.get_icon("delete.png"), |
| 121 | + command=lambda: self.remove_subfolder(ae) |
| 122 | + ).grid(row=0, column=4, sticky="e") |
| 123 | + f.pack(fill="x", pady=1, expand=True) |
| 124 | + |
| 125 | + # Riaggiungi pulsante 'Aggiungi autore', se necessario |
| 126 | + if self.add_button is not None: |
| 127 | + self.add_button.pack(fill="x", pady=2) |
| 128 | + |
| 129 | + def is_subfolder(self, path: str) -> bool: |
| 130 | + data_path = self.data_path_var.get().lower().rstrip("\\").strip() |
| 131 | + return bool(data_path) and path.lower().strip().startswith(data_path) and os.path.isdir(path) |
| 132 | + |
| 133 | + def update_entry_status_label(self, entry: SubfolderEntry) -> None: |
| 134 | + entry.status_label.configure( |
| 135 | + image=icons.get_icon( |
| 136 | + "success.png" |
| 137 | + if self.is_subfolder(entry.var.get()) else |
| 138 | + "warning.png" |
| 139 | + ) |
| 140 | + ) |
| 141 | + |
| 142 | + def remove_subfolder(self, author_entry): |
| 143 | + """ |
| 144 | + Rimuove un autore dalla lista degli autori |
| 145 | +
|
| 146 | + :param author_entry: `AutorEntry` dell'autore da rimuovere |
| 147 | + :return: |
| 148 | + """ |
| 149 | + author_entry.frame.pack_forget() |
| 150 | + self._subfolder_entries.remove(author_entry) |
| 151 | + |
| 152 | + @property |
| 153 | + def subfolders(self): |
| 154 | + """ |
| 155 | + Ritorna gli autori |
| 156 | + :return: |
| 157 | + """ |
| 158 | + return self._subfolder_entries |
| 159 | + |
| 160 | + @subfolders.setter |
| 161 | + def subfolders(self, authors): |
| 162 | + """ |
| 163 | + Imposta gli autori e ricostruisce la lista dei widget |
| 164 | +
|
| 165 | + :param authors: |
| 166 | + :return: |
| 167 | + """ |
| 168 | + self._subfolder_entries.clear() |
| 169 | + for widget in self.pack_slaves(): |
| 170 | + widget.pack_forget() |
| 171 | + for author in authors: |
| 172 | + self.add_subfolder(author) |
| 173 | + |
| 174 | + def reevaluate_statuses(self): |
| 175 | + for x in self._subfolder_entries: |
| 176 | + self.update_entry_status_label(x) |
| 177 | + |
| 178 | + |
| 179 | +class MainFrame(Frame): |
| 180 | + MIN_ARCHIVE_SIZE = 100 * 1024 |
| 181 | + MAX_ARCHIVE_SIZE = 2.5 * 1024 * 1024 |
| 182 | + |
| 183 | + def __init__(self, master, **kwargs): |
| 184 | + super(MainFrame, self).__init__(master, **kwargs) |
| 185 | + self.master = master |
| 186 | + |
| 187 | + # Configurazione griglia |
| 188 | + self.grid_columnconfigure(0, weight=1, pad=10) |
| 189 | + self.grid_columnconfigure(1, weight=5, pad=10) |
| 190 | + self.grid_columnconfigure(2, weight=1, pad=10) |
| 191 | + # for i in range(0, 10): |
| 192 | + # self.grid_rowconfigure(i, pad=0, weight=1) |
| 193 | + |
| 194 | + # Titolo |
| 195 | + # Label(self, text="Pigroman", font=("Segoe UI", 16), image=icons.get_icon("icon.png"), |
| 196 | + # compound="left").grid(row=0, column=0, padx=(0, 30), sticky="w") |
| 197 | + |
| 198 | + self.data_path = StringVar() |
| 199 | + self.data_path.trace( |
| 200 | + "w", lambda *_: self.subfolders_list_frame.reevaluate_statuses() |
| 201 | + ) |
| 202 | + self.output_path = StringVar() |
| 203 | + self.output_name = StringVar() |
| 204 | + self.max_block_size = IntVar() |
| 205 | + self.max_block_size.trace( |
| 206 | + "w", lambda *_: self.max_archive_size_label.configure( |
| 207 | + text=conversions.number_to_readable_size(self.max_block_size.get()) |
| 208 | + ) |
| 209 | + ) |
| 210 | + self.compress = BooleanVar() |
| 211 | + self.create_esl = BooleanVar() |
| 212 | + self.file_types = defaultdict(BooleanVar) |
| 213 | + |
| 214 | + Label(self, text="Data path").grid(row=1, column=0, sticky="w") |
| 215 | + BrowseFrame(self, self.data_path).grid(row=1, column=1, sticky="we", columnspan=4) |
| 216 | + |
| 217 | + Label(self, text="Output path").grid(row=2, column=0, sticky="w") |
| 218 | + BrowseFrame(self, self.output_path).grid(row=2, column=1, sticky="we", columnspan=4) |
| 219 | + |
| 220 | + Label(self, text="Output name").grid(row=3, column=0, sticky="w") |
| 221 | + Entry(self, textvariable=self.output_name).grid(row=3, column=1, sticky="we", columnspan=2) |
| 222 | + |
| 223 | + Label(self, text="Max archive size").grid(row=4, column=0, sticky="w") |
| 224 | + Scale( |
| 225 | + self, from_=self.MIN_ARCHIVE_SIZE, to=self.MAX_ARCHIVE_SIZE, variable=self.max_block_size |
| 226 | + ).grid(row=4, column=1, sticky="we") |
| 227 | + self.max_archive_size_label = Label(self) |
| 228 | + self.max_archive_size_label.grid(row=4, column=2, sticky="e") |
| 229 | + |
| 230 | + self.folders_group = LabelFrame(self, text="Subfolders") |
| 231 | + self.folders_group.grid(row=5, column=0, columnspan=5, sticky="nswe") |
| 232 | + self.subfolders_list_frame = SubfoldersListFrame(self.folders_group, self.data_path) |
| 233 | + self.subfolders_list_frame.pack(fill="both") |
| 234 | + |
| 235 | + self.options_group = LabelFrame(self, text="Options") |
| 236 | + self.options_group.grid(row=6, column=0, sticky="nswe", columnspan=3) |
| 237 | + Checkbutton(self.options_group, text="Compress", variable=self.compress).pack(side="left") |
| 238 | + Checkbutton(self.options_group, text="Create ESLs", variable=self.create_esl).pack(side="left") |
| 239 | + |
| 240 | + self.file_types_group = LabelFrame(self, text="File types (coming soon)") |
| 241 | + self.file_types_group.grid(row=7, column=0, sticky="nswe", columnspan=4) |
| 242 | + r = 0 |
| 243 | + c = 0 |
| 244 | + for i, x in enumerate(("Meshes", "Textures", "Menus", "Sounds", "Voices", "Shaders", "Trees", "Fonts", "Misc")): |
| 245 | + Checkbutton(self.file_types_group, text=x, variable=self.file_types[x.lower()], state="disabled").grid( |
| 246 | + row=r, column=c, sticky="we" |
| 247 | + ) |
| 248 | + c += 1 |
| 249 | + if c == 3: |
| 250 | + c = 0 |
| 251 | + r += 1 |
| 252 | + |
| 253 | + self.build_button = Button( |
| 254 | + self, text="Create packages!", image=icons.get_icon("save.png"), |
| 255 | + compound="left", command=self.build |
| 256 | + ) |
| 257 | + self.build_button.grid( |
| 258 | + row=8, column=0, columnspan=4, sticky="we" |
| 259 | + ) |
| 260 | + |
| 261 | + self.max_block_size.set(1.5 * 1024 * 1024) |
| 262 | + |
| 263 | + def build(self): |
| 264 | + try: |
| 265 | + self.master.check_archive() |
| 266 | + self.check_settings() |
| 267 | + self.build_button.config(state="disabled") |
| 268 | + except ValueError as e: |
| 269 | + messagebox.showerror("Error", str(e)) |
| 270 | + finally: |
| 271 | + self.build_button.config(state="enabled") |
| 272 | + |
| 273 | + def check_settings(self): |
| 274 | + if not self.data_path.get().lower().strip().endswith("data"): |
| 275 | + raise ValueError("The data path must be a folder named 'data'.") |
| 276 | + if not os.path.isdir(self.data_path.get()): |
| 277 | + raise ValueError("The data path does not exist.") |
| 278 | + if not os.path.isdir(self.output_path.get()): |
| 279 | + raise ValueError("The output path does not exist.") |
| 280 | + if not self.MIN_ARCHIVE_SIZE < self.max_block_size.get() < self.MAX_ARCHIVE_SIZE: |
| 281 | + raise ValueError("The archive size must be between 100MB and 2.5GB") |
| 282 | + if not self.output_name.get().strip(): |
| 283 | + raise ValueError("Invalid output name") |
| 284 | + if not [x for x in self.subfolders_list_frame.subfolders if bool(x.var.get().strip())]: |
| 285 | + raise ValueError("No subfolders specified") |
| 286 | + for subfolder in self.subfolders_list_frame.subfolders: |
| 287 | + path = subfolder.var.get() |
| 288 | + if not self.subfolders_list_frame.is_subfolder(path): |
| 289 | + raise ValueError(f"{path} is not a data subfolder or does not exist!") |
| 290 | + |
| 291 | + |
| 292 | +class MainWindow(Tk): |
| 293 | + def __init__(self, *args, **kwargs): |
| 294 | + super(MainWindow, self).__init__(*args, **kwargs) |
| 295 | + |
| 296 | + self.minsize(250, 280) |
| 297 | + self.title("Pigroman") |
| 298 | + self.config_file = ConfigFile("config.json", load=True) |
| 299 | + if os.name == "nt": |
| 300 | + self.iconbitmap("icons/app.ico") |
| 301 | + |
| 302 | + mb = Menu(self) |
| 303 | + fm = Menu(mb, tearoff=0) |
| 304 | + fm.add_command(label="Set Archive.exe path", command=self.set_archive_path) |
| 305 | + fm.add_command(label="Save these settings", command=self.save_preset) |
| 306 | + mb.add_cascade(label="File", menu=fm) |
| 307 | + self.config(menu=mb) |
| 308 | + |
| 309 | + self.main_frame = MainFrame(self) |
| 310 | + self.main_frame.pack(fill="both", padx=10, pady=10) |
| 311 | + |
| 312 | + def set_archive_path(self): |
| 313 | + file_path = filedialog.askopenfilename(filetypes=[("Archive.exe", "Archive.exe")]) |
| 314 | + file_path = file_path.replace("/", "\\") |
| 315 | + if not file_path.endswith("Archive.exe"): |
| 316 | + messagebox.showerror("Invalid path", "Please select Archive.exe") |
| 317 | + return |
| 318 | + folder = "\\".join(file_path.split("\\")[:-1]) |
| 319 | + messagebox.showinfo("Success", f"Archive.exe path set to {folder}\\Archive.exe") |
| 320 | + self.config_file["archive_tool_path"] = folder |
| 321 | + self.config_file.save() |
| 322 | + |
| 323 | + def check_archive(self): |
| 324 | + archive_path = self.config_file.get("archive_tool_path", "") |
| 325 | + if not archive_path: |
| 326 | + raise ValueError("You must set Archive.exe's path first.") |
| 327 | + elif not os.path.isfile(f"{archive_path}\\Archive.exe"): |
| 328 | + raise ValueError(f"File not found: {archive_path}\\Archive.exe") |
| 329 | + |
| 330 | + def save_preset(self): |
| 331 | + messagebox.showinfo("Success", "Current settings saved as default.") |
0 commit comments