ankigammon 1.0.5__tar.gz → 1.0.10__tar.gz
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-1.0.5 → ankigammon-1.0.10}/PKG-INFO +1 -1
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/__init__.py +1 -1
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/gui/app.py +6 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/gui/dialogs/__init__.py +2 -1
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/gui/dialogs/import_options_dialog.py +34 -20
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/gui/dialogs/input_dialog.py +14 -13
- ankigammon-1.0.10/ankigammon/gui/dialogs/shortcuts_dialog.py +192 -0
- ankigammon-1.0.10/ankigammon/gui/dialogs/update_dialog.py +374 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/gui/main_window.py +150 -32
- ankigammon-1.0.10/ankigammon/gui/silent_messagebox.py +117 -0
- ankigammon-1.0.10/ankigammon/gui/update_checker.py +301 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/gui/widgets/position_list.py +4 -3
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/parsers/gnubg_parser.py +26 -25
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/settings.py +52 -8
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/utils/xgid.py +9 -10
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon.egg-info/PKG-INFO +1 -1
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon.egg-info/SOURCES.txt +4 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/pyproject.toml +1 -1
- {ankigammon-1.0.5 → ankigammon-1.0.10}/tests/test_basic.py +3 -1
- {ankigammon-1.0.5 → ankigammon-1.0.10}/LICENSE +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/README.md +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/__main__.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/analysis/__init__.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/analysis/score_matrix.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/anki/__init__.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/anki/ankiconnect.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/anki/apkg_exporter.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/anki/card_generator.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/anki/card_styles.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/gui/__init__.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/gui/dialogs/export_dialog.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/gui/dialogs/note_dialog.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/gui/dialogs/settings_dialog.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/gui/format_detector.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/gui/resources/down-arrow.svg +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/gui/resources/icon.icns +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/gui/resources/icon.ico +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/gui/resources/icon.png +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/gui/resources/style.qss +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/gui/resources.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/gui/widgets/__init__.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/gui/widgets/smart_input.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/models.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/parsers/__init__.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/parsers/gnubg_match_parser.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/parsers/sgf_parser.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/parsers/xg_binary_parser.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/parsers/xg_text_parser.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/renderer/__init__.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/renderer/animation_controller.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/renderer/animation_helper.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/renderer/color_schemes.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/renderer/svg_board_renderer.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/thirdparty/__init__.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/thirdparty/xgdatatools/__init__.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/thirdparty/xgdatatools/xgimport.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/thirdparty/xgdatatools/xgstruct.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/thirdparty/xgdatatools/xgutils.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/thirdparty/xgdatatools/xgzarc.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/utils/__init__.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/utils/gnubg_analyzer.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/utils/gnuid.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/utils/move_parser.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon/utils/ogid.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon.egg-info/dependency_links.txt +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon.egg-info/entry_points.txt +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon.egg-info/requires.txt +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/ankigammon.egg-info/top_level.txt +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/setup.cfg +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/tests/test_format_detector.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/tests/test_import_dialog_player_matching.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/tests/test_played_move_injection.py +0 -0
- {ankigammon-1.0.5 → ankigammon-1.0.10}/tests/test_settings.py +0 -0
|
@@ -127,7 +127,13 @@ def main():
|
|
|
127
127
|
Returns:
|
|
128
128
|
int: Application exit code
|
|
129
129
|
"""
|
|
130
|
+
import multiprocessing
|
|
130
131
|
import logging
|
|
132
|
+
|
|
133
|
+
# CRITICAL: Required for PyInstaller + multiprocessing on Windows
|
|
134
|
+
# Without this, worker processes will spawn new GUI windows when using ProcessPoolExecutor
|
|
135
|
+
multiprocessing.freeze_support()
|
|
136
|
+
|
|
131
137
|
logging.basicConfig(
|
|
132
138
|
level=logging.INFO,
|
|
133
139
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
GUI dialogs package.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
__all__ = ['SettingsDialog', 'ExportDialog', 'InputDialog', 'ImportOptionsDialog']
|
|
5
|
+
__all__ = ['SettingsDialog', 'ExportDialog', 'InputDialog', 'ImportOptionsDialog', 'ShortcutsDialog']
|
|
6
6
|
|
|
7
7
|
from .settings_dialog import SettingsDialog
|
|
8
8
|
from .export_dialog import ExportDialog
|
|
9
9
|
from .input_dialog import InputDialog
|
|
10
10
|
from .import_options_dialog import ImportOptionsDialog
|
|
11
|
+
from .shortcuts_dialog import ShortcutsDialog
|
|
@@ -19,15 +19,15 @@ class ImportOptionsDialog(QDialog):
|
|
|
19
19
|
Dialog for configuring XG import filtering options.
|
|
20
20
|
|
|
21
21
|
Allows users to filter imported positions by:
|
|
22
|
-
- Error
|
|
22
|
+
- Error thresholds (separate for checker play and cube decisions)
|
|
23
23
|
- Player selection (import mistakes from X, O, or both)
|
|
24
24
|
|
|
25
25
|
Signals:
|
|
26
|
-
options_accepted(float, bool, bool): Emitted when user accepts
|
|
27
|
-
Args: (
|
|
26
|
+
options_accepted(float, float, bool, bool): Emitted when user accepts
|
|
27
|
+
Args: (checker_threshold, cube_threshold, include_player_x, include_player_o)
|
|
28
28
|
"""
|
|
29
29
|
|
|
30
|
-
options_accepted = Signal(float, bool, bool)
|
|
30
|
+
options_accepted = Signal(float, float, bool, bool)
|
|
31
31
|
|
|
32
32
|
def __init__(
|
|
33
33
|
self,
|
|
@@ -76,18 +76,28 @@ class ImportOptionsDialog(QDialog):
|
|
|
76
76
|
|
|
77
77
|
def _create_threshold_group(self) -> QGroupBox:
|
|
78
78
|
"""Create error threshold settings group."""
|
|
79
|
-
group = QGroupBox("Error
|
|
79
|
+
group = QGroupBox("Error Thresholds")
|
|
80
80
|
form = QFormLayout(group)
|
|
81
81
|
|
|
82
|
-
#
|
|
83
|
-
self.
|
|
84
|
-
self.
|
|
85
|
-
self.
|
|
86
|
-
self.
|
|
87
|
-
self.
|
|
88
|
-
self.
|
|
89
|
-
self.
|
|
90
|
-
form.addRow("
|
|
82
|
+
# Checker play threshold spinbox
|
|
83
|
+
self.spin_checker_threshold = QDoubleSpinBox()
|
|
84
|
+
self.spin_checker_threshold.setMinimum(0.000)
|
|
85
|
+
self.spin_checker_threshold.setMaximum(1.000)
|
|
86
|
+
self.spin_checker_threshold.setSingleStep(0.001)
|
|
87
|
+
self.spin_checker_threshold.setDecimals(3)
|
|
88
|
+
self.spin_checker_threshold.setValue(0.080)
|
|
89
|
+
self.spin_checker_threshold.setCursor(Qt.PointingHandCursor)
|
|
90
|
+
form.addRow("Checker Play:", self.spin_checker_threshold)
|
|
91
|
+
|
|
92
|
+
# Cube decision threshold spinbox
|
|
93
|
+
self.spin_cube_threshold = QDoubleSpinBox()
|
|
94
|
+
self.spin_cube_threshold.setMinimum(0.000)
|
|
95
|
+
self.spin_cube_threshold.setMaximum(1.000)
|
|
96
|
+
self.spin_cube_threshold.setSingleStep(0.001)
|
|
97
|
+
self.spin_cube_threshold.setDecimals(3)
|
|
98
|
+
self.spin_cube_threshold.setValue(0.080)
|
|
99
|
+
self.spin_cube_threshold.setCursor(Qt.PointingHandCursor)
|
|
100
|
+
form.addRow("Cube Decisions:", self.spin_cube_threshold)
|
|
91
101
|
|
|
92
102
|
return group
|
|
93
103
|
|
|
@@ -126,7 +136,8 @@ class ImportOptionsDialog(QDialog):
|
|
|
126
136
|
|
|
127
137
|
def _load_settings(self):
|
|
128
138
|
"""Load current settings into widgets, matching by player name."""
|
|
129
|
-
self.
|
|
139
|
+
self.spin_checker_threshold.setValue(self.settings.import_checker_error_threshold)
|
|
140
|
+
self.spin_cube_threshold.setValue(self.settings.import_cube_error_threshold)
|
|
130
141
|
|
|
131
142
|
selected_names = self.settings.import_selected_player_names
|
|
132
143
|
selected_names_lower = [name.lower() for name in selected_names]
|
|
@@ -165,7 +176,8 @@ class ImportOptionsDialog(QDialog):
|
|
|
165
176
|
|
|
166
177
|
def accept(self):
|
|
167
178
|
"""Save settings and emit options."""
|
|
168
|
-
self.settings.
|
|
179
|
+
self.settings.import_checker_error_threshold = self.spin_checker_threshold.value()
|
|
180
|
+
self.settings.import_cube_error_threshold = self.spin_cube_threshold.value()
|
|
169
181
|
|
|
170
182
|
selected_names = []
|
|
171
183
|
if self.chk_player_o.isChecked():
|
|
@@ -180,22 +192,24 @@ class ImportOptionsDialog(QDialog):
|
|
|
180
192
|
self.settings.import_include_player_o = self.chk_player_o.isChecked()
|
|
181
193
|
|
|
182
194
|
self.options_accepted.emit(
|
|
183
|
-
self.
|
|
195
|
+
self.spin_checker_threshold.value(),
|
|
196
|
+
self.spin_cube_threshold.value(),
|
|
184
197
|
self.chk_player_x.isChecked(),
|
|
185
198
|
self.chk_player_o.isChecked()
|
|
186
199
|
)
|
|
187
200
|
|
|
188
201
|
super().accept()
|
|
189
202
|
|
|
190
|
-
def get_options(self) -> tuple[float, bool, bool]:
|
|
203
|
+
def get_options(self) -> tuple[float, float, bool, bool]:
|
|
191
204
|
"""
|
|
192
205
|
Get the selected import options.
|
|
193
206
|
|
|
194
207
|
Returns:
|
|
195
|
-
Tuple of (
|
|
208
|
+
Tuple of (checker_threshold, cube_threshold, include_player_x, include_player_o)
|
|
196
209
|
"""
|
|
197
210
|
return (
|
|
198
|
-
self.
|
|
211
|
+
self.spin_checker_threshold.value(),
|
|
212
|
+
self.spin_cube_threshold.value(),
|
|
199
213
|
self.chk_player_x.isChecked(),
|
|
200
214
|
self.chk_player_o.isChecked()
|
|
201
215
|
)
|
|
@@ -25,6 +25,7 @@ from ankigammon.settings import Settings
|
|
|
25
25
|
from ankigammon.models import Decision, Position, Player, CubeState, DecisionType
|
|
26
26
|
from ankigammon.parsers.xg_text_parser import XGTextParser
|
|
27
27
|
from ankigammon.gui.dialogs.settings_dialog import SettingsDialog
|
|
28
|
+
from ankigammon.gui import silent_messagebox
|
|
28
29
|
from ankigammon.utils.gnuid import parse_gnuid
|
|
29
30
|
from ankigammon.utils.ogid import parse_ogid
|
|
30
31
|
from ankigammon.utils.xgid import parse_xgid
|
|
@@ -189,14 +190,14 @@ class PendingListWidget(QListWidget):
|
|
|
189
190
|
title = "Delete Positions"
|
|
190
191
|
|
|
191
192
|
# Show confirmation dialog
|
|
192
|
-
reply =
|
|
193
|
+
reply = silent_messagebox.question(
|
|
193
194
|
self,
|
|
194
195
|
title,
|
|
195
196
|
message,
|
|
196
|
-
QMessageBox.Yes | QMessageBox.No
|
|
197
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
|
197
198
|
)
|
|
198
199
|
|
|
199
|
-
if reply == QMessageBox.Yes:
|
|
200
|
+
if reply == QMessageBox.StandardButton.Yes:
|
|
200
201
|
# Delete in descending order to avoid index shifting
|
|
201
202
|
rows_to_delete = sorted([self.row(item) for item in selected_items], reverse=True)
|
|
202
203
|
for row in rows_to_delete:
|
|
@@ -467,7 +468,7 @@ class InputDialog(QDialog):
|
|
|
467
468
|
result = self.input_widget.get_last_result()
|
|
468
469
|
|
|
469
470
|
if not text.strip():
|
|
470
|
-
|
|
471
|
+
silent_messagebox.warning(
|
|
471
472
|
self,
|
|
472
473
|
"No Input",
|
|
473
474
|
"Please paste some text first"
|
|
@@ -475,7 +476,7 @@ class InputDialog(QDialog):
|
|
|
475
476
|
return
|
|
476
477
|
|
|
477
478
|
if not result or result.format == InputFormat.UNKNOWN:
|
|
478
|
-
|
|
479
|
+
silent_messagebox.warning(
|
|
479
480
|
self,
|
|
480
481
|
"Invalid Format",
|
|
481
482
|
"Could not detect valid position format.\n\n"
|
|
@@ -485,14 +486,14 @@ class InputDialog(QDialog):
|
|
|
485
486
|
|
|
486
487
|
# Check for GnuBG requirement
|
|
487
488
|
if result.format == InputFormat.POSITION_IDS and not self.settings.is_gnubg_available():
|
|
488
|
-
reply =
|
|
489
|
+
reply = silent_messagebox.question(
|
|
489
490
|
self,
|
|
490
491
|
"GnuBG Required",
|
|
491
492
|
"Position IDs require GnuBG analysis, but GnuBG is not configured.\n\n"
|
|
492
493
|
"Would you like to configure GnuBG in Settings?",
|
|
493
|
-
QMessageBox.Yes | QMessageBox.No
|
|
494
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
|
494
495
|
)
|
|
495
|
-
if reply == QMessageBox.Yes:
|
|
496
|
+
if reply == QMessageBox.StandardButton.Yes:
|
|
496
497
|
# Open settings dialog to configure GnuBG
|
|
497
498
|
dialog = SettingsDialog(self.settings, self)
|
|
498
499
|
dialog.exec()
|
|
@@ -503,7 +504,7 @@ class InputDialog(QDialog):
|
|
|
503
504
|
decisions = self._parse_input(text, result.format)
|
|
504
505
|
|
|
505
506
|
if not decisions:
|
|
506
|
-
|
|
507
|
+
silent_messagebox.warning(
|
|
507
508
|
self,
|
|
508
509
|
"Parse Failed",
|
|
509
510
|
"Could not parse any valid positions from input."
|
|
@@ -530,7 +531,7 @@ class InputDialog(QDialog):
|
|
|
530
531
|
self.pending_list.setCurrentRow(len(self.pending_decisions) - len(decisions))
|
|
531
532
|
|
|
532
533
|
except Exception as e:
|
|
533
|
-
|
|
534
|
+
silent_messagebox.critical(
|
|
534
535
|
self,
|
|
535
536
|
"Parse Error",
|
|
536
537
|
f"Failed to parse input:\n{str(e)}"
|
|
@@ -646,14 +647,14 @@ class InputDialog(QDialog):
|
|
|
646
647
|
if not self.pending_decisions:
|
|
647
648
|
return
|
|
648
649
|
|
|
649
|
-
reply =
|
|
650
|
+
reply = silent_messagebox.question(
|
|
650
651
|
self,
|
|
651
652
|
"Clear All",
|
|
652
653
|
f"Remove all {len(self.pending_decisions)} pending position(s)?",
|
|
653
|
-
QMessageBox.Yes | QMessageBox.No
|
|
654
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
|
654
655
|
)
|
|
655
656
|
|
|
656
|
-
if reply == QMessageBox.Yes:
|
|
657
|
+
if reply == QMessageBox.StandardButton.Yes:
|
|
657
658
|
self.pending_decisions.clear()
|
|
658
659
|
self.pending_list.clear()
|
|
659
660
|
self._update_count_label()
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tips and shortcuts reference dialog.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from PySide6.QtWidgets import (
|
|
7
|
+
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QScrollArea, QWidget, QGridLayout
|
|
8
|
+
)
|
|
9
|
+
from PySide6.QtCore import Qt
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ShortcutsDialog(QDialog):
|
|
13
|
+
"""Dialog displaying tips and keyboard shortcuts."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, parent=None):
|
|
16
|
+
super().__init__(parent)
|
|
17
|
+
self.setWindowTitle("Tips & Shortcuts")
|
|
18
|
+
self.setMinimumSize(600, 500)
|
|
19
|
+
self.setModal(False) # Allow interaction with main window
|
|
20
|
+
|
|
21
|
+
self._setup_ui()
|
|
22
|
+
|
|
23
|
+
def _setup_ui(self):
|
|
24
|
+
"""Initialize the user interface."""
|
|
25
|
+
layout = QVBoxLayout(self)
|
|
26
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
27
|
+
layout.setSpacing(0)
|
|
28
|
+
|
|
29
|
+
# Scrollable content area
|
|
30
|
+
scroll = QScrollArea()
|
|
31
|
+
scroll.setWidgetResizable(True)
|
|
32
|
+
scroll.setStyleSheet("border: none; background-color: #181825;")
|
|
33
|
+
|
|
34
|
+
content = QWidget()
|
|
35
|
+
content_layout = QVBoxLayout(content)
|
|
36
|
+
content_layout.setContentsMargins(20, 16, 20, 16)
|
|
37
|
+
content_layout.setSpacing(12)
|
|
38
|
+
|
|
39
|
+
# Tips section (moved to top)
|
|
40
|
+
tips_title = QLabel("💡 Did you know?")
|
|
41
|
+
tips_title.setStyleSheet("font-size: 16px; font-weight: bold; color: #f5e0dc;")
|
|
42
|
+
content_layout.addWidget(tips_title)
|
|
43
|
+
|
|
44
|
+
tips = [
|
|
45
|
+
"Right-click any position to edit its note or delete it",
|
|
46
|
+
"Drag and drop .xg, .mat, or .sgf files anywhere to import",
|
|
47
|
+
"You can select multiple positions and delete them all at once",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
for tip in tips:
|
|
51
|
+
tip_card = QWidget()
|
|
52
|
+
tip_card.setStyleSheet("""
|
|
53
|
+
QWidget {
|
|
54
|
+
background-color: rgba(137, 180, 250, 0.08);
|
|
55
|
+
border-left: 3px solid #89b4fa;
|
|
56
|
+
border-radius: 4px;
|
|
57
|
+
padding: 10px;
|
|
58
|
+
}
|
|
59
|
+
""")
|
|
60
|
+
tip_layout = QHBoxLayout(tip_card)
|
|
61
|
+
tip_layout.setContentsMargins(10, 10, 10, 10)
|
|
62
|
+
|
|
63
|
+
tip_label = QLabel(tip)
|
|
64
|
+
tip_label.setStyleSheet("color: #cdd6f4; font-size: 13px; background: transparent; border: none;")
|
|
65
|
+
tip_label.setWordWrap(True)
|
|
66
|
+
tip_layout.addWidget(tip_label)
|
|
67
|
+
|
|
68
|
+
content_layout.addWidget(tip_card)
|
|
69
|
+
|
|
70
|
+
# Shortcuts section (moved below tips)
|
|
71
|
+
content_layout.addSpacing(8)
|
|
72
|
+
|
|
73
|
+
shortcuts_title = QLabel("⌨️ Keyboard Shortcuts")
|
|
74
|
+
shortcuts_title.setStyleSheet("font-size: 16px; font-weight: bold; color: #f5e0dc;")
|
|
75
|
+
content_layout.addWidget(shortcuts_title)
|
|
76
|
+
|
|
77
|
+
# Determine modifier key based on OS
|
|
78
|
+
modifier = "Cmd" if sys.platform == "darwin" else "Ctrl"
|
|
79
|
+
|
|
80
|
+
shortcuts = [
|
|
81
|
+
(f"{modifier}+N", "Add positions"),
|
|
82
|
+
(f"{modifier}+O", "Import file"),
|
|
83
|
+
(f"{modifier}+E", "Export to Anki"),
|
|
84
|
+
(f"{modifier}+,", "Open Settings"),
|
|
85
|
+
("Delete", "Remove selected"),
|
|
86
|
+
("Shift+Click", "Select range"),
|
|
87
|
+
(f"{modifier}+Click", "Select multiple"),
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
grid = QGridLayout()
|
|
91
|
+
grid.setSpacing(8)
|
|
92
|
+
grid.setContentsMargins(0, 0, 0, 0)
|
|
93
|
+
|
|
94
|
+
for i, (key, desc) in enumerate(shortcuts):
|
|
95
|
+
row = i // 2
|
|
96
|
+
col = i % 2
|
|
97
|
+
|
|
98
|
+
card = self._create_shortcut_card(key, desc)
|
|
99
|
+
grid.addWidget(card, row, col)
|
|
100
|
+
|
|
101
|
+
content_layout.addLayout(grid)
|
|
102
|
+
|
|
103
|
+
content_layout.addStretch()
|
|
104
|
+
|
|
105
|
+
scroll.setWidget(content)
|
|
106
|
+
layout.addWidget(scroll)
|
|
107
|
+
|
|
108
|
+
# Footer with close button
|
|
109
|
+
footer = QWidget()
|
|
110
|
+
footer.setStyleSheet("background-color: #1e1e2e; padding: 16px;")
|
|
111
|
+
footer_layout = QHBoxLayout(footer)
|
|
112
|
+
footer_layout.setContentsMargins(20, 16, 20, 16)
|
|
113
|
+
|
|
114
|
+
close_hint = QLabel("Press ESC to close")
|
|
115
|
+
close_hint.setStyleSheet("color: #6c7086; font-size: 11px;")
|
|
116
|
+
footer_layout.addWidget(close_hint)
|
|
117
|
+
|
|
118
|
+
footer_layout.addStretch()
|
|
119
|
+
|
|
120
|
+
close_btn = QPushButton("Got it")
|
|
121
|
+
close_btn.setDefault(True)
|
|
122
|
+
close_btn.setStyleSheet("""
|
|
123
|
+
QPushButton {
|
|
124
|
+
background-color: #89b4fa;
|
|
125
|
+
color: #1e1e2e;
|
|
126
|
+
border: none;
|
|
127
|
+
padding: 8px 24px;
|
|
128
|
+
border-radius: 6px;
|
|
129
|
+
font-weight: 600;
|
|
130
|
+
}
|
|
131
|
+
QPushButton:hover {
|
|
132
|
+
background-color: #74c7ec;
|
|
133
|
+
}
|
|
134
|
+
""")
|
|
135
|
+
close_btn.setCursor(Qt.PointingHandCursor)
|
|
136
|
+
close_btn.clicked.connect(self.accept)
|
|
137
|
+
footer_layout.addWidget(close_btn)
|
|
138
|
+
|
|
139
|
+
layout.addWidget(footer)
|
|
140
|
+
|
|
141
|
+
def _create_shortcut_card(self, key: str, description: str) -> QWidget:
|
|
142
|
+
"""Create a card widget for a keyboard shortcut."""
|
|
143
|
+
card = QWidget()
|
|
144
|
+
card.setStyleSheet("""
|
|
145
|
+
QWidget {
|
|
146
|
+
background-color: #262637;
|
|
147
|
+
border-radius: 6px;
|
|
148
|
+
padding: 10px;
|
|
149
|
+
}
|
|
150
|
+
""")
|
|
151
|
+
|
|
152
|
+
card_layout = QHBoxLayout(card)
|
|
153
|
+
card_layout.setContentsMargins(10, 10, 10, 10)
|
|
154
|
+
card_layout.setSpacing(10)
|
|
155
|
+
|
|
156
|
+
# Key badge
|
|
157
|
+
key_badge = QLabel(key)
|
|
158
|
+
key_badge.setStyleSheet("""
|
|
159
|
+
QLabel {
|
|
160
|
+
background-color: #45475a;
|
|
161
|
+
color: #f5e0dc;
|
|
162
|
+
padding: 4px 10px;
|
|
163
|
+
border-radius: 4px;
|
|
164
|
+
font-family: 'Consolas', monospace;
|
|
165
|
+
font-size: 11px;
|
|
166
|
+
font-weight: bold;
|
|
167
|
+
border: none;
|
|
168
|
+
}
|
|
169
|
+
""")
|
|
170
|
+
key_badge.setAlignment(Qt.AlignCenter)
|
|
171
|
+
card_layout.addWidget(key_badge)
|
|
172
|
+
|
|
173
|
+
# Description
|
|
174
|
+
desc_label = QLabel(description)
|
|
175
|
+
desc_label.setStyleSheet("""
|
|
176
|
+
QLabel {
|
|
177
|
+
color: #cdd6f4;
|
|
178
|
+
font-size: 13px;
|
|
179
|
+
background: transparent;
|
|
180
|
+
border: none;
|
|
181
|
+
}
|
|
182
|
+
""")
|
|
183
|
+
card_layout.addWidget(desc_label, stretch=1)
|
|
184
|
+
|
|
185
|
+
return card
|
|
186
|
+
|
|
187
|
+
def keyPressEvent(self, event):
|
|
188
|
+
"""Close on ESC key."""
|
|
189
|
+
if event.key() == Qt.Key_Escape:
|
|
190
|
+
self.accept()
|
|
191
|
+
else:
|
|
192
|
+
super().keyPressEvent(event)
|