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.
- ankigammon/__init__.py +7 -0
- ankigammon/__main__.py +6 -0
- ankigammon/analysis/__init__.py +13 -0
- ankigammon/analysis/score_matrix.py +391 -0
- ankigammon/anki/__init__.py +6 -0
- ankigammon/anki/ankiconnect.py +216 -0
- ankigammon/anki/apkg_exporter.py +111 -0
- ankigammon/anki/card_generator.py +1325 -0
- ankigammon/anki/card_styles.py +1054 -0
- ankigammon/gui/__init__.py +8 -0
- ankigammon/gui/app.py +192 -0
- ankigammon/gui/dialogs/__init__.py +10 -0
- ankigammon/gui/dialogs/export_dialog.py +594 -0
- ankigammon/gui/dialogs/import_options_dialog.py +201 -0
- ankigammon/gui/dialogs/input_dialog.py +762 -0
- ankigammon/gui/dialogs/note_dialog.py +93 -0
- ankigammon/gui/dialogs/settings_dialog.py +420 -0
- ankigammon/gui/dialogs/update_dialog.py +373 -0
- ankigammon/gui/format_detector.py +377 -0
- ankigammon/gui/main_window.py +1611 -0
- ankigammon/gui/resources/down-arrow.svg +3 -0
- ankigammon/gui/resources/icon.icns +0 -0
- ankigammon/gui/resources/icon.ico +0 -0
- ankigammon/gui/resources/icon.png +0 -0
- ankigammon/gui/resources/style.qss +402 -0
- ankigammon/gui/resources.py +26 -0
- ankigammon/gui/update_checker.py +259 -0
- ankigammon/gui/widgets/__init__.py +8 -0
- ankigammon/gui/widgets/position_list.py +166 -0
- ankigammon/gui/widgets/smart_input.py +268 -0
- ankigammon/models.py +356 -0
- ankigammon/parsers/__init__.py +7 -0
- ankigammon/parsers/gnubg_match_parser.py +1094 -0
- ankigammon/parsers/gnubg_parser.py +468 -0
- ankigammon/parsers/sgf_parser.py +290 -0
- ankigammon/parsers/xg_binary_parser.py +1097 -0
- ankigammon/parsers/xg_text_parser.py +688 -0
- ankigammon/renderer/__init__.py +5 -0
- ankigammon/renderer/animation_controller.py +391 -0
- ankigammon/renderer/animation_helper.py +191 -0
- ankigammon/renderer/color_schemes.py +145 -0
- ankigammon/renderer/svg_board_renderer.py +791 -0
- ankigammon/settings.py +315 -0
- ankigammon/thirdparty/__init__.py +7 -0
- ankigammon/thirdparty/xgdatatools/__init__.py +17 -0
- ankigammon/thirdparty/xgdatatools/xgimport.py +160 -0
- ankigammon/thirdparty/xgdatatools/xgstruct.py +1032 -0
- ankigammon/thirdparty/xgdatatools/xgutils.py +118 -0
- ankigammon/thirdparty/xgdatatools/xgzarc.py +260 -0
- ankigammon/utils/__init__.py +13 -0
- ankigammon/utils/gnubg_analyzer.py +590 -0
- ankigammon/utils/gnuid.py +577 -0
- ankigammon/utils/move_parser.py +204 -0
- ankigammon/utils/ogid.py +326 -0
- ankigammon/utils/xgid.py +387 -0
- ankigammon-1.0.6.dist-info/METADATA +352 -0
- ankigammon-1.0.6.dist-info/RECORD +61 -0
- ankigammon-1.0.6.dist-info/WHEEL +5 -0
- ankigammon-1.0.6.dist-info/entry_points.txt +2 -0
- ankigammon-1.0.6.dist-info/licenses/LICENSE +21 -0
- 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)
|