setiastrosuitepro 1.6.1.post1__py3-none-any.whl → 1.6.4__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.
- setiastro/images/Background_startup.jpg +0 -0
- setiastro/images/rotatearbitrary.png +0 -0
- setiastro/qml/ResourceMonitor.qml +126 -0
- setiastro/saspro/__main__.py +162 -25
- setiastro/saspro/_generated/build_info.py +2 -1
- setiastro/saspro/abe.py +62 -11
- setiastro/saspro/aberration_ai.py +3 -3
- setiastro/saspro/add_stars.py +5 -2
- setiastro/saspro/astrobin_exporter.py +3 -0
- setiastro/saspro/astrospike_python.py +3 -1
- setiastro/saspro/autostretch.py +4 -2
- setiastro/saspro/backgroundneutral.py +60 -9
- setiastro/saspro/batch_convert.py +3 -0
- setiastro/saspro/batch_renamer.py +3 -0
- setiastro/saspro/blemish_blaster.py +3 -0
- setiastro/saspro/blink_comparator_pro.py +474 -251
- setiastro/saspro/cheat_sheet.py +50 -15
- setiastro/saspro/clahe.py +27 -1
- setiastro/saspro/comet_stacking.py +103 -38
- setiastro/saspro/convo.py +3 -0
- setiastro/saspro/copyastro.py +3 -0
- setiastro/saspro/cosmicclarity.py +70 -45
- setiastro/saspro/crop_dialog_pro.py +28 -1
- setiastro/saspro/curve_editor_pro.py +18 -0
- setiastro/saspro/debayer.py +3 -0
- setiastro/saspro/doc_manager.py +40 -17
- setiastro/saspro/fitsmodifier.py +3 -0
- setiastro/saspro/frequency_separation.py +8 -2
- setiastro/saspro/function_bundle.py +18 -16
- setiastro/saspro/generate_translations.py +715 -1
- setiastro/saspro/ghs_dialog_pro.py +3 -0
- setiastro/saspro/graxpert.py +3 -0
- setiastro/saspro/gui/main_window.py +364 -92
- setiastro/saspro/gui/mixins/dock_mixin.py +119 -7
- setiastro/saspro/gui/mixins/file_mixin.py +7 -0
- setiastro/saspro/gui/mixins/geometry_mixin.py +105 -5
- setiastro/saspro/gui/mixins/menu_mixin.py +29 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +33 -10
- setiastro/saspro/gui/statistics_dialog.py +47 -0
- setiastro/saspro/halobgon.py +29 -3
- setiastro/saspro/histogram.py +3 -0
- setiastro/saspro/history_explorer.py +2 -0
- setiastro/saspro/i18n.py +22 -10
- setiastro/saspro/image_combine.py +3 -0
- setiastro/saspro/image_peeker_pro.py +3 -0
- setiastro/saspro/imageops/stretch.py +5 -13
- setiastro/saspro/isophote.py +3 -0
- setiastro/saspro/legacy/numba_utils.py +64 -47
- setiastro/saspro/linear_fit.py +3 -0
- setiastro/saspro/live_stacking.py +13 -2
- setiastro/saspro/mask_creation.py +3 -0
- setiastro/saspro/mfdeconv.py +5 -0
- setiastro/saspro/morphology.py +30 -5
- setiastro/saspro/multiscale_decomp.py +713 -256
- setiastro/saspro/nbtorgb_stars.py +12 -2
- setiastro/saspro/numba_utils.py +148 -47
- setiastro/saspro/ops/scripts.py +77 -17
- setiastro/saspro/ops/settings.py +1 -43
- setiastro/saspro/perfect_palette_picker.py +1 -0
- setiastro/saspro/pixelmath.py +6 -2
- setiastro/saspro/plate_solver.py +1 -0
- setiastro/saspro/remove_green.py +18 -1
- setiastro/saspro/remove_stars.py +136 -162
- setiastro/saspro/remove_stars_preset.py +55 -13
- setiastro/saspro/resources.py +36 -10
- setiastro/saspro/rgb_combination.py +1 -0
- setiastro/saspro/rgbalign.py +4 -4
- setiastro/saspro/save_options.py +1 -0
- setiastro/saspro/selective_color.py +79 -20
- setiastro/saspro/sfcc.py +50 -8
- setiastro/saspro/shortcuts.py +94 -21
- setiastro/saspro/signature_insert.py +3 -0
- setiastro/saspro/stacking_suite.py +924 -446
- setiastro/saspro/star_alignment.py +291 -331
- setiastro/saspro/star_spikes.py +116 -32
- setiastro/saspro/star_stretch.py +38 -1
- setiastro/saspro/stat_stretch.py +35 -3
- setiastro/saspro/status_log_dock.py +1 -1
- setiastro/saspro/subwindow.py +63 -2
- setiastro/saspro/supernovaasteroidhunter.py +3 -0
- setiastro/saspro/swap_manager.py +77 -42
- setiastro/saspro/translations/all_source_strings.json +4726 -0
- setiastro/saspro/translations/ar_translations.py +4096 -0
- setiastro/saspro/translations/de_translations.py +441 -446
- setiastro/saspro/translations/es_translations.py +278 -32
- setiastro/saspro/translations/fr_translations.py +280 -32
- setiastro/saspro/translations/hi_translations.py +3803 -0
- setiastro/saspro/translations/integrate_translations.py +38 -1
- setiastro/saspro/translations/it_translations.py +1211 -145
- setiastro/saspro/translations/ja_translations.py +556 -307
- setiastro/saspro/translations/pt_translations.py +3316 -3322
- setiastro/saspro/translations/ru_translations.py +3082 -0
- setiastro/saspro/translations/saspro_ar.qm +0 -0
- setiastro/saspro/translations/saspro_ar.ts +16019 -0
- setiastro/saspro/translations/saspro_de.qm +0 -0
- setiastro/saspro/translations/saspro_de.ts +14428 -133
- setiastro/saspro/translations/saspro_es.qm +0 -0
- setiastro/saspro/translations/saspro_es.ts +11503 -7821
- setiastro/saspro/translations/saspro_fr.qm +0 -0
- setiastro/saspro/translations/saspro_fr.ts +11168 -7812
- setiastro/saspro/translations/saspro_hi.qm +0 -0
- setiastro/saspro/translations/saspro_hi.ts +14855 -0
- setiastro/saspro/translations/saspro_it.qm +0 -0
- setiastro/saspro/translations/saspro_it.ts +14347 -7821
- setiastro/saspro/translations/saspro_ja.qm +0 -0
- setiastro/saspro/translations/saspro_ja.ts +14860 -137
- setiastro/saspro/translations/saspro_pt.qm +0 -0
- setiastro/saspro/translations/saspro_pt.ts +14904 -137
- setiastro/saspro/translations/saspro_ru.qm +0 -0
- setiastro/saspro/translations/saspro_ru.ts +11835 -0
- setiastro/saspro/translations/saspro_sw.qm +0 -0
- setiastro/saspro/translations/saspro_sw.ts +15237 -0
- setiastro/saspro/translations/saspro_uk.qm +0 -0
- setiastro/saspro/translations/saspro_uk.ts +15248 -0
- setiastro/saspro/translations/saspro_zh.qm +0 -0
- setiastro/saspro/translations/saspro_zh.ts +10581 -7812
- setiastro/saspro/translations/sw_translations.py +3897 -0
- setiastro/saspro/translations/uk_translations.py +3929 -0
- setiastro/saspro/translations/zh_translations.py +283 -32
- setiastro/saspro/versioning.py +36 -5
- setiastro/saspro/view_bundle.py +20 -17
- setiastro/saspro/wavescale_hdr.py +22 -1
- setiastro/saspro/wavescalede.py +23 -1
- setiastro/saspro/whitebalance.py +39 -3
- setiastro/saspro/widgets/minigame/game.js +991 -0
- setiastro/saspro/widgets/minigame/index.html +53 -0
- setiastro/saspro/widgets/minigame/style.css +241 -0
- setiastro/saspro/widgets/resource_monitor.py +263 -0
- setiastro/saspro/widgets/spinboxes.py +18 -0
- setiastro/saspro/widgets/wavelet_utils.py +52 -20
- setiastro/saspro/wimi.py +100 -80
- setiastro/saspro/wims.py +33 -33
- setiastro/saspro/window_shelf.py +2 -2
- {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/METADATA +15 -4
- {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/RECORD +139 -115
- {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/licenses/license.txt +0 -0
|
@@ -14,7 +14,7 @@ from PyQt6.QtWidgets import (
|
|
|
14
14
|
QVBoxLayout, QWidget, QTextEdit, QListWidget, QListWidgetItem,
|
|
15
15
|
QAbstractItemView, QApplication
|
|
16
16
|
)
|
|
17
|
-
from PyQt6.QtGui import QTextCursor
|
|
17
|
+
from PyQt6.QtGui import QTextCursor, QAction
|
|
18
18
|
|
|
19
19
|
if TYPE_CHECKING:
|
|
20
20
|
from PyQt6.QtWidgets import QAction
|
|
@@ -194,6 +194,99 @@ class DockMixin:
|
|
|
194
194
|
except Exception:
|
|
195
195
|
pass
|
|
196
196
|
|
|
197
|
+
def _init_resource_monitor_overlay(self):
|
|
198
|
+
"""Initialize the QML System Resource Monitor as a floating overlay."""
|
|
199
|
+
try:
|
|
200
|
+
from setiastro.saspro.widgets.resource_monitor import SystemMonitorWidget
|
|
201
|
+
|
|
202
|
+
# Create as a child of the central widget or self to sit on top
|
|
203
|
+
# Using self (QMainWindow) allows it to float over everything including status bar if we want,
|
|
204
|
+
# but usually we want it over MDI area. Let's try self first for "floating" feel.
|
|
205
|
+
self.resource_monitor = SystemMonitorWidget(self)
|
|
206
|
+
self.resource_monitor.setObjectName("ResourceMonitorOverlay")
|
|
207
|
+
|
|
208
|
+
# Make it a proper independent window to allow true transparency (translucent background)
|
|
209
|
+
# without black artifacts from parent composition.
|
|
210
|
+
# Fixed: Removed WindowStaysOnTopHint to allow it to be obscured by other apps (Alt-Tab support)
|
|
211
|
+
self.resource_monitor.setWindowFlags(
|
|
212
|
+
Qt.WindowType.Window |
|
|
213
|
+
Qt.WindowType.FramelessWindowHint |
|
|
214
|
+
Qt.WindowType.Tool
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Sizing and Transparency
|
|
218
|
+
self.resource_monitor.setFixedSize(200, 60)
|
|
219
|
+
# self.resource_monitor.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True) # Optional: if we want click-through
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# Initial placement (will be updated by resizeEvent)
|
|
223
|
+
self._update_monitor_position()
|
|
224
|
+
|
|
225
|
+
# Defer visibility to MainWindow.showEvent to prevent appearing before main window
|
|
226
|
+
# visible = self.settings.value("ui/resource_monitor_visible", True, type=bool)
|
|
227
|
+
# if visible:
|
|
228
|
+
# self.resource_monitor.show()
|
|
229
|
+
# else:
|
|
230
|
+
# self.resource_monitor.hide()
|
|
231
|
+
except Exception as e:
|
|
232
|
+
print(f"WARNING: Could not initialize System Monitor overlay: {e}")
|
|
233
|
+
self.resource_monitor = None
|
|
234
|
+
|
|
235
|
+
def _toggle_resource_monitor(self, checked: bool):
|
|
236
|
+
"""Toggle floating monitor visibility."""
|
|
237
|
+
if hasattr(self, 'resource_monitor') and self.resource_monitor:
|
|
238
|
+
if checked:
|
|
239
|
+
self.resource_monitor.show()
|
|
240
|
+
self._update_monitor_position()
|
|
241
|
+
else:
|
|
242
|
+
self.resource_monitor.hide()
|
|
243
|
+
self.settings.setValue("ui/resource_monitor_visible", checked)
|
|
244
|
+
|
|
245
|
+
def _update_monitor_position(self):
|
|
246
|
+
"""Snap monitor to bottom-right corner or restore saved position."""
|
|
247
|
+
if hasattr(self, 'resource_monitor') and self.resource_monitor:
|
|
248
|
+
from PyQt6.QtCore import QPoint
|
|
249
|
+
|
|
250
|
+
# Check for saved position first
|
|
251
|
+
saved_x = self.settings.value("ui/resource_monitor_pos_x", type=int)
|
|
252
|
+
saved_y = self.settings.value("ui/resource_monitor_pos_y", type=int)
|
|
253
|
+
|
|
254
|
+
if saved_x != 0 and saved_y != 0: # Basic validity check (0,0 is unlikely to be desired but also default if missing)
|
|
255
|
+
# Actually 0,0 is valid but type=int returns 0 if missing.
|
|
256
|
+
# Let's check string existence to be safer or just accept 0 if set.
|
|
257
|
+
# Checking existence via `contains` is better but value() logic is ok for now.
|
|
258
|
+
if self.settings.contains("ui/resource_monitor_pos_x"):
|
|
259
|
+
self.resource_monitor.move(saved_x, saved_y)
|
|
260
|
+
self.resource_monitor.raise_()
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
m = 5 # margin
|
|
264
|
+
|
|
265
|
+
screen = self.screen()
|
|
266
|
+
geom = screen.availableGeometry()
|
|
267
|
+
|
|
268
|
+
mw = self.resource_monitor.width()
|
|
269
|
+
mh = self.resource_monitor.height()
|
|
270
|
+
|
|
271
|
+
x = geom.x() + geom.width() - mw - m
|
|
272
|
+
y = geom.y() + geom.height() - mh - m
|
|
273
|
+
|
|
274
|
+
self.resource_monitor.move(x, y)
|
|
275
|
+
self.resource_monitor.raise_()
|
|
276
|
+
|
|
277
|
+
# We need to hook resizeEvent to call _update_monitor_position.
|
|
278
|
+
# Since this is a mixin, we can't easily override resizeEvent of the MainWindow without being careful.
|
|
279
|
+
# Best way: install an event filter on self, or since we are a mixin mixed into MainWindow,
|
|
280
|
+
# we can rely on MainWindow calling a specific method or we can patch it...
|
|
281
|
+
# Actually, MainWindow likely has resizeEvent.
|
|
282
|
+
# simpler: QTimer check? No.
|
|
283
|
+
# Correct way for Mixin: The MainWindow class should call something.
|
|
284
|
+
# BUT, I can just installEventFilter(self) ? No, infinite loop risk.
|
|
285
|
+
#
|
|
286
|
+
# Let's use the 'GeometryMixin' or just add a standard method `_on_resize_for_monitor`
|
|
287
|
+
# and assume I can hook it in MainWindow.py.
|
|
288
|
+
|
|
289
|
+
|
|
197
290
|
# ⌠Remove this old line; it let random mouse-over updates hijack the dock:
|
|
198
291
|
# self.currentDocumentChanged.disconnect(self.header_viewer.set_document) # if previously connected
|
|
199
292
|
# (If you prefer to keep the signal for explicit tab switches, it's fine to leave
|
|
@@ -210,13 +303,28 @@ class DockMixin:
|
|
|
210
303
|
|
|
211
304
|
# Friendly ordering for common ones; others follow alphabetically.
|
|
212
305
|
order_hint = {
|
|
213
|
-
"Explorer": 10,
|
|
214
|
-
"Console / Status": 20,
|
|
215
|
-
"Header Viewer": 30,
|
|
216
|
-
"Layers": 40,
|
|
217
|
-
"Window Shelf": 50,
|
|
218
|
-
"Command Search": 60,
|
|
306
|
+
self.tr("Explorer"): 10,
|
|
307
|
+
self.tr("Console / Status"): 20,
|
|
308
|
+
self.tr("Header Viewer"): 30,
|
|
309
|
+
self.tr("Layers"): 40,
|
|
310
|
+
self.tr("Window Shelf"): 50,
|
|
311
|
+
self.tr("Command Search"): 60,
|
|
219
312
|
}
|
|
313
|
+
|
|
314
|
+
# Add special action for overlay monitor
|
|
315
|
+
mon_act = QAction(self.tr("System Monitor"), self)
|
|
316
|
+
mon_act.setCheckable(True)
|
|
317
|
+
mon_act.setChecked(self.settings.value("ui/resource_monitor_visible", True, type=bool))
|
|
318
|
+
mon_act.triggered.connect(self._toggle_resource_monitor)
|
|
319
|
+
|
|
320
|
+
# We need to insert it into the logic that populates the menu.
|
|
321
|
+
# But 'dock_mixin' automates menu from self.findChildren(QDockWidget).
|
|
322
|
+
# So we have to manually inject this action into the "Panels" menu if possible
|
|
323
|
+
# or expose it such that main_window can add it.
|
|
324
|
+
#
|
|
325
|
+
# Easier: allow main_window to add it, or ...
|
|
326
|
+
# If I can't easily see where menu is built, I'll bind it to self.act_toggle_monitor = mon_act
|
|
327
|
+
self.act_toggle_monitor = mon_act
|
|
220
328
|
|
|
221
329
|
def key_fn(d: QDockWidget):
|
|
222
330
|
t = d.windowTitle()
|
|
@@ -224,6 +332,10 @@ class DockMixin:
|
|
|
224
332
|
|
|
225
333
|
for dock in sorted(docks, key=key_fn):
|
|
226
334
|
self._register_dock_in_view_menu(dock)
|
|
335
|
+
|
|
336
|
+
if hasattr(self, "act_toggle_monitor"):
|
|
337
|
+
menu.addSeparator()
|
|
338
|
+
menu.addAction(self.act_toggle_monitor)
|
|
227
339
|
|
|
228
340
|
def _add_doc_to_explorer(self, doc):
|
|
229
341
|
base = self._normalize_base_doc(doc)
|
|
@@ -120,6 +120,13 @@ class FileMixin:
|
|
|
120
120
|
doc = self.docman.open_path(p) # this emits documentAdded
|
|
121
121
|
self._log(f"Opened: {p}")
|
|
122
122
|
self._add_recent_image(p) # âœ... track in MRU
|
|
123
|
+
|
|
124
|
+
# Increment statistics
|
|
125
|
+
try:
|
|
126
|
+
count = self.settings.value("stats/opened_images_count", 0, type=int)
|
|
127
|
+
self.settings.setValue("stats/opened_images_count", count + 1)
|
|
128
|
+
except Exception:
|
|
129
|
+
pass
|
|
123
130
|
except Exception as e:
|
|
124
131
|
QMessageBox.warning(self, self.tr("Open failed"), f"{p}\n\n{e}")
|
|
125
132
|
|
|
@@ -51,12 +51,10 @@ except ImportError:
|
|
|
51
51
|
return cv2.resize(arr, (new_w, new_h), interpolation=cv2.INTER_LINEAR)
|
|
52
52
|
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
try:
|
|
56
|
-
from setiastro.saspro.wcs_utils import update_wcs_after_crop
|
|
57
|
-
except ImportError:
|
|
58
|
-
update_wcs_after_crop = None
|
|
54
|
+
from setiastro.saspro.wcs_update import update_wcs_after_crop
|
|
59
55
|
|
|
56
|
+
import cv2
|
|
57
|
+
import math
|
|
60
58
|
|
|
61
59
|
if TYPE_CHECKING:
|
|
62
60
|
pass
|
|
@@ -209,6 +207,44 @@ class GeometryMixin:
|
|
|
209
207
|
except Exception as e:
|
|
210
208
|
QMessageBox.critical(self, "Rotate 180°", str(e))
|
|
211
209
|
|
|
210
|
+
def _exec_geom_rot_any(self):
|
|
211
|
+
sw = self.mdi.activeSubWindow() if hasattr(self, "mdi") else None
|
|
212
|
+
view = sw.widget() if sw else None
|
|
213
|
+
doc = getattr(view, "document", None)
|
|
214
|
+
if doc is None or getattr(doc, "image", None) is None:
|
|
215
|
+
QMessageBox.information(self, self.tr("Rotate..."), self.tr("Active view has no image."))
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
if cv2 is None:
|
|
219
|
+
QMessageBox.warning(self, self.tr("Rotate..."), self.tr("OpenCV (cv2) is required for arbitrary rotation."))
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
dlg = QInputDialog(self)
|
|
223
|
+
dlg.setWindowTitle(self.tr("Rotate..."))
|
|
224
|
+
dlg.setLabelText(self.tr("Angle in degrees (positive = CCW):"))
|
|
225
|
+
dlg.setInputMode(QInputDialog.InputMode.DoubleInput)
|
|
226
|
+
dlg.setDoubleRange(-360.0, 360.0)
|
|
227
|
+
dlg.setDoubleDecimals(2)
|
|
228
|
+
dlg.setDoubleValue(0.0)
|
|
229
|
+
dlg.setWindowFlag(Qt.WindowType.Window, True)
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
from setiastro.saspro.resources import rotatearbitrary_path
|
|
233
|
+
dlg.setWindowIcon(QIcon(rotatearbitrary_path))
|
|
234
|
+
except Exception:
|
|
235
|
+
pass
|
|
236
|
+
|
|
237
|
+
if dlg.exec() != QDialog.DialogCode.Accepted:
|
|
238
|
+
return
|
|
239
|
+
|
|
240
|
+
angle = float(dlg.doubleValue())
|
|
241
|
+
try:
|
|
242
|
+
self._apply_geom_rot_any_to_doc(doc, angle_deg=angle)
|
|
243
|
+
self._log(f"Rotate ({angle:g}°) applied to active view")
|
|
244
|
+
except Exception as e:
|
|
245
|
+
QMessageBox.critical(self, self.tr("Rotate..."), str(e))
|
|
246
|
+
|
|
247
|
+
|
|
212
248
|
def _exec_geom_rescale(self):
|
|
213
249
|
"""Execute rescale operation on active view with dialog."""
|
|
214
250
|
sw = self.mdi.activeSubWindow() if hasattr(self, "mdi") else None
|
|
@@ -334,6 +370,70 @@ class GeometryMixin:
|
|
|
334
370
|
|
|
335
371
|
self._apply_geom_with_wcs(doc, out, M_src_to_dst=M, step_name="Rotate 180°")
|
|
336
372
|
|
|
373
|
+
def _apply_geom_rot_any_to_doc(self, doc, *, angle_deg: float):
|
|
374
|
+
if cv2 is None:
|
|
375
|
+
raise RuntimeError("cv2 is required for arbitrary rotation")
|
|
376
|
+
|
|
377
|
+
src = np.asarray(doc.image, dtype=np.float32, order="C")
|
|
378
|
+
h, w = src.shape[:2]
|
|
379
|
+
|
|
380
|
+
# Rotation about center
|
|
381
|
+
cx = (w - 1) * 0.5
|
|
382
|
+
cy = (h - 1) * 0.5
|
|
383
|
+
|
|
384
|
+
# OpenCV uses CCW degrees
|
|
385
|
+
A2 = cv2.getRotationMatrix2D((cx, cy), angle_deg, 1.0) # 2x3
|
|
386
|
+
|
|
387
|
+
# Convert to 3x3
|
|
388
|
+
M = np.array([
|
|
389
|
+
[A2[0,0], A2[0,1], A2[0,2]],
|
|
390
|
+
[A2[1,0], A2[1,1], A2[1,2]],
|
|
391
|
+
[0.0, 0.0, 1.0 ],
|
|
392
|
+
], dtype=np.float32)
|
|
393
|
+
|
|
394
|
+
# Compute output bounds by rotating the four corners
|
|
395
|
+
corners = np.array([
|
|
396
|
+
[0.0, 0.0, 1.0],
|
|
397
|
+
[w - 1.0, 0.0, 1.0],
|
|
398
|
+
[w - 1.0, h - 1.0, 1.0],
|
|
399
|
+
[0.0, h - 1.0, 1.0],
|
|
400
|
+
], dtype=np.float32).T # 3x4
|
|
401
|
+
|
|
402
|
+
rc = (M @ corners) # 3x4
|
|
403
|
+
xs = rc[0, :]
|
|
404
|
+
ys = rc[1, :]
|
|
405
|
+
|
|
406
|
+
min_x = float(xs.min())
|
|
407
|
+
max_x = float(xs.max())
|
|
408
|
+
min_y = float(ys.min())
|
|
409
|
+
max_y = float(ys.max())
|
|
410
|
+
|
|
411
|
+
out_w = int(math.ceil(max_x - min_x + 1.0))
|
|
412
|
+
out_h = int(math.ceil(max_y - min_y + 1.0))
|
|
413
|
+
if out_w <= 0 or out_h <= 0:
|
|
414
|
+
raise RuntimeError("Invalid output size after rotation")
|
|
415
|
+
|
|
416
|
+
# Shift so that min corner maps to (0,0)
|
|
417
|
+
T = np.array([
|
|
418
|
+
[1.0, 0.0, -min_x],
|
|
419
|
+
[0.0, 1.0, -min_y],
|
|
420
|
+
[0.0, 0.0, 1.0],
|
|
421
|
+
], dtype=np.float32)
|
|
422
|
+
|
|
423
|
+
M = (T @ M).astype(np.float32) # final src->dst 3x3
|
|
424
|
+
|
|
425
|
+
# Warp
|
|
426
|
+
# cv2.warpPerspective expects (W,H)
|
|
427
|
+
flags = cv2.INTER_LANCZOS4
|
|
428
|
+
if src.ndim == 2:
|
|
429
|
+
out = cv2.warpPerspective(src, M, (out_w, out_h), flags=flags)
|
|
430
|
+
else:
|
|
431
|
+
# warpPerspective works on multi-channel too
|
|
432
|
+
out = cv2.warpPerspective(src, M, (out_w, out_h), flags=flags)
|
|
433
|
+
|
|
434
|
+
self._apply_geom_with_wcs(doc, out, M_src_to_dst=M, step_name=f"Rotate ({angle_deg:g}°)")
|
|
435
|
+
|
|
436
|
+
|
|
337
437
|
def _apply_geom_rescale_to_doc(self, doc, *, factor: float):
|
|
338
438
|
"""Apply rescale to document with WCS update."""
|
|
339
439
|
factor = float(max(0.1, min(10.0, factor)))
|
|
@@ -25,6 +25,28 @@ class MenuMixin:
|
|
|
25
25
|
# This method will be implemented as part of the main window
|
|
26
26
|
# For now, this is a placeholder showing the mixin pattern
|
|
27
27
|
pass
|
|
28
|
+
|
|
29
|
+
def _show_statistics(self):
|
|
30
|
+
from setiastro.saspro.gui.statistics_dialog import StatisticsDialog
|
|
31
|
+
dlg = StatisticsDialog(self)
|
|
32
|
+
dlg.exec()
|
|
33
|
+
|
|
34
|
+
def _hook_tool_stats(self, menus):
|
|
35
|
+
if not hasattr(self, "_on_tool_triggered"):
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
seen = set()
|
|
39
|
+
for menu in menus:
|
|
40
|
+
for action in self._iter_menu_actions(menu):
|
|
41
|
+
if action in seen: continue
|
|
42
|
+
seen.add(action)
|
|
43
|
+
if action.isSeparator(): continue
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
action.triggered.connect(self._on_tool_triggered)
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
49
|
+
|
|
28
50
|
|
|
29
51
|
def _rebuild_recent_menus(self):
|
|
30
52
|
"""Rebuild the recent files and projects menus."""
|
|
@@ -163,6 +185,7 @@ class MenuMixin:
|
|
|
163
185
|
m_geom.addAction(self.act_geom_rot_cw)
|
|
164
186
|
m_geom.addAction(self.act_geom_rot_ccw)
|
|
165
187
|
m_geom.addAction(self.act_geom_rot_180)
|
|
188
|
+
m_geom.addAction(self.act_geom_rot_any)
|
|
166
189
|
m_geom.addSeparator()
|
|
167
190
|
m_geom.addAction(self.act_geom_rescale)
|
|
168
191
|
m_geom.addSeparator()
|
|
@@ -298,6 +321,12 @@ class MenuMixin:
|
|
|
298
321
|
m_about.addAction(self.act_check_updates)
|
|
299
322
|
|
|
300
323
|
|
|
324
|
+
m_about.addSeparator()
|
|
325
|
+
m_about.addAction(self.tr("Statistics..."), self._show_statistics)
|
|
326
|
+
|
|
327
|
+
# Connect tool stats
|
|
328
|
+
self._hook_tool_stats([m_fn, m_tools, mCosmic, m_geom, m_star, m_masks, m_header, m_scripts])
|
|
329
|
+
|
|
301
330
|
# initialize enabled state + names
|
|
302
331
|
self.update_undo_redo_action_labels()
|
|
303
332
|
|
|
@@ -10,6 +10,9 @@ from PyQt6.QtCore import Qt, QTimer, QUrl
|
|
|
10
10
|
from PyQt6.QtGui import QAction, QActionGroup, QIcon, QKeySequence, QDesktopServices
|
|
11
11
|
from PyQt6.QtWidgets import QMenu, QToolButton
|
|
12
12
|
|
|
13
|
+
from PyQt6.QtCore import QElapsedTimer
|
|
14
|
+
|
|
15
|
+
|
|
13
16
|
if TYPE_CHECKING:
|
|
14
17
|
pass
|
|
15
18
|
|
|
@@ -20,7 +23,7 @@ from setiastro.saspro.resources import (
|
|
|
20
23
|
LInsert_path, rgbcombo_path, rgbextract_path, graxperticon_path,
|
|
21
24
|
cropicon_path, openfile_path, abeicon_path, undoicon_path, redoicon_path,
|
|
22
25
|
blastericon_path, hdr_path, invert_path, fliphorizontal_path,
|
|
23
|
-
flipvertical_path, rotateclockwise_path, rotatecounterclockwise_path,
|
|
26
|
+
flipvertical_path, rotateclockwise_path, rotatecounterclockwise_path,rotatearbitrary_path,
|
|
24
27
|
rotate180_path, maskcreate_path, maskapply_path, maskremove_path,
|
|
25
28
|
pixelmath_path, histogram_path, mosaic_path, rescale_path, staralign_path,
|
|
26
29
|
platesolve_path, psf_path, supernova_path, starregistration_path,
|
|
@@ -73,7 +76,8 @@ class ToolbarMixin:
|
|
|
73
76
|
|
|
74
77
|
def _init_toolbar(self):
|
|
75
78
|
# View toolbar (Undo / Redo / Display-Stretch)
|
|
76
|
-
tb = DraggableToolBar("View", self)
|
|
79
|
+
tb = DraggableToolBar(self.tr("View"), self)
|
|
80
|
+
tb.setObjectName("View")
|
|
77
81
|
tb.setSettingsKey("Toolbar/View")
|
|
78
82
|
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb)
|
|
79
83
|
|
|
@@ -175,7 +179,8 @@ class ToolbarMixin:
|
|
|
175
179
|
pass
|
|
176
180
|
|
|
177
181
|
# Functions toolbar
|
|
178
|
-
tb_fn = DraggableToolBar("Functions", self)
|
|
182
|
+
tb_fn = DraggableToolBar(self.tr("Functions"), self)
|
|
183
|
+
tb_fn.setObjectName("Functions")
|
|
179
184
|
tb_fn.setSettingsKey("Toolbar/Functions")
|
|
180
185
|
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_fn)
|
|
181
186
|
|
|
@@ -227,7 +232,8 @@ class ToolbarMixin:
|
|
|
227
232
|
except Exception:
|
|
228
233
|
pass
|
|
229
234
|
|
|
230
|
-
tbCosmic = DraggableToolBar("Cosmic Clarity", self)
|
|
235
|
+
tbCosmic = DraggableToolBar(self.tr("Cosmic Clarity"), self)
|
|
236
|
+
tbCosmic.setObjectName("Cosmic Clarity")
|
|
231
237
|
tbCosmic.setSettingsKey("Toolbar/Cosmic")
|
|
232
238
|
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tbCosmic)
|
|
233
239
|
|
|
@@ -241,7 +247,8 @@ class ToolbarMixin:
|
|
|
241
247
|
except Exception:
|
|
242
248
|
pass
|
|
243
249
|
|
|
244
|
-
tb_tl = DraggableToolBar("Tools", self)
|
|
250
|
+
tb_tl = DraggableToolBar(self.tr("Tools"), self)
|
|
251
|
+
tb_tl.setObjectName("Tools")
|
|
245
252
|
tb_tl.setSettingsKey("Toolbar/Tools")
|
|
246
253
|
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_tl)
|
|
247
254
|
|
|
@@ -260,7 +267,8 @@ class ToolbarMixin:
|
|
|
260
267
|
except Exception:
|
|
261
268
|
pass
|
|
262
269
|
|
|
263
|
-
tb_geom = DraggableToolBar("Geometry", self)
|
|
270
|
+
tb_geom = DraggableToolBar(self.tr("Geometry"), self)
|
|
271
|
+
tb_geom.setObjectName("Geometry")
|
|
264
272
|
tb_geom.setSettingsKey("Toolbar/Geometry")
|
|
265
273
|
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_geom)
|
|
266
274
|
|
|
@@ -272,6 +280,7 @@ class ToolbarMixin:
|
|
|
272
280
|
tb_geom.addAction(self.act_geom_rot_cw)
|
|
273
281
|
tb_geom.addAction(self.act_geom_rot_ccw)
|
|
274
282
|
tb_geom.addAction(self.act_geom_rot_180)
|
|
283
|
+
tb_geom.addAction(self.act_geom_rot_any)
|
|
275
284
|
tb_geom.addSeparator()
|
|
276
285
|
tb_geom.addAction(self.act_geom_rescale)
|
|
277
286
|
tb_geom.addSeparator()
|
|
@@ -283,7 +292,8 @@ class ToolbarMixin:
|
|
|
283
292
|
except Exception:
|
|
284
293
|
pass
|
|
285
294
|
|
|
286
|
-
tb_star = DraggableToolBar("Star Stuff", self)
|
|
295
|
+
tb_star = DraggableToolBar(self.tr("Star Stuff"), self)
|
|
296
|
+
tb_star.setObjectName("Star Stuff")
|
|
287
297
|
tb_star.setSettingsKey("Toolbar/StarStuff")
|
|
288
298
|
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_star)
|
|
289
299
|
|
|
@@ -308,7 +318,8 @@ class ToolbarMixin:
|
|
|
308
318
|
except Exception:
|
|
309
319
|
pass
|
|
310
320
|
|
|
311
|
-
tb_msk = DraggableToolBar("Masks", self)
|
|
321
|
+
tb_msk = DraggableToolBar(self.tr("Masks"), self)
|
|
322
|
+
tb_msk.setObjectName("Masks")
|
|
312
323
|
tb_msk.setSettingsKey("Toolbar/Masks")
|
|
313
324
|
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_msk)
|
|
314
325
|
|
|
@@ -322,7 +333,8 @@ class ToolbarMixin:
|
|
|
322
333
|
except Exception:
|
|
323
334
|
pass
|
|
324
335
|
|
|
325
|
-
tb_wim = DraggableToolBar("What's In My...", self)
|
|
336
|
+
tb_wim = DraggableToolBar(self.tr("What's In My..."), self)
|
|
337
|
+
tb_wim.setObjectName("What's In My...")
|
|
326
338
|
tb_wim.setSettingsKey("Toolbar/WhatsInMy")
|
|
327
339
|
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_wim)
|
|
328
340
|
|
|
@@ -335,7 +347,8 @@ class ToolbarMixin:
|
|
|
335
347
|
except Exception:
|
|
336
348
|
pass
|
|
337
349
|
|
|
338
|
-
tb_bundle = DraggableToolBar("Bundles", self)
|
|
350
|
+
tb_bundle = DraggableToolBar(self.tr("Bundles"), self)
|
|
351
|
+
tb_bundle.setObjectName("Bundles")
|
|
339
352
|
tb_bundle.setSettingsKey("Toolbar/Bundles")
|
|
340
353
|
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_bundle)
|
|
341
354
|
|
|
@@ -873,6 +886,12 @@ class ToolbarMixin:
|
|
|
873
886
|
self.act_geom_rot_180.setStatusTip(self.tr("Rotate image 180°"))
|
|
874
887
|
self.act_geom_rot_180.triggered.connect(self._exec_geom_rot_180)
|
|
875
888
|
|
|
889
|
+
self.act_geom_rot_any = QAction(QIcon(rotatearbitrary_path), self.tr("Rotate..."), self)
|
|
890
|
+
self.act_geom_rot_any.setIconVisibleInMenu(True)
|
|
891
|
+
self.act_geom_rot_any.setStatusTip(self.tr("Rotate image by an arbitrary angle (degrees)"))
|
|
892
|
+
self.act_geom_rot_any.triggered.connect(self._exec_geom_rot_any)
|
|
893
|
+
|
|
894
|
+
|
|
876
895
|
self.act_geom_rescale = QAction(QIcon(rescale_path), self.tr("Rescale..."), self)
|
|
877
896
|
self.act_geom_rescale.setIconVisibleInMenu(True)
|
|
878
897
|
self.act_geom_rescale.setStatusTip(self.tr("Rescale image by a factor"))
|
|
@@ -1196,6 +1215,7 @@ class ToolbarMixin:
|
|
|
1196
1215
|
reg("geom_rotate_clockwise", self.act_geom_rot_cw)
|
|
1197
1216
|
reg("geom_rotate_counterclockwise",self.act_geom_rot_ccw)
|
|
1198
1217
|
reg("geom_rotate_180", self.act_geom_rot_180)
|
|
1218
|
+
reg("geom_rotate_any", self.act_geom_rot_any)
|
|
1199
1219
|
reg("geom_rescale", self.act_geom_rescale)
|
|
1200
1220
|
reg("project_new", self.act_project_new)
|
|
1201
1221
|
reg("project_save", self.act_project_save)
|
|
@@ -1387,6 +1407,7 @@ class ToolbarMixin:
|
|
|
1387
1407
|
a.setStatusTip(tip)
|
|
1388
1408
|
a.setEnabled(False)
|
|
1389
1409
|
|
|
1410
|
+
|
|
1390
1411
|
def _sync_link_action_state(self):
|
|
1391
1412
|
g = self._current_group_of_active()
|
|
1392
1413
|
self.act_link_group.blockSignals(True)
|
|
@@ -1432,6 +1453,7 @@ class ToolbarMixin:
|
|
|
1432
1453
|
QTimer.singleShot(0, self.update_undo_redo_action_labels)
|
|
1433
1454
|
|
|
1434
1455
|
def _refresh_mask_action_states(self):
|
|
1456
|
+
|
|
1435
1457
|
active_doc = self._active_doc()
|
|
1436
1458
|
|
|
1437
1459
|
can_apply = bool(active_doc and self._list_candidate_mask_sources(exclude_doc=active_doc))
|
|
@@ -1455,3 +1477,4 @@ class ToolbarMixin:
|
|
|
1455
1477
|
if hasattr(self, "act_hide_mask"):
|
|
1456
1478
|
self.act_hide_mask.setEnabled(has_mask and overlay_on)
|
|
1457
1479
|
|
|
1480
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QLabel, QFormLayout, QPushButton
|
|
2
|
+
from PyQt6.QtCore import Qt, QSettings
|
|
3
|
+
from PyQt6.QtGui import QIcon
|
|
4
|
+
|
|
5
|
+
class StatisticsDialog(QDialog):
|
|
6
|
+
def __init__(self, parent=None):
|
|
7
|
+
super().__init__(parent)
|
|
8
|
+
self.setWindowTitle(self.tr("App Statistics"))
|
|
9
|
+
self.setWindowFlags(self.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint)
|
|
10
|
+
self.resize(300, 200)
|
|
11
|
+
|
|
12
|
+
# Settings to read stats
|
|
13
|
+
self.settings = QSettings("SetiAstro", "SetiAstroSuitePro")
|
|
14
|
+
|
|
15
|
+
layout = QVBoxLayout(self)
|
|
16
|
+
|
|
17
|
+
form_layout = QFormLayout()
|
|
18
|
+
|
|
19
|
+
# Time Spent
|
|
20
|
+
total_seconds = self.settings.value("stats/total_time_seconds", 0, type=float)
|
|
21
|
+
days = int(total_seconds // 86400)
|
|
22
|
+
hours = int((total_seconds % 86400) // 3600)
|
|
23
|
+
minutes = int((total_seconds % 3600) // 60)
|
|
24
|
+
|
|
25
|
+
time_str = f"{days} {self.tr('Days')}, {hours} {self.tr('Hours')}, {minutes} {self.tr('Minutes')}"
|
|
26
|
+
if days == 0:
|
|
27
|
+
time_str = f"{hours} {self.tr('Hours')}, {minutes} {self.tr('Minutes')}"
|
|
28
|
+
|
|
29
|
+
self.lbl_time = QLabel(time_str)
|
|
30
|
+
form_layout.addRow(self.tr("Time Spent:"), self.lbl_time)
|
|
31
|
+
|
|
32
|
+
# Images Opened
|
|
33
|
+
images_count = self.settings.value("stats/opened_images_count", 0, type=int)
|
|
34
|
+
self.lbl_images = QLabel(str(images_count))
|
|
35
|
+
form_layout.addRow(self.tr("Images Opened:"), self.lbl_images)
|
|
36
|
+
|
|
37
|
+
# Tools Opened
|
|
38
|
+
tools_count = self.settings.value("stats/opened_tools_count", 0, type=int)
|
|
39
|
+
self.lbl_tools = QLabel(str(tools_count))
|
|
40
|
+
form_layout.addRow(self.tr("Tools Opened:"), self.lbl_tools)
|
|
41
|
+
|
|
42
|
+
layout.addLayout(form_layout)
|
|
43
|
+
|
|
44
|
+
# Close button
|
|
45
|
+
btn_close = QPushButton(self.tr("Close"))
|
|
46
|
+
btn_close.clicked.connect(self.accept)
|
|
47
|
+
layout.addWidget(btn_close, alignment=Qt.AlignmentFlag.AlignRight)
|
setiastro/saspro/halobgon.py
CHANGED
|
@@ -249,6 +249,9 @@ class HaloBGonDialogPro(QDialog):
|
|
|
249
249
|
def __init__(self, parent, doc, icon: Optional[QIcon] = None):
|
|
250
250
|
super().__init__(parent)
|
|
251
251
|
self.setWindowTitle("Halo-B-Gon")
|
|
252
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
253
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
254
|
+
self.setModal(False)
|
|
252
255
|
if icon:
|
|
253
256
|
try: self.setWindowIcon(icon)
|
|
254
257
|
except Exception as e:
|
|
@@ -427,7 +430,8 @@ class HaloBGonDialogPro(QDialog):
|
|
|
427
430
|
except Exception:
|
|
428
431
|
pass
|
|
429
432
|
|
|
430
|
-
|
|
433
|
+
# Dialog stays open - refresh document for next operation
|
|
434
|
+
self._refresh_document_from_active()
|
|
431
435
|
return
|
|
432
436
|
else:
|
|
433
437
|
# Fallback: try legacy spawner if present; else warn and overwrite.
|
|
@@ -437,7 +441,8 @@ class HaloBGonDialogPro(QDialog):
|
|
|
437
441
|
if callable(spawner):
|
|
438
442
|
title = self.doc.display_name() if hasattr(self.doc, "display_name") else "Image"
|
|
439
443
|
spawner(out, f"{title} [Halo-B-Gon]")
|
|
440
|
-
|
|
444
|
+
# Dialog stays open - refresh document for next operation
|
|
445
|
+
self._refresh_document_from_active()
|
|
441
446
|
return
|
|
442
447
|
else:
|
|
443
448
|
QMessageBox.warning(
|
|
@@ -448,11 +453,32 @@ class HaloBGonDialogPro(QDialog):
|
|
|
448
453
|
|
|
449
454
|
# Overwrite current (original behavior)
|
|
450
455
|
self._apply_overwrite(out)
|
|
451
|
-
|
|
456
|
+
# Dialog stays open - refresh document for next operation
|
|
457
|
+
self._refresh_document_from_active()
|
|
452
458
|
|
|
453
459
|
except Exception as e:
|
|
454
460
|
QMessageBox.critical(self, "Halo-B-Gon", f"Failed to apply:\n{e}")
|
|
455
461
|
|
|
462
|
+
def _refresh_document_from_active(self):
|
|
463
|
+
"""
|
|
464
|
+
Refresh the dialog's document reference to the currently active document.
|
|
465
|
+
This allows reusing the same dialog on different images.
|
|
466
|
+
"""
|
|
467
|
+
try:
|
|
468
|
+
main = self.parent()
|
|
469
|
+
if main and hasattr(main, "_active_doc"):
|
|
470
|
+
new_doc = main._active_doc()
|
|
471
|
+
if new_doc is not None and new_doc is not self.doc:
|
|
472
|
+
self.doc = new_doc
|
|
473
|
+
# Refresh preview for new document
|
|
474
|
+
self.orig = np.clip(np.asarray(new_doc.image, dtype=np.float32), 0.0, 1.0)
|
|
475
|
+
disp = self.orig
|
|
476
|
+
if disp.ndim == 2: disp = disp[..., None].repeat(3, axis=2)
|
|
477
|
+
elif disp.ndim == 3 and disp.shape[2] == 1: disp = disp.repeat(3, axis=2)
|
|
478
|
+
self._disp_base = disp
|
|
479
|
+
self._update_preview()
|
|
480
|
+
except Exception:
|
|
481
|
+
pass
|
|
456
482
|
|
|
457
483
|
|
|
458
484
|
def _reset(self):
|
setiastro/saspro/histogram.py
CHANGED
|
@@ -30,6 +30,9 @@ class HistogramDialog(QDialog):
|
|
|
30
30
|
def __init__(self, parent, document):
|
|
31
31
|
super().__init__(parent)
|
|
32
32
|
self.setWindowTitle(self.tr("Histogram"))
|
|
33
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
34
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
35
|
+
self.setModal(False)
|
|
33
36
|
self.doc = document
|
|
34
37
|
self.image = _to_float_preserve(document.image)
|
|
35
38
|
|
|
@@ -436,6 +436,8 @@ class HistoryExplorerDialog(QDialog):
|
|
|
436
436
|
def __init__(self, document, parent=None):
|
|
437
437
|
super().__init__(parent)
|
|
438
438
|
self.setWindowTitle("History Explorer")
|
|
439
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
440
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
439
441
|
self.setModal(False)
|
|
440
442
|
self.doc = document
|
|
441
443
|
|
setiastro/saspro/i18n.py
CHANGED
|
@@ -25,22 +25,34 @@ AVAILABLE_LANGUAGES: Dict[str, str] = {
|
|
|
25
25
|
"de": "Deutsch",
|
|
26
26
|
"pt": "Português",
|
|
27
27
|
"ja": "日本語",
|
|
28
|
+
"hi": "हिन्दी",
|
|
29
|
+
"sw": "Kiswahili",
|
|
30
|
+
"uk": "Українська",
|
|
31
|
+
"ru": "Русский",
|
|
32
|
+
"ar": "العربية",
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
|
|
31
36
|
def get_translations_dir() -> str:
|
|
32
37
|
"""Get the path to the translations directory."""
|
|
33
|
-
#
|
|
38
|
+
# Source / installed package location
|
|
34
39
|
module_dir = os.path.dirname(os.path.abspath(__file__))
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
#
|
|
38
|
-
if hasattr(os.sys,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
pkg_dir = os.path.join(module_dir, "translations")
|
|
41
|
+
|
|
42
|
+
# PyInstaller frozen builds
|
|
43
|
+
if hasattr(os.sys, "_MEIPASS"):
|
|
44
|
+
# New bundle layout (preferred)
|
|
45
|
+
frozen_internal = os.path.join(os.sys._MEIPASS, "_internal", "translations")
|
|
46
|
+
if os.path.exists(frozen_internal):
|
|
47
|
+
return frozen_internal
|
|
48
|
+
|
|
49
|
+
# Legacy bundle layout fallback
|
|
50
|
+
frozen_legacy = os.path.join(os.sys._MEIPASS, "translations")
|
|
51
|
+
if os.path.exists(frozen_legacy):
|
|
52
|
+
return frozen_legacy
|
|
53
|
+
|
|
54
|
+
return pkg_dir
|
|
55
|
+
|
|
44
56
|
|
|
45
57
|
|
|
46
58
|
def get_available_languages() -> Dict[str, str]:
|