ankigammon 1.0.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ankigammon/__init__.py +7 -0
- ankigammon/__main__.py +6 -0
- ankigammon/analysis/__init__.py +13 -0
- ankigammon/analysis/score_matrix.py +391 -0
- ankigammon/anki/__init__.py +6 -0
- ankigammon/anki/ankiconnect.py +216 -0
- ankigammon/anki/apkg_exporter.py +111 -0
- ankigammon/anki/card_generator.py +1325 -0
- ankigammon/anki/card_styles.py +1054 -0
- ankigammon/gui/__init__.py +8 -0
- ankigammon/gui/app.py +192 -0
- ankigammon/gui/dialogs/__init__.py +10 -0
- ankigammon/gui/dialogs/export_dialog.py +594 -0
- ankigammon/gui/dialogs/import_options_dialog.py +201 -0
- ankigammon/gui/dialogs/input_dialog.py +762 -0
- ankigammon/gui/dialogs/note_dialog.py +93 -0
- ankigammon/gui/dialogs/settings_dialog.py +420 -0
- ankigammon/gui/dialogs/update_dialog.py +373 -0
- ankigammon/gui/format_detector.py +377 -0
- ankigammon/gui/main_window.py +1611 -0
- ankigammon/gui/resources/down-arrow.svg +3 -0
- ankigammon/gui/resources/icon.icns +0 -0
- ankigammon/gui/resources/icon.ico +0 -0
- ankigammon/gui/resources/icon.png +0 -0
- ankigammon/gui/resources/style.qss +402 -0
- ankigammon/gui/resources.py +26 -0
- ankigammon/gui/update_checker.py +259 -0
- ankigammon/gui/widgets/__init__.py +8 -0
- ankigammon/gui/widgets/position_list.py +166 -0
- ankigammon/gui/widgets/smart_input.py +268 -0
- ankigammon/models.py +356 -0
- ankigammon/parsers/__init__.py +7 -0
- ankigammon/parsers/gnubg_match_parser.py +1094 -0
- ankigammon/parsers/gnubg_parser.py +468 -0
- ankigammon/parsers/sgf_parser.py +290 -0
- ankigammon/parsers/xg_binary_parser.py +1097 -0
- ankigammon/parsers/xg_text_parser.py +688 -0
- ankigammon/renderer/__init__.py +5 -0
- ankigammon/renderer/animation_controller.py +391 -0
- ankigammon/renderer/animation_helper.py +191 -0
- ankigammon/renderer/color_schemes.py +145 -0
- ankigammon/renderer/svg_board_renderer.py +791 -0
- ankigammon/settings.py +315 -0
- ankigammon/thirdparty/__init__.py +7 -0
- ankigammon/thirdparty/xgdatatools/__init__.py +17 -0
- ankigammon/thirdparty/xgdatatools/xgimport.py +160 -0
- ankigammon/thirdparty/xgdatatools/xgstruct.py +1032 -0
- ankigammon/thirdparty/xgdatatools/xgutils.py +118 -0
- ankigammon/thirdparty/xgdatatools/xgzarc.py +260 -0
- ankigammon/utils/__init__.py +13 -0
- ankigammon/utils/gnubg_analyzer.py +590 -0
- ankigammon/utils/gnuid.py +577 -0
- ankigammon/utils/move_parser.py +204 -0
- ankigammon/utils/ogid.py +326 -0
- ankigammon/utils/xgid.py +387 -0
- ankigammon-1.0.6.dist-info/METADATA +352 -0
- ankigammon-1.0.6.dist-info/RECORD +61 -0
- ankigammon-1.0.6.dist-info/WHEEL +5 -0
- ankigammon-1.0.6.dist-info/entry_points.txt +2 -0
- ankigammon-1.0.6.dist-info/licenses/LICENSE +21 -0
- ankigammon-1.0.6.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,166 @@
|
|
|
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
|
+
self.setText(f"#{index + 1}: {decision.get_short_display_text()}")
|
|
27
|
+
|
|
28
|
+
tooltip = decision.get_metadata_text()
|
|
29
|
+
if decision.note:
|
|
30
|
+
tooltip += f"\n\nNote: {decision.note}"
|
|
31
|
+
self.setToolTip(tooltip)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PositionListWidget(QListWidget):
|
|
35
|
+
"""
|
|
36
|
+
List widget for displaying parsed positions.
|
|
37
|
+
|
|
38
|
+
Signals:
|
|
39
|
+
position_selected(Decision): Emitted when user selects a position
|
|
40
|
+
positions_deleted(list): Emitted when user deletes position(s) - List[int] of indices
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
position_selected = Signal(Decision)
|
|
44
|
+
positions_deleted = Signal(list)
|
|
45
|
+
|
|
46
|
+
def __init__(self, parent: Optional[QWidget] = None):
|
|
47
|
+
super().__init__(parent)
|
|
48
|
+
self.decisions: List[Decision] = []
|
|
49
|
+
|
|
50
|
+
self.setVerticalScrollMode(QListWidget.ScrollPerPixel)
|
|
51
|
+
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
|
52
|
+
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
|
53
|
+
self.customContextMenuRequested.connect(self._show_context_menu)
|
|
54
|
+
self.currentItemChanged.connect(self._on_selection_changed)
|
|
55
|
+
|
|
56
|
+
def set_decisions(self, decisions: List[Decision]):
|
|
57
|
+
"""Load decisions into the list."""
|
|
58
|
+
self.clear()
|
|
59
|
+
self.decisions = decisions
|
|
60
|
+
|
|
61
|
+
for i, decision in enumerate(decisions):
|
|
62
|
+
item = PositionListItem(decision, i)
|
|
63
|
+
self.addItem(item)
|
|
64
|
+
|
|
65
|
+
if decisions:
|
|
66
|
+
self.setCurrentRow(0)
|
|
67
|
+
|
|
68
|
+
@Slot(QListWidgetItem, QListWidgetItem)
|
|
69
|
+
def _on_selection_changed(self, current, previous):
|
|
70
|
+
"""Handle selection change."""
|
|
71
|
+
if current and isinstance(current, PositionListItem):
|
|
72
|
+
self.position_selected.emit(current.decision)
|
|
73
|
+
|
|
74
|
+
@Slot()
|
|
75
|
+
def _show_context_menu(self, pos):
|
|
76
|
+
"""Show context menu for delete action."""
|
|
77
|
+
selected_items = self.selectedItems()
|
|
78
|
+
|
|
79
|
+
if not selected_items:
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
menu = QMenu(self)
|
|
83
|
+
menu.setCursor(Qt.PointingHandCursor)
|
|
84
|
+
|
|
85
|
+
if len(selected_items) == 1:
|
|
86
|
+
item = selected_items[0]
|
|
87
|
+
edit_note_action = QAction(
|
|
88
|
+
qta.icon('fa6s.note-sticky', color='#f9e2af'),
|
|
89
|
+
"Edit Note...",
|
|
90
|
+
self
|
|
91
|
+
)
|
|
92
|
+
edit_note_action.triggered.connect(lambda: self._edit_note(item))
|
|
93
|
+
menu.addAction(edit_note_action)
|
|
94
|
+
|
|
95
|
+
menu.addSeparator()
|
|
96
|
+
|
|
97
|
+
delete_text = "Delete" if len(selected_items) == 1 else f"Delete {len(selected_items)} Items"
|
|
98
|
+
delete_action = QAction(
|
|
99
|
+
qta.icon('fa6s.trash', color='#f38ba8'),
|
|
100
|
+
delete_text,
|
|
101
|
+
self
|
|
102
|
+
)
|
|
103
|
+
delete_action.triggered.connect(self._delete_selected_items)
|
|
104
|
+
menu.addAction(delete_action)
|
|
105
|
+
|
|
106
|
+
menu.exec(self.mapToGlobal(pos))
|
|
107
|
+
|
|
108
|
+
def _edit_note(self, item: PositionListItem):
|
|
109
|
+
"""Edit the note for a position."""
|
|
110
|
+
current_note = item.decision.note or ""
|
|
111
|
+
|
|
112
|
+
dialog = NoteEditDialog(current_note, f"Note for position #{item.index + 1}:", self)
|
|
113
|
+
|
|
114
|
+
if dialog.exec() == QDialog.Accepted:
|
|
115
|
+
new_note = dialog.get_text()
|
|
116
|
+
|
|
117
|
+
item.decision.note = new_note.strip() if new_note.strip() else None
|
|
118
|
+
|
|
119
|
+
tooltip = item.decision.get_metadata_text()
|
|
120
|
+
if item.decision.note:
|
|
121
|
+
tooltip += f"\n\nNote: {item.decision.note}"
|
|
122
|
+
item.setToolTip(tooltip)
|
|
123
|
+
|
|
124
|
+
def _delete_selected_items(self):
|
|
125
|
+
"""Delete selected items with confirmation."""
|
|
126
|
+
selected_items = self.selectedItems()
|
|
127
|
+
|
|
128
|
+
if not selected_items:
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
if len(selected_items) == 1:
|
|
132
|
+
item = selected_items[0]
|
|
133
|
+
message = f"Delete position #{item.index + 1}?\n\n{item.decision.get_short_display_text()}"
|
|
134
|
+
title = "Delete Position"
|
|
135
|
+
else:
|
|
136
|
+
message = f"Delete {len(selected_items)} selected position(s)?"
|
|
137
|
+
title = "Delete Positions"
|
|
138
|
+
|
|
139
|
+
reply = QMessageBox.question(
|
|
140
|
+
self,
|
|
141
|
+
title,
|
|
142
|
+
message,
|
|
143
|
+
QMessageBox.Yes | QMessageBox.No
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
if reply == QMessageBox.Yes:
|
|
147
|
+
indices_to_delete = sorted([item.index for item in selected_items], reverse=True)
|
|
148
|
+
rows_to_delete = sorted([self.row(item) for item in selected_items], reverse=True)
|
|
149
|
+
for row in rows_to_delete:
|
|
150
|
+
self.takeItem(row)
|
|
151
|
+
|
|
152
|
+
self.positions_deleted.emit(indices_to_delete)
|
|
153
|
+
|
|
154
|
+
def keyPressEvent(self, event: QKeyEvent):
|
|
155
|
+
"""Handle keyboard shortcuts for deletion."""
|
|
156
|
+
if event.key() in (Qt.Key_Delete, Qt.Key_Backspace):
|
|
157
|
+
self._delete_selected_items()
|
|
158
|
+
else:
|
|
159
|
+
super().keyPressEvent(event)
|
|
160
|
+
|
|
161
|
+
def get_selected_decision(self) -> Optional[Decision]:
|
|
162
|
+
"""Get currently selected decision."""
|
|
163
|
+
item = self.currentItem()
|
|
164
|
+
if isinstance(item, PositionListItem):
|
|
165
|
+
return item.decision
|
|
166
|
+
return None
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Smart input widget with auto-detection and visual feedback.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from PySide6.QtWidgets import (
|
|
6
|
+
QWidget, QVBoxLayout, QHBoxLayout, QPlainTextEdit,
|
|
7
|
+
QLabel, QFrame, QPushButton
|
|
8
|
+
)
|
|
9
|
+
from PySide6.QtCore import Qt, Signal, QTimer
|
|
10
|
+
from PySide6.QtGui import QFont
|
|
11
|
+
import qtawesome as qta
|
|
12
|
+
|
|
13
|
+
from ankigammon.settings import Settings
|
|
14
|
+
from ankigammon.gui.format_detector import FormatDetector, DetectionResult, InputFormat
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SmartInputWidget(QWidget):
|
|
18
|
+
"""
|
|
19
|
+
Input widget with intelligent format detection.
|
|
20
|
+
|
|
21
|
+
Signals:
|
|
22
|
+
format_detected(DetectionResult): Emitted when format is detected
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
format_detected = Signal(DetectionResult)
|
|
26
|
+
|
|
27
|
+
def __init__(self, settings: Settings, parent=None):
|
|
28
|
+
super().__init__(parent)
|
|
29
|
+
self.settings = settings
|
|
30
|
+
self.detector = FormatDetector(settings)
|
|
31
|
+
self.last_result = None
|
|
32
|
+
|
|
33
|
+
# Debounce timer for detection
|
|
34
|
+
self.detection_timer = QTimer()
|
|
35
|
+
self.detection_timer.setSingleShot(True)
|
|
36
|
+
self.detection_timer.timeout.connect(self._run_detection)
|
|
37
|
+
|
|
38
|
+
self._setup_ui()
|
|
39
|
+
|
|
40
|
+
def _setup_ui(self):
|
|
41
|
+
"""Initialize the user interface."""
|
|
42
|
+
layout = QVBoxLayout(self)
|
|
43
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
44
|
+
layout.setSpacing(12)
|
|
45
|
+
|
|
46
|
+
# Label
|
|
47
|
+
label = QLabel("Input Text:")
|
|
48
|
+
label.setStyleSheet("font-weight: 600; color: #cdd6f4;")
|
|
49
|
+
layout.addWidget(label)
|
|
50
|
+
|
|
51
|
+
# Text input area
|
|
52
|
+
self.text_area = QPlainTextEdit()
|
|
53
|
+
self.text_area.setPlaceholderText(
|
|
54
|
+
"Paste XG analysis or position IDs here...\n\n"
|
|
55
|
+
"Examples:\n"
|
|
56
|
+
"• Full XG analysis (Ctrl+C from eXtreme Gammon)\n"
|
|
57
|
+
"• XGID, OGID, or GNUID position IDs (one per line)\n"
|
|
58
|
+
"• Mixed formats supported - auto-detected"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Use fixed-width font for better XGID readability
|
|
62
|
+
font = QFont("Consolas", 10)
|
|
63
|
+
if not font.exactMatch():
|
|
64
|
+
font = QFont("Courier New", 10)
|
|
65
|
+
self.text_area.setFont(font)
|
|
66
|
+
|
|
67
|
+
self.text_area.setLineWrapMode(QPlainTextEdit.NoWrap)
|
|
68
|
+
self.text_area.setTabChangesFocus(True)
|
|
69
|
+
self.text_area.setMinimumHeight(300)
|
|
70
|
+
|
|
71
|
+
# Dark theme styling
|
|
72
|
+
self.text_area.setStyleSheet("""
|
|
73
|
+
QPlainTextEdit {
|
|
74
|
+
background-color: #1e1e2e;
|
|
75
|
+
color: #cdd6f4;
|
|
76
|
+
border: 2px solid #313244;
|
|
77
|
+
border-radius: 8px;
|
|
78
|
+
padding: 12px;
|
|
79
|
+
selection-background-color: #585b70;
|
|
80
|
+
}
|
|
81
|
+
QPlainTextEdit:focus {
|
|
82
|
+
border-color: #89b4fa;
|
|
83
|
+
}
|
|
84
|
+
""")
|
|
85
|
+
|
|
86
|
+
self.text_area.textChanged.connect(self._on_text_changed)
|
|
87
|
+
layout.addWidget(self.text_area, stretch=1)
|
|
88
|
+
|
|
89
|
+
# Feedback container (outer wrapper with rounded corners)
|
|
90
|
+
self.feedback_container = QWidget()
|
|
91
|
+
self.feedback_container.setStyleSheet("""
|
|
92
|
+
QWidget {
|
|
93
|
+
background-color: #313244;
|
|
94
|
+
border-radius: 6px;
|
|
95
|
+
}
|
|
96
|
+
""")
|
|
97
|
+
self.feedback_container.setVisible(False)
|
|
98
|
+
|
|
99
|
+
container_layout = QHBoxLayout(self.feedback_container)
|
|
100
|
+
container_layout.setContentsMargins(0, 0, 0, 0)
|
|
101
|
+
container_layout.setSpacing(0)
|
|
102
|
+
|
|
103
|
+
# Left accent bar (separate widget - avoids Qt border-left + border-radius bug)
|
|
104
|
+
self.accent_bar = QWidget()
|
|
105
|
+
self.accent_bar.setFixedWidth(4)
|
|
106
|
+
self.accent_bar.setStyleSheet("background-color: #6c7086;")
|
|
107
|
+
container_layout.addWidget(self.accent_bar)
|
|
108
|
+
|
|
109
|
+
# Inner content panel
|
|
110
|
+
self.feedback_panel = QWidget()
|
|
111
|
+
self.feedback_panel.setStyleSheet("background-color: transparent;")
|
|
112
|
+
container_layout.addWidget(self.feedback_panel, stretch=1)
|
|
113
|
+
|
|
114
|
+
feedback_layout = QHBoxLayout(self.feedback_panel)
|
|
115
|
+
feedback_layout.setContentsMargins(12, 12, 12, 12)
|
|
116
|
+
feedback_layout.setSpacing(12) # Add spacing between icon and text
|
|
117
|
+
|
|
118
|
+
# Icon
|
|
119
|
+
self.feedback_icon = QLabel()
|
|
120
|
+
self.feedback_icon.setPixmap(qta.icon('fa6s.circle-info', color='#60a5fa').pixmap(20, 20))
|
|
121
|
+
self.feedback_icon.setMinimumSize(20, 20) # Minimum size instead of fixed
|
|
122
|
+
self.feedback_icon.setAlignment(Qt.AlignCenter)
|
|
123
|
+
self.feedback_icon.setScaledContents(False) # Prevent pixmap stretching/artifacts
|
|
124
|
+
feedback_layout.addWidget(self.feedback_icon, alignment=Qt.AlignTop)
|
|
125
|
+
|
|
126
|
+
# Text content
|
|
127
|
+
text_content = QVBoxLayout()
|
|
128
|
+
text_content.setSpacing(4)
|
|
129
|
+
|
|
130
|
+
self.feedback_title = QLabel()
|
|
131
|
+
self.feedback_title.setStyleSheet("font-weight: 600; font-size: 13px;")
|
|
132
|
+
text_content.addWidget(self.feedback_title)
|
|
133
|
+
|
|
134
|
+
self.feedback_detail = QLabel()
|
|
135
|
+
self.feedback_detail.setStyleSheet("font-size: 12px; color: #a6adc8;")
|
|
136
|
+
self.feedback_detail.setWordWrap(True)
|
|
137
|
+
text_content.addWidget(self.feedback_detail)
|
|
138
|
+
|
|
139
|
+
feedback_layout.addLayout(text_content, stretch=1)
|
|
140
|
+
|
|
141
|
+
# Override button
|
|
142
|
+
self.override_btn = QPushButton("Override...")
|
|
143
|
+
self.override_btn.setVisible(False)
|
|
144
|
+
self.override_btn.setStyleSheet("""
|
|
145
|
+
QPushButton {
|
|
146
|
+
background-color: #45475a;
|
|
147
|
+
color: #cdd6f4;
|
|
148
|
+
border: none;
|
|
149
|
+
padding: 6px 12px;
|
|
150
|
+
border-radius: 4px;
|
|
151
|
+
font-size: 11px;
|
|
152
|
+
}
|
|
153
|
+
QPushButton:hover {
|
|
154
|
+
background-color: #585b70;
|
|
155
|
+
}
|
|
156
|
+
""")
|
|
157
|
+
self.override_btn.setCursor(Qt.PointingHandCursor)
|
|
158
|
+
feedback_layout.addWidget(self.override_btn, alignment=Qt.AlignTop)
|
|
159
|
+
|
|
160
|
+
layout.addWidget(self.feedback_container)
|
|
161
|
+
|
|
162
|
+
def _on_text_changed(self):
|
|
163
|
+
"""Handle text change (debounced)."""
|
|
164
|
+
# Cancel previous timer, start new one
|
|
165
|
+
self.detection_timer.stop()
|
|
166
|
+
self.detection_timer.start(500) # 500ms debounce
|
|
167
|
+
|
|
168
|
+
def _run_detection(self):
|
|
169
|
+
"""Run format detection (after debounce)."""
|
|
170
|
+
text = self.text_area.toPlainText()
|
|
171
|
+
|
|
172
|
+
if not text.strip():
|
|
173
|
+
self.feedback_container.setVisible(False)
|
|
174
|
+
self.last_result = None
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
result = self.detector.detect(text)
|
|
178
|
+
self.last_result = result
|
|
179
|
+
self._update_feedback_ui(result)
|
|
180
|
+
self.format_detected.emit(result)
|
|
181
|
+
|
|
182
|
+
def _set_feedback_icon(self, icon_name: str, color: str):
|
|
183
|
+
"""Helper to properly set feedback icon."""
|
|
184
|
+
self.feedback_icon.clear() # Clear old pixmap first
|
|
185
|
+
self.feedback_icon.setPixmap(qta.icon(icon_name, color=color).pixmap(20, 20))
|
|
186
|
+
|
|
187
|
+
def _set_feedback_style(self, bg_color: str, accent_color: str):
|
|
188
|
+
"""Helper to properly set feedback panel style (avoids Qt border-left + border-radius bug)."""
|
|
189
|
+
self.feedback_container.setStyleSheet(f"""
|
|
190
|
+
QWidget {{
|
|
191
|
+
background-color: {bg_color};
|
|
192
|
+
border-radius: 6px;
|
|
193
|
+
}}
|
|
194
|
+
""")
|
|
195
|
+
self.accent_bar.setStyleSheet(f"background-color: {accent_color};")
|
|
196
|
+
|
|
197
|
+
def _update_feedback_ui(self, result: DetectionResult):
|
|
198
|
+
"""Update feedback panel with detection result."""
|
|
199
|
+
self.feedback_container.setVisible(True)
|
|
200
|
+
|
|
201
|
+
# Update icon and styling based on result
|
|
202
|
+
if result.format == InputFormat.POSITION_IDS:
|
|
203
|
+
if result.warnings:
|
|
204
|
+
# Warning state (GnuBG not configured)
|
|
205
|
+
self._set_feedback_icon('fa6s.triangle-exclamation', '#fab387')
|
|
206
|
+
self._set_feedback_style('#2e2416', '#f9e2af')
|
|
207
|
+
self.feedback_title.setStyleSheet("font-weight: 600; font-size: 13px; color: #f9e2af;")
|
|
208
|
+
self.feedback_title.setText(f"{result.details}")
|
|
209
|
+
self.feedback_detail.setText(
|
|
210
|
+
result.warnings[0] + "\nConfigure GnuBG in Settings to analyze positions."
|
|
211
|
+
)
|
|
212
|
+
else:
|
|
213
|
+
# Success state
|
|
214
|
+
self._set_feedback_icon('fa6s.circle-check', '#a6e3a1')
|
|
215
|
+
self._set_feedback_style('#1e2d1f', '#a6e3a1')
|
|
216
|
+
self.feedback_title.setStyleSheet("font-weight: 600; font-size: 13px; color: #a6e3a1;")
|
|
217
|
+
self.feedback_title.setText(f"{result.details}")
|
|
218
|
+
|
|
219
|
+
# Calculate estimated time
|
|
220
|
+
est_seconds = result.count * 5 # ~5 seconds per position
|
|
221
|
+
self.feedback_detail.setText(
|
|
222
|
+
f"Will analyze with GnuBG ({self.settings.gnubg_analysis_ply}-ply)\n"
|
|
223
|
+
f"Estimated time: ~{est_seconds} seconds"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
elif result.format == InputFormat.FULL_ANALYSIS:
|
|
227
|
+
# Success state (blue)
|
|
228
|
+
self._set_feedback_icon('fa6s.circle-check', '#89b4fa')
|
|
229
|
+
self._set_feedback_style('#1e2633', '#89b4fa')
|
|
230
|
+
self.feedback_title.setStyleSheet("font-weight: 600; font-size: 13px; color: #89b4fa;")
|
|
231
|
+
self.feedback_title.setText(f"{result.details}")
|
|
232
|
+
|
|
233
|
+
# Show preview of first position if available
|
|
234
|
+
preview_text = "Ready to add to export list"
|
|
235
|
+
if result.position_previews:
|
|
236
|
+
preview_text += f"\nFirst position: {result.position_previews[0]}"
|
|
237
|
+
|
|
238
|
+
if result.warnings:
|
|
239
|
+
preview_text += f"\n{result.warnings[0]}"
|
|
240
|
+
|
|
241
|
+
self.feedback_detail.setText(preview_text)
|
|
242
|
+
|
|
243
|
+
else:
|
|
244
|
+
# Unknown/error state
|
|
245
|
+
self._set_feedback_icon('fa6s.triangle-exclamation', '#fab387')
|
|
246
|
+
self._set_feedback_style('#2e2416', '#fab387')
|
|
247
|
+
self.feedback_title.setStyleSheet("font-weight: 600; font-size: 13px; color: #fab387;")
|
|
248
|
+
self.feedback_title.setText(f"{result.details}")
|
|
249
|
+
|
|
250
|
+
warning_text = "Paste XGID/OGID/GNUID or full XG analysis text"
|
|
251
|
+
if result.warnings:
|
|
252
|
+
warning_text = "\n".join(result.warnings)
|
|
253
|
+
|
|
254
|
+
self.feedback_detail.setText(warning_text)
|
|
255
|
+
|
|
256
|
+
def get_text(self) -> str:
|
|
257
|
+
"""Get current input text."""
|
|
258
|
+
return self.text_area.toPlainText()
|
|
259
|
+
|
|
260
|
+
def clear_text(self):
|
|
261
|
+
"""Clear input text."""
|
|
262
|
+
self.text_area.clear()
|
|
263
|
+
self.feedback_container.setVisible(False)
|
|
264
|
+
self.last_result = None
|
|
265
|
+
|
|
266
|
+
def get_last_result(self) -> DetectionResult:
|
|
267
|
+
"""Get last detection result."""
|
|
268
|
+
return self.last_result
|