setiastrosuitepro 1.6.0__py3-none-any.whl → 1.6.4.post1__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/data/SASP_data.fits +0 -0
- setiastro/data/catalogs/List_of_Galaxies_with_Distances_Gly.csv +488 -0
- setiastro/data/catalogs/astrobin_filters.csv +890 -0
- setiastro/data/catalogs/astrobin_filters_page1_local.csv +51 -0
- setiastro/data/catalogs/cali2.csv +63 -0
- setiastro/data/catalogs/cali2color.csv +65 -0
- setiastro/data/catalogs/celestial_catalog - original.csv +16471 -0
- setiastro/data/catalogs/celestial_catalog.csv +24031 -0
- setiastro/data/catalogs/detected_stars.csv +24784 -0
- setiastro/data/catalogs/fits_header_data.csv +46 -0
- setiastro/data/catalogs/test.csv +8 -0
- setiastro/data/catalogs/updated_celestial_catalog.csv +16471 -0
- setiastro/images/Astro_Spikes.png +0 -0
- setiastro/images/Background_startup.jpg +0 -0
- setiastro/images/HRDiagram.png +0 -0
- setiastro/images/LExtract.png +0 -0
- setiastro/images/LInsert.png +0 -0
- setiastro/images/Oxygenation-atm-2.svg.png +0 -0
- setiastro/images/RGB080604.png +0 -0
- setiastro/images/abeicon.png +0 -0
- setiastro/images/aberration.png +0 -0
- setiastro/images/andromedatry.png +0 -0
- setiastro/images/andromedatry_satellited.png +0 -0
- setiastro/images/annotated.png +0 -0
- setiastro/images/aperture.png +0 -0
- setiastro/images/astrosuite.ico +0 -0
- setiastro/images/astrosuite.png +0 -0
- setiastro/images/astrosuitepro.icns +0 -0
- setiastro/images/astrosuitepro.ico +0 -0
- setiastro/images/astrosuitepro.png +0 -0
- setiastro/images/background.png +0 -0
- setiastro/images/background2.png +0 -0
- setiastro/images/benchmark.png +0 -0
- setiastro/images/big_moon_stabilizer_timeline.png +0 -0
- setiastro/images/big_moon_stabilizer_timeline_clean.png +0 -0
- setiastro/images/blaster.png +0 -0
- setiastro/images/blink.png +0 -0
- setiastro/images/clahe.png +0 -0
- setiastro/images/collage.png +0 -0
- setiastro/images/colorwheel.png +0 -0
- setiastro/images/contsub.png +0 -0
- setiastro/images/convo.png +0 -0
- setiastro/images/copyslot.png +0 -0
- setiastro/images/cosmic.png +0 -0
- setiastro/images/cosmicsat.png +0 -0
- setiastro/images/crop1.png +0 -0
- setiastro/images/cropicon.png +0 -0
- setiastro/images/curves.png +0 -0
- setiastro/images/cvs.png +0 -0
- setiastro/images/debayer.png +0 -0
- setiastro/images/denoise_cnn_custom.png +0 -0
- setiastro/images/denoise_cnn_graph.png +0 -0
- setiastro/images/disk.png +0 -0
- setiastro/images/dse.png +0 -0
- setiastro/images/exoicon.png +0 -0
- setiastro/images/eye.png +0 -0
- setiastro/images/fliphorizontal.png +0 -0
- setiastro/images/flipvertical.png +0 -0
- setiastro/images/font.png +0 -0
- setiastro/images/freqsep.png +0 -0
- setiastro/images/functionbundle.png +0 -0
- setiastro/images/graxpert.png +0 -0
- setiastro/images/green.png +0 -0
- setiastro/images/gridicon.png +0 -0
- setiastro/images/halo.png +0 -0
- setiastro/images/hdr.png +0 -0
- setiastro/images/histogram.png +0 -0
- setiastro/images/hubble.png +0 -0
- setiastro/images/imagecombine.png +0 -0
- setiastro/images/invert.png +0 -0
- setiastro/images/isophote.png +0 -0
- setiastro/images/isophote_demo_figure.png +0 -0
- setiastro/images/isophote_demo_image.png +0 -0
- setiastro/images/isophote_demo_model.png +0 -0
- setiastro/images/isophote_demo_residual.png +0 -0
- setiastro/images/jwstpupil.png +0 -0
- setiastro/images/linearfit.png +0 -0
- setiastro/images/livestacking.png +0 -0
- setiastro/images/mask.png +0 -0
- setiastro/images/maskapply.png +0 -0
- setiastro/images/maskcreate.png +0 -0
- setiastro/images/maskremove.png +0 -0
- setiastro/images/morpho.png +0 -0
- setiastro/images/mosaic.png +0 -0
- setiastro/images/multiscale_decomp.png +0 -0
- setiastro/images/nbtorgb.png +0 -0
- setiastro/images/neutral.png +0 -0
- setiastro/images/nuke.png +0 -0
- setiastro/images/openfile.png +0 -0
- setiastro/images/pedestal.png +0 -0
- setiastro/images/pen.png +0 -0
- setiastro/images/pixelmath.png +0 -0
- setiastro/images/platesolve.png +0 -0
- setiastro/images/ppp.png +0 -0
- setiastro/images/pro.png +0 -0
- setiastro/images/project.png +0 -0
- setiastro/images/psf.png +0 -0
- setiastro/images/redo.png +0 -0
- setiastro/images/redoicon.png +0 -0
- setiastro/images/rescale.png +0 -0
- setiastro/images/rgbalign.png +0 -0
- setiastro/images/rgbcombo.png +0 -0
- setiastro/images/rgbextract.png +0 -0
- setiastro/images/rotate180.png +0 -0
- setiastro/images/rotatearbitrary.png +0 -0
- setiastro/images/rotateclockwise.png +0 -0
- setiastro/images/rotatecounterclockwise.png +0 -0
- setiastro/images/satellite.png +0 -0
- setiastro/images/script.png +0 -0
- setiastro/images/selectivecolor.png +0 -0
- setiastro/images/simbad.png +0 -0
- setiastro/images/slot0.png +0 -0
- setiastro/images/slot1.png +0 -0
- setiastro/images/slot2.png +0 -0
- setiastro/images/slot3.png +0 -0
- setiastro/images/slot4.png +0 -0
- setiastro/images/slot5.png +0 -0
- setiastro/images/slot6.png +0 -0
- setiastro/images/slot7.png +0 -0
- setiastro/images/slot8.png +0 -0
- setiastro/images/slot9.png +0 -0
- setiastro/images/spcc.png +0 -0
- setiastro/images/spin_precession_vs_lunar_distance.png +0 -0
- setiastro/images/spinner.gif +0 -0
- setiastro/images/stacking.png +0 -0
- setiastro/images/staradd.png +0 -0
- setiastro/images/staralign.png +0 -0
- setiastro/images/starnet.png +0 -0
- setiastro/images/starregistration.png +0 -0
- setiastro/images/starspike.png +0 -0
- setiastro/images/starstretch.png +0 -0
- setiastro/images/statstretch.png +0 -0
- setiastro/images/supernova.png +0 -0
- setiastro/images/uhs.png +0 -0
- setiastro/images/undoicon.png +0 -0
- setiastro/images/upscale.png +0 -0
- setiastro/images/viewbundle.png +0 -0
- setiastro/images/whitebalance.png +0 -0
- setiastro/images/wimi_icon_256x256.png +0 -0
- setiastro/images/wimilogo.png +0 -0
- setiastro/images/wims.png +0 -0
- setiastro/images/wrench_icon.png +0 -0
- setiastro/images/xisfliberator.png +0 -0
- setiastro/qml/ResourceMonitor.qml +126 -0
- setiastro/saspro/__main__.py +228 -67
- setiastro/saspro/_generated/build_info.py +2 -1
- setiastro/saspro/abe.py +76 -25
- setiastro/saspro/aberration_ai.py +14 -14
- setiastro/saspro/add_stars.py +15 -12
- setiastro/saspro/astrobin_exporter.py +61 -58
- setiastro/saspro/astrospike_python.py +3 -1
- setiastro/saspro/autostretch.py +4 -2
- setiastro/saspro/backgroundneutral.py +65 -14
- setiastro/saspro/batch_convert.py +8 -5
- setiastro/saspro/batch_renamer.py +39 -36
- setiastro/saspro/blemish_blaster.py +15 -12
- setiastro/saspro/blink_comparator_pro.py +605 -379
- setiastro/saspro/cheat_sheet.py +62 -17
- setiastro/saspro/clahe.py +34 -8
- setiastro/saspro/comet_stacking.py +103 -38
- setiastro/saspro/common_tr.py +107 -0
- setiastro/saspro/continuum_subtract.py +7 -7
- setiastro/saspro/convo.py +12 -9
- setiastro/saspro/copyastro.py +3 -0
- setiastro/saspro/cosmicclarity.py +77 -52
- setiastro/saspro/crop_dialog_pro.py +80 -45
- setiastro/saspro/curve_editor_pro.py +51 -33
- setiastro/saspro/debayer.py +6 -3
- setiastro/saspro/doc_manager.py +49 -19
- setiastro/saspro/exoplanet_detector.py +11 -11
- setiastro/saspro/fitsmodifier.py +48 -44
- setiastro/saspro/fix_bom.py +32 -0
- setiastro/saspro/frequency_separation.py +18 -12
- setiastro/saspro/function_bundle.py +18 -16
- setiastro/saspro/generate_translations.py +3092 -0
- setiastro/saspro/ghs_dialog_pro.py +19 -16
- setiastro/saspro/graxpert.py +3 -0
- setiastro/saspro/gui/main_window.py +471 -126
- setiastro/saspro/gui/mixins/dock_mixin.py +123 -11
- setiastro/saspro/gui/mixins/file_mixin.py +25 -20
- setiastro/saspro/gui/mixins/geometry_mixin.py +115 -15
- setiastro/saspro/gui/mixins/header_mixin.py +6 -6
- setiastro/saspro/gui/mixins/mask_mixin.py +8 -8
- setiastro/saspro/gui/mixins/menu_mixin.py +62 -33
- setiastro/saspro/gui/mixins/toolbar_mixin.py +382 -226
- setiastro/saspro/gui/mixins/update_mixin.py +26 -26
- setiastro/saspro/gui/statistics_dialog.py +47 -0
- setiastro/saspro/halobgon.py +29 -3
- setiastro/saspro/header_viewer.py +21 -18
- setiastro/saspro/histogram.py +29 -26
- setiastro/saspro/history_explorer.py +2 -0
- setiastro/saspro/i18n.py +168 -0
- setiastro/saspro/image_combine.py +3 -0
- setiastro/saspro/image_peeker_pro.py +52 -44
- 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 +180 -22
- setiastro/saspro/mfdeconv.py +5 -0
- setiastro/saspro/morphology.py +38 -13
- setiastro/saspro/multiscale_decomp.py +713 -256
- setiastro/saspro/nbtorgb_stars.py +12 -2
- setiastro/saspro/numba_utils.py +149 -48
- setiastro/saspro/ops/scripts.py +77 -17
- setiastro/saspro/ops/settings.py +177 -100
- setiastro/saspro/perfect_palette_picker.py +25 -7
- setiastro/saspro/pixelmath.py +114 -110
- setiastro/saspro/plate_solver.py +118 -108
- setiastro/saspro/remove_green.py +24 -7
- setiastro/saspro/remove_stars.py +136 -162
- setiastro/saspro/remove_stars_preset.py +55 -13
- setiastro/saspro/resources.py +46 -15
- setiastro/saspro/rgb_combination.py +19 -18
- setiastro/saspro/rgbalign.py +11 -11
- setiastro/saspro/save_options.py +5 -4
- setiastro/saspro/selective_color.py +84 -25
- setiastro/saspro/sfcc.py +119 -72
- setiastro/saspro/shortcuts.py +345 -36
- setiastro/saspro/signature_insert.py +4 -1
- setiastro/saspro/stacking_suite.py +2066 -1119
- setiastro/saspro/star_alignment.py +291 -331
- setiastro/saspro/star_spikes.py +137 -53
- setiastro/saspro/star_stretch.py +47 -10
- setiastro/saspro/stat_stretch.py +52 -16
- setiastro/saspro/status_log_dock.py +1 -1
- setiastro/saspro/subwindow.py +97 -36
- setiastro/saspro/supernovaasteroidhunter.py +68 -61
- 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 +3728 -0
- setiastro/saspro/translations/es_translations.py +4169 -0
- setiastro/saspro/translations/fr_translations.py +4090 -0
- setiastro/saspro/translations/hi_translations.py +3803 -0
- setiastro/saspro/translations/integrate_translations.py +271 -0
- setiastro/saspro/translations/it_translations.py +4728 -0
- setiastro/saspro/translations/ja_translations.py +3834 -0
- setiastro/saspro/translations/pt_translations.py +3847 -0
- 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 +14548 -0
- setiastro/saspro/translations/saspro_es.qm +0 -0
- setiastro/saspro/translations/saspro_es.ts +16202 -0
- setiastro/saspro/translations/saspro_fr.qm +0 -0
- setiastro/saspro/translations/saspro_fr.ts +15870 -0
- 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 +19046 -0
- setiastro/saspro/translations/saspro_ja.qm +0 -0
- setiastro/saspro/translations/saspro_ja.ts +14980 -0
- setiastro/saspro/translations/saspro_pt.qm +0 -0
- setiastro/saspro/translations/saspro_pt.ts +15024 -0
- 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 +15289 -0
- setiastro/saspro/translations/sw_translations.py +3897 -0
- setiastro/saspro/translations/uk_translations.py +3929 -0
- setiastro/saspro/translations/zh_translations.py +3910 -0
- setiastro/saspro/versioning.py +77 -0
- setiastro/saspro/view_bundle.py +20 -17
- setiastro/saspro/wavescale_hdr.py +54 -33
- setiastro/saspro/wavescale_hdr_preset.py +6 -5
- setiastro/saspro/wavescalede.py +54 -31
- setiastro/saspro/wavescalede_preset.py +9 -7
- setiastro/saspro/whitebalance.py +58 -22
- setiastro/saspro/widgets/common_utilities.py +12 -11
- 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/preview_dialogs.py +8 -8
- 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 +7996 -0
- setiastro/saspro/wims.py +578 -0
- setiastro/saspro/window_shelf.py +2 -2
- {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/METADATA +15 -3
- setiastrosuitepro-1.6.4.post1.dist-info/RECORD +368 -0
- setiastrosuitepro-1.6.0.dist-info/RECORD +0 -174
- {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.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
|
|
@@ -30,7 +30,7 @@ class DockMixin:
|
|
|
30
30
|
|
|
31
31
|
def _init_log_dock(self):
|
|
32
32
|
"""Initialize the system log dock widget."""
|
|
33
|
-
self.log_dock = QDockWidget("System Log", self)
|
|
33
|
+
self.log_dock = QDockWidget(self.tr("System Log"), self)
|
|
34
34
|
self.log_dock.setObjectName("LogDock")
|
|
35
35
|
self.log_dock.setAllowedAreas(
|
|
36
36
|
Qt.DockWidgetArea.BottomDockWidgetArea
|
|
@@ -45,7 +45,7 @@ class DockMixin:
|
|
|
45
45
|
self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.log_dock)
|
|
46
46
|
|
|
47
47
|
self.act_toggle_log = self.log_dock.toggleViewAction()
|
|
48
|
-
self.act_toggle_log.setText("Show System Log Panel")
|
|
48
|
+
self.act_toggle_log.setText(self.tr("Show System Log Panel"))
|
|
49
49
|
|
|
50
50
|
def _append_log_text(self, text: str):
|
|
51
51
|
"""Append text to the system log dock."""
|
|
@@ -111,7 +111,7 @@ class DockMixin:
|
|
|
111
111
|
# Double-click: same behavior
|
|
112
112
|
self.explorer.itemDoubleClicked.connect(self._activate_or_open_from_explorer)
|
|
113
113
|
|
|
114
|
-
dock = QDockWidget("Explorer", self)
|
|
114
|
+
dock = QDockWidget(self.tr("Explorer"), self)
|
|
115
115
|
dock.setWidget(self.explorer)
|
|
116
116
|
dock.setObjectName("ExplorerDock")
|
|
117
117
|
self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, dock)
|
|
@@ -126,7 +126,7 @@ class DockMixin:
|
|
|
126
126
|
self.console.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
|
127
127
|
self.console.customContextMenuRequested.connect(self._on_console_context_menu)
|
|
128
128
|
|
|
129
|
-
dock = QDockWidget("Console / Status", self)
|
|
129
|
+
dock = QDockWidget(self.tr("Console / Status"), self)
|
|
130
130
|
dock.setWidget(self.console)
|
|
131
131
|
dock.setObjectName("ConsoleDock")
|
|
132
132
|
self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, dock)
|
|
@@ -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)
|
|
@@ -104,7 +104,7 @@ class FileMixin:
|
|
|
104
104
|
if last_dir and not os.path.isdir(last_dir):
|
|
105
105
|
last_dir = ""
|
|
106
106
|
|
|
107
|
-
paths, _ = QFileDialog.getOpenFileNames(self, "Open Images", last_dir, filters)
|
|
107
|
+
paths, _ = QFileDialog.getOpenFileNames(self, self.tr("Open Images"), last_dir, filters)
|
|
108
108
|
if not paths:
|
|
109
109
|
return
|
|
110
110
|
|
|
@@ -120,8 +120,15 @@ 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
|
-
QMessageBox.warning(self, "Open failed", f"{p}\n\n{e}")
|
|
131
|
+
QMessageBox.warning(self, self.tr("Open failed"), f"{p}\n\n{e}")
|
|
125
132
|
|
|
126
133
|
def save_active(self):
|
|
127
134
|
from setiastro.saspro.main_helpers import (
|
|
@@ -176,7 +183,7 @@ class FileMixin:
|
|
|
176
183
|
suggested_path = os.path.join(candidate_dir, suggested_safe)
|
|
177
184
|
|
|
178
185
|
# --- Open dialog ----------------------------------------
|
|
179
|
-
path, selected_filter = QFileDialog.getSaveFileName(self, "Save As", suggested_path, filters)
|
|
186
|
+
path, selected_filter = QFileDialog.getSaveFileName(self, self.tr("Save As"), suggested_path, filters)
|
|
180
187
|
if not path:
|
|
181
188
|
return
|
|
182
189
|
|
|
@@ -201,7 +208,7 @@ class FileMixin:
|
|
|
201
208
|
self._log(f"Saved: {path} ({chosen_bd})")
|
|
202
209
|
self.settings.setValue("paths/last_save_dir", os.path.dirname(path))
|
|
203
210
|
except Exception as e:
|
|
204
|
-
QMessageBox.critical(self, "Save failed", str(e))
|
|
211
|
+
QMessageBox.critical(self, self.tr("Save failed"), str(e))
|
|
205
212
|
|
|
206
213
|
def _load_recent_lists(self):
|
|
207
214
|
"""Load MRU lists from QSettings."""
|
|
@@ -251,9 +258,8 @@ class FileMixin:
|
|
|
251
258
|
if not os.path.exists(path):
|
|
252
259
|
if QMessageBox.question(
|
|
253
260
|
self,
|
|
254
|
-
"File not found",
|
|
255
|
-
|
|
256
|
-
"Remove it from the recent images list?",
|
|
261
|
+
self.tr("File not found"),
|
|
262
|
+
self.tr("The file does not exist:\n{path}\n\nRemove it from the recent images list?").replace("{path}", path),
|
|
257
263
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
258
264
|
) == QMessageBox.StandardButton.Yes:
|
|
259
265
|
self._recent_image_paths = [p for p in self._recent_image_paths if p != path]
|
|
@@ -267,7 +273,7 @@ class FileMixin:
|
|
|
267
273
|
# bump to front
|
|
268
274
|
self._add_recent_image(path)
|
|
269
275
|
except Exception as e:
|
|
270
|
-
QMessageBox.warning(self, "Open failed", f"{path}\n\n{e}")
|
|
276
|
+
QMessageBox.warning(self, self.tr("Open failed"), f"{path}\n\n{e}")
|
|
271
277
|
|
|
272
278
|
def _open_recent_project(self, path: str):
|
|
273
279
|
if not path:
|
|
@@ -275,9 +281,8 @@ class FileMixin:
|
|
|
275
281
|
if not os.path.exists(path):
|
|
276
282
|
if QMessageBox.question(
|
|
277
283
|
self,
|
|
278
|
-
"Project not found",
|
|
279
|
-
|
|
280
|
-
"Remove it from the recent projects list?",
|
|
284
|
+
self.tr("Project not found"),
|
|
285
|
+
self.tr("The project file does not exist:\n{path}\n\nRemove it from the recent projects list?").replace("{path}", path),
|
|
281
286
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
282
287
|
) == QMessageBox.StandardButton.Yes:
|
|
283
288
|
self._recent_project_paths = [p for p in self._recent_project_paths if p != path]
|
|
@@ -310,7 +315,7 @@ class FileMixin:
|
|
|
310
315
|
|
|
311
316
|
def _save_project(self):
|
|
312
317
|
path, _ = QFileDialog.getSaveFileName(
|
|
313
|
-
self, "Save Project", "", "SetiAstro Project (*.sas)"
|
|
318
|
+
self, self.tr("Save Project"), "", "SetiAstro Project (*.sas)"
|
|
314
319
|
)
|
|
315
320
|
if not path:
|
|
316
321
|
return
|
|
@@ -319,15 +324,15 @@ class FileMixin:
|
|
|
319
324
|
|
|
320
325
|
docs = self._collect_open_documents()
|
|
321
326
|
if not docs:
|
|
322
|
-
QMessageBox.warning(self, "Save Project", "No documents to save.")
|
|
327
|
+
QMessageBox.warning(self, self.tr("Save Project"), self.tr("No documents to save."))
|
|
323
328
|
return
|
|
324
329
|
|
|
325
330
|
try:
|
|
326
331
|
compress = self._ask_project_compress() # your existing yes/no dialog
|
|
327
332
|
|
|
328
333
|
# Busy dialog (indeterminate)
|
|
329
|
-
dlg = QProgressDialog("Saving project...", "", 0, 0, self)
|
|
330
|
-
dlg.setWindowTitle("Saving")
|
|
334
|
+
dlg = QProgressDialog(self.tr("Saving project..."), "", 0, 0, self)
|
|
335
|
+
dlg.setWindowTitle(self.tr("Saving"))
|
|
331
336
|
# PyQt6 (with PyQt5 fallback if you ever run it there)
|
|
332
337
|
try:
|
|
333
338
|
dlg.setWindowModality(Qt.WindowModality.ApplicationModal)
|
|
@@ -365,7 +370,7 @@ class FileMixin:
|
|
|
365
370
|
self._proj_save_worker.error.connect(
|
|
366
371
|
lambda msg: (
|
|
367
372
|
dlg.close(),
|
|
368
|
-
QMessageBox.critical(self, "Save Project",
|
|
373
|
+
QMessageBox.critical(self, self.tr("Save Project"), self.tr("Failed to save:\n{msg}").replace("{msg}", msg)),
|
|
369
374
|
)
|
|
370
375
|
)
|
|
371
376
|
self._proj_save_worker.finished.connect(
|
|
@@ -374,7 +379,7 @@ class FileMixin:
|
|
|
374
379
|
self._proj_save_worker.start()
|
|
375
380
|
|
|
376
381
|
except Exception as e:
|
|
377
|
-
QMessageBox.critical(self, "Save Project",
|
|
382
|
+
QMessageBox.critical(self, self.tr("Save Project"), self.tr("Failed to save:\n{e}").replace("{e}", str(e)))
|
|
378
383
|
|
|
379
384
|
def _load_project(self):
|
|
380
385
|
# warn / clear current desktop
|
|
@@ -382,7 +387,7 @@ class FileMixin:
|
|
|
382
387
|
return
|
|
383
388
|
|
|
384
389
|
path, _ = QFileDialog.getOpenFileName(
|
|
385
|
-
self, "Load Project", "", "SetiAstro Project (*.sas)"
|
|
390
|
+
self, self.tr("Load Project"), "", "SetiAstro Project (*.sas)"
|
|
386
391
|
)
|
|
387
392
|
if not path:
|
|
388
393
|
return
|
|
@@ -390,8 +395,8 @@ class FileMixin:
|
|
|
390
395
|
self._do_load_project_path(path)
|
|
391
396
|
|
|
392
397
|
def _new_project(self):
|
|
393
|
-
if not self._confirm_discard(title="New Project",
|
|
394
|
-
msg="Start a new project? This closes all views and clears desktop shortcuts."):
|
|
398
|
+
if not self._confirm_discard(title=self.tr("New Project"),
|
|
399
|
+
msg=self.tr("Start a new project? This closes all views and clears desktop shortcuts.")):
|
|
395
400
|
return
|
|
396
401
|
|
|
397
402
|
# Close views + docs + shelf
|
|
@@ -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
|
|
@@ -131,7 +129,7 @@ class GeometryMixin:
|
|
|
131
129
|
view = sw.widget() if sw else None
|
|
132
130
|
doc = getattr(view, "document", None)
|
|
133
131
|
if doc is None or getattr(doc, "image", None) is None:
|
|
134
|
-
QMessageBox.information(self, "Invert", "Active view has no image.")
|
|
132
|
+
QMessageBox.information(self, self.tr("Invert"), self.tr("Active view has no image."))
|
|
135
133
|
return
|
|
136
134
|
try:
|
|
137
135
|
self._apply_geom_invert_to_doc(doc)
|
|
@@ -145,7 +143,7 @@ class GeometryMixin:
|
|
|
145
143
|
view = sw.widget() if sw else None
|
|
146
144
|
doc = getattr(view, "document", None)
|
|
147
145
|
if doc is None or getattr(doc, "image", None) is None:
|
|
148
|
-
QMessageBox.information(self, "Flip Horizontal", "Active view has no image.")
|
|
146
|
+
QMessageBox.information(self, self.tr("Flip Horizontal"), self.tr("Active view has no image."))
|
|
149
147
|
return
|
|
150
148
|
try:
|
|
151
149
|
self._apply_geom_flip_h_to_doc(doc)
|
|
@@ -159,7 +157,7 @@ class GeometryMixin:
|
|
|
159
157
|
view = sw.widget() if sw else None
|
|
160
158
|
doc = getattr(view, "document", None)
|
|
161
159
|
if doc is None or getattr(doc, "image", None) is None:
|
|
162
|
-
QMessageBox.information(self, "Flip Vertical", "Active view has no image.")
|
|
160
|
+
QMessageBox.information(self, self.tr("Flip Vertical"), self.tr("Active view has no image."))
|
|
163
161
|
return
|
|
164
162
|
try:
|
|
165
163
|
self._apply_geom_flip_v_to_doc(doc)
|
|
@@ -173,7 +171,7 @@ class GeometryMixin:
|
|
|
173
171
|
view = sw.widget() if sw else None
|
|
174
172
|
doc = getattr(view, "document", None)
|
|
175
173
|
if doc is None or getattr(doc, "image", None) is None:
|
|
176
|
-
QMessageBox.information(self, "Rotate 90° CW", "Active view has no image.")
|
|
174
|
+
QMessageBox.information(self, self.tr("Rotate 90° CW"), self.tr("Active view has no image."))
|
|
177
175
|
return
|
|
178
176
|
try:
|
|
179
177
|
self._apply_geom_rot_cw_to_doc(doc)
|
|
@@ -187,7 +185,7 @@ class GeometryMixin:
|
|
|
187
185
|
view = sw.widget() if sw else None
|
|
188
186
|
doc = getattr(view, "document", None)
|
|
189
187
|
if doc is None or getattr(doc, "image", None) is None:
|
|
190
|
-
QMessageBox.information(self, "Rotate 90° CCW", "Active view has no image.")
|
|
188
|
+
QMessageBox.information(self, self.tr("Rotate 90° CCW"), self.tr("Active view has no image."))
|
|
191
189
|
return
|
|
192
190
|
try:
|
|
193
191
|
self._apply_geom_rot_ccw_to_doc(doc)
|
|
@@ -201,7 +199,7 @@ class GeometryMixin:
|
|
|
201
199
|
view = sw.widget() if sw else None
|
|
202
200
|
doc = getattr(view, "document", None)
|
|
203
201
|
if doc is None or getattr(doc, "image", None) is None:
|
|
204
|
-
QMessageBox.information(self, "Rotate 180°", "Active view has no image.")
|
|
202
|
+
QMessageBox.information(self, self.tr("Rotate 180°"), self.tr("Active view has no image."))
|
|
205
203
|
return
|
|
206
204
|
try:
|
|
207
205
|
self._apply_geom_rot_180_to_doc(doc)
|
|
@@ -209,13 +207,51 @@ 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
|
|
215
251
|
view = sw.widget() if sw else None
|
|
216
252
|
doc = getattr(view, "document", None)
|
|
217
253
|
if doc is None or getattr(doc, "image", None) is None:
|
|
218
|
-
QMessageBox.information(self, "Rescale Image", "Active view has no image.")
|
|
254
|
+
QMessageBox.information(self, self.tr("Rescale Image"), self.tr("Active view has no image."))
|
|
219
255
|
return
|
|
220
256
|
|
|
221
257
|
# remember last value
|
|
@@ -223,8 +259,8 @@ class GeometryMixin:
|
|
|
223
259
|
self._last_rescale_factor = 1.0
|
|
224
260
|
|
|
225
261
|
dlg = QInputDialog(self)
|
|
226
|
-
dlg.setWindowTitle("Rescale Image")
|
|
227
|
-
dlg.setLabelText("Enter scaling factor (e.g., 0.5 for 50%, 2 for 200%):")
|
|
262
|
+
dlg.setWindowTitle(self.tr("Rescale Image"))
|
|
263
|
+
dlg.setLabelText(self.tr("Enter scaling factor (e.g., 0.5 for 50%, 2 for 200%):"))
|
|
228
264
|
dlg.setInputMode(QInputDialog.InputMode.DoubleInput)
|
|
229
265
|
dlg.setDoubleRange(0.1, 10.0)
|
|
230
266
|
dlg.setDoubleDecimals(2)
|
|
@@ -249,7 +285,7 @@ class GeometryMixin:
|
|
|
249
285
|
self._last_rescale_factor = factor
|
|
250
286
|
self._log(f"Rescale ({factor:g}×) applied to active view")
|
|
251
287
|
except Exception as e:
|
|
252
|
-
QMessageBox.critical(self, "Rescale Image", str(e))
|
|
288
|
+
QMessageBox.critical(self, self.tr("Rescale Image"), str(e))
|
|
253
289
|
|
|
254
290
|
# --- Geometry: headless apply-to-doc helpers ---
|
|
255
291
|
|
|
@@ -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)))
|
|
@@ -199,7 +199,7 @@ class HeaderMixin:
|
|
|
199
199
|
# Fallback path: extract -> populate, all guarded.
|
|
200
200
|
rows = self._extract_header_pairs(doc)
|
|
201
201
|
if not rows:
|
|
202
|
-
self._clear_header_viewer("No header" if doc else "No image")
|
|
202
|
+
self._clear_header_viewer(self.tr("No header") if doc else self.tr("No image"))
|
|
203
203
|
else:
|
|
204
204
|
self._populate_header_viewer(rows)
|
|
205
205
|
except Exception as e:
|
|
@@ -245,7 +245,7 @@ class HeaderMixin:
|
|
|
245
245
|
out.append((str(k), str(v), ""))
|
|
246
246
|
return out
|
|
247
247
|
if fmt == "repr":
|
|
248
|
-
return [("Header", str(snap.get("text", "")), "")]
|
|
248
|
+
return [(self.tr("Header"), str(snap.get("text", "")), "")]
|
|
249
249
|
|
|
250
250
|
# 2) Live header object(s) (can be astropy, dict, or random).
|
|
251
251
|
hdr = (meta.get("original_header")
|
|
@@ -297,7 +297,7 @@ class HeaderMixin:
|
|
|
297
297
|
return out
|
|
298
298
|
|
|
299
299
|
# Fallback: string repr
|
|
300
|
-
return [("Header", str(hdr), "")]
|
|
300
|
+
return [(self.tr("Header"), str(hdr), "")]
|
|
301
301
|
except Exception as e:
|
|
302
302
|
print("[header] extract suppressed:", e)
|
|
303
303
|
return []
|
|
@@ -319,7 +319,7 @@ class HeaderMixin:
|
|
|
319
319
|
try:
|
|
320
320
|
w.setRowCount(0)
|
|
321
321
|
w.setColumnCount(3)
|
|
322
|
-
w.setHorizontalHeaderLabels(["Key", "Value", "Comment"])
|
|
322
|
+
w.setHorizontalHeaderLabels([self.tr("Key"), self.tr("Value"), self.tr("Comment")])
|
|
323
323
|
for r, (k, v, c) in enumerate(rows):
|
|
324
324
|
w.insertRow(r)
|
|
325
325
|
w.setItem(r, 0, QTableWidgetItem(k))
|
|
@@ -370,7 +370,7 @@ class HeaderMixin:
|
|
|
370
370
|
if isinstance(w, QTableWidget):
|
|
371
371
|
w.setRowCount(0)
|
|
372
372
|
w.setColumnCount(3)
|
|
373
|
-
w.setHorizontalHeaderLabels(["Key", "Value", "Comment"])
|
|
373
|
+
w.setHorizontalHeaderLabels([self.tr("Key"), self.tr("Value"), self.tr("Comment")])
|
|
374
374
|
return
|
|
375
375
|
except Exception:
|
|
376
376
|
pass
|
|
@@ -421,7 +421,7 @@ class HeaderMixin:
|
|
|
421
421
|
def _on_doc_removed_for_header_sync(self, doc):
|
|
422
422
|
"""If the removed doc was the active one, clear header."""
|
|
423
423
|
if doc is self._active_doc():
|
|
424
|
-
self._clear_header_viewer("No image")
|
|
424
|
+
self._clear_header_viewer(self.tr("No image"))
|
|
425
425
|
hv = getattr(self, "header_viewer", None)
|
|
426
426
|
if hv and hasattr(hv, "set_document"):
|
|
427
427
|
try:
|
|
@@ -32,7 +32,7 @@ class MaskMixin:
|
|
|
32
32
|
doc = getattr(vw, "document", None)
|
|
33
33
|
has_mask = bool(doc and getattr(doc, "active_mask_id", None))
|
|
34
34
|
if not has_mask:
|
|
35
|
-
QMessageBox.information(self, "Mask Overlay", "No active mask on this image.")
|
|
35
|
+
QMessageBox.information(self, self.tr("Mask Overlay"), self.tr("No active mask on this image."))
|
|
36
36
|
return
|
|
37
37
|
vw.show_mask_overlay = True
|
|
38
38
|
# ensure visuals are up-to-date immediately
|
|
@@ -94,7 +94,7 @@ class MaskMixin:
|
|
|
94
94
|
|
|
95
95
|
doc = self._current_document()
|
|
96
96
|
if doc is None or getattr(doc, "image", None) is None:
|
|
97
|
-
QMessageBox.information(self, "No image", "Open an image first.")
|
|
97
|
+
QMessageBox.information(self, self.tr("No image"), self.tr("Open an image first."))
|
|
98
98
|
return
|
|
99
99
|
created = create_mask_and_attach(self, doc)
|
|
100
100
|
# Optional toast/log
|
|
@@ -200,12 +200,12 @@ class MaskMixin:
|
|
|
200
200
|
"""Show dialog to apply a mask from another document."""
|
|
201
201
|
target_doc = self._active_doc()
|
|
202
202
|
if not target_doc:
|
|
203
|
-
QMessageBox.information(self, "Mask", "No active document.")
|
|
203
|
+
QMessageBox.information(self, self.tr("Mask"), self.tr("No active document."))
|
|
204
204
|
return
|
|
205
205
|
|
|
206
206
|
candidates = self._list_candidate_mask_sources(exclude_doc=target_doc)
|
|
207
207
|
if not candidates:
|
|
208
|
-
QMessageBox.information(self, "Mask", "Open another image to use as a mask.")
|
|
208
|
+
QMessageBox.information(self, self.tr("Mask"), self.tr("Open another image to use as a mask."))
|
|
209
209
|
return
|
|
210
210
|
|
|
211
211
|
# If there are multiple, ask which one to use
|
|
@@ -215,8 +215,8 @@ class MaskMixin:
|
|
|
215
215
|
else:
|
|
216
216
|
from PyQt6.QtWidgets import QInputDialog
|
|
217
217
|
names = [f"{i + 1}. {d.display_name()}" for i, d in enumerate(candidates)]
|
|
218
|
-
choice, ok = QInputDialog.getItem(self, "Choose Mask Image",
|
|
219
|
-
"Use this image as mask:", names, 0, False)
|
|
218
|
+
choice, ok = QInputDialog.getItem(self, self.tr("Choose Mask Image"),
|
|
219
|
+
self.tr("Use this image as mask:"), names, 0, False)
|
|
220
220
|
if not ok:
|
|
221
221
|
return
|
|
222
222
|
idx = names.index(choice)
|
|
@@ -286,7 +286,7 @@ class MaskMixin:
|
|
|
286
286
|
return
|
|
287
287
|
mid = getattr(doc, "active_mask_id", None)
|
|
288
288
|
if not mid:
|
|
289
|
-
QMessageBox.information(self, "Mask", "No active mask to remove.")
|
|
289
|
+
QMessageBox.information(self, self.tr("Mask"), self.tr("No active mask to remove."))
|
|
290
290
|
return
|
|
291
291
|
try:
|
|
292
292
|
doc.remove_mask(mid)
|
|
@@ -347,7 +347,7 @@ class MaskMixin:
|
|
|
347
347
|
|
|
348
348
|
if src_doc is None:
|
|
349
349
|
print(f"[MainWindow] _handle_mask_drop: no src_doc for ptr={src_ptr}")
|
|
350
|
-
QMessageBox.warning(self, "Mask", "Could not resolve mask document.")
|
|
350
|
+
QMessageBox.warning(self, self.tr("Mask"), self.tr("Could not resolve mask document."))
|
|
351
351
|
return
|
|
352
352
|
|
|
353
353
|
# --- 2) Resolve target view / doc ----------------------------------
|