-
Notifications
You must be signed in to change notification settings - Fork 24
/
retoolgui.py
312 lines (254 loc) · 11.3 KB
/
retoolgui.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
#!/usr/bin/env python
"""
Filters DATs from [Redump](http://redump.org/) and [No-Intro](https://www.no-intro.org) to
remove titles you don't want.
https://github.com/unexpectedpanda/retool
"""
import multiprocessing
import os
import pathlib
import sys
import traceback
from typing import Any
from PySide6 import QtCore as qtc
from PySide6 import QtGui as qtg
from PySide6 import QtWidgets as qtw
import retool
from modules.config import Config
from modules.gui.gui_config import import_config, write_config
from modules.gui.gui_setup import setup_gui_global, setup_gui_system
from modules.gui.gui_utils import enable_go_button, show_hide
from modules.gui.gui_widgets import CustomComboBox, CustomList, custom_widgets
from modules.gui.retool_ui import Ui_MainWindow # type: ignore
from modules.input import UserInput
from modules.utils import Font, eprint
# Require at least Python 3.10
assert sys.version_info >= (3, 10)
dat_details: dict[str, dict[str, str]] = {}
def main() -> None:
multiprocessing.freeze_support()
# Make sure everything scales as expected across multiple PPI settings
os.environ['QT_AUTO_SCREEN_SCALE_FACTOR'] = '1'
# Encourage the user not to close the CLI
eprint('Don\'t close this window, Retool uses it for processing.', indent=0)
# Set variables
app: qtw.QApplication = qtw.QApplication(sys.argv)
window: MainWindow = MainWindow()
# Show any line edits we need to if an associated checkbox is selected in
# user-config.yaml. This has to be delayed, or they don't show.
show_hide(window.ui.checkBoxGlobalOptions1G1RNames, window.ui.frameGlobalOptions1G1RPrefix)
show_hide(window.ui.checkBoxGlobalOptionsTrace, window.ui.frameGlobalOptionsTrace)
# Check if the "Process DAT files" button should be enabled
enable_go_button(window)
# Show the main window
window.show()
# Prompt the user if clone lists or metadata are needed
if window.clonelistmetadata_needed:
msg = qtw.QMessageBox()
msg.setText(
'This might be the first time you\'ve run Retool, as its clone lists\n'
'or metadata files are missing.\n\n'
'Retool is more accurate with these files. Do you want to\n'
'download them?'
)
msg.setWindowTitle('Clone lists or metadata needed')
msg.setStandardButtons(qtw.QMessageBox.Yes | qtw.QMessageBox.No) # type: ignore
icon = qtg.QIcon()
icon.addFile(':/retoolIcon/images/retool.ico', qtc.QSize(), qtg.QIcon.Normal, qtg.QIcon.Off) # type: ignore
msg.setWindowIcon(icon)
download_update: int = msg.exec()
if download_update == qtw.QMessageBox.Yes: # type: ignore
config: Config = import_config()
write_config(
window,
dat_details,
config,
settings_window=None,
run_retool=True,
update_clone_list=True,
)
sys.exit(app.exec())
class MainWindow(qtw.QMainWindow):
"""The main window for RetoolGUI."""
# Import the user config
config: Config = import_config()
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.data: dict[Any, Any] = {}
self.threadpool: qtc.QThreadPool = qtc.QThreadPool()
self.clonelistmetadata_needed: bool = False
# Limit the number of CLI threads that can run to 1. Potentially if we get
# out of the CLI in the future and into full GUI this can be increased.
self.threadpool.setMaxThreadCount(1)
# Fix the taskbar icon not loading on Windows
if sys.platform.startswith('win'):
import ctypes
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID('retool')
# Replace default QT widgets with customized versions
self = custom_widgets(self)
# Disable the system settings for first launch
self.ui.tabWidgetSystemSettings.setEnabled(False)
# Populate the global settings with data and set up user interactions
setup_gui_global(self, dat_details, self.config)
# Populate the system settings with data and set up user interactions
setup_gui_system(self, dat_details, self.config)
# Check if clone lists or metadata files are required
if not (
pathlib.Path(self.config.path_clone_list).is_dir()
and pathlib.Path(self.config.path_metadata).is_dir()
):
self.clonelistmetadata_needed = True
# Set up a timer on the splitter move before writing to config
timer_splitter = qtc.QTimer(self)
timer_splitter.setSingleShot(True)
timer_splitter.timeout.connect(
lambda: write_config(self, dat_details, self.config, settings_window=None)
)
self.ui.splitter.splitterMoved.connect(lambda: timer_splitter.start(500))
# Set up a timer on the window resize move before writing to config
self.timer_resize = qtc.QTimer(self)
self.timer_resize.setSingleShot(True)
self.timer_resize.timeout.connect(
lambda: write_config(self, dat_details, self.config, settings_window=None)
)
# Add all widgets to a list that should trigger a config write if interacted with
interactive_widgets = []
interactive_widgets.extend(
self.ui.centralwidget.findChildren(
qtw.QPushButton,
qtc.QRegularExpression(
'button(Global|System)(Choose|Clear|Language|Region|Localization|Video|Deselect|Select|Default).*'
),
)
)
interactive_widgets.extend(
self.ui.centralwidget.findChildren(
qtw.QCheckBox,
qtc.QRegularExpression('checkBox(Global|System)(Exclude|Options|Replace).*'),
)
)
interactive_widgets.extend(
self.ui.centralwidget.findChildren(
qtw.QTextEdit,
qtc.QRegularExpression('textEdit(Global|System)(Exclude|Include|Filter).*'),
)
)
interactive_widgets.extend(
self.ui.centralwidget.findChildren(
qtw.QLineEdit, qtc.QRegularExpression('lineEdit(Global|System)Options.*')
)
)
interactive_widgets.extend(
self.ui.centralwidget.findChildren(
qtw.QListWidget, qtc.QRegularExpression('listWidget(Global|System).*')
)
)
interactive_widgets.extend(
self.ui.centralwidget.findChildren(
qtw.QComboBox, qtc.QRegularExpression('comboBox(Global|System).*')
)
)
# Track all meaningful interactions, write the config file if one happens
for interactive_widget in interactive_widgets:
try:
if (
type(interactive_widget) is not CustomList
and type(interactive_widget) is not qtw.QComboBox
):
interactive_widget.clicked.connect(
lambda: write_config(self, dat_details, self.config, settings_window=None)
)
except Exception:
pass
try:
if type(interactive_widget) is CustomComboBox:
interactive_widget.activated.connect(
lambda: write_config(self, dat_details, self.config, settings_window=None)
)
except Exception:
pass
try:
interactive_widget.keyPressed.connect(
lambda: write_config(self, dat_details, self.config, settings_window=None)
)
except Exception:
pass
try:
interactive_widget.dropped.connect(
lambda: write_config(self, dat_details, self.config, settings_window=None)
)
except Exception:
pass
def closeEvent(self, event: Any) -> None:
qtw.QApplication.closeAllWindows()
event.accept()
def enable_app(self) -> None:
"""If all the threads have finished, re-enable the interface."""
if self.threadpool.activeThreadCount() == 0:
self.ui.buttonGo.setEnabled(True)
self.ui.buttonStop.hide()
self.ui.buttonGo.show()
self.ui.buttonStop.setText(qtc.QCoreApplication.translate("MainWindow", "Stop", None))
self.ui.buttonStop.setEnabled(True)
self.ui.frame.setEnabled(True)
def resizeEvent(self, event: Any) -> None:
"""Record the window size when the user resizes it."""
# Set up a timer on the resize before writing to config
self.timer_resize.start(500)
def start_retool_thread(self, data: UserInput | None = None) -> None:
"""
Start the thread that calls Retool CLI.
Args:
data (UserInput): The Retool user input object. Defaults to `None`.
"""
self.data = {} # reset
self.new_thread = RunThread('Retool', data)
# Check to see if we can enable the interface
self.new_thread.signals.finished.connect(self.enable_app)
self.threadpool.start(self.new_thread)
# Run the Retool process in a separate thread. Needed so we can disable/enable
# bits of the Retool GUI as required, or cancel the process.
# Modified from
# https://www.pythonguis.com/faq/postpone-the-execution-of-sequential-processes-until-previous-thread-emit-the-result/
class Signals(qtc.QObject):
finished = qtc.Signal(UserInput)
class ThreadTask(qtc.QRunnable):
def __init__(self, data: Any, argument: Any) -> None:
super().__init__()
self.data = data
self.argument = argument
@qtc.Slot()
def run(self) -> None:
try:
retool.main(self.argument)
except retool.ExitRetool: # type: ignore
# Quietly re-enable the GUI on this exception
pass
except Exception:
eprint(
'Retool has had an unexpected error. Please raise an issue at https://github.com/unexpectedpanda/retool/issues, '
'attaching the DAT file that caused the problem and the following trace:\n',
level='error',
indent=0,
)
eprint(f'{Font.error}{"-"*70}')
traceback.print_exc()
eprint(f'{Font.error}{"-"*70}')
eprint(
f'The error occurred on this file:\n{self.argument.input_file_name}\n',
level='error',
)
if pathlib.Path('.dev').is_file():
eprint(
'\nPress enter to continue. This message is only shown in dev mode.',
level='disabled',
)
input()
self.signals.finished.emit(self.data) # type: ignore
self.signals.finished.emit(self.data) # type: ignore
class RunThread(ThreadTask):
signals: Signals = Signals()
if __name__ == '__main__':
main()