setiastrosuitepro 1.6.2__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.
Potentially problematic release.
This version of setiastrosuitepro might be problematic. Click here for more details.
- setiastro/images/rotatearbitrary.png +0 -0
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/backgroundneutral.py +10 -1
- setiastro/saspro/blink_comparator_pro.py +474 -251
- setiastro/saspro/crop_dialog_pro.py +11 -1
- setiastro/saspro/doc_manager.py +1 -1
- setiastro/saspro/function_bundle.py +16 -16
- setiastro/saspro/gui/main_window.py +93 -64
- setiastro/saspro/gui/mixins/dock_mixin.py +31 -18
- setiastro/saspro/gui/mixins/geometry_mixin.py +105 -5
- setiastro/saspro/gui/mixins/menu_mixin.py +1 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +33 -10
- setiastro/saspro/multiscale_decomp.py +710 -256
- setiastro/saspro/remove_stars_preset.py +55 -13
- setiastro/saspro/resources.py +30 -11
- setiastro/saspro/selective_color.py +79 -20
- setiastro/saspro/shortcuts.py +94 -21
- setiastro/saspro/stacking_suite.py +296 -107
- setiastro/saspro/star_alignment.py +275 -330
- setiastro/saspro/status_log_dock.py +1 -1
- setiastro/saspro/swap_manager.py +77 -42
- setiastro/saspro/translations/all_source_strings.json +1588 -516
- setiastro/saspro/translations/ar_translations.py +915 -684
- setiastro/saspro/translations/de_translations.py +442 -463
- setiastro/saspro/translations/es_translations.py +277 -47
- setiastro/saspro/translations/fr_translations.py +279 -47
- setiastro/saspro/translations/hi_translations.py +253 -21
- setiastro/saspro/translations/integrate_translations.py +3 -2
- setiastro/saspro/translations/it_translations.py +1211 -161
- setiastro/saspro/translations/ja_translations.py +3340 -3107
- setiastro/saspro/translations/pt_translations.py +3315 -3337
- setiastro/saspro/translations/ru_translations.py +351 -117
- setiastro/saspro/translations/saspro_ar.qm +0 -0
- setiastro/saspro/translations/saspro_ar.ts +15902 -138
- 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 +14733 -135
- 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 +11766 -168
- setiastro/saspro/translations/saspro_sw.qm +0 -0
- setiastro/saspro/translations/saspro_sw.ts +15115 -135
- setiastro/saspro/translations/saspro_uk.qm +0 -0
- setiastro/saspro/translations/saspro_uk.ts +11206 -6729
- setiastro/saspro/translations/saspro_zh.qm +0 -0
- setiastro/saspro/translations/saspro_zh.ts +10581 -7812
- setiastro/saspro/translations/sw_translations.py +282 -56
- setiastro/saspro/translations/uk_translations.py +264 -35
- setiastro/saspro/translations/zh_translations.py +282 -47
- setiastro/saspro/view_bundle.py +17 -17
- setiastro/saspro/widgets/minigame/game.js +11 -6
- setiastro/saspro/widgets/resource_monitor.py +26 -0
- setiastro/saspro/widgets/spinboxes.py +18 -0
- setiastro/saspro/wimi.py +65 -65
- setiastro/saspro/wims.py +33 -33
- setiastro/saspro/window_shelf.py +2 -2
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.4.dist-info}/METADATA +7 -7
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.4.dist-info}/RECORD +72 -71
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.4.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.4.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.4.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.4.dist-info}/licenses/license.txt +0 -0
|
@@ -302,7 +302,17 @@ class CropDialogPro(QDialog):
|
|
|
302
302
|
row.addStretch(1)
|
|
303
303
|
row.addWidget(QLabel(self.tr("Aspect Ratio:")))
|
|
304
304
|
self.cmb_ar = QComboBox()
|
|
305
|
-
self.cmb_ar.addItems([
|
|
305
|
+
self.cmb_ar.addItems([
|
|
306
|
+
self.tr("Free"), self.tr("Original"),
|
|
307
|
+
"1:1",
|
|
308
|
+
"3:2", "2:3",
|
|
309
|
+
"4:3", "3:4",
|
|
310
|
+
"4:5", "5:4",
|
|
311
|
+
"16:9", "9:16",
|
|
312
|
+
"21:9", "9:21",
|
|
313
|
+
"2:1", "1:2",
|
|
314
|
+
"3:5", "5:3",
|
|
315
|
+
])
|
|
306
316
|
row.addWidget(self.cmb_ar)
|
|
307
317
|
row.addStretch(1)
|
|
308
318
|
main.addLayout(row)
|
setiastro/saspro/doc_manager.py
CHANGED
|
@@ -202,7 +202,7 @@ class FunctionBundleChip(QWidget):
|
|
|
202
202
|
from PyQt6.QtWidgets import QMenu # already imported at top, but safe
|
|
203
203
|
|
|
204
204
|
m = QMenu(self)
|
|
205
|
-
act_del = m.addAction("Delete Chip")
|
|
205
|
+
act_del = m.addAction(self._panel.tr("Delete Chip"))
|
|
206
206
|
act = m.exec(ev.globalPos())
|
|
207
207
|
if act is act_del:
|
|
208
208
|
try:
|
|
@@ -333,7 +333,7 @@ class FunctionBundleDialog(QDialog):
|
|
|
333
333
|
def __init__(self, parent: QWidget | None = None):
|
|
334
334
|
super().__init__(parent)
|
|
335
335
|
_pin_on_top_mac(self)
|
|
336
|
-
self.setWindowTitle("Function Bundles")
|
|
336
|
+
self.setWindowTitle(self.tr("Function Bundles"))
|
|
337
337
|
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
338
338
|
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
339
339
|
self.setModal(False)
|
|
@@ -350,9 +350,9 @@ class FunctionBundleDialog(QDialog):
|
|
|
350
350
|
self.list.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
|
351
351
|
self.list.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
|
352
352
|
|
|
353
|
-
self.btn_new = QPushButton("New")
|
|
354
|
-
self.btn_dup = QPushButton("Duplicate")
|
|
355
|
-
self.btn_del = QPushButton("Delete")
|
|
353
|
+
self.btn_new = QPushButton(self.tr("New"))
|
|
354
|
+
self.btn_dup = QPushButton(self.tr("Duplicate"))
|
|
355
|
+
self.btn_del = QPushButton(self.tr("Delete"))
|
|
356
356
|
|
|
357
357
|
# right: steps
|
|
358
358
|
self.steps = QListWidget()
|
|
@@ -368,26 +368,26 @@ class FunctionBundleDialog(QDialog):
|
|
|
368
368
|
self.steps.setResizeMode(QListView.ResizeMode.Adjust) # recompute item layout on width change
|
|
369
369
|
self.steps.setUniformItemSizes(False)
|
|
370
370
|
|
|
371
|
-
self.add_hint = QLabel("Drop shortcuts here to add steps")
|
|
371
|
+
self.add_hint = QLabel(self.tr("Drop shortcuts here to add steps"))
|
|
372
372
|
self.add_hint.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
373
373
|
self.add_hint.setStyleSheet("color:#aaa; padding:6px; border:1px dashed #666; border-radius:6px;")
|
|
374
374
|
|
|
375
|
-
self.btn_edit_preset = QPushButton("Edit Preset…")
|
|
375
|
+
self.btn_edit_preset = QPushButton(self.tr("Edit Preset…"))
|
|
376
376
|
self.btn_edit_preset.setEnabled(False) # enabled when exactly one step is selected
|
|
377
377
|
|
|
378
|
-
self.btn_remove = QPushButton("Remove Selected")
|
|
379
|
-
self.btn_clear = QPushButton("Clear Steps")
|
|
380
|
-
self.btn_up = QPushButton("▲ Move Up")
|
|
381
|
-
self.btn_down = QPushButton("▼ Move Down")
|
|
378
|
+
self.btn_remove = QPushButton(self.tr("Remove Selected"))
|
|
379
|
+
self.btn_clear = QPushButton(self.tr("Clear Steps"))
|
|
380
|
+
self.btn_up = QPushButton(self.tr("▲ Move Up"))
|
|
381
|
+
self.btn_down = QPushButton(self.tr("▼ Move Down"))
|
|
382
382
|
|
|
383
|
-
self.btn_drag_bundle = QPushButton("Drag Bundle")
|
|
384
|
-
self.btn_run_active = QPushButton("Apply to Active View")
|
|
385
|
-
self.btn_apply_to_vbundle = QPushButton("Apply to View Bundle…")
|
|
386
|
-
self.btn_chip = QPushButton("Compress to Chip")
|
|
383
|
+
self.btn_drag_bundle = QPushButton(self.tr("Drag Bundle"))
|
|
384
|
+
self.btn_run_active = QPushButton(self.tr("Apply to Active View"))
|
|
385
|
+
self.btn_apply_to_vbundle = QPushButton(self.tr("Apply to View Bundle…"))
|
|
386
|
+
self.btn_chip = QPushButton(self.tr("Compress to Chip"))
|
|
387
387
|
|
|
388
388
|
# layout
|
|
389
389
|
left = QVBoxLayout()
|
|
390
|
-
left.addWidget(QLabel("Function Bundles"))
|
|
390
|
+
left.addWidget(QLabel(self.tr("Function Bundles")))
|
|
391
391
|
left.addWidget(self.list, 1)
|
|
392
392
|
row = QHBoxLayout()
|
|
393
393
|
row.addWidget(self.btn_new); row.addWidget(self.btn_dup); row.addWidget(self.btn_del)
|
|
@@ -129,7 +129,7 @@ from PyQt6.QtGui import (QPixmap, QColor, QIcon, QKeySequence, QShortcut,
|
|
|
129
129
|
|
|
130
130
|
# ----- QtCore -----
|
|
131
131
|
from PyQt6.QtCore import (Qt, pyqtSignal, QCoreApplication, QTimer, QSize, QSignalBlocker, QModelIndex, QThread, QUrl, QSettings, QEvent, QByteArray, QObject,
|
|
132
|
-
QPropertyAnimation, QEasingCurve
|
|
132
|
+
QPropertyAnimation, QEasingCurve, QElapsedTimer
|
|
133
133
|
)
|
|
134
134
|
|
|
135
135
|
from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
|
|
@@ -187,7 +187,7 @@ from setiastro.saspro.resources import (
|
|
|
187
187
|
platesolve_path, psf_path, supernova_path, starregistration_path,
|
|
188
188
|
stacking_path, pedestal_icon_path, starspike_path, aperture_path,
|
|
189
189
|
jwstpupil_path, signature_icon_path, livestacking_path, hrdiagram_path,
|
|
190
|
-
convoicon_path, spcc_icon_path, sasp_data_path, exoicon_path, peeker_icon,
|
|
190
|
+
convoicon_path, spcc_icon_path, sasp_data_path, exoicon_path, peeker_icon,rotatearbitrary_path,
|
|
191
191
|
dse_icon_path, astrobin_filters_csv_path, isophote_path, statstretch_path,
|
|
192
192
|
starstretch_path, curves_path, disk_path, uhs_path, blink_path, ppp_path,
|
|
193
193
|
nbtorgb_path, freqsep_path, contsub_path, halo_path, cosmic_path,
|
|
@@ -310,7 +310,7 @@ class AstroSuiteProMainWindow(
|
|
|
310
310
|
from setiastro.saspro.window_shelf import WindowShelf, MinimizeInterceptor
|
|
311
311
|
from setiastro.saspro.imageops.mdi_snap import MdiSnapController
|
|
312
312
|
from setiastro.saspro.ops.scripts import ScriptManager
|
|
313
|
-
self._version =
|
|
313
|
+
self._version = version
|
|
314
314
|
self._build_timestamp = build_timestamp
|
|
315
315
|
self.setWindowTitle(f"Seti Astro Suite Pro v{self._version}")
|
|
316
316
|
self.resize(1400, 900)
|
|
@@ -467,26 +467,23 @@ class AstroSuiteProMainWindow(
|
|
|
467
467
|
self.mdi.linkViewDropped.connect(self._on_linkview_drop)
|
|
468
468
|
|
|
469
469
|
self.doc_manager.set_mdi_area(self.mdi)
|
|
470
|
-
|
|
470
|
+
# Coalesce undo/redo label refreshes
|
|
471
|
+
self._undo_redo_refresh_pending = False
|
|
472
|
+
self._undo_redo_refresh_timer = QTimer(self)
|
|
473
|
+
self._undo_redo_refresh_timer.setSingleShot(True)
|
|
474
|
+
self._undo_redo_refresh_timer.timeout.connect(self._do_undo_redo_label_refresh)
|
|
471
475
|
# Keep the toolbar in sync whenever anything relevant changes
|
|
472
|
-
self.doc_manager.documentAdded.connect(lambda *_: self.
|
|
473
|
-
self.doc_manager.documentRemoved.connect(lambda *_: self.
|
|
474
|
-
self.doc_manager.imageRegionUpdated.connect(lambda *_: self.
|
|
475
|
-
self.doc_manager.previewRepaintRequested.connect(lambda *_: self.
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
try:
|
|
484
|
-
QApplication.instance().focusChanged.connect(
|
|
485
|
-
lambda *_: QTimer.singleShot(0, self.update_undo_redo_action_labels)
|
|
486
|
-
)
|
|
487
|
-
except Exception:
|
|
488
|
-
pass
|
|
489
|
-
|
|
476
|
+
self.doc_manager.documentAdded.connect(lambda *_: self._schedule_undo_redo_label_refresh())
|
|
477
|
+
self.doc_manager.documentRemoved.connect(lambda *_: self._schedule_undo_redo_label_refresh())
|
|
478
|
+
self.doc_manager.imageRegionUpdated.connect(lambda *_: self._schedule_undo_redo_label_refresh())
|
|
479
|
+
self.doc_manager.previewRepaintRequested.connect(lambda *_: self._schedule_undo_redo_label_refresh())
|
|
480
|
+
self.mdi.subWindowActivated.connect(lambda *_: self._schedule_undo_redo_label_refresh())
|
|
481
|
+
|
|
482
|
+
# optional: keep, but schedule (or remove entirely)
|
|
483
|
+
#try:
|
|
484
|
+
# QApplication.instance().focusChanged.connect(lambda *_: self._schedule_undo_redo_label_refresh())
|
|
485
|
+
#except Exception:
|
|
486
|
+
# pass
|
|
490
487
|
self.shortcuts.load_shortcuts()
|
|
491
488
|
self._ensure_persistent_names()
|
|
492
489
|
self._restore_window_placement()
|
|
@@ -570,6 +567,22 @@ class AstroSuiteProMainWindow(
|
|
|
570
567
|
|
|
571
568
|
# _init_log_dock, _hook_stdout_stderr, and _append_log_text are now in DockMixin
|
|
572
569
|
|
|
570
|
+
def _schedule_undo_redo_label_refresh(self):
|
|
571
|
+
# Coalesce many triggers into one UI update
|
|
572
|
+
if getattr(self, "_undo_redo_refresh_pending", False):
|
|
573
|
+
return
|
|
574
|
+
self._undo_redo_refresh_pending = True
|
|
575
|
+
# 0ms is fine *if* it’s a real attribute timer (not a local)
|
|
576
|
+
self._undo_redo_refresh_timer.start(0)
|
|
577
|
+
|
|
578
|
+
def _do_undo_redo_label_refresh(self):
|
|
579
|
+
self._undo_redo_refresh_pending = False
|
|
580
|
+
try:
|
|
581
|
+
self.update_undo_redo_action_labels()
|
|
582
|
+
except Exception:
|
|
583
|
+
pass
|
|
584
|
+
|
|
585
|
+
|
|
573
586
|
def _rebuild_menus_for_language(self):
|
|
574
587
|
"""Rebuild menus after language change to apply new translations."""
|
|
575
588
|
try:
|
|
@@ -608,7 +621,7 @@ class AstroSuiteProMainWindow(
|
|
|
608
621
|
doc.changed.connect(self.update_undo_redo_action_labels)
|
|
609
622
|
except Exception:
|
|
610
623
|
pass
|
|
611
|
-
self.
|
|
624
|
+
self._schedule_undo_redo_label_refresh()
|
|
612
625
|
|
|
613
626
|
def _promote_roi_preview_to_real_doc(self, st: dict, preview_doc) -> None:
|
|
614
627
|
"""
|
|
@@ -1206,11 +1219,11 @@ class AstroSuiteProMainWindow(
|
|
|
1206
1219
|
global_pos = lw.viewport().mapToGlobal(pos)
|
|
1207
1220
|
|
|
1208
1221
|
menu = QMenu(lw)
|
|
1209
|
-
act_copy_selected = menu.addAction("Copy Selected")
|
|
1210
|
-
act_copy_all = menu.addAction("Copy All")
|
|
1222
|
+
act_copy_selected = menu.addAction(self.tr("Copy Selected"))
|
|
1223
|
+
act_copy_all = menu.addAction(self.tr("Copy All"))
|
|
1211
1224
|
menu.addSeparator()
|
|
1212
|
-
act_select_all = menu.addAction("Select All Lines")
|
|
1213
|
-
act_clear = menu.addAction("Clear Console")
|
|
1225
|
+
act_select_all = menu.addAction(self.tr("Select All Lines"))
|
|
1226
|
+
act_clear = menu.addAction(self.tr("Clear Console"))
|
|
1214
1227
|
|
|
1215
1228
|
action = menu.exec(global_pos)
|
|
1216
1229
|
if action is None:
|
|
@@ -1804,7 +1817,7 @@ class AstroSuiteProMainWindow(
|
|
|
1804
1817
|
|
|
1805
1818
|
show_view_bundles(self)
|
|
1806
1819
|
except Exception as e:
|
|
1807
|
-
QMessageBox.warning(self, "View Bundles", f"Open failed:\n{e}")
|
|
1820
|
+
QMessageBox.warning(self, self.tr("View Bundles"), f"Open failed:\n{e}")
|
|
1808
1821
|
|
|
1809
1822
|
def _open_function_bundles(self):
|
|
1810
1823
|
from setiastro.saspro.function_bundle import show_function_bundles
|
|
@@ -1812,7 +1825,7 @@ class AstroSuiteProMainWindow(
|
|
|
1812
1825
|
|
|
1813
1826
|
show_function_bundles(self)
|
|
1814
1827
|
except Exception as e:
|
|
1815
|
-
QMessageBox.warning(self, "Function Bundles", f"Open failed:\n{e}")
|
|
1828
|
+
QMessageBox.warning(self, self.tr("Function Bundles"), f"Open failed:\n{e}")
|
|
1816
1829
|
|
|
1817
1830
|
def _open_scripts_folder(self):
|
|
1818
1831
|
if hasattr(self, "scriptman"):
|
|
@@ -1874,43 +1887,43 @@ class AstroSuiteProMainWindow(
|
|
|
1874
1887
|
# Manual list (extend anytime). Format: (Gesture, Context, Effect)
|
|
1875
1888
|
rows = [
|
|
1876
1889
|
# Command search
|
|
1877
|
-
("A", "Display Stretch", "Toggle Display Auto-Stretch"),
|
|
1878
|
-
("Ctrl+I", "Invert", "Invert the Image"),
|
|
1879
|
-
("Ctrl+Shift+P", "Command Search", "Focus the command search bar; Enter runs first match"),
|
|
1890
|
+
("A", "Display Stretch", self.tr("Toggle Display Auto-Stretch")),
|
|
1891
|
+
("Ctrl+I", "Invert", self.tr("Invert the Image")),
|
|
1892
|
+
("Ctrl+Shift+P", "Command Search", self.tr("Focus the command search bar; Enter runs first match")),
|
|
1880
1893
|
|
|
1881
1894
|
# View Icon
|
|
1882
|
-
("Drag view -> Off to Canvas", "View", "Duplicate Image"),
|
|
1883
|
-
("Drag view -> On to Other Image", "View", "Copy Zoom and Pan"),
|
|
1884
|
-
("Shift+Drag -> On to Other Image", "View", "Apply that image to the other as a mask"),
|
|
1885
|
-
("Ctrl+Drag -> On to Other Image", "View", "Copy Astrometric Solution"),
|
|
1895
|
+
("Drag view -> Off to Canvas", "View", self.tr("Duplicate Image")),
|
|
1896
|
+
("Drag view -> On to Other Image", "View", self.tr("Copy Zoom and Pan")),
|
|
1897
|
+
("Shift+Drag -> On to Other Image", "View", self.tr("Apply that image to the other as a mask")),
|
|
1898
|
+
("Ctrl+Drag -> On to Other Image", "View", self.tr("Copy Astrometric Solution")),
|
|
1886
1899
|
|
|
1887
1900
|
# View zoom
|
|
1888
|
-
("Ctrl+1", "View", "Zoom to 100% (1:1)"),
|
|
1889
|
-
("Ctrl+0", "View", "Fit image to current window"),
|
|
1890
|
-
("Ctrl++", "View", "Zoom In"),
|
|
1891
|
-
("Ctrl+-", "View", "Zoom Out"),
|
|
1901
|
+
("Ctrl+1", "View", self.tr("Zoom to 100% (1:1)")),
|
|
1902
|
+
("Ctrl+0", "View", self.tr("Fit image to current window")),
|
|
1903
|
+
("Ctrl++", "View", self.tr("Zoom In")),
|
|
1904
|
+
("Ctrl+-", "View", self.tr("Zoom Out")),
|
|
1892
1905
|
|
|
1893
1906
|
# Window switching
|
|
1894
|
-
("Ctrl+PgDown", "MDI", "Switch to previously active view"),
|
|
1895
|
-
("Ctrl+PgUp", "MDI", "Switch to next active view"),
|
|
1907
|
+
("Ctrl+PgDown", "MDI", self.tr("Switch to previously active view")),
|
|
1908
|
+
("Ctrl+PgUp", "MDI", self.tr("Switch to next active view")),
|
|
1896
1909
|
|
|
1897
1910
|
# Shortcuts canvas + buttons
|
|
1898
|
-
("Alt+Drag (toolbar button)", "Toolbar", "Create a desktop shortcut for that action"),
|
|
1899
|
-
("Alt+Drag (shortcut button -> view)", "Shortcuts", "Headless apply the shortcut's command/preset to a view"),
|
|
1900
|
-
("Ctrl/Shift+Click", "Shortcuts", "Multi-select shortcut buttons"),
|
|
1901
|
-
("Drag (selection)", "Shortcuts", "Move selected shortcut buttons"),
|
|
1902
|
-
("Delete / Backspace", "Shortcuts", "Delete selected shortcut buttons"),
|
|
1903
|
-
("Ctrl+A", "Shortcuts", "Select all shortcut buttons"),
|
|
1904
|
-
("Double-click empty area", "MDI background", "Open files dialog"),
|
|
1911
|
+
("Alt+Drag (toolbar button)", "Toolbar", self.tr("Create a desktop shortcut for that action")),
|
|
1912
|
+
("Alt+Drag (shortcut button -> view)", "Shortcuts", self.tr("Headless apply the shortcut's command/preset to a view")),
|
|
1913
|
+
("Ctrl/Shift+Click", "Shortcuts", self.tr("Multi-select shortcut buttons")),
|
|
1914
|
+
("Drag (selection)", "Shortcuts", self.tr("Move selected shortcut buttons")),
|
|
1915
|
+
("Delete / Backspace", "Shortcuts", self.tr("Delete selected shortcut buttons")),
|
|
1916
|
+
("Ctrl+A", "Shortcuts", self.tr("Select all shortcut buttons")),
|
|
1917
|
+
("Double-click empty area", "MDI background", self.tr("Open files dialog")),
|
|
1905
1918
|
|
|
1906
1919
|
# Layers dock
|
|
1907
|
-
("Drag view -> Layers list", "Layers", "Add dragged view as a new layer (on top)"),
|
|
1908
|
-
("Shift+Drag mask -> Layers list", "Layers", "Attach dragged image as mask to the selected layer"),
|
|
1920
|
+
("Drag view -> Layers list", "Layers", self.tr("Add dragged view as a new layer (on top)")),
|
|
1921
|
+
("Shift+Drag mask -> Layers list", "Layers", self.tr("Attach dragged image as mask to the selected layer")),
|
|
1909
1922
|
|
|
1910
1923
|
# Crop tool
|
|
1911
|
-
("Click-drag", "Crop Tool", "Draw a crop rectangle"),
|
|
1912
|
-
("Drag corner handles", "Crop Tool", "Resize crop rectangle"),
|
|
1913
|
-
("Shift+Drag on box", "Crop Tool", "Rotate crop rectangle"),
|
|
1924
|
+
("Click-drag", "Crop Tool", self.tr("Draw a crop rectangle")),
|
|
1925
|
+
("Drag corner handles", "Crop Tool", self.tr("Resize crop rectangle")),
|
|
1926
|
+
("Shift+Drag on box", "Crop Tool", self.tr("Rotate crop rectangle")),
|
|
1914
1927
|
]
|
|
1915
1928
|
return rows
|
|
1916
1929
|
|
|
@@ -2027,7 +2040,7 @@ class AstroSuiteProMainWindow(
|
|
|
2027
2040
|
if getattr(self, "doc_manager", None) and self.doc_manager._docs:
|
|
2028
2041
|
if not self._confirm_discard(
|
|
2029
2042
|
title=title,
|
|
2030
|
-
msg=(
|
|
2043
|
+
msg=self.tr(
|
|
2031
2044
|
"Loading a project will close current views and replace desktop shortcuts.\n"
|
|
2032
2045
|
"Continue?"
|
|
2033
2046
|
),
|
|
@@ -5542,6 +5555,10 @@ class AstroSuiteProMainWindow(
|
|
|
5542
5555
|
"rotate_180": "geom_rotate_180",
|
|
5543
5556
|
"geom_rotate_180": "geom_rotate_180",
|
|
5544
5557
|
|
|
5558
|
+
"rotate_any": "geom_rotate_any",
|
|
5559
|
+
"rotate_arbitrary": "geom_rotate_any",
|
|
5560
|
+
"geom_rotate_any": "geom_rotate_any",
|
|
5561
|
+
|
|
5545
5562
|
"invert": "geom_invert",
|
|
5546
5563
|
"geom_invert": "geom_invert",
|
|
5547
5564
|
|
|
@@ -6542,6 +6559,17 @@ class AstroSuiteProMainWindow(
|
|
|
6542
6559
|
QMessageBox.warning(self, "Rotate 180Â deg", str(e))
|
|
6543
6560
|
return
|
|
6544
6561
|
|
|
6562
|
+
if cid == "geom_rotate_any":
|
|
6563
|
+
try:
|
|
6564
|
+
angle = float(preset.get("angle_deg", preset.get("angle", 0.0)))
|
|
6565
|
+
called = _call_any(["_apply_geom_rot_any_to_doc"], doc, angle_deg=angle)
|
|
6566
|
+
if not called:
|
|
6567
|
+
raise RuntimeError("No rotate-any apply method found")
|
|
6568
|
+
self._log(f"Rotate ({angle:g}°) applied to '{target_sw.windowTitle()}'")
|
|
6569
|
+
except Exception as e:
|
|
6570
|
+
QMessageBox.warning(self, "Rotate...", str(e))
|
|
6571
|
+
return
|
|
6572
|
+
|
|
6545
6573
|
if cid == "geom_rescale":
|
|
6546
6574
|
try:
|
|
6547
6575
|
factor = float(preset.get("factor", 1.0))
|
|
@@ -7347,7 +7375,7 @@ class AstroSuiteProMainWindow(
|
|
|
7347
7375
|
self._search_dock = None
|
|
7348
7376
|
|
|
7349
7377
|
# --- Right-side mini dock with the search box ---
|
|
7350
|
-
self._search_dock = QDockWidget("Command Search", self)
|
|
7378
|
+
self._search_dock = QDockWidget(self.tr("Command Search"), self)
|
|
7351
7379
|
self._search_dock.setObjectName("CommandSearchDock")
|
|
7352
7380
|
# âœ... Allow moving/closing like other panels
|
|
7353
7381
|
self._search_dock.setAllowedAreas(
|
|
@@ -7562,7 +7590,7 @@ class AstroSuiteProMainWindow(
|
|
|
7562
7590
|
except Exception:
|
|
7563
7591
|
pass
|
|
7564
7592
|
|
|
7565
|
-
try: self.
|
|
7593
|
+
try: self._schedule_undo_redo_label_refresh()
|
|
7566
7594
|
except Exception as e:
|
|
7567
7595
|
import logging
|
|
7568
7596
|
logging.debug(f"Exception suppressed: {type(e).__name__}: {e}")
|
|
@@ -8050,7 +8078,7 @@ class AstroSuiteProMainWindow(
|
|
|
8050
8078
|
# If no subwindows remain, clear all "active doc" UI bits, including header
|
|
8051
8079
|
if not self.mdi.subWindowList():
|
|
8052
8080
|
self.currentDocumentChanged.emit(None) # drives HeaderViewerDock.set_document(None)
|
|
8053
|
-
self.
|
|
8081
|
+
self._schedule_undo_redo_label_refresh()
|
|
8054
8082
|
self._hdr_refresh_timer.start(0) # belt-and-suspenders for manual widgets
|
|
8055
8083
|
# If your dock has its own set_document, call it explicitly too
|
|
8056
8084
|
hv = getattr(self, "header_viewer", None)
|
|
@@ -8392,19 +8420,20 @@ class AstroSuiteProMainWindow(
|
|
|
8392
8420
|
|
|
8393
8421
|
# Misc UI refreshes (guarded)
|
|
8394
8422
|
try:
|
|
8395
|
-
self.
|
|
8396
|
-
except Exception:
|
|
8397
|
-
pass
|
|
8398
|
-
try:
|
|
8399
|
-
if hasattr(self, "_hdr_refresh_timer") and self._hdr_refresh_timer is not None:
|
|
8400
|
-
self._hdr_refresh_timer.start(0)
|
|
8423
|
+
self._schedule_undo_redo_label_refresh()
|
|
8401
8424
|
except Exception:
|
|
8402
8425
|
pass
|
|
8426
|
+
#try:
|
|
8427
|
+
# if hasattr(self, "_hdr_refresh_timer") and self._hdr_refresh_timer is not None:
|
|
8428
|
+
# self._hdr_refresh_timer.start(0)
|
|
8429
|
+
#except Exception:
|
|
8430
|
+
# pass
|
|
8403
8431
|
try:
|
|
8404
8432
|
self._refresh_mask_action_states()
|
|
8405
8433
|
except Exception:
|
|
8406
8434
|
pass
|
|
8407
8435
|
|
|
8436
|
+
|
|
8408
8437
|
def _sync_docman_active(self, doc):
|
|
8409
8438
|
dm = self.doc_manager
|
|
8410
8439
|
try:
|
|
@@ -243,22 +243,35 @@ class DockMixin:
|
|
|
243
243
|
self.settings.setValue("ui/resource_monitor_visible", checked)
|
|
244
244
|
|
|
245
245
|
def _update_monitor_position(self):
|
|
246
|
-
"""Snap monitor to bottom-right corner."""
|
|
246
|
+
"""Snap monitor to bottom-right corner or restore saved position."""
|
|
247
247
|
if hasattr(self, 'resource_monitor') and self.resource_monitor:
|
|
248
248
|
from PyQt6.QtCore import QPoint
|
|
249
|
-
m = 5 # margin
|
|
250
|
-
# Position relative to the main window geometry
|
|
251
|
-
w = self.resource_monitor.width()
|
|
252
|
-
h = self.resource_monitor.height()
|
|
253
249
|
|
|
254
|
-
#
|
|
255
|
-
|
|
256
|
-
|
|
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)
|
|
257
253
|
|
|
258
|
-
#
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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)
|
|
262
275
|
self.resource_monitor.raise_()
|
|
263
276
|
|
|
264
277
|
# We need to hook resizeEvent to call _update_monitor_position.
|
|
@@ -290,12 +303,12 @@ class DockMixin:
|
|
|
290
303
|
|
|
291
304
|
# Friendly ordering for common ones; others follow alphabetically.
|
|
292
305
|
order_hint = {
|
|
293
|
-
"Explorer": 10,
|
|
294
|
-
"Console / Status": 20,
|
|
295
|
-
"Header Viewer": 30,
|
|
296
|
-
"Layers": 40,
|
|
297
|
-
"Window Shelf": 50,
|
|
298
|
-
"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,
|
|
299
312
|
}
|
|
300
313
|
|
|
301
314
|
# Add special action for overlay monitor
|
|
@@ -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)))
|
|
@@ -185,6 +185,7 @@ class MenuMixin:
|
|
|
185
185
|
m_geom.addAction(self.act_geom_rot_cw)
|
|
186
186
|
m_geom.addAction(self.act_geom_rot_ccw)
|
|
187
187
|
m_geom.addAction(self.act_geom_rot_180)
|
|
188
|
+
m_geom.addAction(self.act_geom_rot_any)
|
|
188
189
|
m_geom.addSeparator()
|
|
189
190
|
m_geom.addAction(self.act_geom_rescale)
|
|
190
191
|
m_geom.addSeparator()
|