basebender 0.2.1__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.
- basebender/STRUCTURE.md +13 -0
- basebender/__init__.py +8 -0
- basebender/api/STRUCTURE.md +7 -0
- basebender/api/main.py +249 -0
- basebender/cli.py +231 -0
- basebender/gui/STRUCTURE.md +8 -0
- basebender/gui/__init__.py +3 -0
- basebender/gui/main_window.py +546 -0
- basebender/rebaser/STRUCTURE.md +16 -0
- basebender/rebaser/__init__.py +4 -0
- basebender/rebaser/config_loader.py +269 -0
- basebender/rebaser/digit_set_rebaser.py +303 -0
- basebender/rebaser/digit_sets.py +84 -0
- basebender/rebaser/generated/STRUCTURE.md +10 -0
- basebender/rebaser/generated/__init__.py +0 -0
- basebender/rebaser/generated/app_resources_rc.py +204 -0
- basebender/rebaser/models.py +38 -0
- basebender/rebaser/resources/STRUCTURE.md +12 -0
- basebender/rebaser/resources/app_resources.qrc +8 -0
- basebender/rebaser/resources/data/STRUCTURE.md +7 -0
- basebender/rebaser/resources/data/default_digit_sets.toml +27 -0
- basebender/rebaser/resources/icons/STRUCTURE.md +10 -0
- basebender/rebaser/resources/icons/input.svg +5 -0
- basebender/rebaser/resources/icons/output.svg +4 -0
- basebender/rebaser/resources/icons/source.svg +5 -0
- basebender/rebaser/resources/icons/target.svg +5 -0
- basebender-0.2.1.dist-info/METADATA +174 -0
- basebender-0.2.1.dist-info/RECORD +31 -0
- basebender-0.2.1.dist-info/WHEEL +4 -0
- basebender-0.2.1.dist-info/entry_points.txt +4 -0
- basebender-0.2.1.dist-info/licenses/LICENSE +9 -0
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides the graphical user interface (GUI) for the BaseBender
|
|
3
|
+
tool.
|
|
4
|
+
|
|
5
|
+
It allows users to interactively rebase strings between different digit sets,
|
|
6
|
+
manage digit set presets, and view rebase results.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from PySide6.QtCore import QEvent, QObject, QSize, Qt
|
|
15
|
+
from PySide6.QtSvgWidgets import QSvgWidget
|
|
16
|
+
from PySide6.QtWidgets import (
|
|
17
|
+
QApplication,
|
|
18
|
+
QCheckBox,
|
|
19
|
+
QComboBox,
|
|
20
|
+
QHBoxLayout,
|
|
21
|
+
QLabel,
|
|
22
|
+
QMainWindow,
|
|
23
|
+
QMenu,
|
|
24
|
+
QPushButton,
|
|
25
|
+
QSizePolicy,
|
|
26
|
+
QStatusBar,
|
|
27
|
+
QTextEdit,
|
|
28
|
+
QToolButton,
|
|
29
|
+
QVBoxLayout,
|
|
30
|
+
QWidget,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
from basebender.rebaser.config_loader import load_ui_state, save_ui_state
|
|
34
|
+
from basebender.rebaser.digit_set_rebaser import DigitSetRebaser
|
|
35
|
+
from basebender.rebaser.digit_sets import get_predefined_digit_sets
|
|
36
|
+
from basebender.rebaser.generated import app_resources_rc
|
|
37
|
+
from basebender.rebaser.models import DigitSet
|
|
38
|
+
|
|
39
|
+
ICON_INPUT_QRC_PATH = ":/app/icons/input.svg"
|
|
40
|
+
ICON_OUTPUT_QRC_PATH = ":/app/icons/output.svg"
|
|
41
|
+
ICON_SOURCE_QRC_PATH = ":/app/icons/source.svg"
|
|
42
|
+
ICON_TARGET_QRC_PATH = ":/app/icons/target.svg"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
TEXT_EDIT_FIXED_HEIGHT = 30
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class MainWindow(QMainWindow):
|
|
49
|
+
"""
|
|
50
|
+
Main window for the BaseBender GUI application.
|
|
51
|
+
|
|
52
|
+
Provides an interactive interface for rebaseing strings between different
|
|
53
|
+
digit sets.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self) -> None:
|
|
57
|
+
super().__init__()
|
|
58
|
+
app_resources_rc.qInitResources()
|
|
59
|
+
self.setWindowTitle("BaseBender")
|
|
60
|
+
self.setGeometry(100, 100, 800, 600)
|
|
61
|
+
|
|
62
|
+
self.central_widget: QWidget = QWidget()
|
|
63
|
+
self.setCentralWidget(self.central_widget)
|
|
64
|
+
self.main_layout: QVBoxLayout = QVBoxLayout(self.central_widget)
|
|
65
|
+
|
|
66
|
+
self._setup_ui()
|
|
67
|
+
self._connect_signals()
|
|
68
|
+
self._load_initial_state()
|
|
69
|
+
self.status_bar: QStatusBar = self.statusBar() # Initialize status bar
|
|
70
|
+
|
|
71
|
+
def _load_svg_icon(self, qrc_path: str, size: int = 20) -> QSvgWidget:
|
|
72
|
+
"""
|
|
73
|
+
Loads an SVG icon from the Qt Resource System (QRC).
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
qrc_path: The path to the SVG icon within the Qt Resource System.
|
|
77
|
+
size: The desired size (width and height) for the SVG icon.
|
|
78
|
+
Defaults to 20.
|
|
79
|
+
Returns:
|
|
80
|
+
A QSvgWidget instance displaying the loaded SVG icon.
|
|
81
|
+
"""
|
|
82
|
+
# Create a QSvgWidget to render the SVG
|
|
83
|
+
svg_widget: QSvgWidget = QSvgWidget(qrc_path)
|
|
84
|
+
svg_widget.setFixedSize(QSize(size, size))
|
|
85
|
+
return svg_widget
|
|
86
|
+
|
|
87
|
+
def _setup_ui(self) -> None:
|
|
88
|
+
self._setup_input_area()
|
|
89
|
+
self._setup_digit_set_sections()
|
|
90
|
+
self._setup_rebase_controls()
|
|
91
|
+
self._setup_output_area()
|
|
92
|
+
|
|
93
|
+
def _setup_input_area(self) -> None:
|
|
94
|
+
input_layout = QHBoxLayout()
|
|
95
|
+
self.input_label = QLabel("Input String:")
|
|
96
|
+
input_icon = self._load_svg_icon(ICON_INPUT_QRC_PATH)
|
|
97
|
+
input_layout.addWidget(input_icon)
|
|
98
|
+
input_layout.addWidget(self.input_label)
|
|
99
|
+
input_layout.addStretch()
|
|
100
|
+
self.input_text_edit = QTextEdit()
|
|
101
|
+
self.input_text_edit.setPlaceholderText("Enter string to rebase...")
|
|
102
|
+
self.main_layout.addLayout(input_layout)
|
|
103
|
+
self.main_layout.addWidget(self.input_text_edit)
|
|
104
|
+
|
|
105
|
+
def _setup_digit_set_sections(self) -> None:
|
|
106
|
+
digit_set_layout = QHBoxLayout()
|
|
107
|
+
self.main_layout.addLayout(digit_set_layout)
|
|
108
|
+
self._setup_source_digit_set(digit_set_layout)
|
|
109
|
+
self._setup_target_digit_set(digit_set_layout)
|
|
110
|
+
|
|
111
|
+
def _setup_source_digit_set(self, parent_layout: QHBoxLayout) -> None:
|
|
112
|
+
group_layout = QVBoxLayout()
|
|
113
|
+
label_layout = QHBoxLayout()
|
|
114
|
+
label = QLabel("Input Digit Set:")
|
|
115
|
+
icon = self._load_svg_icon(ICON_SOURCE_QRC_PATH)
|
|
116
|
+
label_layout.addWidget(icon)
|
|
117
|
+
label_layout.addWidget(label)
|
|
118
|
+
label_layout.addStretch()
|
|
119
|
+
|
|
120
|
+
edit_layout = QHBoxLayout()
|
|
121
|
+
self.input_digit_set_text_edit = QTextEdit()
|
|
122
|
+
self.input_digit_set_text_edit.setFixedHeight(TEXT_EDIT_FIXED_HEIGHT)
|
|
123
|
+
self.input_digit_set_text_edit.setPlaceholderText(
|
|
124
|
+
"Enter input digit set or select preset (or leave empty for dynamic derivation)..."
|
|
125
|
+
)
|
|
126
|
+
self.input_digit_set_text_edit.setToolTip(
|
|
127
|
+
"Define the set of characters used in the input string. "
|
|
128
|
+
"Leave empty for dynamic derivation."
|
|
129
|
+
)
|
|
130
|
+
edit_layout.addWidget(self.input_digit_set_text_edit)
|
|
131
|
+
|
|
132
|
+
self.input_digit_set_tool_button = QToolButton()
|
|
133
|
+
self.input_digit_set_tool_button.setText("...")
|
|
134
|
+
self.input_digit_set_tool_button.setToolTip("Digit Set Manipulation Options")
|
|
135
|
+
self.input_digit_set_tool_button.setPopupMode(QToolButton.InstantPopup)
|
|
136
|
+
self.input_digit_set_tool_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
|
137
|
+
self._setup_digit_set_menu(
|
|
138
|
+
self.input_digit_set_tool_button, self.input_digit_set_text_edit
|
|
139
|
+
)
|
|
140
|
+
edit_layout.addWidget(self.input_digit_set_tool_button)
|
|
141
|
+
|
|
142
|
+
self.input_digit_set_preset_combo = QComboBox()
|
|
143
|
+
self.input_digit_set_preset_combo.addItem("Select Preset...")
|
|
144
|
+
self.input_digit_set_preset_combo.addItem("Derived from Input")
|
|
145
|
+
self._populate_digit_set_presets(self.input_digit_set_preset_combo)
|
|
146
|
+
self.input_digit_set_preset_combo.setToolTip(
|
|
147
|
+
"Select a predefined digit set or choose to derive from input."
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
group_layout.addLayout(label_layout)
|
|
151
|
+
group_layout.addLayout(edit_layout)
|
|
152
|
+
group_layout.addWidget(self.input_digit_set_preset_combo)
|
|
153
|
+
parent_layout.addLayout(group_layout)
|
|
154
|
+
|
|
155
|
+
def _setup_target_digit_set(self, parent_layout: QHBoxLayout) -> None:
|
|
156
|
+
group_layout = QVBoxLayout()
|
|
157
|
+
label_layout = QHBoxLayout()
|
|
158
|
+
label = QLabel("Output Digit Set:")
|
|
159
|
+
icon = self._load_svg_icon(ICON_TARGET_QRC_PATH)
|
|
160
|
+
label_layout.addWidget(icon)
|
|
161
|
+
label_layout.addWidget(label)
|
|
162
|
+
label_layout.addStretch()
|
|
163
|
+
|
|
164
|
+
edit_layout = QHBoxLayout()
|
|
165
|
+
self.output_digit_set_text_edit = QTextEdit()
|
|
166
|
+
self.output_digit_set_text_edit.setFixedHeight(TEXT_EDIT_FIXED_HEIGHT)
|
|
167
|
+
self.output_digit_set_text_edit.setPlaceholderText(
|
|
168
|
+
"Enter output digit set or select preset..."
|
|
169
|
+
)
|
|
170
|
+
self.output_digit_set_text_edit.setToolTip(
|
|
171
|
+
"Define the set of characters for the rebased output string."
|
|
172
|
+
)
|
|
173
|
+
edit_layout.addWidget(self.output_digit_set_text_edit)
|
|
174
|
+
|
|
175
|
+
self.output_digit_set_tool_button = QToolButton()
|
|
176
|
+
self.output_digit_set_tool_button.setText("...")
|
|
177
|
+
self.output_digit_set_tool_button.setToolTip("Digit Set Manipulation Options")
|
|
178
|
+
self.output_digit_set_tool_button.setPopupMode(QToolButton.InstantPopup)
|
|
179
|
+
self.output_digit_set_tool_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
|
180
|
+
self._setup_digit_set_menu(
|
|
181
|
+
self.output_digit_set_tool_button, self.output_digit_set_text_edit
|
|
182
|
+
)
|
|
183
|
+
edit_layout.addWidget(self.output_digit_set_tool_button)
|
|
184
|
+
|
|
185
|
+
self.output_digit_set_preset_combo = QComboBox()
|
|
186
|
+
self.output_digit_set_preset_combo.addItem("Select Preset...")
|
|
187
|
+
self._populate_digit_set_presets(self.output_digit_set_preset_combo)
|
|
188
|
+
self.output_digit_set_preset_combo.setToolTip(
|
|
189
|
+
"Select a predefined digit set for the output."
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
group_layout.addLayout(label_layout)
|
|
193
|
+
group_layout.addLayout(edit_layout)
|
|
194
|
+
group_layout.addWidget(self.output_digit_set_preset_combo)
|
|
195
|
+
parent_layout.addLayout(group_layout)
|
|
196
|
+
|
|
197
|
+
def _setup_rebase_controls(self) -> None:
|
|
198
|
+
rebase_control_layout = QHBoxLayout()
|
|
199
|
+
self.rebase_button = QPushButton("Rebase")
|
|
200
|
+
self.rebase_button.setToolTip("Perform the rebase operation based on current inputs.")
|
|
201
|
+
self.realtime_checkbox = QCheckBox("Real-time Rebase")
|
|
202
|
+
self.realtime_checkbox.setToolTip("Enable or disable automatic rebase as you type.")
|
|
203
|
+
rebase_control_layout.addWidget(self.rebase_button)
|
|
204
|
+
rebase_control_layout.addWidget(self.realtime_checkbox)
|
|
205
|
+
self.main_layout.addLayout(rebase_control_layout)
|
|
206
|
+
|
|
207
|
+
def _setup_output_area(self) -> None:
|
|
208
|
+
output_layout = QHBoxLayout()
|
|
209
|
+
self.output_label = QLabel("Rebased String:")
|
|
210
|
+
output_icon = self._load_svg_icon(ICON_OUTPUT_QRC_PATH)
|
|
211
|
+
output_layout.addWidget(output_icon)
|
|
212
|
+
output_layout.addWidget(self.output_label)
|
|
213
|
+
output_layout.addStretch()
|
|
214
|
+
self.output_text_edit = QTextEdit()
|
|
215
|
+
self.output_text_edit.setReadOnly(True)
|
|
216
|
+
self.output_text_edit.setPlaceholderText("Rebased output will appear here...")
|
|
217
|
+
self.output_text_edit.setToolTip("The result of the rebase operation.")
|
|
218
|
+
self.main_layout.addLayout(output_layout)
|
|
219
|
+
self.main_layout.addWidget(self.output_text_edit)
|
|
220
|
+
|
|
221
|
+
def _populate_digit_set_presets(self, combo_box: QComboBox) -> None:
|
|
222
|
+
"""
|
|
223
|
+
Populates the given QComboBox with predefined digit set names.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
combo_box: The QComboBox widget to populate.
|
|
227
|
+
"""
|
|
228
|
+
digit_sets: dict[str, DigitSet] = get_predefined_digit_sets()
|
|
229
|
+
for name in sorted(digit_sets.keys()):
|
|
230
|
+
combo_box.addItem(name)
|
|
231
|
+
|
|
232
|
+
def _connect_signals(self) -> None:
|
|
233
|
+
"""
|
|
234
|
+
Connects UI signals to their respective slots for interactive behavior.
|
|
235
|
+
"""
|
|
236
|
+
self.rebase_button.clicked.connect(self._perform_rebase)
|
|
237
|
+
self.input_digit_set_preset_combo.currentIndexChanged.connect(
|
|
238
|
+
self._update_input_ds_from_preset
|
|
239
|
+
)
|
|
240
|
+
self.output_digit_set_preset_combo.currentIndexChanged.connect(
|
|
241
|
+
self._update_output_ds_from_preset
|
|
242
|
+
)
|
|
243
|
+
self.realtime_checkbox.stateChanged.connect(self._toggle_realtime_rebase)
|
|
244
|
+
self.input_text_edit.textChanged.connect(self._on_input_changed)
|
|
245
|
+
self.input_digit_set_text_edit.textChanged.connect(self._on_digit_set_changed)
|
|
246
|
+
self.input_digit_set_text_edit.textChanged.connect(self._handle_input_ds_dynamic_state)
|
|
247
|
+
# Connect focusInEvent for clearing dynamic state
|
|
248
|
+
self.input_digit_set_text_edit.installEventFilter(self)
|
|
249
|
+
self.output_digit_set_text_edit.textChanged.connect(self._on_digit_set_changed)
|
|
250
|
+
|
|
251
|
+
def _update_input_ds_from_preset(self, index: int) -> None:
|
|
252
|
+
"""
|
|
253
|
+
Updates the input digit set text edit based on the selected preset
|
|
254
|
+
from the input digit set combo box.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
index: The index of the selected item in the combo box.
|
|
258
|
+
"""
|
|
259
|
+
if index > 0: # Skip "Select Preset..."
|
|
260
|
+
selected_name: str = self.input_digit_set_preset_combo.currentText()
|
|
261
|
+
if selected_name == "Derived from Input":
|
|
262
|
+
input_string_content: str = self.input_text_edit.toPlainText()
|
|
263
|
+
|
|
264
|
+
derived_digits = DigitSet.deduplicate_digits(input_string_content)
|
|
265
|
+
|
|
266
|
+
if derived_digits:
|
|
267
|
+
self.input_digit_set_text_edit.setText(derived_digits)
|
|
268
|
+
else:
|
|
269
|
+
self.input_digit_set_text_edit.clear()
|
|
270
|
+
self.input_digit_set_text_edit.setPlaceholderText(
|
|
271
|
+
"Enter input digit set or select preset (or leave empty "
|
|
272
|
+
"for dynamic derivation)..."
|
|
273
|
+
)
|
|
274
|
+
self.input_digit_set_text_edit.setStyleSheet("")
|
|
275
|
+
|
|
276
|
+
else:
|
|
277
|
+
digit_sets: dict[str, DigitSet] = get_predefined_digit_sets()
|
|
278
|
+
digit_set_obj: DigitSet | None = digit_sets.get(selected_name)
|
|
279
|
+
|
|
280
|
+
if digit_set_obj:
|
|
281
|
+
self.input_digit_set_text_edit.setText(digit_set_obj.digits)
|
|
282
|
+
else:
|
|
283
|
+
self.input_digit_set_text_edit.clear()
|
|
284
|
+
self.input_digit_set_text_edit.setPlaceholderText(
|
|
285
|
+
"Enter input digit set or select preset (or leave empty "
|
|
286
|
+
"for dynamic derivation)..."
|
|
287
|
+
)
|
|
288
|
+
self.input_digit_set_text_edit.setStyleSheet("")
|
|
289
|
+
|
|
290
|
+
def _update_output_ds_from_preset(self, index: int) -> None:
|
|
291
|
+
"""
|
|
292
|
+
Updates the output digit set text edit based on the selected preset
|
|
293
|
+
from the output digit set combo box.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
index: The index of the selected item in the combo box.
|
|
297
|
+
"""
|
|
298
|
+
if index > 0:
|
|
299
|
+
selected_name: str = self.output_digit_set_preset_combo.currentText()
|
|
300
|
+
|
|
301
|
+
digit_sets: dict[str, DigitSet] = get_predefined_digit_sets()
|
|
302
|
+
digit_set_obj: DigitSet | None = digit_sets.get(selected_name)
|
|
303
|
+
if digit_set_obj:
|
|
304
|
+
self.output_digit_set_text_edit.setText(digit_set_obj.digits)
|
|
305
|
+
else:
|
|
306
|
+
self.output_digit_set_text_edit.clear()
|
|
307
|
+
self.output_digit_set_text_edit.setStyleSheet("")
|
|
308
|
+
|
|
309
|
+
def _toggle_realtime_rebase(self, state: int) -> None:
|
|
310
|
+
"""
|
|
311
|
+
Toggles real-time rebase functionality based on the checkbox state.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
state: The new state of the checkbox (Qt.Checked or Qt.Unchecked).
|
|
315
|
+
"""
|
|
316
|
+
if state == Qt.Checked:
|
|
317
|
+
self.rebase_button.setEnabled(False)
|
|
318
|
+
self._perform_rebase() # Perform initial rebase
|
|
319
|
+
else:
|
|
320
|
+
self.rebase_button.setEnabled(True)
|
|
321
|
+
|
|
322
|
+
def _on_input_changed(self) -> None:
|
|
323
|
+
"""
|
|
324
|
+
Handles changes in the input string text edit.
|
|
325
|
+
Triggers rebase if real-time rebase is enabled and updates dynamic state.
|
|
326
|
+
"""
|
|
327
|
+
if self.realtime_checkbox.isChecked():
|
|
328
|
+
self._perform_rebase()
|
|
329
|
+
self._handle_input_ds_dynamic_state()
|
|
330
|
+
|
|
331
|
+
def _on_digit_set_changed(self) -> None:
|
|
332
|
+
"""
|
|
333
|
+
Handles changes in the digit set text edits (input and output).
|
|
334
|
+
Triggers rebase if real-time rebase is enabled and updates dynamic state.
|
|
335
|
+
"""
|
|
336
|
+
if self.realtime_checkbox.isChecked():
|
|
337
|
+
self._perform_rebase()
|
|
338
|
+
self._handle_input_ds_dynamic_state()
|
|
339
|
+
|
|
340
|
+
def _handle_input_ds_dynamic_state(self) -> None:
|
|
341
|
+
"""
|
|
342
|
+
Manages the dynamic placeholder text and styling for the input digit set
|
|
343
|
+
text edit based on the content of the input string.
|
|
344
|
+
|
|
345
|
+
If the input digit set field is empty and the input string is not empty,
|
|
346
|
+
it derives and displays a "Derived:" placeholder. Otherwise, it resets
|
|
347
|
+
the placeholder and styling.
|
|
348
|
+
"""
|
|
349
|
+
input_string_content: str = self.input_text_edit.toPlainText()
|
|
350
|
+
if not self.input_digit_set_text_edit.toPlainText(): # If input digit set field is empty
|
|
351
|
+
if input_string_content: # If input string is not empty
|
|
352
|
+
derived_digits = DigitSet.deduplicate_digits(input_string_content)
|
|
353
|
+
|
|
354
|
+
if derived_digits:
|
|
355
|
+
self.input_digit_set_text_edit.setPlaceholderText(f"Derived: {derived_digits}")
|
|
356
|
+
self.input_digit_set_text_edit.setStyleSheet("background-color: lightyellow;")
|
|
357
|
+
else:
|
|
358
|
+
self.input_digit_set_text_edit.setPlaceholderText(
|
|
359
|
+
"Enter input digit set or select preset (or leave empty "
|
|
360
|
+
"for dynamic derivation)..."
|
|
361
|
+
)
|
|
362
|
+
self.input_digit_set_text_edit.setStyleSheet("")
|
|
363
|
+
else:
|
|
364
|
+
self.input_digit_set_text_edit.setPlaceholderText(
|
|
365
|
+
"Enter input digit set or select preset (or leave empty "
|
|
366
|
+
"for dynamic derivation)..."
|
|
367
|
+
)
|
|
368
|
+
self.input_digit_set_text_edit.setStyleSheet("")
|
|
369
|
+
else:
|
|
370
|
+
self.input_digit_set_text_edit.setPlaceholderText(
|
|
371
|
+
"Enter input digit set or select preset (or leave empty for dynamic derivation)..."
|
|
372
|
+
)
|
|
373
|
+
self.input_digit_set_text_edit.setStyleSheet("")
|
|
374
|
+
|
|
375
|
+
def eventFilter(self, obj: QObject, event: QEvent) -> bool:
|
|
376
|
+
"""
|
|
377
|
+
Filters events for specific UI elements, primarily to handle focus
|
|
378
|
+
events for the input digit set text edit.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
obj: The object for which the event occurred.
|
|
382
|
+
event: The event that occurred.
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
True if the event was handled, False otherwise.
|
|
386
|
+
"""
|
|
387
|
+
if obj == self.input_digit_set_text_edit and event.type() == QEvent.FocusIn:
|
|
388
|
+
# If currently showing a derived placeholder
|
|
389
|
+
# and user focuses, clear it
|
|
390
|
+
if (
|
|
391
|
+
self.input_digit_set_text_edit.toPlainText() == ""
|
|
392
|
+
and self.input_digit_set_text_edit.placeholderText().startswith("Derived:")
|
|
393
|
+
):
|
|
394
|
+
self.input_digit_set_text_edit.setPlaceholderText(
|
|
395
|
+
"Enter input digit set or select preset (or leave empty "
|
|
396
|
+
"for dynamic derivation)..."
|
|
397
|
+
)
|
|
398
|
+
self.input_digit_set_text_edit.setStyleSheet("")
|
|
399
|
+
if self.input_digit_set_preset_combo.currentText() == "Derived from Input":
|
|
400
|
+
self.input_digit_set_preset_combo.setCurrentIndex(0)
|
|
401
|
+
return super().eventFilter(obj, event)
|
|
402
|
+
|
|
403
|
+
def _perform_rebase(self) -> None:
|
|
404
|
+
"""
|
|
405
|
+
Performs the rebase operation based on the current input string,
|
|
406
|
+
source digit set, and target digit set from the UI.
|
|
407
|
+
Displays the rebased string or an error message in the status bar.
|
|
408
|
+
"""
|
|
409
|
+
input_string: str = self.input_text_edit.toPlainText()
|
|
410
|
+
input_digit_set_str: str | None = self.input_digit_set_text_edit.toPlainText()
|
|
411
|
+
output_digit_set_str: str | None = self.output_digit_set_text_edit.toPlainText()
|
|
412
|
+
|
|
413
|
+
input_digit_set_obj: DigitSet | None = None
|
|
414
|
+
output_digit_set_obj: DigitSet | None = None
|
|
415
|
+
|
|
416
|
+
# Determine input digit set object
|
|
417
|
+
if input_digit_set_str:
|
|
418
|
+
input_digit_set_obj = DigitSet(
|
|
419
|
+
name="Custom Input",
|
|
420
|
+
digits=input_digit_set_str,
|
|
421
|
+
source="gui_input",
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
# Determine output digit set object
|
|
425
|
+
if output_digit_set_str:
|
|
426
|
+
if len(set(output_digit_set_str)) == 1:
|
|
427
|
+
output_digit_set_obj = None
|
|
428
|
+
else:
|
|
429
|
+
output_digit_set_obj = DigitSet(
|
|
430
|
+
name="Custom Output",
|
|
431
|
+
digits=output_digit_set_str,
|
|
432
|
+
source="gui_input",
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
self.status_bar.clearMessage() # Clear any previous messages
|
|
436
|
+
self.output_text_edit.clear() # Clear output on new rebase attempt
|
|
437
|
+
|
|
438
|
+
try:
|
|
439
|
+
rebaser = DigitSetRebaser(
|
|
440
|
+
out_digit_set=output_digit_set_obj,
|
|
441
|
+
in_digit_set=input_digit_set_obj,
|
|
442
|
+
)
|
|
443
|
+
rebased_string: str = rebaser.rebase(input_string)
|
|
444
|
+
self.output_text_edit.setText(rebased_string)
|
|
445
|
+
self.status_bar.clearMessage()
|
|
446
|
+
except ValueError as exc:
|
|
447
|
+
error_message: str = f"Error: {exc}"
|
|
448
|
+
self.status_bar.showMessage(error_message)
|
|
449
|
+
print(error_message, file=sys.stderr)
|
|
450
|
+
except IndexError as exc:
|
|
451
|
+
error_message = f"Error: {exc}"
|
|
452
|
+
self.status_bar.showMessage(error_message)
|
|
453
|
+
print(error_message, file=sys.stderr)
|
|
454
|
+
except (
|
|
455
|
+
RuntimeError
|
|
456
|
+
) as err: # Catching RuntimeError as an isolation point for unexpected errors.
|
|
457
|
+
error_message = f"An unexpected error occurred: {err}"
|
|
458
|
+
self.status_bar.showMessage(error_message)
|
|
459
|
+
print(error_message, file=sys.stderr)
|
|
460
|
+
|
|
461
|
+
def _setup_digit_set_menu(self, button: QToolButton, text_edit: QTextEdit) -> None:
|
|
462
|
+
"""
|
|
463
|
+
Sets up the context menu for digit set text edits, providing options
|
|
464
|
+
like Clear, Sort, and Deduplicate.
|
|
465
|
+
|
|
466
|
+
Args:
|
|
467
|
+
button: The QToolButton to attach the menu to.
|
|
468
|
+
text_edit: The QTextEdit associated with the digit set.
|
|
469
|
+
"""
|
|
470
|
+
menu = QMenu(self)
|
|
471
|
+
|
|
472
|
+
clear_action = menu.addAction("Clear")
|
|
473
|
+
clear_action.setToolTip("Clear the content of the digit set field.")
|
|
474
|
+
clear_action.triggered.connect(text_edit.clear)
|
|
475
|
+
|
|
476
|
+
sort_action = menu.addAction("Sort")
|
|
477
|
+
sort_action.setToolTip("Sort the characters in the digit set alphabetically.")
|
|
478
|
+
sort_action.triggered.connect(lambda: self._sort_digit_set_text(text_edit))
|
|
479
|
+
|
|
480
|
+
deduplicate_action = menu.addAction("Deduplicate")
|
|
481
|
+
deduplicate_action.setToolTip(
|
|
482
|
+
"Remove duplicate characters from the digit set, preserving order of first appearance."
|
|
483
|
+
)
|
|
484
|
+
deduplicate_action.triggered.connect(lambda: self._deduplicate_digit_set_text(text_edit))
|
|
485
|
+
|
|
486
|
+
button.setMenu(menu)
|
|
487
|
+
|
|
488
|
+
@staticmethod
|
|
489
|
+
def _sort_digit_set_text(text_edit: QTextEdit) -> None:
|
|
490
|
+
current_text = text_edit.toPlainText()
|
|
491
|
+
if current_text:
|
|
492
|
+
text_edit.setText(DigitSet.sorted_digits(current_text))
|
|
493
|
+
|
|
494
|
+
@staticmethod
|
|
495
|
+
def _deduplicate_digit_set_text(text_edit: QTextEdit) -> None:
|
|
496
|
+
current_text = text_edit.toPlainText()
|
|
497
|
+
if current_text:
|
|
498
|
+
text_edit.setText(DigitSet.deduplicate_digits(current_text))
|
|
499
|
+
|
|
500
|
+
def _load_initial_state(self) -> None:
|
|
501
|
+
"""
|
|
502
|
+
Loads the saved UI state (input string, digit sets, real-time setting)
|
|
503
|
+
from the configuration file and applies it to the UI elements.
|
|
504
|
+
"""
|
|
505
|
+
state: dict[str, Any] = load_ui_state()
|
|
506
|
+
if state:
|
|
507
|
+
self.input_text_edit.setText(state.get("last_input", ""))
|
|
508
|
+
self.input_digit_set_text_edit.setText(state.get("last_source_digit_set", ""))
|
|
509
|
+
self.output_digit_set_text_edit.setText(state.get("last_target_digit_set", ""))
|
|
510
|
+
self.realtime_checkbox.setChecked(state.get("realtime_enabled", False))
|
|
511
|
+
self._handle_input_ds_dynamic_state()
|
|
512
|
+
|
|
513
|
+
def closeEvent(self, event: QEvent) -> None:
|
|
514
|
+
"""
|
|
515
|
+
Overrides the default close event to save the current UI state
|
|
516
|
+
before the application exits.
|
|
517
|
+
|
|
518
|
+
Args:
|
|
519
|
+
event: The close event.
|
|
520
|
+
"""
|
|
521
|
+
state_data: dict[str, Any] = {
|
|
522
|
+
"last_input": self.input_text_edit.toPlainText(),
|
|
523
|
+
"last_source_digit_set": self.input_digit_set_text_edit.toPlainText(),
|
|
524
|
+
"last_target_digit_set": self.output_digit_set_text_edit.toPlainText(),
|
|
525
|
+
"realtime_enabled": self.realtime_checkbox.isChecked(),
|
|
526
|
+
}
|
|
527
|
+
save_ui_state(state_data)
|
|
528
|
+
event.accept()
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def run_gui() -> None:
|
|
532
|
+
"""
|
|
533
|
+
Runs the BaseBender GUI application.
|
|
534
|
+
|
|
535
|
+
Initializes the QApplication, creates and shows the MainWindow,
|
|
536
|
+
and starts the application's event loop.
|
|
537
|
+
"""
|
|
538
|
+
app: QApplication = QApplication(sys.argv)
|
|
539
|
+
window: MainWindow = MainWindow()
|
|
540
|
+
window.show()
|
|
541
|
+
# sys.exit() is used here as it's the standard way to exit a Qt application's event loop.
|
|
542
|
+
sys.exit(app.exec())
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
if __name__ == "__main__":
|
|
546
|
+
run_gui()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# `src/rebaser/` Directory Structure
|
|
2
|
+
|
|
3
|
+
This directory contains the core rebase logic and related modules.
|
|
4
|
+
|
|
5
|
+
## Folders:
|
|
6
|
+
|
|
7
|
+
* [`resources/`](src/rebaser/resources/STRUCTURE.md): Contains resources such as data files and icons.
|
|
8
|
+
* [`generated/`](src/rebaser/generated/STRUCTURE.md): Contains generated source files.
|
|
9
|
+
|
|
10
|
+
## Files:
|
|
11
|
+
|
|
12
|
+
* [`__init__.py`](src/rebaser/__init__.py): Initializes the `rebaser` package.
|
|
13
|
+
* [`config_loader.py`](src/rebaser/config_loader.py): Handles tiered configuration loading for digit sets.
|
|
14
|
+
* [`digit_set_rebaser.py`](src/rebaser/digit_set_rebaser.py): Implements the core rebase logic.
|
|
15
|
+
* [`digit_sets.py`](src/rebaser/digit_sets.py): Provides access to pre-defined digit sets and discovery mechanisms.
|
|
16
|
+
* [`models.py`](src/rebaser/models.py): Defines data models used within the rebaser module.
|