ankigammon 1.0.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. ankigammon/__init__.py +7 -0
  2. ankigammon/__main__.py +6 -0
  3. ankigammon/analysis/__init__.py +13 -0
  4. ankigammon/analysis/score_matrix.py +391 -0
  5. ankigammon/anki/__init__.py +6 -0
  6. ankigammon/anki/ankiconnect.py +216 -0
  7. ankigammon/anki/apkg_exporter.py +111 -0
  8. ankigammon/anki/card_generator.py +1325 -0
  9. ankigammon/anki/card_styles.py +1054 -0
  10. ankigammon/gui/__init__.py +8 -0
  11. ankigammon/gui/app.py +192 -0
  12. ankigammon/gui/dialogs/__init__.py +10 -0
  13. ankigammon/gui/dialogs/export_dialog.py +594 -0
  14. ankigammon/gui/dialogs/import_options_dialog.py +201 -0
  15. ankigammon/gui/dialogs/input_dialog.py +762 -0
  16. ankigammon/gui/dialogs/note_dialog.py +93 -0
  17. ankigammon/gui/dialogs/settings_dialog.py +420 -0
  18. ankigammon/gui/dialogs/update_dialog.py +373 -0
  19. ankigammon/gui/format_detector.py +377 -0
  20. ankigammon/gui/main_window.py +1611 -0
  21. ankigammon/gui/resources/down-arrow.svg +3 -0
  22. ankigammon/gui/resources/icon.icns +0 -0
  23. ankigammon/gui/resources/icon.ico +0 -0
  24. ankigammon/gui/resources/icon.png +0 -0
  25. ankigammon/gui/resources/style.qss +402 -0
  26. ankigammon/gui/resources.py +26 -0
  27. ankigammon/gui/update_checker.py +259 -0
  28. ankigammon/gui/widgets/__init__.py +8 -0
  29. ankigammon/gui/widgets/position_list.py +166 -0
  30. ankigammon/gui/widgets/smart_input.py +268 -0
  31. ankigammon/models.py +356 -0
  32. ankigammon/parsers/__init__.py +7 -0
  33. ankigammon/parsers/gnubg_match_parser.py +1094 -0
  34. ankigammon/parsers/gnubg_parser.py +468 -0
  35. ankigammon/parsers/sgf_parser.py +290 -0
  36. ankigammon/parsers/xg_binary_parser.py +1097 -0
  37. ankigammon/parsers/xg_text_parser.py +688 -0
  38. ankigammon/renderer/__init__.py +5 -0
  39. ankigammon/renderer/animation_controller.py +391 -0
  40. ankigammon/renderer/animation_helper.py +191 -0
  41. ankigammon/renderer/color_schemes.py +145 -0
  42. ankigammon/renderer/svg_board_renderer.py +791 -0
  43. ankigammon/settings.py +315 -0
  44. ankigammon/thirdparty/__init__.py +7 -0
  45. ankigammon/thirdparty/xgdatatools/__init__.py +17 -0
  46. ankigammon/thirdparty/xgdatatools/xgimport.py +160 -0
  47. ankigammon/thirdparty/xgdatatools/xgstruct.py +1032 -0
  48. ankigammon/thirdparty/xgdatatools/xgutils.py +118 -0
  49. ankigammon/thirdparty/xgdatatools/xgzarc.py +260 -0
  50. ankigammon/utils/__init__.py +13 -0
  51. ankigammon/utils/gnubg_analyzer.py +590 -0
  52. ankigammon/utils/gnuid.py +577 -0
  53. ankigammon/utils/move_parser.py +204 -0
  54. ankigammon/utils/ogid.py +326 -0
  55. ankigammon/utils/xgid.py +387 -0
  56. ankigammon-1.0.6.dist-info/METADATA +352 -0
  57. ankigammon-1.0.6.dist-info/RECORD +61 -0
  58. ankigammon-1.0.6.dist-info/WHEEL +5 -0
  59. ankigammon-1.0.6.dist-info/entry_points.txt +2 -0
  60. ankigammon-1.0.6.dist-info/licenses/LICENSE +21 -0
  61. ankigammon-1.0.6.dist-info/top_level.txt +1 -0
@@ -0,0 +1,93 @@
1
+ """
2
+ Custom dialog for editing notes with word wrapping.
3
+ """
4
+
5
+ from PySide6.QtWidgets import (
6
+ QDialog, QVBoxLayout, QHBoxLayout, QPushButton,
7
+ QLabel, QPlainTextEdit
8
+ )
9
+ from PySide6.QtCore import Qt
10
+
11
+
12
+ class NoteEditDialog(QDialog):
13
+ """Custom dialog for editing notes with word wrapping."""
14
+
15
+ def __init__(self, current_note: str = "", label_text: str = "Note:", parent=None):
16
+ super().__init__(parent)
17
+ self.setWindowTitle("Edit Note")
18
+ self.setModal(True)
19
+ self.setMinimumSize(500, 300)
20
+
21
+ layout = QVBoxLayout(self)
22
+ layout.setSpacing(12)
23
+
24
+ # Label
25
+ label = QLabel(label_text)
26
+ label.setStyleSheet("color: #cdd6f4; font-weight: 600;")
27
+ layout.addWidget(label)
28
+
29
+ # Text edit with word wrap
30
+ self.text_edit = QPlainTextEdit()
31
+ self.text_edit.setPlainText(current_note)
32
+ self.text_edit.setLineWrapMode(QPlainTextEdit.WidgetWidth)
33
+ self.text_edit.setStyleSheet("""
34
+ QPlainTextEdit {
35
+ background-color: #1e1e2e;
36
+ color: #cdd6f4;
37
+ border: 2px solid #313244;
38
+ border-radius: 6px;
39
+ padding: 8px;
40
+ }
41
+ QPlainTextEdit:focus {
42
+ border-color: #89b4fa;
43
+ }
44
+ """)
45
+ layout.addWidget(self.text_edit, stretch=1)
46
+
47
+ # Buttons
48
+ button_layout = QHBoxLayout()
49
+ button_layout.addStretch()
50
+
51
+ self.btn_ok = QPushButton("OK")
52
+ self.btn_ok.setStyleSheet("""
53
+ QPushButton {
54
+ background-color: #89b4fa;
55
+ color: #1e1e2e;
56
+ border: none;
57
+ padding: 8px 24px;
58
+ border-radius: 6px;
59
+ font-weight: 600;
60
+ }
61
+ QPushButton:hover {
62
+ background-color: #a0c8fc;
63
+ }
64
+ """)
65
+ self.btn_ok.setCursor(Qt.PointingHandCursor)
66
+ self.btn_ok.clicked.connect(self.accept)
67
+ button_layout.addWidget(self.btn_ok)
68
+
69
+ self.btn_cancel = QPushButton("Cancel")
70
+ self.btn_cancel.setStyleSheet("""
71
+ QPushButton {
72
+ background-color: #45475a;
73
+ color: #cdd6f4;
74
+ border: none;
75
+ padding: 8px 24px;
76
+ border-radius: 6px;
77
+ }
78
+ QPushButton:hover {
79
+ background-color: #585b70;
80
+ }
81
+ """)
82
+ self.btn_cancel.setCursor(Qt.PointingHandCursor)
83
+ self.btn_cancel.clicked.connect(self.reject)
84
+ button_layout.addWidget(self.btn_cancel)
85
+
86
+ layout.addLayout(button_layout)
87
+
88
+ # Focus the text edit
89
+ self.text_edit.setFocus()
90
+
91
+ def get_text(self) -> str:
92
+ """Get the edited text."""
93
+ return self.text_edit.toPlainText()
@@ -0,0 +1,420 @@
1
+ """
2
+ Settings configuration dialog.
3
+ """
4
+
5
+ import os
6
+ import sys
7
+ import subprocess
8
+ import tempfile
9
+ from pathlib import Path
10
+ from typing import Optional
11
+ import qtawesome as qta
12
+ from PySide6.QtWidgets import (
13
+ QDialog, QVBoxLayout, QHBoxLayout, QFormLayout,
14
+ QComboBox, QCheckBox, QLineEdit, QPushButton,
15
+ QGroupBox, QFileDialog, QLabel, QDialogButtonBox
16
+ )
17
+ from PySide6.QtCore import Qt, Signal, QThread
18
+
19
+ from ankigammon.settings import Settings
20
+ from ankigammon.renderer.color_schemes import list_schemes
21
+
22
+
23
+ class GnuBGValidationWorker(QThread):
24
+ """Worker thread for validating GnuBG executable without blocking UI."""
25
+
26
+ # Signals to communicate with main thread
27
+ validation_complete = Signal(str, str) # (status_text, status_type)
28
+
29
+ def __init__(self, gnubg_path: str):
30
+ super().__init__()
31
+ self.gnubg_path = gnubg_path
32
+
33
+ def run(self):
34
+ """Run validation in background thread."""
35
+ path_obj = Path(self.gnubg_path)
36
+
37
+ # Check if file exists
38
+ if not path_obj.exists():
39
+ self.validation_complete.emit("File not found", "error")
40
+ return
41
+
42
+ if not path_obj.is_file():
43
+ self.validation_complete.emit("Not a file", "error")
44
+ return
45
+
46
+ # Create a simple command file (same approach as gnubg_analyzer)
47
+ command_file = None
48
+ try:
49
+ # Create temp command file
50
+ fd, command_file = tempfile.mkstemp(suffix=".txt", prefix="gnubg_test_")
51
+ try:
52
+ with os.fdopen(fd, 'w') as f:
53
+ # Simple command that should work on any gnubg
54
+ f.write("quit\n")
55
+ except:
56
+ os.close(fd)
57
+ raise
58
+
59
+ # Try to run gnubg with -t (text mode) and -c (command file)
60
+ # Suppress console window on Windows; allow extra time for neural network loading
61
+ kwargs = {
62
+ 'capture_output': True,
63
+ 'text': True,
64
+ 'timeout': 15
65
+ }
66
+ if sys.platform == 'win32':
67
+ kwargs['creationflags'] = subprocess.CREATE_NO_WINDOW
68
+
69
+ result = subprocess.run(
70
+ [str(self.gnubg_path), "-t", "-c", command_file],
71
+ **kwargs
72
+ )
73
+
74
+ # Check if it's actually GNU Backgammon
75
+ output = result.stdout + result.stderr
76
+ if "GNU Backgammon" in output or result.returncode == 0:
77
+ # Check for GUI version and recommend CLI version on Windows
78
+ exe_name = path_obj.stem.lower()
79
+ if sys.platform == 'win32' and "cli" not in exe_name and exe_name == "gnubg":
80
+ self.validation_complete.emit(
81
+ "GUI version detected (use gnubg-cli.exe)",
82
+ "warning"
83
+ )
84
+ else:
85
+ self.validation_complete.emit(
86
+ "Valid GnuBG executable",
87
+ "valid"
88
+ )
89
+ else:
90
+ self.validation_complete.emit("Not GNU Backgammon", "warning")
91
+
92
+ except subprocess.TimeoutExpired:
93
+ self.validation_complete.emit("Validation timeout", "warning")
94
+ except Exception as e:
95
+ self.validation_complete.emit(
96
+ f"Cannot execute: {type(e).__name__}",
97
+ "warning"
98
+ )
99
+ finally:
100
+ # Clean up temp file
101
+ if command_file:
102
+ try:
103
+ os.unlink(command_file)
104
+ except OSError:
105
+ pass
106
+
107
+
108
+ class SettingsDialog(QDialog):
109
+ """
110
+ Dialog for configuring application settings.
111
+
112
+ Signals:
113
+ settings_changed(Settings): Emitted when user saves changes
114
+ """
115
+
116
+ settings_changed = Signal(Settings)
117
+
118
+ def __init__(self, settings: Settings, parent: Optional[QDialog] = None):
119
+ super().__init__(parent)
120
+ self.settings = settings
121
+ self.original_settings = Settings()
122
+ self.original_settings.color_scheme = settings.color_scheme
123
+ self.original_settings.deck_name = settings.deck_name
124
+ self.original_settings.show_options = settings.show_options
125
+ self.original_settings.interactive_moves = settings.interactive_moves
126
+ self.original_settings.export_method = settings.export_method
127
+ self.original_settings.board_orientation = settings.board_orientation
128
+ self.original_settings.gnubg_path = settings.gnubg_path
129
+ self.original_settings.gnubg_analysis_ply = settings.gnubg_analysis_ply
130
+ self.original_settings.generate_score_matrix = settings.generate_score_matrix
131
+ self.original_settings.max_mcq_options = settings.max_mcq_options
132
+
133
+ # Validation worker
134
+ self.validation_worker: Optional[GnuBGValidationWorker] = None
135
+
136
+ self.setWindowTitle("Settings")
137
+ self.setModal(True)
138
+ self.setMinimumWidth(500)
139
+
140
+ self._setup_ui()
141
+ self._load_settings()
142
+
143
+ def _setup_ui(self):
144
+ """Initialize the user interface."""
145
+ layout = QVBoxLayout(self)
146
+
147
+ # Anki settings group
148
+ anki_group = self._create_anki_group()
149
+ layout.addWidget(anki_group)
150
+
151
+ # Card settings group
152
+ card_group = self._create_card_group()
153
+ layout.addWidget(card_group)
154
+
155
+ # GnuBG settings group
156
+ gnubg_group = self._create_gnubg_group()
157
+ layout.addWidget(gnubg_group)
158
+
159
+ # Dialog buttons
160
+ button_box = QDialogButtonBox(
161
+ QDialogButtonBox.Ok | QDialogButtonBox.Cancel
162
+ )
163
+ button_box.accepted.connect(self.accept)
164
+ button_box.rejected.connect(self.reject)
165
+
166
+ # Add cursor pointers to OK and Cancel buttons
167
+ for button in button_box.buttons():
168
+ button.setCursor(Qt.PointingHandCursor)
169
+
170
+ layout.addWidget(button_box)
171
+
172
+ def _create_anki_group(self) -> QGroupBox:
173
+ """Create Anki settings group."""
174
+ group = QGroupBox("Anki Export")
175
+ form = QFormLayout(group)
176
+
177
+ # Deck name
178
+ self.txt_deck_name = QLineEdit()
179
+ form.addRow("Default Deck Name:", self.txt_deck_name)
180
+
181
+ # Export method
182
+ self.cmb_export_method = QComboBox()
183
+ self.cmb_export_method.addItems(["AnkiConnect", "APKG File"])
184
+ self.cmb_export_method.setCursor(Qt.PointingHandCursor)
185
+ form.addRow("Default Export Method:", self.cmb_export_method)
186
+
187
+ return group
188
+
189
+ def _create_card_group(self) -> QGroupBox:
190
+ """Create card settings group."""
191
+ group = QGroupBox("Card Appearance")
192
+ form = QFormLayout(group)
193
+
194
+ # Board theme
195
+ self.cmb_color_scheme = QComboBox()
196
+ self.cmb_color_scheme.addItems(list_schemes())
197
+ self.cmb_color_scheme.setCursor(Qt.PointingHandCursor)
198
+ form.addRow("Board Theme:", self.cmb_color_scheme)
199
+
200
+ # Board orientation
201
+ self.cmb_board_orientation = QComboBox()
202
+ self.cmb_board_orientation.addItem("Counter-clockwise", "counter-clockwise")
203
+ self.cmb_board_orientation.addItem("Clockwise", "clockwise")
204
+ self.cmb_board_orientation.setCursor(Qt.PointingHandCursor)
205
+ form.addRow("Board Orientation:", self.cmb_board_orientation)
206
+
207
+ # Show options with max options dropdown on same line
208
+ show_options_layout = QHBoxLayout()
209
+ self.chk_show_options = QCheckBox("Show multiple choice options on card front")
210
+ self.chk_show_options.setCursor(Qt.PointingHandCursor)
211
+ show_options_layout.addWidget(self.chk_show_options)
212
+
213
+ # Push max options to the right
214
+ show_options_layout.addStretch()
215
+
216
+ # Max options dropdown (on same line, right-aligned)
217
+ self.lbl_max_options = QLabel("Max Options:")
218
+ show_options_layout.addWidget(self.lbl_max_options)
219
+
220
+ self.cmb_max_mcq_options = QComboBox()
221
+ self.cmb_max_mcq_options.addItems([str(i) for i in range(2, 11)])
222
+ self.cmb_max_mcq_options.setCursor(Qt.PointingHandCursor)
223
+ self.cmb_max_mcq_options.setMaximumWidth(80)
224
+ show_options_layout.addWidget(self.cmb_max_mcq_options)
225
+
226
+ form.addRow(show_options_layout)
227
+
228
+ # Connect checkbox to enable/disable dropdown
229
+ self.chk_show_options.toggled.connect(self._on_show_options_toggled)
230
+
231
+ # Interactive moves
232
+ self.chk_interactive_moves = QCheckBox("Enable interactive move visualization")
233
+ self.chk_interactive_moves.setCursor(Qt.PointingHandCursor)
234
+ form.addRow(self.chk_interactive_moves)
235
+
236
+ return group
237
+
238
+ def _create_gnubg_group(self) -> QGroupBox:
239
+ """Create GnuBG settings group."""
240
+ group = QGroupBox("GnuBG Integration (Optional)")
241
+ form = QFormLayout(group)
242
+
243
+ # GnuBG path
244
+ path_layout = QHBoxLayout()
245
+ self.txt_gnubg_path = QLineEdit()
246
+ btn_browse = QPushButton("Browse...")
247
+ btn_browse.setCursor(Qt.PointingHandCursor)
248
+ btn_browse.clicked.connect(self._browse_gnubg)
249
+ path_layout.addWidget(self.txt_gnubg_path)
250
+ path_layout.addWidget(btn_browse)
251
+ form.addRow("GnuBG CLI Path:", path_layout)
252
+
253
+ # Analysis depth
254
+ self.cmb_gnubg_ply = QComboBox()
255
+ self.cmb_gnubg_ply.addItems(["0", "1", "2", "3", "4"])
256
+ self.cmb_gnubg_ply.setCursor(Qt.PointingHandCursor)
257
+ form.addRow("Analysis Depth (ply):", self.cmb_gnubg_ply)
258
+
259
+ # Score matrix generation
260
+ matrix_layout = QHBoxLayout()
261
+ self.chk_generate_score_matrix = QCheckBox("Generate score matrix for cube decisions")
262
+ self.chk_generate_score_matrix.setCursor(Qt.PointingHandCursor)
263
+ matrix_layout.addWidget(self.chk_generate_score_matrix)
264
+ matrix_warning = QLabel("(time-consuming)")
265
+ matrix_warning.setStyleSheet("font-size: 11px; color: #a6adc8; margin-left: 8px;")
266
+ matrix_layout.addWidget(matrix_warning)
267
+ matrix_layout.addStretch()
268
+ form.addRow(matrix_layout)
269
+
270
+ # Status display (icon + text in horizontal layout)
271
+ status_layout = QHBoxLayout()
272
+ self.lbl_gnubg_status_icon = QLabel()
273
+ self.lbl_gnubg_status_text = QLabel()
274
+ status_layout.addWidget(self.lbl_gnubg_status_icon)
275
+ status_layout.addWidget(self.lbl_gnubg_status_text)
276
+ status_layout.addStretch()
277
+ form.addRow("Status:", status_layout)
278
+
279
+ return group
280
+
281
+ def _load_settings(self):
282
+ """Load current settings into widgets."""
283
+ self.txt_deck_name.setText(self.settings.deck_name)
284
+
285
+ # Export method
286
+ method_index = 0 if self.settings.export_method == "ankiconnect" else 1
287
+ self.cmb_export_method.setCurrentIndex(method_index)
288
+
289
+ # Color scheme
290
+ scheme_index = list_schemes().index(self.settings.color_scheme)
291
+ self.cmb_color_scheme.setCurrentIndex(scheme_index)
292
+
293
+ # Board orientation
294
+ orientation_index = 0 if self.settings.board_orientation == "counter-clockwise" else 1
295
+ self.cmb_board_orientation.setCurrentIndex(orientation_index)
296
+
297
+ self.chk_show_options.setChecked(self.settings.show_options)
298
+ self.chk_interactive_moves.setChecked(self.settings.interactive_moves)
299
+
300
+ # Max MCQ options dropdown (index is value minus 2)
301
+ self.cmb_max_mcq_options.setCurrentIndex(self.settings.max_mcq_options - 2)
302
+
303
+ # Initialize max options enabled state based on show options checkbox
304
+ self._on_show_options_toggled(self.settings.show_options)
305
+
306
+ # GnuBG
307
+ if self.settings.gnubg_path:
308
+ self.txt_gnubg_path.setText(self.settings.gnubg_path)
309
+ self.cmb_gnubg_ply.setCurrentIndex(self.settings.gnubg_analysis_ply)
310
+ self.chk_generate_score_matrix.setChecked(self.settings.generate_score_matrix)
311
+ self._update_gnubg_status()
312
+
313
+ def _browse_gnubg(self):
314
+ """Browse for GnuBG executable."""
315
+ # Platform-specific file filter
316
+ if sys.platform == 'win32':
317
+ file_filter = "Executables (*.exe);;All Files (*)"
318
+ else:
319
+ file_filter = "All Files (*)"
320
+
321
+ file_path, _ = QFileDialog.getOpenFileName(
322
+ self,
323
+ "Select GnuBG Executable",
324
+ "",
325
+ file_filter
326
+ )
327
+ if file_path:
328
+ self.txt_gnubg_path.setText(file_path)
329
+ self._update_gnubg_status()
330
+
331
+ def _update_gnubg_status(self):
332
+ """Update GnuBG status label asynchronously."""
333
+ # Cancel any running validation
334
+ if self.validation_worker and self.validation_worker.isRunning():
335
+ self.validation_worker.quit()
336
+ self.validation_worker.wait()
337
+
338
+ path = self.txt_gnubg_path.text()
339
+ if not path:
340
+ self.lbl_gnubg_status_icon.setPixmap(qta.icon('fa6s.circle', color='#6c7086').pixmap(18, 18))
341
+ self.lbl_gnubg_status_text.setText("Not configured")
342
+ self.lbl_gnubg_status_text.setStyleSheet("")
343
+ return
344
+
345
+ # Show loading state
346
+ self.lbl_gnubg_status_icon.setPixmap(qta.icon('fa6s.spinner', color='#6c7086').pixmap(18, 18))
347
+ self.lbl_gnubg_status_text.setText("Validating...")
348
+ self.lbl_gnubg_status_text.setStyleSheet("color: gray;")
349
+
350
+ # Start validation in background thread
351
+ self.validation_worker = GnuBGValidationWorker(path)
352
+ self.validation_worker.validation_complete.connect(self._on_validation_complete)
353
+ self.validation_worker.start()
354
+
355
+ def _on_validation_complete(self, status_text: str, status_type: str):
356
+ """Handle validation completion."""
357
+ # Determine icon based on status type
358
+ if status_type == "valid":
359
+ icon = qta.icon('fa6s.circle-check', color='#a6e3a1')
360
+ elif status_type == "warning":
361
+ icon = qta.icon('fa6s.triangle-exclamation', color='#fab387')
362
+ elif status_type == "error":
363
+ icon = qta.icon('fa6s.circle-xmark', color='#f38ba8')
364
+ else:
365
+ icon = None
366
+
367
+ # Set icon and text separately
368
+ if icon:
369
+ self.lbl_gnubg_status_icon.setPixmap(icon.pixmap(18, 18))
370
+ self.lbl_gnubg_status_text.setText(status_text)
371
+ self.lbl_gnubg_status_text.setStyleSheet("")
372
+
373
+ def _on_show_options_toggled(self, checked: bool):
374
+ """Enable/disable max options dropdown based on show options checkbox."""
375
+ self.lbl_max_options.setEnabled(checked)
376
+ self.cmb_max_mcq_options.setEnabled(checked)
377
+
378
+ # Add visual feedback for disabled state
379
+ if checked:
380
+ self.lbl_max_options.setStyleSheet("")
381
+ else:
382
+ self.lbl_max_options.setStyleSheet("color: #6c7086;")
383
+
384
+ def accept(self):
385
+ """Save settings and close dialog."""
386
+ # Update settings object
387
+ self.settings.deck_name = self.txt_deck_name.text()
388
+ self.settings.export_method = (
389
+ "ankiconnect" if self.cmb_export_method.currentIndex() == 0 else "apkg"
390
+ )
391
+ self.settings.color_scheme = self.cmb_color_scheme.currentText()
392
+ self.settings.board_orientation = self.cmb_board_orientation.currentData()
393
+ self.settings.show_options = self.chk_show_options.isChecked()
394
+ self.settings.interactive_moves = self.chk_interactive_moves.isChecked()
395
+ self.settings.max_mcq_options = self.cmb_max_mcq_options.currentIndex() + 2
396
+ self.settings.gnubg_path = self.txt_gnubg_path.text() or None
397
+ self.settings.gnubg_analysis_ply = self.cmb_gnubg_ply.currentIndex()
398
+ self.settings.generate_score_matrix = self.chk_generate_score_matrix.isChecked()
399
+
400
+ # Emit signal
401
+ self.settings_changed.emit(self.settings)
402
+
403
+ super().accept()
404
+
405
+ def reject(self):
406
+ """Restore original settings and close dialog."""
407
+ # Clean up validation worker
408
+ if self.validation_worker and self.validation_worker.isRunning():
409
+ self.validation_worker.quit()
410
+ self.validation_worker.wait()
411
+ # Don't modify settings object
412
+ super().reject()
413
+
414
+ def closeEvent(self, event):
415
+ """Clean up when dialog is closed."""
416
+ # Clean up validation worker
417
+ if self.validation_worker and self.validation_worker.isRunning():
418
+ self.validation_worker.quit()
419
+ self.validation_worker.wait()
420
+ super().closeEvent(event)