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.

Files changed (56) 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 +373 -0
  5. ankigammon/anki/__init__.py +6 -0
  6. ankigammon/anki/ankiconnect.py +224 -0
  7. ankigammon/anki/apkg_exporter.py +123 -0
  8. ankigammon/anki/card_generator.py +1307 -0
  9. ankigammon/anki/card_styles.py +1034 -0
  10. ankigammon/gui/__init__.py +8 -0
  11. ankigammon/gui/app.py +209 -0
  12. ankigammon/gui/dialogs/__init__.py +10 -0
  13. ankigammon/gui/dialogs/export_dialog.py +597 -0
  14. ankigammon/gui/dialogs/import_options_dialog.py +163 -0
  15. ankigammon/gui/dialogs/input_dialog.py +776 -0
  16. ankigammon/gui/dialogs/note_dialog.py +93 -0
  17. ankigammon/gui/dialogs/settings_dialog.py +384 -0
  18. ankigammon/gui/format_detector.py +292 -0
  19. ankigammon/gui/main_window.py +1071 -0
  20. ankigammon/gui/resources/icon.icns +0 -0
  21. ankigammon/gui/resources/icon.ico +0 -0
  22. ankigammon/gui/resources/icon.png +0 -0
  23. ankigammon/gui/resources/style.qss +394 -0
  24. ankigammon/gui/resources.py +26 -0
  25. ankigammon/gui/widgets/__init__.py +8 -0
  26. ankigammon/gui/widgets/position_list.py +193 -0
  27. ankigammon/gui/widgets/smart_input.py +268 -0
  28. ankigammon/models.py +322 -0
  29. ankigammon/parsers/__init__.py +7 -0
  30. ankigammon/parsers/gnubg_parser.py +454 -0
  31. ankigammon/parsers/xg_binary_parser.py +870 -0
  32. ankigammon/parsers/xg_text_parser.py +729 -0
  33. ankigammon/renderer/__init__.py +5 -0
  34. ankigammon/renderer/animation_controller.py +406 -0
  35. ankigammon/renderer/animation_helper.py +221 -0
  36. ankigammon/renderer/color_schemes.py +145 -0
  37. ankigammon/renderer/svg_board_renderer.py +824 -0
  38. ankigammon/settings.py +239 -0
  39. ankigammon/thirdparty/__init__.py +7 -0
  40. ankigammon/thirdparty/xgdatatools/__init__.py +17 -0
  41. ankigammon/thirdparty/xgdatatools/xgimport.py +160 -0
  42. ankigammon/thirdparty/xgdatatools/xgstruct.py +1032 -0
  43. ankigammon/thirdparty/xgdatatools/xgutils.py +118 -0
  44. ankigammon/thirdparty/xgdatatools/xgzarc.py +260 -0
  45. ankigammon/utils/__init__.py +13 -0
  46. ankigammon/utils/gnubg_analyzer.py +431 -0
  47. ankigammon/utils/gnuid.py +622 -0
  48. ankigammon/utils/move_parser.py +239 -0
  49. ankigammon/utils/ogid.py +335 -0
  50. ankigammon/utils/xgid.py +419 -0
  51. ankigammon-1.0.0.dist-info/METADATA +370 -0
  52. ankigammon-1.0.0.dist-info/RECORD +56 -0
  53. ankigammon-1.0.0.dist-info/WHEEL +5 -0
  54. ankigammon-1.0.0.dist-info/entry_points.txt +2 -0
  55. ankigammon-1.0.0.dist-info/licenses/LICENSE +21 -0
  56. 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,8 @@
1
+ """
2
+ GUI widgets package.
3
+ """
4
+
5
+ __all__ = ['PositionListWidget', 'SmartInputWidget']
6
+
7
+ from .position_list import PositionListWidget
8
+ from .smart_input import SmartInputWidget
@@ -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