setiastrosuitepro 1.6.2__py3-none-any.whl → 1.6.12__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/abeicon.svg +16 -0
- setiastro/images/acv_icon.png +0 -0
- setiastro/images/colorwheel.svg +97 -0
- setiastro/images/cosmic.svg +40 -0
- setiastro/images/cosmicsat.svg +24 -0
- setiastro/images/first_quarter.png +0 -0
- setiastro/images/full_moon.png +0 -0
- setiastro/images/graxpert.svg +19 -0
- setiastro/images/last_quarter.png +0 -0
- setiastro/images/linearfit.svg +32 -0
- setiastro/images/new_moon.png +0 -0
- setiastro/images/pixelmath.svg +42 -0
- setiastro/images/rotatearbitrary.png +0 -0
- setiastro/images/waning_crescent_1.png +0 -0
- setiastro/images/waning_crescent_2.png +0 -0
- setiastro/images/waning_crescent_3.png +0 -0
- setiastro/images/waning_crescent_4.png +0 -0
- setiastro/images/waning_crescent_5.png +0 -0
- setiastro/images/waning_gibbous_1.png +0 -0
- setiastro/images/waning_gibbous_2.png +0 -0
- setiastro/images/waning_gibbous_3.png +0 -0
- setiastro/images/waning_gibbous_4.png +0 -0
- setiastro/images/waning_gibbous_5.png +0 -0
- setiastro/images/waxing_crescent_1.png +0 -0
- setiastro/images/waxing_crescent_2.png +0 -0
- setiastro/images/waxing_crescent_3.png +0 -0
- setiastro/images/waxing_crescent_4.png +0 -0
- setiastro/images/waxing_crescent_5.png +0 -0
- setiastro/images/waxing_gibbous_1.png +0 -0
- setiastro/images/waxing_gibbous_2.png +0 -0
- setiastro/images/waxing_gibbous_3.png +0 -0
- setiastro/images/waxing_gibbous_4.png +0 -0
- setiastro/images/waxing_gibbous_5.png +0 -0
- setiastro/qml/ResourceMonitor.qml +84 -82
- setiastro/saspro/__main__.py +20 -1
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/abe.py +37 -4
- setiastro/saspro/aberration_ai.py +237 -21
- setiastro/saspro/acv_exporter.py +379 -0
- setiastro/saspro/add_stars.py +33 -6
- setiastro/saspro/backgroundneutral.py +114 -37
- setiastro/saspro/blemish_blaster.py +4 -1
- setiastro/saspro/blink_comparator_pro.py +548 -275
- setiastro/saspro/clahe.py +4 -1
- setiastro/saspro/continuum_subtract.py +4 -1
- setiastro/saspro/convo.py +13 -7
- setiastro/saspro/cosmicclarity.py +129 -18
- setiastro/saspro/crop_dialog_pro.py +134 -8
- setiastro/saspro/curve_editor_pro.py +109 -42
- setiastro/saspro/doc_manager.py +246 -16
- setiastro/saspro/exoplanet_detector.py +120 -28
- setiastro/saspro/frequency_separation.py +1158 -204
- setiastro/saspro/function_bundle.py +16 -16
- setiastro/saspro/ghs_dialog_pro.py +81 -16
- setiastro/saspro/graxpert.py +1 -0
- setiastro/saspro/gui/main_window.py +519 -289
- setiastro/saspro/gui/mixins/dock_mixin.py +276 -42
- setiastro/saspro/gui/mixins/geometry_mixin.py +105 -5
- setiastro/saspro/gui/mixins/menu_mixin.py +28 -1
- setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
- setiastro/saspro/gui/mixins/toolbar_mixin.py +416 -27
- setiastro/saspro/gui/mixins/update_mixin.py +138 -36
- setiastro/saspro/gui/mixins/view_mixin.py +42 -0
- setiastro/saspro/halobgon.py +4 -0
- setiastro/saspro/histogram.py +5 -1
- setiastro/saspro/image_combine.py +4 -0
- setiastro/saspro/image_peeker_pro.py +4 -0
- setiastro/saspro/imageops/starbasedwhitebalance.py +23 -52
- setiastro/saspro/imageops/stretch.py +582 -62
- setiastro/saspro/isophote.py +4 -0
- setiastro/saspro/layers.py +13 -9
- setiastro/saspro/layers_dock.py +183 -3
- setiastro/saspro/legacy/image_manager.py +154 -20
- setiastro/saspro/legacy/numba_utils.py +67 -47
- setiastro/saspro/legacy/xisf.py +240 -98
- setiastro/saspro/live_stacking.py +180 -79
- setiastro/saspro/luminancerecombine.py +228 -27
- setiastro/saspro/mask_creation.py +174 -15
- setiastro/saspro/mfdeconv.py +113 -35
- setiastro/saspro/mfdeconvcudnn.py +119 -70
- setiastro/saspro/mfdeconvsport.py +112 -35
- setiastro/saspro/morphology.py +4 -0
- setiastro/saspro/multiscale_decomp.py +748 -255
- setiastro/saspro/numba_utils.py +72 -57
- setiastro/saspro/ops/commands.py +18 -18
- setiastro/saspro/ops/script_editor.py +10 -2
- setiastro/saspro/ops/scripts.py +122 -0
- setiastro/saspro/perfect_palette_picker.py +37 -3
- setiastro/saspro/plate_solver.py +84 -49
- setiastro/saspro/psf_viewer.py +119 -37
- setiastro/saspro/remove_stars_preset.py +55 -13
- setiastro/saspro/resources.py +97 -11
- setiastro/saspro/rgbalign.py +4 -0
- setiastro/saspro/selective_color.py +83 -21
- setiastro/saspro/sfcc.py +364 -152
- setiastro/saspro/shortcuts.py +253 -49
- setiastro/saspro/signature_insert.py +692 -33
- setiastro/saspro/stacking_suite.py +1610 -574
- setiastro/saspro/star_alignment.py +522 -453
- setiastro/saspro/star_spikes.py +4 -0
- setiastro/saspro/star_stretch.py +38 -3
- setiastro/saspro/stat_stretch.py +743 -128
- setiastro/saspro/status_log_dock.py +1 -1
- setiastro/saspro/subwindow.py +786 -360
- setiastro/saspro/supernovaasteroidhunter.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/wavescale_hdr.py +4 -1
- setiastro/saspro/wavescalede.py +4 -1
- setiastro/saspro/whitebalance.py +84 -12
- setiastro/saspro/widgets/common_utilities.py +28 -21
- setiastro/saspro/widgets/minigame/game.js +11 -6
- setiastro/saspro/widgets/resource_monitor.py +133 -57
- setiastro/saspro/widgets/spinboxes.py +28 -13
- setiastro/saspro/wimi.py +92 -721
- setiastro/saspro/wims.py +46 -36
- setiastro/saspro/window_shelf.py +2 -2
- setiastro/saspro/xisf.py +101 -11
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/METADATA +8 -7
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/RECORD +162 -128
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/licenses/license.txt +0 -0
|
@@ -12,13 +12,43 @@ from PyQt6.QtCore import Qt, QTimer
|
|
|
12
12
|
from PyQt6.QtWidgets import (
|
|
13
13
|
QDockWidget, QPlainTextEdit, QTreeWidget, QTreeWidgetItem,
|
|
14
14
|
QVBoxLayout, QWidget, QTextEdit, QListWidget, QListWidgetItem,
|
|
15
|
-
QAbstractItemView, QApplication
|
|
15
|
+
QAbstractItemView, QApplication, QLineEdit, QMenu
|
|
16
16
|
)
|
|
17
|
-
from PyQt6.QtGui import QTextCursor, QAction
|
|
17
|
+
from PyQt6.QtGui import QTextCursor, QAction, QGuiApplication
|
|
18
18
|
|
|
19
19
|
if TYPE_CHECKING:
|
|
20
20
|
from PyQt6.QtWidgets import QAction
|
|
21
21
|
|
|
22
|
+
import os
|
|
23
|
+
|
|
24
|
+
GLYPHS = "■●◆▲▪▫•◼◻◾◽🔗"
|
|
25
|
+
|
|
26
|
+
def _strip_ui_decorations(text: str) -> str:
|
|
27
|
+
"""
|
|
28
|
+
Strip UI-only decorations from titles:
|
|
29
|
+
- Qt mnemonics (&)
|
|
30
|
+
- link badges like "[LINK]"
|
|
31
|
+
- your glyph badges
|
|
32
|
+
- file extension (optional, but nice for Explorer)
|
|
33
|
+
"""
|
|
34
|
+
if not text:
|
|
35
|
+
return ""
|
|
36
|
+
s = str(text)
|
|
37
|
+
|
|
38
|
+
# remove mnemonics
|
|
39
|
+
s = s.replace("&", "")
|
|
40
|
+
|
|
41
|
+
# remove common prefixes/badges
|
|
42
|
+
s = s.replace("[LINK]", "").strip()
|
|
43
|
+
|
|
44
|
+
# remove glyph badges
|
|
45
|
+
s = s.translate({ord(ch): None for ch in GLYPHS})
|
|
46
|
+
|
|
47
|
+
# collapse whitespace
|
|
48
|
+
s = " ".join(s.split())
|
|
49
|
+
|
|
50
|
+
return s
|
|
51
|
+
|
|
22
52
|
|
|
23
53
|
class DockMixin:
|
|
24
54
|
"""
|
|
@@ -105,14 +135,44 @@ class DockMixin:
|
|
|
105
135
|
self._view_panels_menu.removeAction(action)
|
|
106
136
|
|
|
107
137
|
def _init_explorer_dock(self):
|
|
108
|
-
|
|
109
|
-
|
|
138
|
+
host = QWidget(self)
|
|
139
|
+
lay = QVBoxLayout(host)
|
|
140
|
+
lay.setContentsMargins(4, 4, 4, 4)
|
|
141
|
+
lay.setSpacing(4)
|
|
142
|
+
|
|
143
|
+
# Optional filter box (super useful)
|
|
144
|
+
self.explorer_filter = QLineEdit(host)
|
|
145
|
+
self.explorer_filter.setPlaceholderText(self.tr("Filter open documents…"))
|
|
146
|
+
self.explorer_filter.textChanged.connect(self._explorer_apply_filter)
|
|
147
|
+
lay.addWidget(self.explorer_filter)
|
|
148
|
+
|
|
149
|
+
self.explorer = QTreeWidget(host)
|
|
150
|
+
self.explorer.setObjectName("ExplorerTree")
|
|
151
|
+
self.explorer.setColumnCount(3)
|
|
152
|
+
self.explorer.setHeaderLabels([self.tr("Document"), self.tr("Dims"), self.tr("Type")])
|
|
153
|
+
|
|
154
|
+
# Sorting
|
|
155
|
+
self.explorer.setSortingEnabled(True)
|
|
156
|
+
self.explorer.header().setSortIndicatorShown(True)
|
|
157
|
+
self.explorer.sortByColumn(0, Qt.SortOrder.AscendingOrder)
|
|
158
|
+
|
|
159
|
+
# Selection/activation behavior
|
|
160
|
+
self.explorer.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
|
110
161
|
self.explorer.itemActivated.connect(self._activate_or_open_from_explorer)
|
|
111
|
-
|
|
112
|
-
|
|
162
|
+
|
|
163
|
+
# Inline rename support
|
|
164
|
+
self.explorer.setEditTriggers(
|
|
165
|
+
QAbstractItemView.EditTrigger.EditKeyPressed |
|
|
166
|
+
QAbstractItemView.EditTrigger.SelectedClicked
|
|
167
|
+
)
|
|
168
|
+
self.explorer.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
|
169
|
+
self.explorer.customContextMenuRequested.connect(self._on_explorer_context_menu)
|
|
170
|
+
self.explorer.itemChanged.connect(self._on_explorer_item_changed)
|
|
171
|
+
|
|
172
|
+
lay.addWidget(self.explorer)
|
|
113
173
|
|
|
114
174
|
dock = QDockWidget(self.tr("Explorer"), self)
|
|
115
|
-
dock.setWidget(
|
|
175
|
+
dock.setWidget(host)
|
|
116
176
|
dock.setObjectName("ExplorerDock")
|
|
117
177
|
self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, dock)
|
|
118
178
|
|
|
@@ -243,22 +303,35 @@ class DockMixin:
|
|
|
243
303
|
self.settings.setValue("ui/resource_monitor_visible", checked)
|
|
244
304
|
|
|
245
305
|
def _update_monitor_position(self):
|
|
246
|
-
"""Snap monitor to bottom-right corner."""
|
|
306
|
+
"""Snap monitor to bottom-right corner or restore saved position."""
|
|
247
307
|
if hasattr(self, 'resource_monitor') and self.resource_monitor:
|
|
248
308
|
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
309
|
|
|
254
|
-
#
|
|
255
|
-
|
|
256
|
-
|
|
310
|
+
# Check for saved position first
|
|
311
|
+
saved_x = self.settings.value("ui/resource_monitor_pos_x", type=int)
|
|
312
|
+
saved_y = self.settings.value("ui/resource_monitor_pos_y", type=int)
|
|
257
313
|
|
|
258
|
-
#
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
314
|
+
if saved_x != 0 and saved_y != 0: # Basic validity check (0,0 is unlikely to be desired but also default if missing)
|
|
315
|
+
# Actually 0,0 is valid but type=int returns 0 if missing.
|
|
316
|
+
# Let's check string existence to be safer or just accept 0 if set.
|
|
317
|
+
# Checking existence via `contains` is better but value() logic is ok for now.
|
|
318
|
+
if self.settings.contains("ui/resource_monitor_pos_x"):
|
|
319
|
+
self.resource_monitor.move(saved_x, saved_y)
|
|
320
|
+
self.resource_monitor.raise_()
|
|
321
|
+
return
|
|
322
|
+
|
|
323
|
+
m = 5 # margin
|
|
324
|
+
|
|
325
|
+
screen = self.screen()
|
|
326
|
+
geom = screen.availableGeometry()
|
|
327
|
+
|
|
328
|
+
mw = self.resource_monitor.width()
|
|
329
|
+
mh = self.resource_monitor.height()
|
|
330
|
+
|
|
331
|
+
x = geom.x() + geom.width() - mw - m
|
|
332
|
+
y = geom.y() + geom.height() - mh - m
|
|
333
|
+
|
|
334
|
+
self.resource_monitor.move(x, y)
|
|
262
335
|
self.resource_monitor.raise_()
|
|
263
336
|
|
|
264
337
|
# We need to hook resizeEvent to call _update_monitor_position.
|
|
@@ -290,12 +363,12 @@ class DockMixin:
|
|
|
290
363
|
|
|
291
364
|
# Friendly ordering for common ones; others follow alphabetically.
|
|
292
365
|
order_hint = {
|
|
293
|
-
"Explorer": 10,
|
|
294
|
-
"Console / Status": 20,
|
|
295
|
-
"Header Viewer": 30,
|
|
296
|
-
"Layers": 40,
|
|
297
|
-
"Window Shelf": 50,
|
|
298
|
-
"Command Search": 60,
|
|
366
|
+
self.tr("Explorer"): 10,
|
|
367
|
+
self.tr("Console / Status"): 20,
|
|
368
|
+
self.tr("Header Viewer"): 30,
|
|
369
|
+
self.tr("Layers"): 40,
|
|
370
|
+
self.tr("Window Shelf"): 50,
|
|
371
|
+
self.tr("Command Search"): 60,
|
|
299
372
|
}
|
|
300
373
|
|
|
301
374
|
# Add special action for overlay monitor
|
|
@@ -328,35 +401,196 @@ class DockMixin:
|
|
|
328
401
|
base = self._normalize_base_doc(doc)
|
|
329
402
|
|
|
330
403
|
# de-dupe by identity on base
|
|
331
|
-
for i in range(self.explorer.
|
|
332
|
-
it = self.explorer.
|
|
333
|
-
if it.data(Qt.ItemDataRole.UserRole) is base:
|
|
334
|
-
|
|
335
|
-
it.setText(self._format_explorer_title(base))
|
|
404
|
+
for i in range(self.explorer.topLevelItemCount()):
|
|
405
|
+
it = self.explorer.topLevelItem(i)
|
|
406
|
+
if it.data(0, Qt.ItemDataRole.UserRole) is base:
|
|
407
|
+
self._refresh_explorer_row(it, base)
|
|
336
408
|
return
|
|
337
409
|
|
|
338
|
-
|
|
339
|
-
|
|
410
|
+
it = QTreeWidgetItem()
|
|
411
|
+
it.setData(0, Qt.ItemDataRole.UserRole, base)
|
|
412
|
+
|
|
413
|
+
# Make name editable; other columns read-only
|
|
414
|
+
it.setFlags(it.flags() | Qt.ItemFlag.ItemIsEditable | Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled)
|
|
415
|
+
|
|
416
|
+
self._refresh_explorer_row(it, base)
|
|
417
|
+
|
|
340
418
|
fp = (base.metadata or {}).get("file_path")
|
|
341
419
|
if fp:
|
|
342
|
-
|
|
343
|
-
|
|
420
|
+
it.setToolTip(0, fp)
|
|
421
|
+
|
|
422
|
+
self.explorer.addTopLevelItem(it)
|
|
344
423
|
|
|
345
424
|
# keep row label in sync with edits/resizes/renames
|
|
346
425
|
try:
|
|
347
|
-
base.changed.connect(lambda *_: self._update_explorer_item_for_doc(
|
|
426
|
+
base.changed.connect(lambda *_, d=base: self._update_explorer_item_for_doc(d))
|
|
348
427
|
except Exception:
|
|
349
428
|
pass
|
|
350
429
|
|
|
430
|
+
|
|
351
431
|
def _remove_doc_from_explorer(self, doc):
|
|
352
|
-
"""
|
|
353
|
-
Remove either the exact doc or its base (handles ROI proxies).
|
|
354
|
-
"""
|
|
355
432
|
base = self._normalize_base_doc(doc)
|
|
356
|
-
for i in range(self.explorer.
|
|
357
|
-
it = self.explorer.
|
|
358
|
-
d = it.data(Qt.ItemDataRole.UserRole)
|
|
433
|
+
for i in range(self.explorer.topLevelItemCount()):
|
|
434
|
+
it = self.explorer.topLevelItem(i)
|
|
435
|
+
d = it.data(0, Qt.ItemDataRole.UserRole)
|
|
359
436
|
if d is doc or d is base:
|
|
360
|
-
self.explorer.
|
|
437
|
+
self.explorer.takeTopLevelItem(i)
|
|
361
438
|
break
|
|
362
439
|
|
|
440
|
+
|
|
441
|
+
def _update_explorer_item_for_doc(self, doc):
|
|
442
|
+
for i in range(self.explorer.topLevelItemCount()):
|
|
443
|
+
it = self.explorer.topLevelItem(i)
|
|
444
|
+
if it.data(0, Qt.ItemDataRole.UserRole) is doc:
|
|
445
|
+
self._refresh_explorer_row(it, doc)
|
|
446
|
+
return
|
|
447
|
+
|
|
448
|
+
def _refresh_explorer_row(self, item, doc):
|
|
449
|
+
# Column 0: display name (NO glyph decorations)
|
|
450
|
+
name = _strip_ui_decorations(doc.display_name() or "Untitled")
|
|
451
|
+
|
|
452
|
+
name_no_ext, _ext = os.path.splitext(name)
|
|
453
|
+
if name_no_ext:
|
|
454
|
+
name = name_no_ext
|
|
455
|
+
|
|
456
|
+
item.setText(0, name)
|
|
457
|
+
|
|
458
|
+
# Column 1: dims
|
|
459
|
+
dims = ""
|
|
460
|
+
try:
|
|
461
|
+
import numpy as np
|
|
462
|
+
arr = getattr(doc, "image", None)
|
|
463
|
+
if isinstance(arr, np.ndarray) and arr.size:
|
|
464
|
+
h, w = arr.shape[:2]
|
|
465
|
+
c = arr.shape[2] if arr.ndim == 3 else 1
|
|
466
|
+
dims = f"{h}×{w}×{c}"
|
|
467
|
+
except Exception:
|
|
468
|
+
pass
|
|
469
|
+
item.setText(1, dims)
|
|
470
|
+
|
|
471
|
+
# Column 2: type/bit-depth (whatever you have available)
|
|
472
|
+
md = (doc.metadata or {})
|
|
473
|
+
bit = md.get("bit_depth") or md.get("dtype") or ""
|
|
474
|
+
kind = md.get("format") or md.get("doc_type") or ""
|
|
475
|
+
t = " / ".join([s for s in (str(kind), str(bit)) if s and s != "None"])
|
|
476
|
+
item.setText(2, t)
|
|
477
|
+
|
|
478
|
+
def _on_explorer_item_changed(self, item, col: int):
|
|
479
|
+
if col != 0:
|
|
480
|
+
return
|
|
481
|
+
|
|
482
|
+
doc = item.data(0, Qt.ItemDataRole.UserRole)
|
|
483
|
+
if doc is None:
|
|
484
|
+
return
|
|
485
|
+
|
|
486
|
+
new_name = (item.text(0) or "").strip()
|
|
487
|
+
if not new_name:
|
|
488
|
+
# revert to current doc name
|
|
489
|
+
self._refresh_explorer_row(item, doc)
|
|
490
|
+
return
|
|
491
|
+
|
|
492
|
+
# Avoid infinite loops: only apply if changed
|
|
493
|
+
cur = _strip_ui_decorations(doc.display_name() or "Untitled")
|
|
494
|
+
cur_no_ext, _ = os.path.splitext(cur)
|
|
495
|
+
cur = cur_no_ext or cur
|
|
496
|
+
if new_name == cur:
|
|
497
|
+
return
|
|
498
|
+
|
|
499
|
+
try:
|
|
500
|
+
doc.metadata["display_name"] = new_name
|
|
501
|
+
except Exception:
|
|
502
|
+
# if metadata missing or immutable, revert
|
|
503
|
+
self._refresh_explorer_row(item, doc)
|
|
504
|
+
return
|
|
505
|
+
|
|
506
|
+
try:
|
|
507
|
+
doc.changed.emit()
|
|
508
|
+
except Exception:
|
|
509
|
+
pass
|
|
510
|
+
|
|
511
|
+
def _on_explorer_context_menu(self, pos):
|
|
512
|
+
it = self.explorer.itemAt(pos)
|
|
513
|
+
if it is None:
|
|
514
|
+
return
|
|
515
|
+
doc = it.data(0, Qt.ItemDataRole.UserRole)
|
|
516
|
+
if doc is None:
|
|
517
|
+
return
|
|
518
|
+
|
|
519
|
+
menu = QMenu(self.explorer)
|
|
520
|
+
a_rename = menu.addAction(self.tr("Rename Document…"))
|
|
521
|
+
a_close = menu.addAction(self.tr("Close Document"))
|
|
522
|
+
menu.addSeparator()
|
|
523
|
+
a_copy_path = menu.addAction(self.tr("Copy File Path"))
|
|
524
|
+
a_reveal = menu.addAction(self.tr("Reveal in File Manager"))
|
|
525
|
+
menu.addSeparator()
|
|
526
|
+
a_send_shelf = menu.addAction(self.tr("Send View to Shelf")) # acts on active view for this doc
|
|
527
|
+
|
|
528
|
+
act = menu.exec(self.explorer.viewport().mapToGlobal(pos))
|
|
529
|
+
if act == a_rename:
|
|
530
|
+
# Start inline editing
|
|
531
|
+
self.explorer.editItem(it, 0)
|
|
532
|
+
|
|
533
|
+
elif act == a_close:
|
|
534
|
+
# close only if no other subwindows show it: you already do that in _on_view_about_to_close,
|
|
535
|
+
# but Explorer close is explicit; just close all views of this doc then docman.close_document.
|
|
536
|
+
try:
|
|
537
|
+
self._close_all_views_for_doc(doc)
|
|
538
|
+
except Exception:
|
|
539
|
+
pass
|
|
540
|
+
|
|
541
|
+
elif act == a_copy_path:
|
|
542
|
+
fp = (doc.metadata or {}).get("file_path", "")
|
|
543
|
+
if fp:
|
|
544
|
+
QGuiApplication.clipboard().setText(fp)
|
|
545
|
+
|
|
546
|
+
elif act == a_reveal:
|
|
547
|
+
fp = (doc.metadata or {}).get("file_path", "")
|
|
548
|
+
if fp:
|
|
549
|
+
self._reveal_in_file_manager(fp)
|
|
550
|
+
|
|
551
|
+
elif act == a_send_shelf:
|
|
552
|
+
sw = self._find_subwindow_for_doc(doc)
|
|
553
|
+
if sw and hasattr(sw.widget(), "_send_to_shelf"):
|
|
554
|
+
try:
|
|
555
|
+
sw.widget()._send_to_shelf()
|
|
556
|
+
except Exception:
|
|
557
|
+
pass
|
|
558
|
+
|
|
559
|
+
def _close_all_views_for_doc(self, doc):
|
|
560
|
+
base = self._normalize_base_doc(doc)
|
|
561
|
+
subs = list(self.mdi.subWindowList())
|
|
562
|
+
for sw in subs:
|
|
563
|
+
w = sw.widget()
|
|
564
|
+
if getattr(w, "base_document", None) is base:
|
|
565
|
+
try:
|
|
566
|
+
sw.close()
|
|
567
|
+
except Exception:
|
|
568
|
+
pass
|
|
569
|
+
# If none left (or even if close failed), try docman close defensively
|
|
570
|
+
try:
|
|
571
|
+
self.docman.close_document(base)
|
|
572
|
+
except Exception:
|
|
573
|
+
pass
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _reveal_in_file_manager(self, path: str):
|
|
577
|
+
import sys, os, subprocess
|
|
578
|
+
try:
|
|
579
|
+
if sys.platform.startswith("win"):
|
|
580
|
+
subprocess.Popen(["explorer", "/select,", os.path.normpath(path)])
|
|
581
|
+
elif sys.platform == "darwin":
|
|
582
|
+
subprocess.Popen(["open", "-R", path])
|
|
583
|
+
else:
|
|
584
|
+
# best-effort on Linux
|
|
585
|
+
subprocess.Popen(["xdg-open", os.path.dirname(path)])
|
|
586
|
+
except Exception:
|
|
587
|
+
pass
|
|
588
|
+
|
|
589
|
+
def _explorer_apply_filter(self, text: str):
|
|
590
|
+
t = (text or "").strip().lower()
|
|
591
|
+
for i in range(self.explorer.topLevelItemCount()):
|
|
592
|
+
it = self.explorer.topLevelItem(i)
|
|
593
|
+
name = (it.text(0) or "").lower()
|
|
594
|
+
fp = (it.toolTip(0) or "").lower()
|
|
595
|
+
hide = bool(t) and (t not in name) and (t not in fp)
|
|
596
|
+
it.setHidden(hide)
|
|
@@ -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)))
|
|
@@ -6,8 +6,9 @@ from __future__ import annotations
|
|
|
6
6
|
import os
|
|
7
7
|
from typing import TYPE_CHECKING
|
|
8
8
|
|
|
9
|
-
from PyQt6.QtGui import QAction
|
|
9
|
+
from PyQt6.QtGui import QAction, QKeySequence
|
|
10
10
|
from PyQt6.QtWidgets import QMenu, QToolButton, QWidgetAction
|
|
11
|
+
from PyQt6.QtCore import Qt
|
|
11
12
|
|
|
12
13
|
if TYPE_CHECKING:
|
|
13
14
|
pass
|
|
@@ -185,6 +186,7 @@ class MenuMixin:
|
|
|
185
186
|
m_geom.addAction(self.act_geom_rot_cw)
|
|
186
187
|
m_geom.addAction(self.act_geom_rot_ccw)
|
|
187
188
|
m_geom.addAction(self.act_geom_rot_180)
|
|
189
|
+
m_geom.addAction(self.act_geom_rot_any)
|
|
188
190
|
m_geom.addSeparator()
|
|
189
191
|
m_geom.addAction(self.act_geom_rescale)
|
|
190
192
|
m_geom.addSeparator()
|
|
@@ -228,6 +230,7 @@ class MenuMixin:
|
|
|
228
230
|
|
|
229
231
|
|
|
230
232
|
m_header = mb.addMenu(self.tr("&Header Mods && Misc"))
|
|
233
|
+
m_header.addAction(self.act_acv_exporter)
|
|
231
234
|
m_header.addAction(self.act_astrobin_exporter)
|
|
232
235
|
m_header.addAction(self.act_batch_convert)
|
|
233
236
|
m_header.addAction(self.act_batch_renamer)
|
|
@@ -265,6 +268,14 @@ class MenuMixin:
|
|
|
265
268
|
m_view.addAction(self.act_tile_grid)
|
|
266
269
|
m_view.addSeparator()
|
|
267
270
|
|
|
271
|
+
# NEW: Minimize All Views
|
|
272
|
+
self.act_minimize_all_views = QAction(self.tr("Minimize All Views"), self)
|
|
273
|
+
self.act_minimize_all_views.setShortcut(QKeySequence("Ctrl+Shift+M"))
|
|
274
|
+
self.act_minimize_all_views.setShortcutContext(Qt.ShortcutContext.ApplicationShortcut)
|
|
275
|
+
self.act_minimize_all_views.triggered.connect(self._minimize_all_views)
|
|
276
|
+
m_view.addAction(self.act_minimize_all_views)
|
|
277
|
+
|
|
278
|
+
m_view.addSeparator()
|
|
268
279
|
|
|
269
280
|
# a button that shows current group & opens a drop-down
|
|
270
281
|
self._link_btn = QToolButton(self)
|
|
@@ -387,3 +398,19 @@ class MenuMixin:
|
|
|
387
398
|
if sub is not None:
|
|
388
399
|
yield from self._iter_menu_actions(sub)
|
|
389
400
|
|
|
401
|
+
def _minimize_all_views(self):
|
|
402
|
+
mdi = getattr(self, "mdi", None)
|
|
403
|
+
if mdi is None:
|
|
404
|
+
return
|
|
405
|
+
|
|
406
|
+
try:
|
|
407
|
+
for sw in mdi.subWindowList():
|
|
408
|
+
try:
|
|
409
|
+
if not sw.isVisible():
|
|
410
|
+
continue
|
|
411
|
+
# Minimize each MDI child
|
|
412
|
+
sw.showMinimized()
|
|
413
|
+
except Exception:
|
|
414
|
+
pass
|
|
415
|
+
except Exception:
|
|
416
|
+
pass
|