pysfi 0.1.7__py3-none-any.whl → 0.1.11__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.
- {pysfi-0.1.7.dist-info → pysfi-0.1.11.dist-info}/METADATA +11 -9
- pysfi-0.1.11.dist-info/RECORD +60 -0
- pysfi-0.1.11.dist-info/entry_points.txt +28 -0
- sfi/__init__.py +1 -1
- sfi/alarmclock/alarmclock.py +40 -40
- sfi/bumpversion/__init__.py +1 -1
- sfi/cleanbuild/cleanbuild.py +155 -0
- sfi/condasetup/condasetup.py +116 -0
- sfi/docscan/__init__.py +1 -1
- sfi/docscan/docscan.py +407 -103
- sfi/docscan/docscan_gui.py +1282 -596
- sfi/docscan/lang/eng.py +152 -0
- sfi/docscan/lang/zhcn.py +170 -0
- sfi/filedate/filedate.py +185 -112
- sfi/gittool/__init__.py +2 -0
- sfi/gittool/gittool.py +401 -0
- sfi/llmclient/llmclient.py +592 -0
- sfi/llmquantize/llmquantize.py +480 -0
- sfi/llmserver/llmserver.py +335 -0
- sfi/makepython/makepython.py +31 -30
- sfi/pdfsplit/pdfsplit.py +173 -173
- sfi/pyarchive/pyarchive.py +418 -0
- sfi/pyembedinstall/pyembedinstall.py +629 -0
- sfi/pylibpack/__init__.py +0 -0
- sfi/pylibpack/pylibpack.py +1457 -0
- sfi/pylibpack/rules/numpy.json +22 -0
- sfi/pylibpack/rules/pymupdf.json +10 -0
- sfi/pylibpack/rules/pyqt5.json +19 -0
- sfi/pylibpack/rules/pyside2.json +23 -0
- sfi/pylibpack/rules/scipy.json +23 -0
- sfi/pylibpack/rules/shiboken2.json +24 -0
- sfi/pyloadergen/pyloadergen.py +512 -227
- sfi/pypack/__init__.py +0 -0
- sfi/pypack/pypack.py +1142 -0
- sfi/pyprojectparse/__init__.py +0 -0
- sfi/pyprojectparse/pyprojectparse.py +500 -0
- sfi/pysourcepack/pysourcepack.py +308 -0
- sfi/quizbase/__init__.py +0 -0
- sfi/quizbase/quizbase.py +828 -0
- sfi/quizbase/quizbase_gui.py +987 -0
- sfi/regexvalidate/__init__.py +0 -0
- sfi/regexvalidate/regex_help.html +284 -0
- sfi/regexvalidate/regexvalidate.py +468 -0
- sfi/taskkill/taskkill.py +0 -2
- sfi/workflowengine/__init__.py +0 -0
- sfi/workflowengine/workflowengine.py +444 -0
- pysfi-0.1.7.dist-info/RECORD +0 -31
- pysfi-0.1.7.dist-info/entry_points.txt +0 -15
- sfi/embedinstall/embedinstall.py +0 -418
- sfi/projectparse/projectparse.py +0 -152
- sfi/pypacker/fspacker.py +0 -91
- {pysfi-0.1.7.dist-info → pysfi-0.1.11.dist-info}/WHEEL +0 -0
- /sfi/{embedinstall → docscan/lang}/__init__.py +0 -0
- /sfi/{projectparse → llmquantize}/__init__.py +0 -0
- /sfi/{pypacker → pyembedinstall}/__init__.py +0 -0
|
@@ -0,0 +1,987 @@
|
|
|
1
|
+
"""PySide2 GUI version of quizbase application."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, ClassVar
|
|
12
|
+
|
|
13
|
+
import PySide2
|
|
14
|
+
from PySide2.QtCore import Qt
|
|
15
|
+
from PySide2.QtWidgets import (
|
|
16
|
+
QAbstractItemView,
|
|
17
|
+
QApplication,
|
|
18
|
+
QButtonGroup,
|
|
19
|
+
QCheckBox,
|
|
20
|
+
QComboBox,
|
|
21
|
+
QDialog,
|
|
22
|
+
QDialogButtonBox,
|
|
23
|
+
QFileDialog,
|
|
24
|
+
QFormLayout,
|
|
25
|
+
QGroupBox,
|
|
26
|
+
QHBoxLayout,
|
|
27
|
+
QLabel,
|
|
28
|
+
QLineEdit,
|
|
29
|
+
QListWidget,
|
|
30
|
+
QMainWindow,
|
|
31
|
+
QMessageBox,
|
|
32
|
+
QPushButton,
|
|
33
|
+
QRadioButton,
|
|
34
|
+
QScrollArea,
|
|
35
|
+
QTabWidget,
|
|
36
|
+
QTextEdit,
|
|
37
|
+
QVBoxLayout,
|
|
38
|
+
QWidget,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
from sfi.quizbase.quizbase import (
|
|
43
|
+
AdaptiveQuizSession,
|
|
44
|
+
EssayQuestion,
|
|
45
|
+
FillBlankQuestion,
|
|
46
|
+
MultipleChoiceQuestion,
|
|
47
|
+
Question,
|
|
48
|
+
QuizResult,
|
|
49
|
+
QuizSession,
|
|
50
|
+
TrueFalseQuestion,
|
|
51
|
+
create_sample_quiz_data,
|
|
52
|
+
)
|
|
53
|
+
except ImportError:
|
|
54
|
+
try:
|
|
55
|
+
from quizbase import (
|
|
56
|
+
AdaptiveQuizSession,
|
|
57
|
+
EssayQuestion,
|
|
58
|
+
FillBlankQuestion,
|
|
59
|
+
MultipleChoiceQuestion,
|
|
60
|
+
Question,
|
|
61
|
+
QuizResult,
|
|
62
|
+
QuizSession,
|
|
63
|
+
TrueFalseQuestion,
|
|
64
|
+
create_sample_quiz_data,
|
|
65
|
+
)
|
|
66
|
+
except ImportError:
|
|
67
|
+
from sfi.quizbase.quizbase import (
|
|
68
|
+
AdaptiveQuizSession,
|
|
69
|
+
EssayQuestion,
|
|
70
|
+
FillBlankQuestion,
|
|
71
|
+
MultipleChoiceQuestion,
|
|
72
|
+
Question,
|
|
73
|
+
QuizResult,
|
|
74
|
+
QuizSession,
|
|
75
|
+
TrueFalseQuestion,
|
|
76
|
+
create_sample_quiz_data,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
|
80
|
+
logger = logging.getLogger(__name__)
|
|
81
|
+
|
|
82
|
+
# Set Qt platform plugin path for Windows
|
|
83
|
+
qt_dir = Path(PySide2.__file__).parent
|
|
84
|
+
plugin_path = str(qt_dir / "plugins" / "platforms")
|
|
85
|
+
os.environ["QT_QPA_PLATFORM_PLUGIN_PATH"] = plugin_path
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
_WRONG_ANSWERS_FILE = Path.home() / ".sfi" / "wrong_answers.json"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class QuizConfig:
|
|
93
|
+
"""Quiz GUI configuration."""
|
|
94
|
+
|
|
95
|
+
window_width: int = 1000
|
|
96
|
+
window_height: int = 700
|
|
97
|
+
window_x: int = 100
|
|
98
|
+
window_y: int = 100
|
|
99
|
+
random_order: bool = False
|
|
100
|
+
wrong_answers_file: str = str(_WRONG_ANSWERS_FILE)
|
|
101
|
+
recent_files: list[str] | None = None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class ConfigManager:
|
|
105
|
+
"""Manage GUI configuration persistence."""
|
|
106
|
+
|
|
107
|
+
DEFAULT_CONFIG: ClassVar[dict[str, Any]] = {
|
|
108
|
+
"window_width": 1000,
|
|
109
|
+
"window_height": 700,
|
|
110
|
+
"window_x": 100,
|
|
111
|
+
"window_y": 100,
|
|
112
|
+
"random_order": False,
|
|
113
|
+
"wrong_answers_file": str(_WRONG_ANSWERS_FILE),
|
|
114
|
+
"recent_files": [],
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
MAX_RECENT_ITEMS = 10
|
|
118
|
+
|
|
119
|
+
def __init__(self, config_file: Path | None = None):
|
|
120
|
+
"""Initialize configuration manager."""
|
|
121
|
+
if config_file is None:
|
|
122
|
+
config_dir = Path.home() / ".sfi"
|
|
123
|
+
config_dir.mkdir(exist_ok=True)
|
|
124
|
+
config_file = config_dir / "quizbase_gui.json"
|
|
125
|
+
self.config_file = config_file
|
|
126
|
+
self.config = self._load_config()
|
|
127
|
+
|
|
128
|
+
def _load_config(self) -> dict[str, Any]:
|
|
129
|
+
"""Load configuration from file."""
|
|
130
|
+
if self.config_file.exists():
|
|
131
|
+
try:
|
|
132
|
+
with open(self.config_file, encoding="utf-8") as f:
|
|
133
|
+
config = json.load(f)
|
|
134
|
+
return {**self.DEFAULT_CONFIG, **config}
|
|
135
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
136
|
+
logger.warning(f"Failed to load config: {e}. Using defaults.")
|
|
137
|
+
return self.DEFAULT_CONFIG.copy()
|
|
138
|
+
|
|
139
|
+
def save_config(self) -> None:
|
|
140
|
+
"""Save configuration to file."""
|
|
141
|
+
try:
|
|
142
|
+
with open(self.config_file, "w", encoding="utf-8") as f:
|
|
143
|
+
json.dump(self.config, f, indent=2, ensure_ascii=False)
|
|
144
|
+
except OSError as e:
|
|
145
|
+
logger.warning(f"Failed to save config: {e}")
|
|
146
|
+
|
|
147
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
148
|
+
"""Get configuration value."""
|
|
149
|
+
return self.config.get(key, default)
|
|
150
|
+
|
|
151
|
+
def set(self, key: str, value: Any) -> None:
|
|
152
|
+
"""Set configuration value."""
|
|
153
|
+
self.config[key] = value
|
|
154
|
+
|
|
155
|
+
def add_recent_file(self, file_path: str) -> None:
|
|
156
|
+
"""Add file to recent files list."""
|
|
157
|
+
recent_files = self.config.get("recent_files", [])
|
|
158
|
+
recent_files = [f for f in recent_files if f != file_path]
|
|
159
|
+
recent_files.insert(0, file_path)
|
|
160
|
+
recent_files = recent_files[: self.MAX_RECENT_ITEMS]
|
|
161
|
+
self.config["recent_files"] = recent_files
|
|
162
|
+
|
|
163
|
+
def get_config(self) -> QuizConfig:
|
|
164
|
+
"""Get configuration as QuizConfig dataclass."""
|
|
165
|
+
return QuizConfig(
|
|
166
|
+
window_width=self.get("window_width", 1000),
|
|
167
|
+
window_height=self.get("window_height", 700),
|
|
168
|
+
window_x=self.get("window_x", 100),
|
|
169
|
+
window_y=self.get("window_y", 100),
|
|
170
|
+
random_order=self.get("random_order", False),
|
|
171
|
+
wrong_answers_file=self.get("wrong_answers_file", str(_WRONG_ANSWERS_FILE)),
|
|
172
|
+
recent_files=self.get("recent_files", []),
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class QuestionAnswerWidget(QWidget):
|
|
177
|
+
"""Widget for answering questions based on question type."""
|
|
178
|
+
|
|
179
|
+
def __init__(self, parent: QWidget | None = None):
|
|
180
|
+
"""Initialize question answer widget."""
|
|
181
|
+
super().__init__(parent)
|
|
182
|
+
self.question: Question | None = None
|
|
183
|
+
self.layout = QVBoxLayout(self)
|
|
184
|
+
self.current_answer_widget: QWidget | None = None
|
|
185
|
+
|
|
186
|
+
def set_question(self, question: Question) -> None:
|
|
187
|
+
"""Set question and create appropriate answer widget."""
|
|
188
|
+
self.question = question
|
|
189
|
+
|
|
190
|
+
# Clear existing widget
|
|
191
|
+
if self.current_answer_widget:
|
|
192
|
+
self.layout.removeWidget(self.current_answer_widget) # type: ignore
|
|
193
|
+
self.current_answer_widget.deleteLater()
|
|
194
|
+
self.current_answer_widget = None
|
|
195
|
+
|
|
196
|
+
# Clean up old attributes that belong to the previous question type
|
|
197
|
+
if hasattr(self, "radio_buttons"):
|
|
198
|
+
delattr(self, "radio_buttons")
|
|
199
|
+
if hasattr(self, "checkboxes"):
|
|
200
|
+
delattr(self, "checkboxes")
|
|
201
|
+
if hasattr(self, "text_input"):
|
|
202
|
+
delattr(self, "text_input")
|
|
203
|
+
if hasattr(self, "text_edit"):
|
|
204
|
+
delattr(self, "text_edit")
|
|
205
|
+
if hasattr(self, "true_radio"):
|
|
206
|
+
delattr(self, "true_radio")
|
|
207
|
+
if hasattr(self, "false_radio"):
|
|
208
|
+
delattr(self, "false_radio")
|
|
209
|
+
if hasattr(self, "radio_button_group"):
|
|
210
|
+
delattr(self, "radio_button_group")
|
|
211
|
+
|
|
212
|
+
# Create appropriate answer widget based on question type
|
|
213
|
+
if isinstance(question, MultipleChoiceQuestion):
|
|
214
|
+
widget = self._create_multiple_choice_widget(question)
|
|
215
|
+
elif isinstance(question, FillBlankQuestion):
|
|
216
|
+
widget = self._create_fill_blank_widget(question)
|
|
217
|
+
elif isinstance(question, TrueFalseQuestion):
|
|
218
|
+
widget = self._create_true_false_widget(question)
|
|
219
|
+
elif isinstance(question, EssayQuestion):
|
|
220
|
+
widget = self._create_essay_widget(question)
|
|
221
|
+
else:
|
|
222
|
+
widget = QLabel("Unknown question type")
|
|
223
|
+
|
|
224
|
+
self.current_answer_widget = widget
|
|
225
|
+
self.layout.addWidget(widget) # type: ignore
|
|
226
|
+
|
|
227
|
+
def _create_multiple_choice_widget(
|
|
228
|
+
self, question: MultipleChoiceQuestion
|
|
229
|
+
) -> QWidget:
|
|
230
|
+
"""Create widget for multiple choice questions."""
|
|
231
|
+
widget = QWidget()
|
|
232
|
+
layout = QVBoxLayout(widget)
|
|
233
|
+
|
|
234
|
+
if question.allow_multiple:
|
|
235
|
+
# Checkboxes for multiple selections
|
|
236
|
+
self.checkboxes: list[QCheckBox] = []
|
|
237
|
+
for i, option in enumerate(question.options):
|
|
238
|
+
checkbox = QCheckBox(option)
|
|
239
|
+
checkbox.setProperty("option_index", i)
|
|
240
|
+
self.checkboxes.append(checkbox)
|
|
241
|
+
layout.addWidget(checkbox)
|
|
242
|
+
else:
|
|
243
|
+
# Radio buttons for single selection
|
|
244
|
+
self.radio_buttons: list[QRadioButton] = []
|
|
245
|
+
# Create a button group for radio buttons to ensure mutual exclusivity
|
|
246
|
+
self.radio_button_group = QButtonGroup(self)
|
|
247
|
+
for i, option in enumerate(question.options):
|
|
248
|
+
radio = QRadioButton(option)
|
|
249
|
+
radio.setProperty("option_index", i)
|
|
250
|
+
self.radio_buttons.append(radio)
|
|
251
|
+
self.radio_button_group.addButton(
|
|
252
|
+
radio, i
|
|
253
|
+
) # Add to button group with ID
|
|
254
|
+
layout.addWidget(radio)
|
|
255
|
+
|
|
256
|
+
return widget
|
|
257
|
+
|
|
258
|
+
def _create_fill_blank_widget(self, question: FillBlankQuestion) -> QWidget:
|
|
259
|
+
"""Create widget for fill in the blank questions."""
|
|
260
|
+
widget = QWidget()
|
|
261
|
+
layout = QHBoxLayout(widget)
|
|
262
|
+
|
|
263
|
+
label = QLabel("Answer:")
|
|
264
|
+
self.text_input = QLineEdit()
|
|
265
|
+
self.text_input.setPlaceholderText("Enter your answer here...")
|
|
266
|
+
|
|
267
|
+
layout.addWidget(label)
|
|
268
|
+
layout.addWidget(self.text_input)
|
|
269
|
+
|
|
270
|
+
return widget
|
|
271
|
+
|
|
272
|
+
def _create_true_false_widget(self, question: TrueFalseQuestion) -> QWidget:
|
|
273
|
+
"""Create widget for true/false questions."""
|
|
274
|
+
widget = QWidget()
|
|
275
|
+
layout = QHBoxLayout(widget)
|
|
276
|
+
|
|
277
|
+
self.true_radio = QRadioButton("True")
|
|
278
|
+
self.false_radio = QRadioButton("False")
|
|
279
|
+
self.true_radio.setProperty("answer", True)
|
|
280
|
+
self.false_radio.setProperty("answer", False)
|
|
281
|
+
|
|
282
|
+
layout.addWidget(self.true_radio)
|
|
283
|
+
layout.addWidget(self.false_radio)
|
|
284
|
+
|
|
285
|
+
return widget
|
|
286
|
+
|
|
287
|
+
def _create_essay_widget(self, question: EssayQuestion) -> QWidget:
|
|
288
|
+
"""Create widget for essay questions."""
|
|
289
|
+
widget = QWidget()
|
|
290
|
+
layout = QVBoxLayout(widget)
|
|
291
|
+
|
|
292
|
+
label = QLabel("Your Answer:")
|
|
293
|
+
label.setStyleSheet("font-weight: bold; margin-top: 10px;")
|
|
294
|
+
|
|
295
|
+
self.text_edit = QTextEdit()
|
|
296
|
+
self.text_edit.setPlaceholderText("Type your answer here...")
|
|
297
|
+
self.text_edit.setMinimumHeight(200)
|
|
298
|
+
|
|
299
|
+
if question.keywords:
|
|
300
|
+
hint = QLabel(f"Key points to cover: {', '.join(question.keywords)}")
|
|
301
|
+
hint.setStyleSheet("color: gray; font-style: italic; margin-bottom: 5px;")
|
|
302
|
+
layout.addWidget(hint)
|
|
303
|
+
|
|
304
|
+
layout.addWidget(label)
|
|
305
|
+
layout.addWidget(self.text_edit)
|
|
306
|
+
|
|
307
|
+
return widget
|
|
308
|
+
|
|
309
|
+
def get_answer(self) -> Any:
|
|
310
|
+
"""Get user's answer."""
|
|
311
|
+
if isinstance(self.question, MultipleChoiceQuestion):
|
|
312
|
+
if self.question.allow_multiple:
|
|
313
|
+
return [
|
|
314
|
+
cb.property("option_index")
|
|
315
|
+
for cb in getattr(self, "checkboxes", [])
|
|
316
|
+
if cb.isChecked()
|
|
317
|
+
]
|
|
318
|
+
else:
|
|
319
|
+
for radio in getattr(self, "radio_buttons", []):
|
|
320
|
+
if radio.isChecked():
|
|
321
|
+
return radio.property("option_index")
|
|
322
|
+
return None
|
|
323
|
+
elif isinstance(self.question, FillBlankQuestion):
|
|
324
|
+
return getattr(self, "text_input", QLineEdit()).text()
|
|
325
|
+
elif isinstance(self.question, TrueFalseQuestion):
|
|
326
|
+
if getattr(self, "true_radio", None) and self.true_radio.isChecked():
|
|
327
|
+
return True
|
|
328
|
+
elif getattr(self, "false_radio", None) and self.false_radio.isChecked():
|
|
329
|
+
return False
|
|
330
|
+
return None
|
|
331
|
+
elif isinstance(self.question, EssayQuestion):
|
|
332
|
+
return getattr(self, "text_edit", QTextEdit()).toPlainText()
|
|
333
|
+
return None
|
|
334
|
+
|
|
335
|
+
def clear_answer(self) -> None:
|
|
336
|
+
"""Clear user's answer."""
|
|
337
|
+
if isinstance(self.question, MultipleChoiceQuestion):
|
|
338
|
+
if self.question.allow_multiple:
|
|
339
|
+
for cb in getattr(self, "checkboxes", []):
|
|
340
|
+
cb.setChecked(False)
|
|
341
|
+
else:
|
|
342
|
+
# Get radio buttons directly from self attribute
|
|
343
|
+
radio_buttons = getattr(self, "radio_buttons", None)
|
|
344
|
+
# Also try to access the button group if it exists
|
|
345
|
+
button_group = getattr(self, "radio_button_group", None)
|
|
346
|
+
|
|
347
|
+
if button_group:
|
|
348
|
+
# Temporarily disable the exclusive property of the button group
|
|
349
|
+
button_group.setExclusive(False)
|
|
350
|
+
|
|
351
|
+
if radio_buttons:
|
|
352
|
+
# For radio buttons, we need to temporarily block signals
|
|
353
|
+
# to clear all selections properly
|
|
354
|
+
for radio in radio_buttons:
|
|
355
|
+
radio.blockSignals(True)
|
|
356
|
+
radio.setChecked(False)
|
|
357
|
+
radio.blockSignals(False)
|
|
358
|
+
|
|
359
|
+
if button_group:
|
|
360
|
+
# Re-enable exclusive property after clearing
|
|
361
|
+
button_group.setExclusive(True)
|
|
362
|
+
|
|
363
|
+
# Force processing of events to ensure UI updates
|
|
364
|
+
from PySide2.QtWidgets import QApplication
|
|
365
|
+
|
|
366
|
+
app = QApplication.instance()
|
|
367
|
+
if app:
|
|
368
|
+
app.processEvents()
|
|
369
|
+
elif isinstance(self.question, FillBlankQuestion):
|
|
370
|
+
getattr(self, "text_input", QLineEdit()).clear()
|
|
371
|
+
elif isinstance(self.question, TrueFalseQuestion):
|
|
372
|
+
if getattr(self, "true_radio", None):
|
|
373
|
+
self.true_radio.blockSignals(True)
|
|
374
|
+
self.true_radio.setChecked(False)
|
|
375
|
+
self.true_radio.blockSignals(False)
|
|
376
|
+
if getattr(self, "false_radio", None):
|
|
377
|
+
self.false_radio.blockSignals(True)
|
|
378
|
+
self.false_radio.setChecked(False)
|
|
379
|
+
self.false_radio.blockSignals(False)
|
|
380
|
+
# Force processing of events to ensure UI updates
|
|
381
|
+
from PySide2.QtWidgets import QApplication
|
|
382
|
+
|
|
383
|
+
app = QApplication.instance()
|
|
384
|
+
if app:
|
|
385
|
+
app.processEvents()
|
|
386
|
+
elif isinstance(self.question, EssayQuestion):
|
|
387
|
+
getattr(self, "text_edit", QTextEdit()).clear()
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
class WrongAnswersDialog(QDialog):
|
|
391
|
+
"""Dialog to display wrong answers."""
|
|
392
|
+
|
|
393
|
+
def __init__(self, results: list[QuizResult], parent: QWidget | None = None):
|
|
394
|
+
"""Initialize wrong answers dialog."""
|
|
395
|
+
super().__init__(parent)
|
|
396
|
+
self.results = results
|
|
397
|
+
self.setWindowTitle("Wrong Answers Review")
|
|
398
|
+
self.setMinimumSize(800, 600)
|
|
399
|
+
self._setup_ui()
|
|
400
|
+
|
|
401
|
+
def _setup_ui(self) -> None:
|
|
402
|
+
"""Setup dialog UI."""
|
|
403
|
+
layout = QVBoxLayout(self)
|
|
404
|
+
|
|
405
|
+
# Scroll area for results
|
|
406
|
+
scroll = QScrollArea()
|
|
407
|
+
scroll.setWidgetResizable(True)
|
|
408
|
+
|
|
409
|
+
content_widget = QWidget()
|
|
410
|
+
content_layout = QVBoxLayout(content_widget)
|
|
411
|
+
|
|
412
|
+
for i, result in enumerate(self.results, 1):
|
|
413
|
+
result_group = QGroupBox(f"Question {i}")
|
|
414
|
+
result_layout = QVBoxLayout()
|
|
415
|
+
|
|
416
|
+
# Question
|
|
417
|
+
question_label = QLabel(f"<b>Question:</b> {result.question.question_text}")
|
|
418
|
+
question_label.setWordWrap(True)
|
|
419
|
+
result_layout.addWidget(question_label)
|
|
420
|
+
|
|
421
|
+
# User answer
|
|
422
|
+
user_answer_label = QLabel(f"<b>Your Answer:</b> {result.user_answer}")
|
|
423
|
+
user_answer_label.setWordWrap(True)
|
|
424
|
+
result_layout.addWidget(user_answer_label)
|
|
425
|
+
|
|
426
|
+
# Explanation
|
|
427
|
+
explanation_label = QLabel(f"<b>Explanation:</b> {result.explanation}")
|
|
428
|
+
explanation_label.setWordWrap(True)
|
|
429
|
+
explanation_label.setStyleSheet("color: red;")
|
|
430
|
+
result_layout.addWidget(explanation_label)
|
|
431
|
+
|
|
432
|
+
result_group.setLayout(result_layout)
|
|
433
|
+
content_layout.addWidget(result_group)
|
|
434
|
+
|
|
435
|
+
scroll.setWidget(content_widget)
|
|
436
|
+
layout.addWidget(scroll)
|
|
437
|
+
|
|
438
|
+
# Close button
|
|
439
|
+
button_box = QDialogButtonBox(QDialogButtonBox.Close)
|
|
440
|
+
button_box.rejected.connect(self.accept)
|
|
441
|
+
layout.addWidget(button_box)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
class QuizBaseMainWindow(QMainWindow):
|
|
445
|
+
"""Main window for QuizBase GUI application."""
|
|
446
|
+
|
|
447
|
+
def __init__(self):
|
|
448
|
+
"""Initialize main window."""
|
|
449
|
+
super().__init__()
|
|
450
|
+
self.config_manager = ConfigManager()
|
|
451
|
+
self.config = self.config_manager.get_config()
|
|
452
|
+
self.session: QuizSession | None = None
|
|
453
|
+
self.current_result: QuizResult | None = None
|
|
454
|
+
self._setup_ui()
|
|
455
|
+
self._load_settings()
|
|
456
|
+
|
|
457
|
+
def _setup_ui(self) -> None:
|
|
458
|
+
"""Setup user interface."""
|
|
459
|
+
self.setWindowTitle("QuizBase - Universal Quiz System")
|
|
460
|
+
self.setMinimumSize(900, 700)
|
|
461
|
+
|
|
462
|
+
# Central widget
|
|
463
|
+
central_widget = QWidget()
|
|
464
|
+
self.setCentralWidget(central_widget)
|
|
465
|
+
|
|
466
|
+
# Main layout
|
|
467
|
+
main_layout = QVBoxLayout(central_widget)
|
|
468
|
+
|
|
469
|
+
# Top toolbar
|
|
470
|
+
toolbar_layout = QHBoxLayout()
|
|
471
|
+
|
|
472
|
+
# Open file button
|
|
473
|
+
self.open_button = QPushButton("Open Quiz File")
|
|
474
|
+
self.open_button.clicked.connect(self._open_quiz_file)
|
|
475
|
+
toolbar_layout.addWidget(self.open_button)
|
|
476
|
+
|
|
477
|
+
# Recent files dropdown
|
|
478
|
+
self.recent_combo = QComboBox()
|
|
479
|
+
self.recent_combo.setMinimumWidth(300)
|
|
480
|
+
self.recent_combo.currentIndexChanged.connect(self._load_recent_file)
|
|
481
|
+
toolbar_layout.addWidget(self.recent_combo)
|
|
482
|
+
|
|
483
|
+
# Random order checkbox
|
|
484
|
+
self.random_checkbox = QCheckBox("Random Order")
|
|
485
|
+
self.random_checkbox.setChecked(self.config.random_order)
|
|
486
|
+
self.random_checkbox.stateChanged.connect(self._on_random_changed)
|
|
487
|
+
toolbar_layout.addWidget(self.random_checkbox)
|
|
488
|
+
|
|
489
|
+
# Adaptive mode checkbox
|
|
490
|
+
self.adaptive_checkbox = QCheckBox("Adaptive Mode")
|
|
491
|
+
self.adaptive_checkbox.setChecked(False)
|
|
492
|
+
self.adaptive_checkbox.stateChanged.connect(self._on_adaptive_changed)
|
|
493
|
+
toolbar_layout.addWidget(self.adaptive_checkbox)
|
|
494
|
+
|
|
495
|
+
toolbar_layout.addStretch()
|
|
496
|
+
|
|
497
|
+
# Create sample button
|
|
498
|
+
self.create_sample_button = QPushButton("Create Sample")
|
|
499
|
+
self.create_sample_button.clicked.connect(self._create_sample_quiz)
|
|
500
|
+
toolbar_layout.addWidget(self.create_sample_button)
|
|
501
|
+
|
|
502
|
+
main_layout.addLayout(toolbar_layout)
|
|
503
|
+
|
|
504
|
+
# Tab widget
|
|
505
|
+
self.tab_widget = QTabWidget()
|
|
506
|
+
main_layout.addWidget(self.tab_widget)
|
|
507
|
+
|
|
508
|
+
# Quiz tab
|
|
509
|
+
self.quiz_tab = self._create_quiz_tab()
|
|
510
|
+
self.tab_widget.addTab(self.quiz_tab, "Quiz")
|
|
511
|
+
|
|
512
|
+
# Summary tab
|
|
513
|
+
self.summary_tab = self._create_summary_tab()
|
|
514
|
+
self.tab_widget.addTab(self.summary_tab, "Summary")
|
|
515
|
+
|
|
516
|
+
# Wrong answers tab
|
|
517
|
+
self.wrong_tab = self._create_wrong_answers_tab()
|
|
518
|
+
self.tab_widget.addTab(self.wrong_tab, "Wrong Answers")
|
|
519
|
+
|
|
520
|
+
# Status bar
|
|
521
|
+
self.status_label = QLabel("No quiz loaded")
|
|
522
|
+
self.statusBar().addWidget(self.status_label)
|
|
523
|
+
|
|
524
|
+
# Initially disable tabs
|
|
525
|
+
self._set_quiz_enabled(False)
|
|
526
|
+
|
|
527
|
+
def _create_quiz_tab(self) -> QWidget:
|
|
528
|
+
"""Create quiz tab."""
|
|
529
|
+
widget = QWidget()
|
|
530
|
+
layout = QVBoxLayout(widget)
|
|
531
|
+
|
|
532
|
+
# Progress info
|
|
533
|
+
progress_layout = QHBoxLayout()
|
|
534
|
+
self.progress_label = QLabel("Progress: - / -")
|
|
535
|
+
progress_layout.addWidget(self.progress_label)
|
|
536
|
+
progress_layout.addStretch()
|
|
537
|
+
|
|
538
|
+
# Score info
|
|
539
|
+
self.score_label = QLabel("Score: 0 / 0")
|
|
540
|
+
progress_layout.addWidget(self.score_label)
|
|
541
|
+
|
|
542
|
+
layout.addLayout(progress_layout)
|
|
543
|
+
|
|
544
|
+
# Question info
|
|
545
|
+
info_group = QGroupBox("Question Information")
|
|
546
|
+
info_layout = QFormLayout()
|
|
547
|
+
|
|
548
|
+
self.question_type_label = QLabel("-")
|
|
549
|
+
self.question_points_label = QLabel("-")
|
|
550
|
+
|
|
551
|
+
info_layout.addRow("Type:", self.question_type_label)
|
|
552
|
+
info_layout.addRow("Points:", self.question_points_label)
|
|
553
|
+
info_group.setLayout(info_layout)
|
|
554
|
+
layout.addWidget(info_group)
|
|
555
|
+
|
|
556
|
+
# Question text
|
|
557
|
+
question_group = QGroupBox("Question")
|
|
558
|
+
question_layout = QVBoxLayout()
|
|
559
|
+
self.question_text_label = QLabel("No question loaded")
|
|
560
|
+
self.question_text_label.setWordWrap(True)
|
|
561
|
+
self.question_text_label.setStyleSheet(
|
|
562
|
+
"font-size: 14px; font-weight: bold; margin: 10px;"
|
|
563
|
+
)
|
|
564
|
+
question_layout.addWidget(self.question_text_label)
|
|
565
|
+
question_group.setLayout(question_layout)
|
|
566
|
+
layout.addWidget(question_group)
|
|
567
|
+
|
|
568
|
+
# Answer area
|
|
569
|
+
answer_group = QGroupBox("Your Answer")
|
|
570
|
+
answer_layout = QVBoxLayout()
|
|
571
|
+
self.answer_widget = QuestionAnswerWidget()
|
|
572
|
+
answer_layout.addWidget(self.answer_widget)
|
|
573
|
+
answer_group.setLayout(answer_layout)
|
|
574
|
+
layout.addWidget(answer_group)
|
|
575
|
+
|
|
576
|
+
# Result area
|
|
577
|
+
self.result_label = QLabel()
|
|
578
|
+
self.result_label.setWordWrap(True)
|
|
579
|
+
self.result_label.setStyleSheet("padding: 10px; margin: 10px;")
|
|
580
|
+
layout.addWidget(self.result_label)
|
|
581
|
+
|
|
582
|
+
# Buttons
|
|
583
|
+
button_layout = QHBoxLayout()
|
|
584
|
+
|
|
585
|
+
self.submit_button = QPushButton("Submit Answer")
|
|
586
|
+
self.submit_button.setEnabled(False)
|
|
587
|
+
self.submit_button.clicked.connect(self._submit_answer)
|
|
588
|
+
button_layout.addWidget(self.submit_button)
|
|
589
|
+
|
|
590
|
+
self.next_button = QPushButton("Next Question")
|
|
591
|
+
self.next_button.setEnabled(False)
|
|
592
|
+
self.next_button.clicked.connect(self._next_question)
|
|
593
|
+
button_layout.addWidget(self.next_button)
|
|
594
|
+
|
|
595
|
+
self.finish_button = QPushButton("Finish Quiz")
|
|
596
|
+
self.finish_button.setEnabled(False)
|
|
597
|
+
self.finish_button.clicked.connect(self._finish_quiz)
|
|
598
|
+
button_layout.addWidget(self.finish_button)
|
|
599
|
+
|
|
600
|
+
self.reset_button = QPushButton("Reset Quiz")
|
|
601
|
+
self.reset_button.setEnabled(False)
|
|
602
|
+
self.reset_button.clicked.connect(self._reset_quiz)
|
|
603
|
+
button_layout.addWidget(self.reset_button)
|
|
604
|
+
|
|
605
|
+
layout.addLayout(button_layout)
|
|
606
|
+
|
|
607
|
+
return widget
|
|
608
|
+
|
|
609
|
+
def _create_summary_tab(self) -> QWidget:
|
|
610
|
+
"""Create summary tab."""
|
|
611
|
+
widget = QWidget()
|
|
612
|
+
layout = QVBoxLayout(widget)
|
|
613
|
+
|
|
614
|
+
# Summary text
|
|
615
|
+
self.summary_text = QTextEdit()
|
|
616
|
+
self.summary_text.setReadOnly(True)
|
|
617
|
+
self.summary_text.setPlainText("Quiz summary will appear here...")
|
|
618
|
+
layout.addWidget(self.summary_text)
|
|
619
|
+
|
|
620
|
+
return widget
|
|
621
|
+
|
|
622
|
+
def _create_wrong_answers_tab(self) -> QWidget:
|
|
623
|
+
"""Create wrong answers tab."""
|
|
624
|
+
widget = QWidget()
|
|
625
|
+
layout = QVBoxLayout(widget)
|
|
626
|
+
|
|
627
|
+
# Instructions
|
|
628
|
+
instruction = QLabel(
|
|
629
|
+
"Wrong answers will be displayed here after quiz completion."
|
|
630
|
+
)
|
|
631
|
+
layout.addWidget(instruction)
|
|
632
|
+
|
|
633
|
+
# Wrong answers list
|
|
634
|
+
self.wrong_answers_list = QListWidget()
|
|
635
|
+
self.wrong_answers_list.setSelectionMode(QAbstractItemView.SingleSelection)
|
|
636
|
+
layout.addWidget(self.wrong_answers_list)
|
|
637
|
+
|
|
638
|
+
# View detail button
|
|
639
|
+
self.view_detail_button = QPushButton("View Details")
|
|
640
|
+
self.view_detail_button.setEnabled(False)
|
|
641
|
+
self.view_detail_button.clicked.connect(self._view_wrong_answer_detail)
|
|
642
|
+
layout.addWidget(self.view_detail_button)
|
|
643
|
+
|
|
644
|
+
return widget
|
|
645
|
+
|
|
646
|
+
def _set_quiz_enabled(self, enabled: bool) -> bool:
|
|
647
|
+
"""Enable or disable quiz-related controls."""
|
|
648
|
+
self.submit_button.setEnabled(enabled)
|
|
649
|
+
self.next_button.setEnabled(enabled)
|
|
650
|
+
self.finish_button.setEnabled(enabled)
|
|
651
|
+
self.reset_button.setEnabled(enabled)
|
|
652
|
+
return enabled
|
|
653
|
+
|
|
654
|
+
def _load_settings(self) -> None:
|
|
655
|
+
"""Load window settings."""
|
|
656
|
+
self.resize(self.config.window_width, self.config.window_height)
|
|
657
|
+
self.move(self.config.window_x, self.config.window_y)
|
|
658
|
+
self._update_recent_files()
|
|
659
|
+
|
|
660
|
+
def _save_settings(self) -> None:
|
|
661
|
+
"""Save window settings."""
|
|
662
|
+
self.config_manager.set("window_width", self.width())
|
|
663
|
+
self.config_manager.set("window_height", self.height())
|
|
664
|
+
self.config_manager.set("window_x", self.x())
|
|
665
|
+
self.config_manager.set("window_y", self.y())
|
|
666
|
+
self.config_manager.set("random_order", self.random_checkbox.isChecked())
|
|
667
|
+
self.config_manager.save_config()
|
|
668
|
+
|
|
669
|
+
def _update_recent_files(self) -> None:
|
|
670
|
+
"""Update recent files dropdown."""
|
|
671
|
+
self.recent_combo.clear()
|
|
672
|
+
self.recent_combo.addItem("-- Recent Files --", None)
|
|
673
|
+
for file_path in self.config.recent_files or []:
|
|
674
|
+
self.recent_combo.addItem(Path(file_path).name, file_path)
|
|
675
|
+
|
|
676
|
+
def _open_quiz_file(self) -> None:
|
|
677
|
+
"""Open quiz file dialog."""
|
|
678
|
+
file_path, _ = QFileDialog.getOpenFileName(
|
|
679
|
+
self, "Open Quiz File", "", "JSON Files (*.json);;All Files (*)"
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
if file_path:
|
|
683
|
+
self._load_quiz(file_path)
|
|
684
|
+
|
|
685
|
+
def _load_recent_file(self, index: int) -> None:
|
|
686
|
+
"""Load recently used file."""
|
|
687
|
+
if index > 0: # Skip placeholder
|
|
688
|
+
file_path = self.recent_combo.currentData()
|
|
689
|
+
if file_path:
|
|
690
|
+
self._load_quiz(file_path)
|
|
691
|
+
# Reset selection
|
|
692
|
+
self.recent_combo.setCurrentIndex(0)
|
|
693
|
+
|
|
694
|
+
def _load_quiz(self, file_path: str) -> None:
|
|
695
|
+
"""Load quiz from file."""
|
|
696
|
+
try:
|
|
697
|
+
# Check if adaptive mode is enabled
|
|
698
|
+
if self.adaptive_checkbox.isChecked():
|
|
699
|
+
self.session = AdaptiveQuizSession(
|
|
700
|
+
random_order=self.random_checkbox.isChecked()
|
|
701
|
+
)
|
|
702
|
+
# Load performance history if available
|
|
703
|
+
self.session.load_performance_history()
|
|
704
|
+
else:
|
|
705
|
+
self.session = QuizSession(
|
|
706
|
+
random_order=self.random_checkbox.isChecked()
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
self.session.load_from_json(file_path)
|
|
710
|
+
|
|
711
|
+
self.config_manager.add_recent_file(file_path)
|
|
712
|
+
self._update_recent_files()
|
|
713
|
+
|
|
714
|
+
self._set_quiz_enabled(True)
|
|
715
|
+
self.status_label.setText(f"Quiz loaded: {Path(file_path).name}")
|
|
716
|
+
self.tab_widget.setCurrentIndex(0)
|
|
717
|
+
|
|
718
|
+
self._load_current_question()
|
|
719
|
+
self._update_summary()
|
|
720
|
+
|
|
721
|
+
except Exception as e:
|
|
722
|
+
QMessageBox.critical(self, "Error", f"Failed to load quiz: {e}")
|
|
723
|
+
|
|
724
|
+
def _create_sample_quiz(self) -> None:
|
|
725
|
+
"""Create sample quiz data."""
|
|
726
|
+
file_path, _ = QFileDialog.getSaveFileName(
|
|
727
|
+
self, "Save Sample Quiz", "sample_quiz.json", "JSON Files (*.json)"
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
if file_path:
|
|
731
|
+
try:
|
|
732
|
+
create_sample_quiz_data(file_path)
|
|
733
|
+
self._load_quiz(file_path)
|
|
734
|
+
QMessageBox.information(
|
|
735
|
+
self, "Success", "Sample quiz created successfully!"
|
|
736
|
+
)
|
|
737
|
+
except Exception as e:
|
|
738
|
+
QMessageBox.critical(
|
|
739
|
+
self, "Error", f"Failed to create sample quiz: {e}"
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
def _on_random_changed(self, state: int) -> None:
|
|
743
|
+
"""Handle random order checkbox change."""
|
|
744
|
+
if self.session and not self.session.results:
|
|
745
|
+
# Only reset if no answers yet
|
|
746
|
+
self.session.random_order = state == Qt.Checked
|
|
747
|
+
self.session.reset()
|
|
748
|
+
|
|
749
|
+
def _on_adaptive_changed(self, state: int) -> None:
|
|
750
|
+
"""Handle adaptive mode checkbox change."""
|
|
751
|
+
if (
|
|
752
|
+
self.session
|
|
753
|
+
and hasattr(self.session, "adaptive_mode")
|
|
754
|
+
and not self.session.results
|
|
755
|
+
):
|
|
756
|
+
# Only reset if no answers yet
|
|
757
|
+
self.session.adaptive_mode = state == Qt.Checked
|
|
758
|
+
self.session.reset()
|
|
759
|
+
|
|
760
|
+
def _load_current_question(self) -> None:
|
|
761
|
+
"""Load current question into UI."""
|
|
762
|
+
if not self.session:
|
|
763
|
+
return
|
|
764
|
+
|
|
765
|
+
question = self.session.get_current_question()
|
|
766
|
+
|
|
767
|
+
if question is None:
|
|
768
|
+
self._finish_quiz()
|
|
769
|
+
return
|
|
770
|
+
|
|
771
|
+
# Update question info
|
|
772
|
+
self.question_type_label.setText(question.question_type.value)
|
|
773
|
+
self.question_points_label.setText(str(question.points))
|
|
774
|
+
self.question_text_label.setText(question.question_text)
|
|
775
|
+
|
|
776
|
+
# Load answer widget
|
|
777
|
+
self.answer_widget.set_question(question)
|
|
778
|
+
|
|
779
|
+
# Update progress
|
|
780
|
+
self._update_progress()
|
|
781
|
+
|
|
782
|
+
# Clear result
|
|
783
|
+
self.result_label.clear()
|
|
784
|
+
self.result_label.setStyleSheet("padding: 10px; margin: 10px;")
|
|
785
|
+
|
|
786
|
+
# Enable submit, disable next
|
|
787
|
+
self.submit_button.setEnabled(True)
|
|
788
|
+
self.next_button.setEnabled(False)
|
|
789
|
+
|
|
790
|
+
def _update_progress(self) -> None:
|
|
791
|
+
"""Update progress display."""
|
|
792
|
+
if not self.session:
|
|
793
|
+
return
|
|
794
|
+
|
|
795
|
+
summary = self.session.get_summary()
|
|
796
|
+
self.progress_label.setText(
|
|
797
|
+
f"Progress: {summary['answered']} / {summary['total_questions']}"
|
|
798
|
+
)
|
|
799
|
+
self.score_label.setText(
|
|
800
|
+
f"Score: {summary['earned_points']:.1f} / {summary['total_points']:.1f}"
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
def _submit_answer(self) -> None:
|
|
804
|
+
"""Submit answer for current question."""
|
|
805
|
+
if not self.session:
|
|
806
|
+
return
|
|
807
|
+
|
|
808
|
+
answer = self.answer_widget.get_answer()
|
|
809
|
+
|
|
810
|
+
if answer is None:
|
|
811
|
+
QMessageBox.warning(self, "Warning", "Please provide an answer.")
|
|
812
|
+
return
|
|
813
|
+
|
|
814
|
+
# Submit to session
|
|
815
|
+
self.current_result = self.session.submit_answer(answer)
|
|
816
|
+
|
|
817
|
+
if self.current_result:
|
|
818
|
+
# Display result
|
|
819
|
+
if self.current_result.is_correct:
|
|
820
|
+
self.result_label.setText(
|
|
821
|
+
f"✓ Correct!\n{self.current_result.explanation}"
|
|
822
|
+
)
|
|
823
|
+
self.result_label.setStyleSheet(
|
|
824
|
+
"background-color: #d4edda; color: #155724; padding: 10px; margin: 10px; border-radius: 5px;"
|
|
825
|
+
)
|
|
826
|
+
else:
|
|
827
|
+
self.result_label.setText(
|
|
828
|
+
f"✗ Incorrect!\n{self.current_result.explanation}"
|
|
829
|
+
)
|
|
830
|
+
self.result_label.setStyleSheet(
|
|
831
|
+
"background-color: #f8d7da; color: #721c24; padding: 10px; margin: 10px; border-radius: 5px;"
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
# Update progress
|
|
835
|
+
self._update_progress()
|
|
836
|
+
self._update_summary()
|
|
837
|
+
|
|
838
|
+
# Disable submit, enable next
|
|
839
|
+
self.submit_button.setEnabled(False)
|
|
840
|
+
self.answer_widget.clear_answer()
|
|
841
|
+
|
|
842
|
+
if not self.session.is_finished():
|
|
843
|
+
# Automatically advance to next question after a brief delay
|
|
844
|
+
self._load_current_question() # Auto-advance to next question
|
|
845
|
+
else:
|
|
846
|
+
self.finish_button.setEnabled(True)
|
|
847
|
+
self.next_button.setEnabled(False)
|
|
848
|
+
|
|
849
|
+
def _next_question(self) -> None:
|
|
850
|
+
"""Load next question."""
|
|
851
|
+
self._load_current_question()
|
|
852
|
+
|
|
853
|
+
def _finish_quiz(self) -> None:
|
|
854
|
+
"""Finish quiz and show summary."""
|
|
855
|
+
if not self.session:
|
|
856
|
+
return
|
|
857
|
+
|
|
858
|
+
# Save wrong answers
|
|
859
|
+
wrong_answers_file = self.config_manager.get(
|
|
860
|
+
"wrong_answers_file", str(_WRONG_ANSWERS_FILE)
|
|
861
|
+
)
|
|
862
|
+
self.session.wrong_answer_file = wrong_answers_file
|
|
863
|
+
self.session.save_wrong_answers()
|
|
864
|
+
|
|
865
|
+
# Update summary tab
|
|
866
|
+
self._update_summary()
|
|
867
|
+
self._update_wrong_answers()
|
|
868
|
+
|
|
869
|
+
# Switch to summary tab
|
|
870
|
+
self.tab_widget.setCurrentIndex(1)
|
|
871
|
+
|
|
872
|
+
# Show completion message
|
|
873
|
+
summary = self.session.get_summary()
|
|
874
|
+
message = (
|
|
875
|
+
f"Quiz Completed!\n\n"
|
|
876
|
+
f"Total Questions: {summary['total_questions']}\n"
|
|
877
|
+
f"Correct: {summary['correct']}\n"
|
|
878
|
+
f"Wrong: {summary['wrong']}\n"
|
|
879
|
+
f"Score: {summary['earned_points']:.1f} / {summary['total_points']:.1f}\n"
|
|
880
|
+
f"Accuracy: {summary['accuracy']:.1f}%"
|
|
881
|
+
)
|
|
882
|
+
QMessageBox.information(self, "Quiz Completed", message)
|
|
883
|
+
|
|
884
|
+
self.status_label.setText("Quiz completed")
|
|
885
|
+
|
|
886
|
+
def _update_summary(self) -> None:
|
|
887
|
+
"""Update summary tab."""
|
|
888
|
+
if not self.session:
|
|
889
|
+
return
|
|
890
|
+
|
|
891
|
+
summary = self.session.get_summary()
|
|
892
|
+
|
|
893
|
+
summary_text = (
|
|
894
|
+
f"Quiz Summary\n"
|
|
895
|
+
f"{'=' * 50}\n\n"
|
|
896
|
+
f"Total Questions: {summary['total_questions']}\n"
|
|
897
|
+
f"Answered: {summary['answered']}\n"
|
|
898
|
+
f"Correct: {summary['correct']}\n"
|
|
899
|
+
f"Wrong: {summary['wrong']}\n\n"
|
|
900
|
+
f"Points Earned: {summary['earned_points']:.1f} / {summary['total_points']:.1f}\n"
|
|
901
|
+
f"Accuracy: {summary['accuracy']:.1f}%\n"
|
|
902
|
+
f"Status: {'Completed' if summary['is_finished'] else 'In Progress'}\n"
|
|
903
|
+
)
|
|
904
|
+
|
|
905
|
+
# Add question-by-question breakdown
|
|
906
|
+
if self.session.results:
|
|
907
|
+
summary_text += f"\n{'=' * 50}\n\nQuestion Breakdown:\n\n"
|
|
908
|
+
for i, result in enumerate(self.session.results, 1):
|
|
909
|
+
status = "✓" if result.is_correct else "✗"
|
|
910
|
+
summary_text += f"{i}. {status} ({result.question.points} pts)\n"
|
|
911
|
+
|
|
912
|
+
self.summary_text.setPlainText(summary_text)
|
|
913
|
+
|
|
914
|
+
def _update_wrong_answers(self) -> None:
|
|
915
|
+
"""Update wrong answers tab."""
|
|
916
|
+
if not self.session:
|
|
917
|
+
return
|
|
918
|
+
|
|
919
|
+
self.wrong_answers_list.clear()
|
|
920
|
+
|
|
921
|
+
wrong_results = self.session.get_wrong_answers()
|
|
922
|
+
|
|
923
|
+
if not wrong_results:
|
|
924
|
+
self.wrong_answers_list.addItem("No wrong answers! Great job!")
|
|
925
|
+
self.view_detail_button.setEnabled(False)
|
|
926
|
+
return
|
|
927
|
+
|
|
928
|
+
for i, result in enumerate(wrong_results, 1):
|
|
929
|
+
self.wrong_answers_list.addItem(
|
|
930
|
+
f"{i}. {result.question.question_text[:50]}..."
|
|
931
|
+
)
|
|
932
|
+
|
|
933
|
+
self.wrong_answers = wrong_results
|
|
934
|
+
self.view_detail_button.setEnabled(True)
|
|
935
|
+
|
|
936
|
+
def _view_wrong_answer_detail(self) -> None:
|
|
937
|
+
"""View detail of selected wrong answer."""
|
|
938
|
+
if not hasattr(self, "wrong_answers") or not self.wrong_answers:
|
|
939
|
+
return
|
|
940
|
+
|
|
941
|
+
selected = self.wrong_answers_list.currentRow()
|
|
942
|
+
if selected < 0 or selected >= len(self.wrong_answers):
|
|
943
|
+
return
|
|
944
|
+
|
|
945
|
+
result = self.wrong_answers[selected]
|
|
946
|
+
dialog = WrongAnswersDialog([result], self)
|
|
947
|
+
dialog.exec_()
|
|
948
|
+
|
|
949
|
+
def _reset_quiz(self) -> None:
|
|
950
|
+
"""Reset quiz to beginning."""
|
|
951
|
+
if not self.session:
|
|
952
|
+
return
|
|
953
|
+
|
|
954
|
+
reply = QMessageBox.question(
|
|
955
|
+
self,
|
|
956
|
+
"Reset Quiz",
|
|
957
|
+
"Are you sure you want to reset this quiz? All progress will be lost.",
|
|
958
|
+
QMessageBox.Yes | QMessageBox.No,
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
if reply == QMessageBox.Yes:
|
|
962
|
+
self.session.reset(random_order=self.random_checkbox.isChecked())
|
|
963
|
+
self.current_result = None
|
|
964
|
+
self._load_current_question()
|
|
965
|
+
self._update_summary()
|
|
966
|
+
self.tab_widget.setCurrentIndex(0)
|
|
967
|
+
self.status_label.setText("Quiz reset")
|
|
968
|
+
|
|
969
|
+
def closeEvent(self, event) -> None: # noqa: N802
|
|
970
|
+
"""Handle window close event."""
|
|
971
|
+
self._save_settings()
|
|
972
|
+
event.accept()
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
def main():
|
|
976
|
+
"""Main entry point for GUI application."""
|
|
977
|
+
app = QApplication(sys.argv)
|
|
978
|
+
app.setStyle("Fusion")
|
|
979
|
+
|
|
980
|
+
window = QuizBaseMainWindow()
|
|
981
|
+
window.show()
|
|
982
|
+
|
|
983
|
+
sys.exit(app.exec_())
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
if __name__ == "__main__":
|
|
987
|
+
main()
|