ankigammon 1.0.0__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.
Potentially problematic release.
This version of ankigammon might be problematic. Click here for more details.
- ankigammon/__init__.py +7 -0
- ankigammon/__main__.py +6 -0
- ankigammon/analysis/__init__.py +13 -0
- ankigammon/analysis/score_matrix.py +373 -0
- ankigammon/anki/__init__.py +6 -0
- ankigammon/anki/ankiconnect.py +224 -0
- ankigammon/anki/apkg_exporter.py +123 -0
- ankigammon/anki/card_generator.py +1307 -0
- ankigammon/anki/card_styles.py +1034 -0
- ankigammon/gui/__init__.py +8 -0
- ankigammon/gui/app.py +209 -0
- ankigammon/gui/dialogs/__init__.py +10 -0
- ankigammon/gui/dialogs/export_dialog.py +597 -0
- ankigammon/gui/dialogs/import_options_dialog.py +163 -0
- ankigammon/gui/dialogs/input_dialog.py +776 -0
- ankigammon/gui/dialogs/note_dialog.py +93 -0
- ankigammon/gui/dialogs/settings_dialog.py +384 -0
- ankigammon/gui/format_detector.py +292 -0
- ankigammon/gui/main_window.py +1071 -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 +394 -0
- ankigammon/gui/resources.py +26 -0
- ankigammon/gui/widgets/__init__.py +8 -0
- ankigammon/gui/widgets/position_list.py +193 -0
- ankigammon/gui/widgets/smart_input.py +268 -0
- ankigammon/models.py +322 -0
- ankigammon/parsers/__init__.py +7 -0
- ankigammon/parsers/gnubg_parser.py +454 -0
- ankigammon/parsers/xg_binary_parser.py +870 -0
- ankigammon/parsers/xg_text_parser.py +729 -0
- ankigammon/renderer/__init__.py +5 -0
- ankigammon/renderer/animation_controller.py +406 -0
- ankigammon/renderer/animation_helper.py +221 -0
- ankigammon/renderer/color_schemes.py +145 -0
- ankigammon/renderer/svg_board_renderer.py +824 -0
- ankigammon/settings.py +239 -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 +431 -0
- ankigammon/utils/gnuid.py +622 -0
- ankigammon/utils/move_parser.py +239 -0
- ankigammon/utils/ogid.py +335 -0
- ankigammon/utils/xgid.py +419 -0
- ankigammon-1.0.0.dist-info/METADATA +370 -0
- ankigammon-1.0.0.dist-info/RECORD +56 -0
- ankigammon-1.0.0.dist-info/WHEEL +5 -0
- ankigammon-1.0.0.dist-info/entry_points.txt +2 -0
- ankigammon-1.0.0.dist-info/licenses/LICENSE +21 -0
- ankigammon-1.0.0.dist-info/top_level.txt +1 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/* Modern Dark Theme for AnkiGammon GUI - Inspired by Catppuccin Mocha */
|
|
2
|
+
|
|
3
|
+
/* Main Window and Dialogs */
|
|
4
|
+
QMainWindow, QDialog, QWidget {
|
|
5
|
+
background-color: #1e1e2e;
|
|
6
|
+
color: #cdd6f4;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/* Primary Buttons */
|
|
10
|
+
QPushButton {
|
|
11
|
+
background-color: #89b4fa;
|
|
12
|
+
color: #1e1e2e;
|
|
13
|
+
border: none;
|
|
14
|
+
padding: 10px 18px;
|
|
15
|
+
border-radius: 6px;
|
|
16
|
+
font-weight: 600;
|
|
17
|
+
font-size: 13px;
|
|
18
|
+
min-height: 28px;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
QPushButton:hover {
|
|
22
|
+
background-color: #a0c8fc;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
QPushButton:pressed {
|
|
26
|
+
background-color: #74c7ec;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
QPushButton:disabled {
|
|
30
|
+
background-color: #313244;
|
|
31
|
+
color: #6c7086;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Settings and Secondary Buttons */
|
|
35
|
+
QPushButton#btn_settings {
|
|
36
|
+
background-color: transparent;
|
|
37
|
+
color: #cdd6f4;
|
|
38
|
+
border: 2px solid #45475a;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
QPushButton#btn_settings:hover {
|
|
42
|
+
background-color: rgba(137, 180, 250, 0.12);
|
|
43
|
+
color: #f5e0dc;
|
|
44
|
+
border-color: #89b4fa;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
QPushButton#btn_settings:pressed {
|
|
48
|
+
background-color: rgba(137, 180, 250, 0.18);
|
|
49
|
+
color: #f5e0dc;
|
|
50
|
+
border-color: #89b4fa;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* Position List Widget */
|
|
54
|
+
QListWidget {
|
|
55
|
+
background-color: #181825;
|
|
56
|
+
border: 2px solid #313244;
|
|
57
|
+
border-radius: 8px;
|
|
58
|
+
padding: 6px;
|
|
59
|
+
outline: none;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
QListWidget::item {
|
|
63
|
+
padding: 12px 10px;
|
|
64
|
+
border-radius: 6px;
|
|
65
|
+
margin: 2px 0px;
|
|
66
|
+
color: #cdd6f4;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
QListWidget::item:selected {
|
|
70
|
+
background-color: #89b4fa;
|
|
71
|
+
color: #1e1e2e;
|
|
72
|
+
font-weight: 600;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
QListWidget::item:hover {
|
|
76
|
+
background-color: #313244;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
QListWidget::item:selected:hover {
|
|
80
|
+
background-color: #a0c8fc;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* Group Boxes */
|
|
84
|
+
QGroupBox {
|
|
85
|
+
border: 2px solid #45475a;
|
|
86
|
+
border-radius: 8px;
|
|
87
|
+
margin-top: 16px;
|
|
88
|
+
font-weight: 700;
|
|
89
|
+
padding-top: 16px;
|
|
90
|
+
color: #f5e0dc;
|
|
91
|
+
font-size: 13px;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
QGroupBox::title {
|
|
95
|
+
subcontrol-origin: margin;
|
|
96
|
+
subcontrol-position: top left;
|
|
97
|
+
padding: 4px 12px;
|
|
98
|
+
background-color: #1e1e2e;
|
|
99
|
+
border-radius: 4px;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* Labels */
|
|
103
|
+
QLabel {
|
|
104
|
+
color: #cdd6f4;
|
|
105
|
+
font-size: 13px;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
QLabel#title {
|
|
109
|
+
color: #f5e0dc;
|
|
110
|
+
font-size: 16px;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* Input Fields */
|
|
114
|
+
QLineEdit, QComboBox {
|
|
115
|
+
padding: 8px 12px;
|
|
116
|
+
border: 2px solid #45475a;
|
|
117
|
+
border-radius: 6px;
|
|
118
|
+
background-color: #181825;
|
|
119
|
+
color: #cdd6f4;
|
|
120
|
+
selection-background-color: #89b4fa;
|
|
121
|
+
selection-color: #1e1e2e;
|
|
122
|
+
font-size: 13px;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
QLineEdit:focus, QComboBox:focus {
|
|
126
|
+
border: 2px solid #89b4fa;
|
|
127
|
+
background-color: #1e1e2e;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
QComboBox:hover {
|
|
131
|
+
border-color: #6c7086;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
QComboBox::drop-down {
|
|
135
|
+
border: none;
|
|
136
|
+
padding-right: 8px;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
QComboBox::down-arrow {
|
|
140
|
+
image: none;
|
|
141
|
+
border-left: 4px solid transparent;
|
|
142
|
+
border-right: 4px solid transparent;
|
|
143
|
+
border-top: 6px solid #cdd6f4;
|
|
144
|
+
margin-right: 8px;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
QComboBox QAbstractItemView {
|
|
148
|
+
background-color: #181825;
|
|
149
|
+
border: 2px solid #45475a;
|
|
150
|
+
selection-background-color: #89b4fa;
|
|
151
|
+
selection-color: #1e1e2e;
|
|
152
|
+
color: #cdd6f4;
|
|
153
|
+
padding: 4px;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* Checkboxes */
|
|
157
|
+
QCheckBox {
|
|
158
|
+
spacing: 10px;
|
|
159
|
+
color: #cdd6f4;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
QCheckBox::indicator {
|
|
163
|
+
width: 20px;
|
|
164
|
+
height: 20px;
|
|
165
|
+
border-radius: 4px;
|
|
166
|
+
border: 3px solid #45475a;
|
|
167
|
+
background-color: #181825;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
QCheckBox::indicator:hover {
|
|
171
|
+
border-color: #89b4fa;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
QCheckBox::indicator:checked {
|
|
175
|
+
background-color: #89b4fa;
|
|
176
|
+
border: 3px solid #45475a;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
QCheckBox::indicator:checked:hover {
|
|
180
|
+
background-color: #a0c8fc;
|
|
181
|
+
border-color: #89b4fa;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/* Radio Buttons */
|
|
185
|
+
QRadioButton {
|
|
186
|
+
spacing: 10px;
|
|
187
|
+
color: #cdd6f4;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
QRadioButton::indicator {
|
|
191
|
+
width: 20px;
|
|
192
|
+
height: 20px;
|
|
193
|
+
border-radius: 10px;
|
|
194
|
+
border: 2px solid #45475a;
|
|
195
|
+
background-color: #181825;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
QRadioButton::indicator:hover {
|
|
199
|
+
border-color: #89b4fa;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
QRadioButton::indicator:checked {
|
|
203
|
+
background-color: #ffffff;
|
|
204
|
+
border: 5px solid #89b4fa;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* Progress Bar */
|
|
208
|
+
QProgressBar {
|
|
209
|
+
border: 2px solid #45475a;
|
|
210
|
+
border-radius: 6px;
|
|
211
|
+
text-align: center;
|
|
212
|
+
background-color: #181825;
|
|
213
|
+
color: #ffffff;
|
|
214
|
+
font-weight: 700;
|
|
215
|
+
height: 24px;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
QProgressBar::chunk {
|
|
219
|
+
background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0,
|
|
220
|
+
stop:0 #89b4fa, stop:1 #74c7ec);
|
|
221
|
+
border-radius: 4px;
|
|
222
|
+
margin: 2px;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/* Text Edit */
|
|
226
|
+
QTextEdit {
|
|
227
|
+
background-color: #181825;
|
|
228
|
+
border: 2px solid #45475a;
|
|
229
|
+
border-radius: 6px;
|
|
230
|
+
padding: 10px;
|
|
231
|
+
color: #cdd6f4;
|
|
232
|
+
selection-background-color: #89b4fa;
|
|
233
|
+
selection-color: #1e1e2e;
|
|
234
|
+
font-family: 'Consolas', 'Monaco', monospace;
|
|
235
|
+
font-size: 12px;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/* Status Bar */
|
|
239
|
+
QStatusBar {
|
|
240
|
+
background-color: #181825;
|
|
241
|
+
color: #a6adc8;
|
|
242
|
+
border-top: 1px solid #313244;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
QStatusBar::item {
|
|
246
|
+
border: none;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/* Menu Bar */
|
|
250
|
+
QMenuBar {
|
|
251
|
+
background-color: #181825;
|
|
252
|
+
color: #cdd6f4;
|
|
253
|
+
border-bottom: 2px solid #313244;
|
|
254
|
+
padding: 4px;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
QMenuBar::item {
|
|
258
|
+
padding: 6px 14px;
|
|
259
|
+
border-radius: 4px;
|
|
260
|
+
background-color: transparent;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
QMenuBar::item:selected {
|
|
264
|
+
background-color: #313244;
|
|
265
|
+
color: #f5e0dc;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
QMenuBar::item:pressed {
|
|
269
|
+
background-color: #45475a;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/* Menus */
|
|
273
|
+
QMenu {
|
|
274
|
+
background-color: #181825;
|
|
275
|
+
border: 2px solid #313244;
|
|
276
|
+
border-radius: 8px;
|
|
277
|
+
padding: 6px;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
QMenu::item {
|
|
281
|
+
padding: 8px 32px 8px 16px;
|
|
282
|
+
border-radius: 4px;
|
|
283
|
+
color: #cdd6f4;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
QMenu::item:selected {
|
|
287
|
+
background-color: #89b4fa;
|
|
288
|
+
color: #1e1e2e;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
QMenu::separator {
|
|
292
|
+
height: 2px;
|
|
293
|
+
background-color: #313244;
|
|
294
|
+
margin: 6px 10px;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
QMenu::icon {
|
|
298
|
+
padding-left: 10px;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/* Dialog Button Box */
|
|
302
|
+
QDialogButtonBox QPushButton {
|
|
303
|
+
min-width: 90px;
|
|
304
|
+
padding: 8px 16px;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/* Scrollbars */
|
|
308
|
+
QScrollBar:vertical {
|
|
309
|
+
border: none;
|
|
310
|
+
background-color: #181825;
|
|
311
|
+
width: 12px;
|
|
312
|
+
border-radius: 6px;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
QScrollBar::handle:vertical {
|
|
316
|
+
background-color: #45475a;
|
|
317
|
+
border-radius: 6px;
|
|
318
|
+
min-height: 30px;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
QScrollBar::handle:vertical:hover {
|
|
322
|
+
background-color: #585b70;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
|
|
326
|
+
height: 0px;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
QScrollBar:horizontal {
|
|
330
|
+
border: none;
|
|
331
|
+
background-color: #181825;
|
|
332
|
+
height: 12px;
|
|
333
|
+
border-radius: 6px;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
QScrollBar::handle:horizontal {
|
|
337
|
+
background-color: #45475a;
|
|
338
|
+
border-radius: 6px;
|
|
339
|
+
min-width: 30px;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
QScrollBar::handle:horizontal:hover {
|
|
343
|
+
background-color: #585b70;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {
|
|
347
|
+
width: 0px;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/* Tooltips */
|
|
351
|
+
QToolTip {
|
|
352
|
+
background-color: #313244;
|
|
353
|
+
color: #cdd6f4;
|
|
354
|
+
border: 2px solid #45475a;
|
|
355
|
+
border-radius: 6px;
|
|
356
|
+
padding: 8px;
|
|
357
|
+
font-size: 12px;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/* Message Boxes */
|
|
361
|
+
QMessageBox {
|
|
362
|
+
background-color: #1e1e2e;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
QMessageBox QLabel {
|
|
366
|
+
color: #cdd6f4;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/* Tab Widgets (for future use) */
|
|
370
|
+
QTabWidget::pane {
|
|
371
|
+
border: 2px solid #313244;
|
|
372
|
+
border-radius: 8px;
|
|
373
|
+
background-color: #181825;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
QTabBar::tab {
|
|
377
|
+
background-color: #313244;
|
|
378
|
+
color: #a6adc8;
|
|
379
|
+
padding: 10px 20px;
|
|
380
|
+
border-top-left-radius: 6px;
|
|
381
|
+
border-top-right-radius: 6px;
|
|
382
|
+
margin-right: 2px;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
QTabBar::tab:selected {
|
|
386
|
+
background-color: #89b4fa;
|
|
387
|
+
color: #1e1e2e;
|
|
388
|
+
font-weight: 600;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
QTabBar::tab:hover {
|
|
392
|
+
background-color: #45475a;
|
|
393
|
+
color: #cdd6f4;
|
|
394
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Resource path utilities for GUI.
|
|
3
|
+
"""
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_resource_path(relative_path: str) -> Path:
|
|
9
|
+
"""
|
|
10
|
+
Get absolute path to resource, works for dev and PyInstaller.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
relative_path: Relative path to resource (e.g., "gui/resources/icon.png")
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
Path: Absolute path to resource
|
|
17
|
+
"""
|
|
18
|
+
try:
|
|
19
|
+
# PyInstaller creates a temp folder and stores path in _MEIPASS
|
|
20
|
+
base_path = Path(sys._MEIPASS)
|
|
21
|
+
except AttributeError:
|
|
22
|
+
# Running in normal Python environment
|
|
23
|
+
# __file__ is in ankigammon/gui/resources.py, so parent.parent gets us to repo root
|
|
24
|
+
base_path = Path(__file__).parent.parent.parent
|
|
25
|
+
|
|
26
|
+
return base_path / relative_path
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Widget for displaying list of parsed positions.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
from PySide6.QtWidgets import (
|
|
7
|
+
QListWidget, QListWidgetItem, QWidget, QVBoxLayout, QLabel, QMenu, QMessageBox,
|
|
8
|
+
QDialog, QAbstractItemView
|
|
9
|
+
)
|
|
10
|
+
from PySide6.QtCore import Qt, Signal, Slot
|
|
11
|
+
from PySide6.QtGui import QIcon, QAction, QKeyEvent
|
|
12
|
+
import qtawesome as qta
|
|
13
|
+
|
|
14
|
+
from ankigammon.models import Decision, DecisionType, Player
|
|
15
|
+
from ankigammon.gui.dialogs.note_dialog import NoteEditDialog
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PositionListItem(QListWidgetItem):
|
|
19
|
+
"""Custom list item for a decision/position."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, decision: Decision, index: int):
|
|
22
|
+
super().__init__()
|
|
23
|
+
self.decision = decision
|
|
24
|
+
self.index = index
|
|
25
|
+
|
|
26
|
+
# Set display text
|
|
27
|
+
self.setText(f"#{index + 1}: {decision.get_short_display_text()}")
|
|
28
|
+
|
|
29
|
+
# Set tooltip with metadata and note (if present)
|
|
30
|
+
tooltip = decision.get_metadata_text()
|
|
31
|
+
if decision.note:
|
|
32
|
+
tooltip += f"\n\nNote: {decision.note}"
|
|
33
|
+
self.setToolTip(tooltip)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class PositionListWidget(QListWidget):
|
|
37
|
+
"""
|
|
38
|
+
List widget for displaying parsed positions.
|
|
39
|
+
|
|
40
|
+
Signals:
|
|
41
|
+
position_selected(Decision): Emitted when user selects a position
|
|
42
|
+
positions_deleted(list): Emitted when user deletes position(s) - List[int] of indices
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
position_selected = Signal(Decision)
|
|
46
|
+
positions_deleted = Signal(list)
|
|
47
|
+
|
|
48
|
+
def __init__(self, parent: Optional[QWidget] = None):
|
|
49
|
+
super().__init__(parent)
|
|
50
|
+
self.decisions: List[Decision] = []
|
|
51
|
+
|
|
52
|
+
# Enable smooth scrolling
|
|
53
|
+
self.setVerticalScrollMode(QListWidget.ScrollPerPixel)
|
|
54
|
+
|
|
55
|
+
# Enable multi-selection (Ctrl+Click, Shift+Click)
|
|
56
|
+
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
|
57
|
+
|
|
58
|
+
# Enable context menu for delete
|
|
59
|
+
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
|
60
|
+
self.customContextMenuRequested.connect(self._show_context_menu)
|
|
61
|
+
|
|
62
|
+
# Connect selection signal
|
|
63
|
+
self.currentItemChanged.connect(self._on_selection_changed)
|
|
64
|
+
|
|
65
|
+
def set_decisions(self, decisions: List[Decision]):
|
|
66
|
+
"""Load decisions into the list."""
|
|
67
|
+
self.clear()
|
|
68
|
+
self.decisions = decisions
|
|
69
|
+
|
|
70
|
+
for i, decision in enumerate(decisions):
|
|
71
|
+
item = PositionListItem(decision, i)
|
|
72
|
+
self.addItem(item)
|
|
73
|
+
|
|
74
|
+
# Select first item
|
|
75
|
+
if decisions:
|
|
76
|
+
self.setCurrentRow(0)
|
|
77
|
+
|
|
78
|
+
@Slot(QListWidgetItem, QListWidgetItem)
|
|
79
|
+
def _on_selection_changed(self, current, previous):
|
|
80
|
+
"""Handle selection change."""
|
|
81
|
+
if current and isinstance(current, PositionListItem):
|
|
82
|
+
self.position_selected.emit(current.decision)
|
|
83
|
+
|
|
84
|
+
@Slot()
|
|
85
|
+
def _show_context_menu(self, pos):
|
|
86
|
+
"""Show context menu for delete action."""
|
|
87
|
+
# Get all selected items (not just the one at pos)
|
|
88
|
+
selected_items = self.selectedItems()
|
|
89
|
+
|
|
90
|
+
if not selected_items:
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
# Create context menu
|
|
94
|
+
menu = QMenu(self)
|
|
95
|
+
# Set cursor pointer for the menu
|
|
96
|
+
menu.setCursor(Qt.PointingHandCursor)
|
|
97
|
+
|
|
98
|
+
# Edit Note action (only for single selection)
|
|
99
|
+
if len(selected_items) == 1:
|
|
100
|
+
item = selected_items[0]
|
|
101
|
+
edit_note_action = QAction(
|
|
102
|
+
qta.icon('fa6s.note-sticky', color='#f9e2af'), # Yellow note icon
|
|
103
|
+
"Edit Note...",
|
|
104
|
+
self
|
|
105
|
+
)
|
|
106
|
+
edit_note_action.triggered.connect(lambda: self._edit_note(item))
|
|
107
|
+
menu.addAction(edit_note_action)
|
|
108
|
+
|
|
109
|
+
menu.addSeparator()
|
|
110
|
+
|
|
111
|
+
# Delete action (works for single or multiple)
|
|
112
|
+
delete_text = "Delete" if len(selected_items) == 1 else f"Delete {len(selected_items)} Items"
|
|
113
|
+
delete_action = QAction(
|
|
114
|
+
qta.icon('fa6s.trash', color='#f38ba8'), # Red delete icon
|
|
115
|
+
delete_text,
|
|
116
|
+
self
|
|
117
|
+
)
|
|
118
|
+
delete_action.triggered.connect(self._delete_selected_items)
|
|
119
|
+
menu.addAction(delete_action)
|
|
120
|
+
|
|
121
|
+
# Show menu at cursor position
|
|
122
|
+
menu.exec(self.mapToGlobal(pos))
|
|
123
|
+
|
|
124
|
+
def _edit_note(self, item: PositionListItem):
|
|
125
|
+
"""Edit the note for a position."""
|
|
126
|
+
current_note = item.decision.note or ""
|
|
127
|
+
|
|
128
|
+
# Create custom note edit dialog
|
|
129
|
+
dialog = NoteEditDialog(current_note, f"Note for position #{item.index + 1}:", self)
|
|
130
|
+
|
|
131
|
+
# Show dialog and get result
|
|
132
|
+
if dialog.exec() == QDialog.Accepted:
|
|
133
|
+
new_note = dialog.get_text()
|
|
134
|
+
|
|
135
|
+
# Update the decision's note
|
|
136
|
+
item.decision.note = new_note.strip() if new_note.strip() else None
|
|
137
|
+
|
|
138
|
+
# Update tooltip to reflect the new note
|
|
139
|
+
tooltip = item.decision.get_metadata_text()
|
|
140
|
+
if item.decision.note:
|
|
141
|
+
tooltip += f"\n\nNote: {item.decision.note}"
|
|
142
|
+
item.setToolTip(tooltip)
|
|
143
|
+
|
|
144
|
+
def _delete_selected_items(self):
|
|
145
|
+
"""Delete all selected items from the list with confirmation."""
|
|
146
|
+
selected_items = self.selectedItems()
|
|
147
|
+
|
|
148
|
+
if not selected_items:
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
# Build confirmation message
|
|
152
|
+
if len(selected_items) == 1:
|
|
153
|
+
item = selected_items[0]
|
|
154
|
+
message = f"Delete position #{item.index + 1}?\n\n{item.decision.get_short_display_text()}"
|
|
155
|
+
title = "Delete Position"
|
|
156
|
+
else:
|
|
157
|
+
message = f"Delete {len(selected_items)} selected position(s)?"
|
|
158
|
+
title = "Delete Positions"
|
|
159
|
+
|
|
160
|
+
# Confirm deletion
|
|
161
|
+
reply = QMessageBox.question(
|
|
162
|
+
self,
|
|
163
|
+
title,
|
|
164
|
+
message,
|
|
165
|
+
QMessageBox.Yes | QMessageBox.No
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
if reply == QMessageBox.Yes:
|
|
169
|
+
# Get indices and sort in descending order
|
|
170
|
+
indices_to_delete = sorted([item.index for item in selected_items], reverse=True)
|
|
171
|
+
|
|
172
|
+
# Delete from widget (highest to lowest to avoid index shifting)
|
|
173
|
+
rows_to_delete = sorted([self.row(item) for item in selected_items], reverse=True)
|
|
174
|
+
for row in rows_to_delete:
|
|
175
|
+
self.takeItem(row)
|
|
176
|
+
|
|
177
|
+
# Emit single signal with all deleted indices
|
|
178
|
+
self.positions_deleted.emit(indices_to_delete)
|
|
179
|
+
|
|
180
|
+
def keyPressEvent(self, event: QKeyEvent):
|
|
181
|
+
"""Handle keyboard events for deletion."""
|
|
182
|
+
if event.key() in (Qt.Key_Delete, Qt.Key_Backspace):
|
|
183
|
+
# Support multi-selection deletion via keyboard
|
|
184
|
+
self._delete_selected_items()
|
|
185
|
+
else:
|
|
186
|
+
super().keyPressEvent(event)
|
|
187
|
+
|
|
188
|
+
def get_selected_decision(self) -> Optional[Decision]:
|
|
189
|
+
"""Get currently selected decision."""
|
|
190
|
+
item = self.currentItem()
|
|
191
|
+
if isinstance(item, PositionListItem):
|
|
192
|
+
return item.decision
|
|
193
|
+
return None
|