Skip to content

Commit 1ec8825

Browse files
committed
GUI prototype
1 parent 8e62abc commit 1ec8825

24 files changed

+379
-17
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.idea
22
venv/
33
out_*.txt
4-
**/__pycache__/
4+
**/__pycache__/
5+
config.json

gui/__init__.py

Whitespace-only changes.

gui/main.py

+331
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
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.")

gui_start.py

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from gui.main import MainWindow
2+
3+
4+
def main():
5+
root = MainWindow()
6+
root.mainloop()
7+
8+
9+
if __name__ == "__main__":
10+
main()

icons/add.png

3.15 KB
Loading

icons/api.png

3.24 KB
Loading

icons/app.ico

5.3 KB
Binary file not shown.

icons/book.png

738 Bytes
Loading

icons/delete.png

3.23 KB
Loading

icons/fastforward.png

3.03 KB
Loading

icons/flag.png

3.18 KB
Loading

icons/folder.png

3.08 KB
Loading

icons/icon.png

1.67 KB
Loading

icons/loading.png

817 Bytes
Loading

icons/position.png

3.06 KB
Loading

icons/save.png

3.06 KB
Loading

icons/search.png

3.47 KB
Loading

icons/success.png

3.2 KB
Loading

icons/tools.png

3.19 KB
Loading

icons/warning.png

3.28 KB
Loading

pigroman.py

+3-16
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
from cached_property import cached_property
1111
import xxhash
1212

13+
from utils import conversions
14+
1315

1416
def is_ascii(s: str) -> bool:
1517
"""
@@ -22,21 +24,6 @@ def is_ascii(s: str) -> bool:
2224
return all(ord(c) < 128 for c in s)
2325

2426

25-
def readable_size_to_number(s: str) -> int:
26-
unit = s.lower()[-1]
27-
mapping = {
28-
"b": 1,
29-
"k": 1024,
30-
"m": 1024 * 1024,
31-
"g": 1024 * 1024 * 1024
32-
}
33-
if unit.isdigit():
34-
return int(s)
35-
if unit in mapping:
36-
return round(float(s[:-1]) * mapping[unit])
37-
raise ValueError("Invalid block size. Examples: 1G, 800M, 1073741824")
38-
39-
4027
class File:
4128
"""
4229
A class representing a file that will be packet
@@ -357,7 +344,7 @@ def main(
357344
required=True
358345
)
359346
args = parser.parse_args()
360-
max_block_size = readable_size_to_number(args.max_block_size)
347+
max_block_size = conversions.readable_size_to_number(args.max_block_size)
361348
st = time.monotonic()
362349
print(f"# Data path: {args.data}")
363350
print(f"# Folders to pack: {args.folder}")

utils/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)