setiastrosuitepro 1.6.4__py3-none-any.whl → 1.7.1.post2__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/TextureClarity.svg +56 -0
- 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/narrowbandnormalization.png +0 -0
- setiastro/images/new_moon.png +0 -0
- setiastro/images/pixelmath.svg +42 -0
- setiastro/images/planetarystacker.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 +364 -33
- setiastro/saspro/aberration_ai_preset.py +29 -3
- setiastro/saspro/acv_exporter.py +379 -0
- setiastro/saspro/add_stars.py +33 -6
- setiastro/saspro/astrospike_python.py +45 -3
- setiastro/saspro/backgroundneutral.py +108 -40
- setiastro/saspro/blemish_blaster.py +4 -1
- setiastro/saspro/blink_comparator_pro.py +150 -55
- 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 +123 -7
- setiastro/saspro/curve_editor_pro.py +181 -64
- setiastro/saspro/curves_preset.py +249 -47
- setiastro/saspro/doc_manager.py +245 -15
- setiastro/saspro/exoplanet_detector.py +120 -28
- setiastro/saspro/frequency_separation.py +1158 -204
- setiastro/saspro/ghs_dialog_pro.py +81 -16
- setiastro/saspro/graxpert.py +1 -0
- setiastro/saspro/gui/main_window.py +706 -264
- setiastro/saspro/gui/mixins/dock_mixin.py +245 -24
- setiastro/saspro/gui/mixins/file_mixin.py +35 -16
- setiastro/saspro/gui/mixins/menu_mixin.py +35 -1
- setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
- setiastro/saspro/gui/mixins/toolbar_mixin.py +499 -24
- 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 +184 -8
- setiastro/saspro/image_combine.py +4 -0
- setiastro/saspro/image_peeker_pro.py +4 -0
- setiastro/saspro/imageops/narrowband_normalization.py +816 -0
- setiastro/saspro/imageops/serloader.py +1345 -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 +68 -48
- setiastro/saspro/legacy/xisf.py +240 -98
- setiastro/saspro/live_stacking.py +203 -82
- 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 +81 -29
- setiastro/saspro/narrowband_normalization.py +1618 -0
- 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_green.py +1 -1
- setiastro/saspro/resources.py +73 -0
- setiastro/saspro/rgbalign.py +460 -12
- setiastro/saspro/selective_color.py +4 -1
- setiastro/saspro/ser_stack_config.py +82 -0
- setiastro/saspro/ser_stacker.py +2321 -0
- setiastro/saspro/ser_stacker_dialog.py +1838 -0
- setiastro/saspro/ser_tracking.py +206 -0
- setiastro/saspro/serviewer.py +1625 -0
- setiastro/saspro/sfcc.py +662 -216
- setiastro/saspro/shortcuts.py +171 -33
- setiastro/saspro/signature_insert.py +692 -33
- setiastro/saspro/stacking_suite.py +1347 -485
- setiastro/saspro/star_alignment.py +247 -123
- setiastro/saspro/star_spikes.py +4 -0
- setiastro/saspro/star_stretch.py +38 -3
- setiastro/saspro/stat_stretch.py +892 -129
- setiastro/saspro/subwindow.py +787 -363
- setiastro/saspro/supernovaasteroidhunter.py +1 -1
- setiastro/saspro/texture_clarity.py +593 -0
- 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/resource_monitor.py +209 -111
- setiastro/saspro/widgets/spinboxes.py +10 -13
- setiastro/saspro/wimi.py +27 -656
- setiastro/saspro/wims.py +13 -3
- setiastro/saspro/xisf.py +101 -11
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/METADATA +4 -2
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/RECORD +132 -87
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/licenses/license.txt +0 -0
|
@@ -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, QElapsedTimer
|
|
132
|
+
QPropertyAnimation, QEasingCurve, QElapsedTimer, QPoint
|
|
133
133
|
)
|
|
134
134
|
|
|
135
135
|
from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
|
|
@@ -168,7 +168,7 @@ except Exception:
|
|
|
168
168
|
|
|
169
169
|
|
|
170
170
|
|
|
171
|
-
|
|
171
|
+
_DEBUG_DND_DUP = False
|
|
172
172
|
|
|
173
173
|
|
|
174
174
|
|
|
@@ -193,9 +193,9 @@ from setiastro.saspro.resources import (
|
|
|
193
193
|
nbtorgb_path, freqsep_path, contsub_path, halo_path, cosmic_path,
|
|
194
194
|
satellite_path, imagecombine_path, wrench_path, eye_icon_path,multiscale_decomp_path,
|
|
195
195
|
disk_icon_path, nuke_path, hubble_path, collage_path, annotated_path,
|
|
196
|
-
colorwheel_path, font_path, csv_icon_path, spinner_path, wims_path,
|
|
197
|
-
wimi_path, linearfit_path, debayer_path, aberration_path,
|
|
198
|
-
functionbundles_path, viewbundles_path, selectivecolor_path, rgbalign_path,
|
|
196
|
+
colorwheel_path, font_path, csv_icon_path, spinner_path, wims_path, narrowbandnormalization_path,
|
|
197
|
+
wimi_path, linearfit_path, debayer_path, aberration_path, acv_icon_path,
|
|
198
|
+
functionbundles_path, viewbundles_path, selectivecolor_path, rgbalign_path, planetarystacker_path,
|
|
199
199
|
background_path, script_icon_path
|
|
200
200
|
)
|
|
201
201
|
|
|
@@ -285,6 +285,144 @@ from setiastro.saspro.gui.mixins import (
|
|
|
285
285
|
ThemeMixin, GeometryMixin, ViewMixin, HeaderMixin, MaskMixin, UpdateMixin
|
|
286
286
|
)
|
|
287
287
|
|
|
288
|
+
import sys
|
|
289
|
+
import time
|
|
290
|
+
import threading
|
|
291
|
+
import traceback
|
|
292
|
+
from PyQt6.QtCore import QObject, QTimer
|
|
293
|
+
|
|
294
|
+
class UiStallDetector(QObject):
|
|
295
|
+
"""
|
|
296
|
+
Detects UI stalls by watching QTimer tick drift.
|
|
297
|
+
On stall, prints stack traces for all threads using print().
|
|
298
|
+
(No faulthandler / no fileno() required.)
|
|
299
|
+
"""
|
|
300
|
+
|
|
301
|
+
def __init__(self, parent=None, interval_ms: int = 50, threshold_ms: int = 300):
|
|
302
|
+
super().__init__(parent)
|
|
303
|
+
self.interval_ms = int(interval_ms)
|
|
304
|
+
self.threshold_ms = int(threshold_ms)
|
|
305
|
+
self._last = time.perf_counter()
|
|
306
|
+
self._stall_seq = 0
|
|
307
|
+
|
|
308
|
+
# cooldown state (instance-level)
|
|
309
|
+
self._last_dump_t = 0.0
|
|
310
|
+
|
|
311
|
+
self._timer = QTimer(self)
|
|
312
|
+
self._timer.setInterval(self.interval_ms)
|
|
313
|
+
self._timer.timeout.connect(self._tick)
|
|
314
|
+
|
|
315
|
+
def start(self):
|
|
316
|
+
self._last = time.perf_counter()
|
|
317
|
+
self._timer.start()
|
|
318
|
+
|
|
319
|
+
def stop(self):
|
|
320
|
+
self._timer.stop()
|
|
321
|
+
|
|
322
|
+
def _dump_all_threads_print(self):
|
|
323
|
+
now = time.perf_counter()
|
|
324
|
+
if now - self._last_dump_t < 2.0: # 2s cooldown
|
|
325
|
+
print("[UI STALL] dump skipped (cooldown)", flush=True)
|
|
326
|
+
return
|
|
327
|
+
self._last_dump_t = now
|
|
328
|
+
|
|
329
|
+
frames = sys._current_frames()
|
|
330
|
+
main_ident = threading.main_thread().ident
|
|
331
|
+
|
|
332
|
+
print("[UI STALL] ===== lightweight dump (all threads) =====", flush=True)
|
|
333
|
+
|
|
334
|
+
# Main thread: full stack
|
|
335
|
+
if main_ident in frames:
|
|
336
|
+
print("\n--- MainThread (full) ---", flush=True)
|
|
337
|
+
print("".join(traceback.format_stack(frames[main_ident])), flush=True)
|
|
338
|
+
|
|
339
|
+
# Other threads: only top-frame summary
|
|
340
|
+
for t in threading.enumerate():
|
|
341
|
+
if t.ident is None or t.ident == main_ident:
|
|
342
|
+
continue
|
|
343
|
+
f = frames.get(t.ident)
|
|
344
|
+
if not f:
|
|
345
|
+
continue
|
|
346
|
+
code = f.f_code
|
|
347
|
+
print(
|
|
348
|
+
f"--- Thread {t.ident} ({t.name}) top --- {code.co_filename}:{f.f_lineno} in {code.co_name}",
|
|
349
|
+
flush=True,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
print("[UI STALL] ===== end lightweight dump =====", flush=True)
|
|
353
|
+
|
|
354
|
+
def _tick(self):
|
|
355
|
+
now = time.perf_counter()
|
|
356
|
+
elapsed_ms = (now - self._last) * 1000.0
|
|
357
|
+
self._last = now
|
|
358
|
+
|
|
359
|
+
late_ms = elapsed_ms - self.interval_ms
|
|
360
|
+
if late_ms >= self.threshold_ms:
|
|
361
|
+
print(f"[UI STALL] tick late by {late_ms:.0f} ms (elapsed={elapsed_ms:.0f} ms)", flush=True)
|
|
362
|
+
self._dump_all_threads_print()
|
|
363
|
+
|
|
364
|
+
def _strip_filename_ext(title: str) -> str:
|
|
365
|
+
t = (title or "").strip()
|
|
366
|
+
if not t:
|
|
367
|
+
return t
|
|
368
|
+
base, ext = os.path.splitext(t)
|
|
369
|
+
# treat as extension only if it looks like one: .fit .fits .tif .tiff .xisf etc
|
|
370
|
+
if ext and 1 <= len(ext) <= 10 and all(ch.isalnum() for ch in ext[1:]):
|
|
371
|
+
return base
|
|
372
|
+
return t
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
_DECOR_GLYPHS = "■●◆▲▪▫•◼◻◾◽🔗"
|
|
377
|
+
|
|
378
|
+
def normalize_doc_title(s: str) -> str:
|
|
379
|
+
s = (s or "").strip()
|
|
380
|
+
|
|
381
|
+
# remove our textual prefix too
|
|
382
|
+
if s.startswith("[LINK] "):
|
|
383
|
+
s = s[len("[LINK] "):].strip()
|
|
384
|
+
|
|
385
|
+
# strip common UI decorations if you already have this helper
|
|
386
|
+
try:
|
|
387
|
+
s = _strip_ui_decorations(s)
|
|
388
|
+
except Exception:
|
|
389
|
+
pass
|
|
390
|
+
|
|
391
|
+
# remove any leading decorator glyphs repeatedly: "🔗 ", "■ ", etc.
|
|
392
|
+
while len(s) >= 2 and s[0] in _DECOR_GLYPHS and s[1] == " ":
|
|
393
|
+
s = s[2:].lstrip()
|
|
394
|
+
|
|
395
|
+
# also remove any stray decorator glyphs that got embedded (rare but happens)
|
|
396
|
+
s = re.sub(rf"[{re.escape(_DECOR_GLYPHS)}]", "", s).strip()
|
|
397
|
+
|
|
398
|
+
return s
|
|
399
|
+
|
|
400
|
+
_VIEW_SUFFIX_RE = re.compile(r"\s+\[View\s+\d+\]\s*$")
|
|
401
|
+
|
|
402
|
+
def _normalize_title_for_compare(t: str) -> str:
|
|
403
|
+
t = (t or "").strip()
|
|
404
|
+
if not t:
|
|
405
|
+
return ""
|
|
406
|
+
|
|
407
|
+
# strip UI decorations (🔗, ■, etc)
|
|
408
|
+
try:
|
|
409
|
+
t = _strip_ui_decorations(t)
|
|
410
|
+
except Exception:
|
|
411
|
+
pass
|
|
412
|
+
|
|
413
|
+
# strip trailing "[View N]" if present
|
|
414
|
+
t = _VIEW_SUFFIX_RE.sub("", t).strip()
|
|
415
|
+
|
|
416
|
+
# strip filename-like extension
|
|
417
|
+
try:
|
|
418
|
+
t = _strip_filename_ext(t)
|
|
419
|
+
except Exception:
|
|
420
|
+
# fallback: only strip if it looks like an ext
|
|
421
|
+
base, ext = os.path.splitext(t)
|
|
422
|
+
if ext and len(ext) <= 10:
|
|
423
|
+
t = base
|
|
424
|
+
|
|
425
|
+
return t.strip()
|
|
288
426
|
|
|
289
427
|
class AstroSuiteProMainWindow(
|
|
290
428
|
DockMixin, MenuMixin, ToolbarMixin, FileMixin,
|
|
@@ -299,7 +437,8 @@ class AstroSuiteProMainWindow(
|
|
|
299
437
|
# Prevent white flash: start strictly transparent and force dark bg
|
|
300
438
|
self.setWindowOpacity(0.0)
|
|
301
439
|
self.setStyleSheet("QMainWindow { background-color: #0F0F19; }")
|
|
302
|
-
|
|
440
|
+
#self._stall = UiStallDetector(self, interval_ms=50, threshold_ms=250)
|
|
441
|
+
#self._stall.start()
|
|
303
442
|
# --- Usage Stats ---
|
|
304
443
|
self._session_start_time = time.time()
|
|
305
444
|
self._stats_timer = QTimer(self)
|
|
@@ -465,7 +604,10 @@ class AstroSuiteProMainWindow(
|
|
|
465
604
|
self.docman.documentAdded.connect(self._on_document_added)
|
|
466
605
|
self.mdi.viewStateDropped.connect(self._on_mdi_viewstate_drop)
|
|
467
606
|
self.mdi.linkViewDropped.connect(self._on_linkview_drop)
|
|
468
|
-
|
|
607
|
+
self._mdi_open_batch = 0
|
|
608
|
+
self._mdi_place_mode = "cascade" # or "tile"
|
|
609
|
+
self._mdi_next_pos = None # QPoint in MDI coords
|
|
610
|
+
self._mdi_cascade_step = 28
|
|
469
611
|
self.doc_manager.set_mdi_area(self.mdi)
|
|
470
612
|
# Coalesce undo/redo label refreshes
|
|
471
613
|
self._undo_redo_refresh_pending = False
|
|
@@ -856,7 +998,18 @@ class AstroSuiteProMainWindow(
|
|
|
856
998
|
dm = self.doc_manager
|
|
857
999
|
doc = None
|
|
858
1000
|
|
|
1001
|
+
if _DEBUG_DND_DUP:
|
|
1002
|
+
import json
|
|
1003
|
+
print("\n[DNDDBG:DROP_ENTER] raw st dict:")
|
|
1004
|
+
try:
|
|
1005
|
+
# st is already a dict here
|
|
1006
|
+
for k in sorted(st.keys()):
|
|
1007
|
+
print(f" {k}: {st.get(k)!r}")
|
|
1008
|
+
except Exception as e:
|
|
1009
|
+
print("[DNDDBG:DROP_ENTER] failed printing st:", e)
|
|
859
1010
|
|
|
1011
|
+
# sanity: show which fields are present
|
|
1012
|
+
print("[DNDDBG:DROP_ENTER] has source_view_title?", "source_view_title" in st)
|
|
860
1013
|
|
|
861
1014
|
# Prefer *stable* identifiers over the proxy pointer
|
|
862
1015
|
uid = st.get("doc_uid")
|
|
@@ -959,7 +1112,21 @@ class AstroSuiteProMainWindow(
|
|
|
959
1112
|
print("[VIEWSTATE_DROP] EXIT (no doc)")
|
|
960
1113
|
return
|
|
961
1114
|
|
|
962
|
-
|
|
1115
|
+
if _DEBUG_DND_DUP:
|
|
1116
|
+
try:
|
|
1117
|
+
dname = doc.display_name() if hasattr(doc, "display_name") else None
|
|
1118
|
+
except Exception:
|
|
1119
|
+
dname = None
|
|
1120
|
+
try:
|
|
1121
|
+
meta = getattr(doc, "metadata", {}) or {}
|
|
1122
|
+
except Exception:
|
|
1123
|
+
meta = {}
|
|
1124
|
+
print("\n[DNDDBG:DOC_RESOLVED]")
|
|
1125
|
+
print(" doc_obj:", doc, "type:", type(doc).__name__, "id:", id(doc))
|
|
1126
|
+
print(" doc.uid:", getattr(doc, "uid", None))
|
|
1127
|
+
print(" doc.display_name():", dname)
|
|
1128
|
+
print(" meta.display_name:", meta.get("display_name"))
|
|
1129
|
+
print(" meta.file_path:", meta.get("file_path"))
|
|
963
1130
|
|
|
964
1131
|
# ----------------------------------------
|
|
965
1132
|
# 4) Peek at metadata to see if this is a
|
|
@@ -1050,34 +1217,35 @@ class AstroSuiteProMainWindow(
|
|
|
1050
1217
|
# copy the view transform.
|
|
1051
1218
|
# ----------------------------------------
|
|
1052
1219
|
if force_new:
|
|
1053
|
-
# We're here only if:
|
|
1054
|
-
# - it's NOT a preview (normal full or promoted ROI), or
|
|
1055
|
-
# - ROI promotion didn't apply and we fell through.
|
|
1056
1220
|
base_doc = doc
|
|
1057
1221
|
|
|
1058
|
-
# 1)
|
|
1059
|
-
|
|
1060
|
-
|
|
1222
|
+
# 1) Prefer the dragged view's title
|
|
1223
|
+
base_name = (st.get("source_view_title") or "").strip()
|
|
1224
|
+
|
|
1225
|
+
# 2) Fallback to document display name
|
|
1226
|
+
if not base_name:
|
|
1061
1227
|
try:
|
|
1062
1228
|
base_name = base_doc.display_name() or "Untitled"
|
|
1063
1229
|
except Exception:
|
|
1064
1230
|
base_name = "Untitled"
|
|
1065
1231
|
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
)
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1232
|
+
# 3) Clean it (strip glyphs / "Active View" / etc.)
|
|
1233
|
+
try:
|
|
1234
|
+
base_name = _strip_ui_decorations(base_name)
|
|
1235
|
+
except Exception:
|
|
1236
|
+
if base_name.startswith("Active View: "):
|
|
1237
|
+
base_name = base_name[len("Active View: "):]
|
|
1238
|
+
|
|
1239
|
+
if _DEBUG_DND_DUP:
|
|
1240
|
+
print("\n[DNDDBG:NAME_COMPUTE]")
|
|
1241
|
+
print(" st.source_view_title:", (st.get("source_view_title") or "").strip())
|
|
1242
|
+
print(" base_doc.display_name():", (base_doc.display_name() if hasattr(base_doc,"display_name") else None))
|
|
1243
|
+
print(" base_name(after fallbacks/strip):", base_name)
|
|
1244
|
+
print(" new_name passed:", f"{base_name}_duplicate")
|
|
1245
|
+
|
|
1246
|
+
new_doc = self.docman.duplicate_document(
|
|
1247
|
+
base_doc, new_name=f"{base_name}_duplicate"
|
|
1248
|
+
)
|
|
1081
1249
|
|
|
1082
1250
|
# 2) Let doc_manager's documentAdded handler create the subwindow.
|
|
1083
1251
|
# We just wait for it to show up and then apply the view state.
|
|
@@ -1203,14 +1371,6 @@ class AstroSuiteProMainWindow(
|
|
|
1203
1371
|
return False
|
|
1204
1372
|
|
|
1205
1373
|
def _on_document_added(self, doc):
|
|
1206
|
-
# Helpful debug:
|
|
1207
|
-
try:
|
|
1208
|
-
is_table = (getattr(doc, "metadata", {}).get("doc_type") == "table") or \
|
|
1209
|
-
(hasattr(doc, "rows") and hasattr(doc, "headers"))
|
|
1210
|
-
self._log(f"[documentAdded] {type(doc).__name__} table={is_table} name={getattr(doc, 'display_name', lambda:'?')()}")
|
|
1211
|
-
except Exception:
|
|
1212
|
-
pass
|
|
1213
|
-
|
|
1214
1374
|
self._spawn_subwindow_for(doc)
|
|
1215
1375
|
|
|
1216
1376
|
# --- UI scaffolding ---
|
|
@@ -2503,10 +2663,27 @@ class AstroSuiteProMainWindow(
|
|
|
2503
2663
|
return f"{name}{dims}"
|
|
2504
2664
|
|
|
2505
2665
|
def _update_explorer_item_for_doc(self, doc):
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2666
|
+
# Delegate to DockMixin implementation if present
|
|
2667
|
+
try:
|
|
2668
|
+
return super()._update_explorer_item_for_doc(doc)
|
|
2669
|
+
except Exception:
|
|
2670
|
+
pass
|
|
2671
|
+
|
|
2672
|
+
# Fallback: tree-safe implementation
|
|
2673
|
+
if not hasattr(self, "explorer") or self.explorer is None:
|
|
2674
|
+
return
|
|
2675
|
+
try:
|
|
2676
|
+
n = self.explorer.topLevelItemCount()
|
|
2677
|
+
except Exception:
|
|
2678
|
+
return
|
|
2679
|
+
|
|
2680
|
+
for i in range(n):
|
|
2681
|
+
it = self.explorer.topLevelItem(i)
|
|
2682
|
+
if it.data(0, Qt.ItemDataRole.UserRole) is doc:
|
|
2683
|
+
try:
|
|
2684
|
+
self._refresh_explorer_row(it, doc)
|
|
2685
|
+
except Exception:
|
|
2686
|
+
pass
|
|
2510
2687
|
return
|
|
2511
2688
|
#-----------FUNCTIONS----------------
|
|
2512
2689
|
|
|
@@ -3015,17 +3192,17 @@ class AstroSuiteProMainWindow(
|
|
|
3015
3192
|
|
|
3016
3193
|
self.convo_window.show()
|
|
3017
3194
|
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
3195
|
def _apply_extract_luminance_preset_to_doc(self, doc, preset=None):
|
|
3021
|
-
|
|
3022
3196
|
from PyQt6.QtWidgets import QMessageBox
|
|
3023
|
-
from setiastro.saspro.luminancerecombine import
|
|
3024
|
-
|
|
3197
|
+
from setiastro.saspro.luminancerecombine import (
|
|
3198
|
+
compute_luminance,
|
|
3199
|
+
resolve_luma_profile_weights,
|
|
3200
|
+
)
|
|
3201
|
+
from setiastro.saspro.headless_utils import unwrap_docproxy
|
|
3202
|
+
import numpy as np
|
|
3025
3203
|
|
|
3026
3204
|
doc = unwrap_docproxy(doc)
|
|
3027
3205
|
p = dict(preset or {})
|
|
3028
|
-
mode = (p.get("mode") or "rec709").lower()
|
|
3029
3206
|
|
|
3030
3207
|
if doc is None or getattr(doc, "image", None) is None:
|
|
3031
3208
|
QMessageBox.information(self, "Extract Luminance", "No target image.")
|
|
@@ -3033,52 +3210,43 @@ class AstroSuiteProMainWindow(
|
|
|
3033
3210
|
|
|
3034
3211
|
img = np.asarray(doc.image)
|
|
3035
3212
|
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
w = _LUMA_REC601
|
|
3039
|
-
elif mode == "rec2020":
|
|
3040
|
-
w = _LUMA_REC2020
|
|
3041
|
-
elif mode == "max":
|
|
3042
|
-
w = None
|
|
3043
|
-
else:
|
|
3044
|
-
w = _LUMA_REC709
|
|
3213
|
+
mode = str(p.get("mode", "rec709")).strip()
|
|
3214
|
+
resolved_method, w, profile_name = resolve_luma_profile_weights(mode)
|
|
3045
3215
|
|
|
3046
|
-
L = compute_luminance(img, method=
|
|
3216
|
+
L = compute_luminance(img, method=resolved_method, weights=w)
|
|
3047
3217
|
|
|
3048
3218
|
dm = getattr(self, "doc_manager", None)
|
|
3049
3219
|
if dm is None:
|
|
3050
|
-
# headless fallback: just overwrite active doc
|
|
3051
3220
|
doc.apply_edit(L.astype(np.float32), step_name="Extract Luminance")
|
|
3052
3221
|
return
|
|
3053
3222
|
|
|
3054
|
-
|
|
3223
|
+
meta = {
|
|
3224
|
+
"step_name": "Extract Luminance",
|
|
3225
|
+
"luma_method": resolved_method,
|
|
3226
|
+
}
|
|
3227
|
+
if w is not None:
|
|
3228
|
+
meta["luma_weights"] = np.asarray(w, dtype=np.float32).tolist()
|
|
3229
|
+
if profile_name:
|
|
3230
|
+
meta["luma_profile"] = str(profile_name)
|
|
3231
|
+
|
|
3055
3232
|
try:
|
|
3233
|
+
suffix = f"{profile_name}" if profile_name else resolved_method
|
|
3056
3234
|
new_doc = dm.create_document_from_array(
|
|
3057
3235
|
L.astype(np.float32),
|
|
3058
|
-
name=f"{doc.display_name()} -- Luminance ({
|
|
3236
|
+
name=f"{doc.display_name()} -- Luminance ({suffix})",
|
|
3059
3237
|
is_mono=True,
|
|
3060
|
-
metadata=
|
|
3238
|
+
metadata=meta,
|
|
3061
3239
|
)
|
|
3062
3240
|
dm.add_document(new_doc)
|
|
3063
3241
|
except Exception:
|
|
3064
|
-
# safe fallback
|
|
3065
3242
|
doc.apply_edit(L.astype(np.float32), step_name="Extract Luminance")
|
|
3066
3243
|
|
|
3067
|
-
|
|
3068
3244
|
def _extract_luminance(self, doc=None, preset: dict | None = None):
|
|
3069
|
-
from
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
Preset schema:
|
|
3076
|
-
{
|
|
3077
|
-
"mode": "rec709" | "rec601" | "rec2020" | "max" | "snr" | "equal" | "median",
|
|
3078
|
-
# aliases accepted: method, luma_method, nb_max -> "max", snr_unequal -> "snr"
|
|
3079
|
-
}
|
|
3080
|
-
"""
|
|
3081
|
-
# 1) resolve source document
|
|
3245
|
+
from PyQt6.QtWidgets import QMessageBox
|
|
3246
|
+
from PyQt6.QtGui import QIcon
|
|
3247
|
+
from setiastro.saspro.luminancerecombine import compute_luminance, resolve_luma_profile_weights
|
|
3248
|
+
|
|
3249
|
+
|
|
3082
3250
|
sw = None
|
|
3083
3251
|
if doc is None:
|
|
3084
3252
|
sw = self.mdi.activeSubWindow()
|
|
@@ -3097,70 +3265,19 @@ class AstroSuiteProMainWindow(
|
|
|
3097
3265
|
QMessageBox.information(self, "Extract Luminance", "Luminance extraction requires an RGB image.")
|
|
3098
3266
|
return
|
|
3099
3267
|
|
|
3100
|
-
# 2) normalize to [0,1] float32
|
|
3101
|
-
a = img.astype(np.float32, copy=False)
|
|
3102
|
-
if a.size:
|
|
3103
|
-
m = float(np.nanmax(a))
|
|
3104
|
-
if np.isfinite(m) and m > 1.0:
|
|
3105
|
-
a = a / m
|
|
3106
|
-
a = np.clip(a, 0.0, 1.0)
|
|
3107
|
-
|
|
3108
|
-
# 3) choose luminance method
|
|
3109
3268
|
p = dict(preset or {})
|
|
3110
|
-
|
|
3269
|
+
mode = str(
|
|
3111
3270
|
p.get("mode",
|
|
3112
3271
|
p.get("method",
|
|
3113
3272
|
p.get("luma_method",
|
|
3114
3273
|
getattr(self, "luma_method", "rec709"))))
|
|
3115
|
-
).strip()
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
"k": "rec709",
|
|
3123
|
-
"rec.601": "rec601",
|
|
3124
|
-
"rec-601": "rec601",
|
|
3125
|
-
"rec.2020": "rec2020",
|
|
3126
|
-
"rec-2020": "rec2020",
|
|
3127
|
-
"nb_max": "max",
|
|
3128
|
-
"narrowband": "max",
|
|
3129
|
-
"snr_unequal": "snr",
|
|
3130
|
-
"unequal_noise": "snr",
|
|
3131
|
-
}
|
|
3132
|
-
method = alias.get(method, method)
|
|
3133
|
-
|
|
3134
|
-
# 4) compute luminance per selected method
|
|
3135
|
-
luma_weights = None
|
|
3136
|
-
if method == "rec601":
|
|
3137
|
-
luma_weights = _LUMA_REC601
|
|
3138
|
-
y = np.tensordot(a, _LUMA_REC601, axes=([2],[0]))
|
|
3139
|
-
elif method == "rec2020":
|
|
3140
|
-
luma_weights = _LUMA_REC2020
|
|
3141
|
-
y = np.tensordot(a, _LUMA_REC2020, axes=([2],[0]))
|
|
3142
|
-
elif method == "max":
|
|
3143
|
-
y = a.max(axis=2)
|
|
3144
|
-
elif method == "median":
|
|
3145
|
-
y = np.median(a, axis=2)
|
|
3146
|
-
elif method == "equal":
|
|
3147
|
-
luma_weights = np.array([1/3, 1/3, 1/3], dtype=np.float32)
|
|
3148
|
-
y = a.mean(axis=2)
|
|
3149
|
-
elif method == "snr":
|
|
3150
|
-
from setiastro.saspro.luminancerecombine import _estimate_noise_sigma_per_channel
|
|
3151
|
-
sigma = _estimate_noise_sigma_per_channel(a)
|
|
3152
|
-
w = 1.0 / (sigma[:3]**2 + 1e-12)
|
|
3153
|
-
w = w / w.sum()
|
|
3154
|
-
luma_weights = w.astype(np.float32)
|
|
3155
|
-
y = np.tensordot(a[..., :3], luma_weights, axes=([2],[0]))
|
|
3156
|
-
else: # "rec709" default
|
|
3157
|
-
method = "rec709"
|
|
3158
|
-
luma_weights = _LUMA_REC709
|
|
3159
|
-
y = np.tensordot(a, _LUMA_REC709, axes=([2],[0]))
|
|
3160
|
-
|
|
3161
|
-
y = np.clip(y.astype(np.float32, copy=False), 0.0, 1.0)
|
|
3162
|
-
|
|
3163
|
-
# 5) metadata & title
|
|
3274
|
+
).strip()
|
|
3275
|
+
|
|
3276
|
+
resolved_method, w, profile_name = resolve_luma_profile_weights(mode)
|
|
3277
|
+
|
|
3278
|
+
y = compute_luminance(img, method=resolved_method, weights=w)
|
|
3279
|
+
|
|
3280
|
+
# ---- metadata & title ----
|
|
3164
3281
|
base_meta = {}
|
|
3165
3282
|
try:
|
|
3166
3283
|
base_meta = dict(getattr(doc, "metadata", {}) or {})
|
|
@@ -3172,13 +3289,16 @@ class AstroSuiteProMainWindow(
|
|
|
3172
3289
|
"source": "ExtractLuminance",
|
|
3173
3290
|
"is_mono": True,
|
|
3174
3291
|
"bit_depth": "32f",
|
|
3175
|
-
"luma_method":
|
|
3292
|
+
"luma_method": resolved_method,
|
|
3176
3293
|
}
|
|
3177
|
-
if
|
|
3178
|
-
meta["luma_weights"] = np.asarray(
|
|
3294
|
+
if w is not None:
|
|
3295
|
+
meta["luma_weights"] = np.asarray(w, dtype=np.float32).tolist()
|
|
3296
|
+
if profile_name:
|
|
3297
|
+
meta["luma_profile"] = str(profile_name)
|
|
3179
3298
|
|
|
3180
3299
|
base_title = sw.windowTitle() if sw else (getattr(doc, "title", getattr(doc, "name", "")) or "Untitled")
|
|
3181
|
-
|
|
3300
|
+
suffix = f"{profile_name}" if profile_name else resolved_method
|
|
3301
|
+
title = f"{base_title} -- Luminance ({suffix})"
|
|
3182
3302
|
|
|
3183
3303
|
dm = getattr(self, "docman", None)
|
|
3184
3304
|
if dm is None:
|
|
@@ -3206,19 +3326,19 @@ class AstroSuiteProMainWindow(
|
|
|
3206
3326
|
except Exception:
|
|
3207
3327
|
pass
|
|
3208
3328
|
|
|
3209
|
-
# ðŸ" Remember for Replay (optional but consistent)
|
|
3210
3329
|
try:
|
|
3211
3330
|
remember = getattr(self, "remember_last_headless_command", None) or getattr(self, "_remember_last_headless_command", None)
|
|
3212
3331
|
if callable(remember):
|
|
3213
|
-
remember("extract_luminance", {"mode":
|
|
3332
|
+
remember("extract_luminance", {"mode": mode}, description="Extract Luminance")
|
|
3214
3333
|
except Exception:
|
|
3215
3334
|
pass
|
|
3216
3335
|
|
|
3217
3336
|
if hasattr(self, "_log"):
|
|
3218
|
-
self._log(f"Extract Luminance ({
|
|
3337
|
+
self._log(f"Extract Luminance ({suffix}) -> new mono document created.")
|
|
3219
3338
|
|
|
3220
3339
|
return new_doc
|
|
3221
3340
|
|
|
3341
|
+
|
|
3222
3342
|
def _subwindow_docs(self):
|
|
3223
3343
|
docs = []
|
|
3224
3344
|
for sw in self.mdi.subWindowList():
|
|
@@ -3788,6 +3908,19 @@ class AstroSuiteProMainWindow(
|
|
|
3788
3908
|
|
|
3789
3909
|
dlg.show()
|
|
3790
3910
|
|
|
3911
|
+
def _open_narrowband_normalization_tool(self):
|
|
3912
|
+
# Correct module import
|
|
3913
|
+
from setiastro.saspro.narrowband_normalization import NarrowbandNormalization
|
|
3914
|
+
|
|
3915
|
+
w = NarrowbandNormalization(doc_manager=self.docman, parent=self)
|
|
3916
|
+
w.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
3917
|
+
w.setWindowTitle("Narrowband Normalization")
|
|
3918
|
+
try:
|
|
3919
|
+
w.setWindowIcon(QIcon(narrowbandnormalization_path))
|
|
3920
|
+
except Exception:
|
|
3921
|
+
pass
|
|
3922
|
+
w.show()
|
|
3923
|
+
|
|
3791
3924
|
def _open_ppp_tool(self):
|
|
3792
3925
|
from setiastro.saspro.perfect_palette_picker import PerfectPalettePicker
|
|
3793
3926
|
w = PerfectPalettePicker(doc_manager=self.docman) # parent gives access to _spawn_subwindow_for
|
|
@@ -4139,6 +4272,14 @@ class AstroSuiteProMainWindow(
|
|
|
4139
4272
|
dlg.setWindowIcon(QIcon(livestacking_path))
|
|
4140
4273
|
dlg.show()
|
|
4141
4274
|
|
|
4275
|
+
def _open_planetary_stacker(self):
|
|
4276
|
+
# import locally to avoid startup cost / circular imports
|
|
4277
|
+
from setiastro.saspro.serviewer import SERViewer
|
|
4278
|
+
dlg = SERViewer(self)
|
|
4279
|
+
dlg.setWindowFlag(Qt.WindowType.Window, True)
|
|
4280
|
+
dlg.setWindowIcon(QIcon(planetarystacker_path))
|
|
4281
|
+
dlg.show()
|
|
4282
|
+
|
|
4142
4283
|
def _open_stacking_suite(self):
|
|
4143
4284
|
# Reuse if we already have one
|
|
4144
4285
|
dlg = getattr(self, "_stacking_suite", None)
|
|
@@ -4182,6 +4323,202 @@ class AstroSuiteProMainWindow(
|
|
|
4182
4323
|
except Exception:
|
|
4183
4324
|
pass
|
|
4184
4325
|
|
|
4326
|
+
def _convert_mono_to_rgb_active(self):
|
|
4327
|
+
"""
|
|
4328
|
+
Convert active mono document to RGB by duplicating the channel.
|
|
4329
|
+
Updates the active document in-place (undoable).
|
|
4330
|
+
"""
|
|
4331
|
+
dm = getattr(self, "docman", None)
|
|
4332
|
+
if dm is None:
|
|
4333
|
+
return
|
|
4334
|
+
|
|
4335
|
+
try:
|
|
4336
|
+
doc = dm.get_active_document()
|
|
4337
|
+
except Exception:
|
|
4338
|
+
doc = None
|
|
4339
|
+
if doc is None:
|
|
4340
|
+
return
|
|
4341
|
+
|
|
4342
|
+
img = getattr(doc, "image", None)
|
|
4343
|
+
if img is None:
|
|
4344
|
+
return
|
|
4345
|
+
|
|
4346
|
+
import numpy as np
|
|
4347
|
+
|
|
4348
|
+
x = np.asarray(img)
|
|
4349
|
+
|
|
4350
|
+
# Already RGB?
|
|
4351
|
+
if x.ndim == 3 and x.shape[-1] == 3:
|
|
4352
|
+
try:
|
|
4353
|
+
name = getattr(doc, "display_name", lambda: None)() or getattr(doc, "name", "") or "Active"
|
|
4354
|
+
except Exception:
|
|
4355
|
+
name = "Active"
|
|
4356
|
+
if hasattr(self, "_log"):
|
|
4357
|
+
self._log(f"Mono → RGB: '{name}' is already RGB (shape={getattr(x,'shape',None)}).")
|
|
4358
|
+
return
|
|
4359
|
+
|
|
4360
|
+
# Determine what we're converting FROM
|
|
4361
|
+
src_desc = "unknown"
|
|
4362
|
+
if x.ndim == 2:
|
|
4363
|
+
mono = x
|
|
4364
|
+
src_desc = "mono (H×W)"
|
|
4365
|
+
elif x.ndim == 3 and x.shape[-1] == 1:
|
|
4366
|
+
mono = x[..., 0]
|
|
4367
|
+
src_desc = "mono (H×W×1)"
|
|
4368
|
+
else:
|
|
4369
|
+
# Unknown format (e.g., multi-channel >3)
|
|
4370
|
+
try:
|
|
4371
|
+
name = getattr(doc, "display_name", lambda: None)() or getattr(doc, "name", "") or "Active"
|
|
4372
|
+
except Exception:
|
|
4373
|
+
name = "Active"
|
|
4374
|
+
if hasattr(self, "_log"):
|
|
4375
|
+
self._log(f"Mono → RGB: '{name}' not convertible (shape={getattr(x,'shape',None)}).")
|
|
4376
|
+
return
|
|
4377
|
+
|
|
4378
|
+
before_shape = getattr(x, "shape", None)
|
|
4379
|
+
before_dtype = getattr(x, "dtype", None)
|
|
4380
|
+
|
|
4381
|
+
mono = mono.astype(np.float32, copy=False)
|
|
4382
|
+
rgb = np.stack([mono, mono, mono], axis=-1)
|
|
4383
|
+
|
|
4384
|
+
# metadata: preserve existing, but force "not mono"
|
|
4385
|
+
try:
|
|
4386
|
+
md = dict(getattr(doc, "metadata", None) or {})
|
|
4387
|
+
except Exception:
|
|
4388
|
+
md = {}
|
|
4389
|
+
|
|
4390
|
+
md["is_mono"] = False
|
|
4391
|
+
md["color_model"] = "RGB"
|
|
4392
|
+
md["channels"] = 3
|
|
4393
|
+
md["source"] = (md.get("source") or "Edit")
|
|
4394
|
+
|
|
4395
|
+
# If you track op params for history explorer
|
|
4396
|
+
md["__op_params__"] = {
|
|
4397
|
+
"op": "mono_to_rgb",
|
|
4398
|
+
"mode": "triplicate",
|
|
4399
|
+
"from": str(src_desc),
|
|
4400
|
+
"from_shape": tuple(before_shape) if before_shape is not None else None,
|
|
4401
|
+
"to_shape": tuple(rgb.shape),
|
|
4402
|
+
}
|
|
4403
|
+
|
|
4404
|
+
# name for logging
|
|
4405
|
+
try:
|
|
4406
|
+
name = getattr(doc, "display_name", lambda: None)() or getattr(doc, "name", "") or "Active"
|
|
4407
|
+
except Exception:
|
|
4408
|
+
name = "Active"
|
|
4409
|
+
|
|
4410
|
+
try:
|
|
4411
|
+
dm.update_active_document(
|
|
4412
|
+
rgb,
|
|
4413
|
+
metadata=md,
|
|
4414
|
+
step_name="Mono → RGB",
|
|
4415
|
+
doc=doc, # explicit is safer
|
|
4416
|
+
)
|
|
4417
|
+
|
|
4418
|
+
if hasattr(self, "_log"):
|
|
4419
|
+
self._log(
|
|
4420
|
+
f"Mono → RGB: '{name}' converted {src_desc} "
|
|
4421
|
+
f"(shape={before_shape}, dtype={before_dtype}) → "
|
|
4422
|
+
f"RGB (shape={rgb.shape}, dtype={rgb.dtype})."
|
|
4423
|
+
)
|
|
4424
|
+
|
|
4425
|
+
except Exception:
|
|
4426
|
+
import traceback
|
|
4427
|
+
try:
|
|
4428
|
+
from PyQt6.QtWidgets import QMessageBox
|
|
4429
|
+
QMessageBox.critical(self, "Mono → RGB", traceback.format_exc())
|
|
4430
|
+
except Exception:
|
|
4431
|
+
pass
|
|
4432
|
+
|
|
4433
|
+
def _swap_rb_active(self):
|
|
4434
|
+
"""
|
|
4435
|
+
Swap R and B channels in the active RGB document (undoable).
|
|
4436
|
+
Intended for debayer/channel-order mismatches.
|
|
4437
|
+
"""
|
|
4438
|
+
dm = getattr(self, "docman", None)
|
|
4439
|
+
if dm is None:
|
|
4440
|
+
return
|
|
4441
|
+
|
|
4442
|
+
try:
|
|
4443
|
+
doc = dm.get_active_document()
|
|
4444
|
+
except Exception:
|
|
4445
|
+
doc = None
|
|
4446
|
+
if doc is None:
|
|
4447
|
+
return
|
|
4448
|
+
|
|
4449
|
+
img = getattr(doc, "image", None)
|
|
4450
|
+
if img is None:
|
|
4451
|
+
return
|
|
4452
|
+
|
|
4453
|
+
import numpy as np
|
|
4454
|
+
x = np.asarray(img)
|
|
4455
|
+
|
|
4456
|
+
# Must be RGB
|
|
4457
|
+
if not (x.ndim == 3 and x.shape[-1] == 3):
|
|
4458
|
+
try:
|
|
4459
|
+
name = getattr(doc, "display_name", lambda: None)() or getattr(doc, "name", "") or "Active"
|
|
4460
|
+
except Exception:
|
|
4461
|
+
name = "Active"
|
|
4462
|
+
|
|
4463
|
+
if hasattr(self, "_log"):
|
|
4464
|
+
self._log(f"Swap R/B: '{name}' is not RGB (shape={getattr(x,'shape',None)}).")
|
|
4465
|
+
return
|
|
4466
|
+
|
|
4467
|
+
before_shape = x.shape
|
|
4468
|
+
before_dtype = x.dtype
|
|
4469
|
+
|
|
4470
|
+
# swap channels without changing dtype
|
|
4471
|
+
# (copy is safest so we don't mutate shared views)
|
|
4472
|
+
out = x.copy()
|
|
4473
|
+
out[..., 0], out[..., 2] = x[..., 2], x[..., 0]
|
|
4474
|
+
|
|
4475
|
+
# metadata: preserve existing, but annotate operation
|
|
4476
|
+
try:
|
|
4477
|
+
md = dict(getattr(doc, "metadata", None) or {})
|
|
4478
|
+
except Exception:
|
|
4479
|
+
md = {}
|
|
4480
|
+
|
|
4481
|
+
md["color_model"] = md.get("color_model", "RGB")
|
|
4482
|
+
md["channels"] = 3
|
|
4483
|
+
md["is_mono"] = False
|
|
4484
|
+
md["source"] = (md.get("source") or "Edit")
|
|
4485
|
+
|
|
4486
|
+
# If you track op params for history explorer
|
|
4487
|
+
md["__op_params__"] = {
|
|
4488
|
+
"op": "swap_rb",
|
|
4489
|
+
"from_shape": tuple(before_shape),
|
|
4490
|
+
"to_shape": tuple(out.shape),
|
|
4491
|
+
"dtype": str(before_dtype),
|
|
4492
|
+
}
|
|
4493
|
+
|
|
4494
|
+
try:
|
|
4495
|
+
name = getattr(doc, "display_name", lambda: None)() or getattr(doc, "name", "") or "Active"
|
|
4496
|
+
except Exception:
|
|
4497
|
+
name = "Active"
|
|
4498
|
+
|
|
4499
|
+
try:
|
|
4500
|
+
dm.update_active_document(
|
|
4501
|
+
out,
|
|
4502
|
+
metadata=md,
|
|
4503
|
+
step_name="Swap R ↔ B",
|
|
4504
|
+
doc=doc,
|
|
4505
|
+
)
|
|
4506
|
+
|
|
4507
|
+
if hasattr(self, "_log"):
|
|
4508
|
+
self._log(
|
|
4509
|
+
f"Swap R/B: '{name}' swapped channels "
|
|
4510
|
+
f"(shape={before_shape}, dtype={before_dtype})."
|
|
4511
|
+
)
|
|
4512
|
+
|
|
4513
|
+
except Exception:
|
|
4514
|
+
import traceback
|
|
4515
|
+
try:
|
|
4516
|
+
from PyQt6.QtWidgets import QMessageBox
|
|
4517
|
+
QMessageBox.critical(self, "Swap R/B", traceback.format_exc())
|
|
4518
|
+
except Exception:
|
|
4519
|
+
pass
|
|
4520
|
+
|
|
4521
|
+
|
|
4185
4522
|
def _on_stackingsuite_relaunch(self, old_dir: str, new_dir: str):
|
|
4186
4523
|
# Optional: respond to dialog's relaunch request
|
|
4187
4524
|
try:
|
|
@@ -4282,9 +4619,16 @@ class AstroSuiteProMainWindow(
|
|
|
4282
4619
|
# Create a callback to set the image back to the document
|
|
4283
4620
|
def set_image_callback(image_data, step_name):
|
|
4284
4621
|
"""Apply the result image back to the active document."""
|
|
4285
|
-
if active_doc and hasattr(active_doc, "
|
|
4622
|
+
if active_doc and hasattr(active_doc, "apply_edit"):
|
|
4286
4623
|
print(f"[AstroSpike] Setting image back to document, shape: {image_data.shape}")
|
|
4287
|
-
#
|
|
4624
|
+
# Use apply_edit for proper undo/redo integration
|
|
4625
|
+
meta = {
|
|
4626
|
+
"step_name": step_name,
|
|
4627
|
+
"astrospike": True
|
|
4628
|
+
}
|
|
4629
|
+
active_doc.apply_edit(image_data.astype(np.float32, copy=False), metadata=meta, step_name=step_name)
|
|
4630
|
+
elif active_doc and hasattr(active_doc, "set_image"):
|
|
4631
|
+
print(f"[AstroSpike] Setting image via set_image, shape: {image_data.shape}")
|
|
4288
4632
|
active_doc.set_image(image_data, metadata={}, step_name=step_name)
|
|
4289
4633
|
elif active_doc and hasattr(active_doc, "image"):
|
|
4290
4634
|
print(f"[AstroSpike] Setting image directly, shape: {image_data.shape}")
|
|
@@ -4469,6 +4813,48 @@ class AstroSuiteProMainWindow(
|
|
|
4469
4813
|
dlg.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
4470
4814
|
dlg.show()
|
|
4471
4815
|
|
|
4816
|
+
def _open_acv_exporter(self):
|
|
4817
|
+
from setiastro.saspro.acv_exporter import AstroCatalogueViewerExporterDialog
|
|
4818
|
+
|
|
4819
|
+
dm = getattr(self, "doc_manager", None) or getattr(self, "docman", None)
|
|
4820
|
+
if dm is None:
|
|
4821
|
+
QMessageBox.information(self, "Astro Catalogue Viewer Exporter", "No document manager available.")
|
|
4822
|
+
return
|
|
4823
|
+
|
|
4824
|
+
sw = self.mdi.activeSubWindow()
|
|
4825
|
+
if not sw:
|
|
4826
|
+
QMessageBox.information(self, "Astro Catalogue Viewer Exporter", "Open an image first.")
|
|
4827
|
+
return
|
|
4828
|
+
|
|
4829
|
+
view = sw.widget()
|
|
4830
|
+
active_doc = None
|
|
4831
|
+
|
|
4832
|
+
# Prefer ROI-aware resolution
|
|
4833
|
+
try:
|
|
4834
|
+
if hasattr(dm, "get_document_for_view"):
|
|
4835
|
+
active_doc = dm.get_document_for_view(view)
|
|
4836
|
+
except Exception:
|
|
4837
|
+
active_doc = None
|
|
4838
|
+
|
|
4839
|
+
# Fallback
|
|
4840
|
+
if active_doc is None:
|
|
4841
|
+
try:
|
|
4842
|
+
active_doc = getattr(view, "document", None)
|
|
4843
|
+
except Exception:
|
|
4844
|
+
active_doc = None
|
|
4845
|
+
|
|
4846
|
+
if active_doc is None or getattr(active_doc, "image", None) is None:
|
|
4847
|
+
QMessageBox.information(self, "Astro Catalogue Viewer Exporter", "No active image.")
|
|
4848
|
+
return
|
|
4849
|
+
|
|
4850
|
+
dlg = AstroCatalogueViewerExporterDialog(self, dm, active_doc)
|
|
4851
|
+
try:
|
|
4852
|
+
dlg.setWindowIcon(QIcon(acv_icon_path))
|
|
4853
|
+
except Exception:
|
|
4854
|
+
pass
|
|
4855
|
+
dlg.show()
|
|
4856
|
+
|
|
4857
|
+
|
|
4472
4858
|
def _open_linear_fit(self):
|
|
4473
4859
|
from setiastro.saspro.linear_fit import LinearFitDialog
|
|
4474
4860
|
dm = getattr(self, "doc_manager", None) or getattr(self, "docman", None)
|
|
@@ -4585,12 +4971,6 @@ class AstroSuiteProMainWindow(
|
|
|
4585
4971
|
if max_len and len(hist) > max_len:
|
|
4586
4972
|
del hist[:-max_len]
|
|
4587
4973
|
|
|
4588
|
-
# Logging as before
|
|
4589
|
-
try:
|
|
4590
|
-
self._log(f"[Replay] Last action stored: {desc} (command_id={command_id})")
|
|
4591
|
-
except Exception:
|
|
4592
|
-
print(f"[Replay] Last action stored: {desc} (command_id={command_id})")
|
|
4593
|
-
|
|
4594
4974
|
|
|
4595
4975
|
|
|
4596
4976
|
def _remember_last_headless_command(self, command_id: str, preset: dict | None = None, description: str = ""):
|
|
@@ -4646,17 +5026,6 @@ class AstroSuiteProMainWindow(
|
|
|
4646
5026
|
"""
|
|
4647
5027
|
payload = getattr(self, "_last_headless_command", None)
|
|
4648
5028
|
|
|
4649
|
-
# DEBUG
|
|
4650
|
-
try:
|
|
4651
|
-
self._log(
|
|
4652
|
-
f"[Replay] replay_last_action_on_subwindow: payload={bool(payload)}, "
|
|
4653
|
-
f"target_sw={id(target_sw) if target_sw else None}"
|
|
4654
|
-
)
|
|
4655
|
-
except Exception:
|
|
4656
|
-
print(
|
|
4657
|
-
f"[Replay] replay_last_action_on_subwindow: payload={bool(payload)}, "
|
|
4658
|
-
f"target_sw={id(target_sw) if target_sw else None}"
|
|
4659
|
-
)
|
|
4660
5029
|
|
|
4661
5030
|
if not payload:
|
|
4662
5031
|
QMessageBox.information(
|
|
@@ -4688,17 +5057,6 @@ class AstroSuiteProMainWindow(
|
|
|
4688
5057
|
"""
|
|
4689
5058
|
payload = getattr(self, "_last_headless_command", None) or {}
|
|
4690
5059
|
|
|
4691
|
-
# DEBUG
|
|
4692
|
-
try:
|
|
4693
|
-
self._log(
|
|
4694
|
-
f"[Replay] replay_last_action_on_base: payload={bool(payload)}, "
|
|
4695
|
-
f"target_sw={id(target_sw) if target_sw else None}"
|
|
4696
|
-
)
|
|
4697
|
-
except Exception:
|
|
4698
|
-
print(
|
|
4699
|
-
f"[Replay] replay_last_action_on_base: payload={bool(payload)}, "
|
|
4700
|
-
f"target_sw={id(target_sw) if target_sw else None}"
|
|
4701
|
-
)
|
|
4702
5060
|
|
|
4703
5061
|
if not payload:
|
|
4704
5062
|
QMessageBox.information(
|
|
@@ -4724,17 +5082,6 @@ class AstroSuiteProMainWindow(
|
|
|
4724
5082
|
QMessageBox.information(self, "Replay Last Action", "No base image to apply the action to.")
|
|
4725
5083
|
return
|
|
4726
5084
|
|
|
4727
|
-
# Small debug about which doc we're hitting
|
|
4728
|
-
try:
|
|
4729
|
-
view = target_sw.widget()
|
|
4730
|
-
cur_doc = getattr(view, "document", None)
|
|
4731
|
-
self._log(
|
|
4732
|
-
f"[Replay] base_doc id={id(base_doc)}, "
|
|
4733
|
-
f"view.document id={id(cur_doc)}, "
|
|
4734
|
-
f"same={base_doc is cur_doc}"
|
|
4735
|
-
)
|
|
4736
|
-
except Exception:
|
|
4737
|
-
pass
|
|
4738
5085
|
|
|
4739
5086
|
# ---- Extract cid + preset from payload (support both old + new schemas) ----
|
|
4740
5087
|
cid_raw = payload.get("command_id")
|
|
@@ -7645,12 +7992,33 @@ class AstroSuiteProMainWindow(
|
|
|
7645
7992
|
pass
|
|
7646
7993
|
|
|
7647
7994
|
def _pretty_title(self, doc, *, linked: bool | None = None) -> str:
|
|
7648
|
-
|
|
7649
|
-
|
|
7995
|
+
md = (getattr(doc, "metadata", {}) or {})
|
|
7996
|
+
|
|
7997
|
+
# ✅ 1) Prefer explicit display_name (what duplicate/rename intends)
|
|
7998
|
+
name = (md.get("display_name") or "").strip()
|
|
7999
|
+
|
|
8000
|
+
# 2) Fallback to file_path (but only if display_name is missing)
|
|
8001
|
+
if not name:
|
|
8002
|
+
fp = (md.get("file_path") or "").strip()
|
|
8003
|
+
if fp:
|
|
8004
|
+
name = os.path.splitext(os.path.basename(fp))[0]
|
|
8005
|
+
|
|
8006
|
+
# 3) Fallback to doc.display_name()
|
|
8007
|
+
if not name:
|
|
8008
|
+
name = getattr(doc, "display_name", lambda: "Untitled")()
|
|
8009
|
+
name = (name or "Untitled").replace("[LINK] ", "").strip()
|
|
8010
|
+
|
|
8011
|
+
# If it looks like a filename, drop extension
|
|
8012
|
+
base, ext = os.path.splitext(name)
|
|
8013
|
+
if ext and len(ext) <= 10:
|
|
8014
|
+
name = base
|
|
8015
|
+
|
|
8016
|
+
# linked marker logic
|
|
7650
8017
|
if linked is None:
|
|
7651
|
-
linked = hasattr(doc, "_parent_doc")
|
|
8018
|
+
linked = hasattr(doc, "_parent_doc")
|
|
7652
8019
|
return f"[LINK] {name}" if linked else name
|
|
7653
8020
|
|
|
8021
|
+
|
|
7654
8022
|
def _build_subwindow_title_for_doc(self, doc) -> str:
|
|
7655
8023
|
"""
|
|
7656
8024
|
Build a unique, human-friendly title for a QMdiSubWindow
|
|
@@ -7718,6 +8086,71 @@ class AstroSuiteProMainWindow(
|
|
|
7718
8086
|
return cand
|
|
7719
8087
|
n += 1
|
|
7720
8088
|
|
|
8089
|
+
|
|
8090
|
+
def _doc_window_title(self, doc) -> str:
|
|
8091
|
+
md = getattr(doc, "metadata", {}) or {}
|
|
8092
|
+
|
|
8093
|
+
t = (md.get("display_name") or "").strip()
|
|
8094
|
+
if not t:
|
|
8095
|
+
try:
|
|
8096
|
+
t = (doc.display_name() or "").strip()
|
|
8097
|
+
except Exception:
|
|
8098
|
+
t = ""
|
|
8099
|
+
|
|
8100
|
+
if not t:
|
|
8101
|
+
fp = (md.get("file_path") or "").strip()
|
|
8102
|
+
if fp:
|
|
8103
|
+
t = os.path.splitext(os.path.basename(fp))[0] # ✅ strip ext here too
|
|
8104
|
+
|
|
8105
|
+
t = t or "Untitled"
|
|
8106
|
+
|
|
8107
|
+
# strip glyphs etc
|
|
8108
|
+
try:
|
|
8109
|
+
t = _strip_ui_decorations(t)
|
|
8110
|
+
except Exception:
|
|
8111
|
+
pass
|
|
8112
|
+
|
|
8113
|
+
# ✅ ALWAYS strip filename-like extension at the very end
|
|
8114
|
+
t = _strip_filename_ext(t)
|
|
8115
|
+
|
|
8116
|
+
return t
|
|
8117
|
+
|
|
8118
|
+
def _mdi_begin_open_batch(self, mode: str = "cascade"):
|
|
8119
|
+
self._mdi_open_batch += 1
|
|
8120
|
+
self._mdi_place_mode = mode or "cascade"
|
|
8121
|
+
self._mdi_next_pos = None
|
|
8122
|
+
|
|
8123
|
+
def _mdi_end_open_batch(self):
|
|
8124
|
+
self._mdi_open_batch = max(0, self._mdi_open_batch - 1)
|
|
8125
|
+
if self._mdi_open_batch == 0:
|
|
8126
|
+
self._mdi_next_pos = None
|
|
8127
|
+
|
|
8128
|
+
def _mdi_compute_initial_pos(self) -> QPoint:
|
|
8129
|
+
area = (self.mdi.viewport().geometry() if self.mdi.viewport() else self.mdi.contentsRect())
|
|
8130
|
+
# Put first window a bit inset so titlebars don’t clip
|
|
8131
|
+
return QPoint(area.left() + 18, area.top() + 18)
|
|
8132
|
+
|
|
8133
|
+
def _mdi_place_subwindow(self, sw, target_w: int, target_h: int):
|
|
8134
|
+
"""Deterministic placement. Uses a stable cursor during batch opens."""
|
|
8135
|
+
vp = self.mdi.viewport()
|
|
8136
|
+
area = vp.geometry() if vp else self.mdi.contentsRect()
|
|
8137
|
+
|
|
8138
|
+
if self._mdi_next_pos is None:
|
|
8139
|
+
self._mdi_next_pos = self._mdi_compute_initial_pos()
|
|
8140
|
+
|
|
8141
|
+
x = self._mdi_next_pos.x()
|
|
8142
|
+
y = self._mdi_next_pos.y()
|
|
8143
|
+
|
|
8144
|
+
# keep inside viewport; reset when we hit edge
|
|
8145
|
+
if (x + target_w > area.right() - 10) or (y + 40 > area.bottom() - 10):
|
|
8146
|
+
x = area.left() + 18
|
|
8147
|
+
y = area.top() + 18
|
|
8148
|
+
|
|
8149
|
+
sw.move(x, y)
|
|
8150
|
+
|
|
8151
|
+
# advance cursor
|
|
8152
|
+
step = int(self._mdi_cascade_step)
|
|
8153
|
+
self._mdi_next_pos = QPoint(x + step, y + step)
|
|
7721
8154
|
|
|
7722
8155
|
def _spawn_subwindow_for(self, doc, *, force_new: bool = False):
|
|
7723
8156
|
"""
|
|
@@ -7821,10 +8254,7 @@ class AstroSuiteProMainWindow(
|
|
|
7821
8254
|
if replay_sig is not None:
|
|
7822
8255
|
try:
|
|
7823
8256
|
replay_sig.connect(self._on_view_replay_last_requested)
|
|
7824
|
-
|
|
7825
|
-
self._log(f"[Replay] Connected {sig_name_used} for view id={id(view)}")
|
|
7826
|
-
except Exception:
|
|
7827
|
-
print(f"[Replay] Connected {sig_name_used} for view id={id(view)}")
|
|
8257
|
+
|
|
7828
8258
|
except Exception as e:
|
|
7829
8259
|
try:
|
|
7830
8260
|
self._log(f"[Replay] FAILED to connect {sig_name_used} for view id={id(view)}: {e}")
|
|
@@ -7832,7 +8262,8 @@ class AstroSuiteProMainWindow(
|
|
|
7832
8262
|
print(f"[Replay] FAILED to connect {sig_name_used} for view id={id(view)}: {e}")
|
|
7833
8263
|
|
|
7834
8264
|
self._hook_preview_awareness(view)
|
|
7835
|
-
|
|
8265
|
+
|
|
8266
|
+
base_title = self._doc_window_title(doc) # ✅ use metadata display_name
|
|
7836
8267
|
final_title = self._unique_window_title(base_title)
|
|
7837
8268
|
|
|
7838
8269
|
# -- 6) Add subwindow and set chrome
|
|
@@ -7856,7 +8287,8 @@ class AstroSuiteProMainWindow(
|
|
|
7856
8287
|
# We target ~60% of the viewport height, clamped to sane bounds.
|
|
7857
8288
|
# -------------------------------------------------------------------------
|
|
7858
8289
|
vp = self.mdi.viewport()
|
|
7859
|
-
|
|
8290
|
+
# Use viewport geometry in MDI coordinates (NOT viewport-local rect)
|
|
8291
|
+
area = vp.geometry() if vp else self.mdi.contentsRect()
|
|
7860
8292
|
|
|
7861
8293
|
# Determine aspect ratio
|
|
7862
8294
|
img_w = img_h = None
|
|
@@ -7893,54 +8325,22 @@ class AstroSuiteProMainWindow(
|
|
|
7893
8325
|
target_h = max(200, target_h)
|
|
7894
8326
|
|
|
7895
8327
|
sw.resize(target_w, target_h)
|
|
7896
|
-
sw.showNormal() #
|
|
8328
|
+
sw.showNormal() # clears any "maximized" flag from previous active window
|
|
7897
8329
|
|
|
7898
|
-
#
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
|
|
7902
|
-
|
|
7903
|
-
|
|
7904
|
-
|
|
7905
|
-
active = self.mdi.activeSubWindow()
|
|
7906
|
-
if active and active.isVisible() and not (active.windowState() & Qt.WindowState.WindowMinimized):
|
|
7907
|
-
# Cascade from the active window
|
|
7908
|
-
geo = active.geometry()
|
|
7909
|
-
new_x = geo.x() + 30
|
|
7910
|
-
new_y = geo.y() + 30
|
|
7911
|
-
else:
|
|
7912
|
-
# Fallback: try to find the "last added" visible window to cascade from
|
|
7913
|
-
# (useful if active is None but windows exist)
|
|
7914
|
-
try:
|
|
7915
|
-
subs = [s for s in self.mdi.subWindowList() if s.isVisible() and s is not sw]
|
|
7916
|
-
if subs:
|
|
7917
|
-
# simplistic "last created" might be at end of list
|
|
7918
|
-
last = subs[-1]
|
|
7919
|
-
geo = last.geometry()
|
|
7920
|
-
new_x = geo.x() + 30
|
|
7921
|
-
new_y = geo.y() + 30
|
|
8330
|
+
# Deterministic placement (batch-aware)
|
|
8331
|
+
try:
|
|
8332
|
+
self._mdi_place_subwindow(sw, target_w, target_h)
|
|
8333
|
+
except Exception:
|
|
8334
|
+
# absolute fallback: top-left-ish
|
|
8335
|
+
try:
|
|
8336
|
+
sw.move(area.left() + 18, area.top() + 18)
|
|
7922
8337
|
except Exception:
|
|
7923
8338
|
pass
|
|
7924
8339
|
|
|
7925
|
-
# Bounds check: don't let it drift completely off-screen
|
|
7926
|
-
# (allow valid title bar to be visible at least)
|
|
7927
|
-
if (new_x + target_w > area.width() + 50) or (new_y + 50 > area.height()):
|
|
7928
|
-
new_x = 0
|
|
7929
|
-
new_y = 0
|
|
7930
|
-
|
|
7931
|
-
# Clamp to 0 if negative for some reason
|
|
7932
|
-
new_x = max(0, new_x)
|
|
7933
|
-
new_y = max(0, new_y)
|
|
7934
|
-
|
|
7935
|
-
sw.move(new_x, new_y)
|
|
7936
|
-
|
|
7937
|
-
# ⌠removed the "fill MDI viewport" block - we *don't* want full-monitor first window
|
|
7938
|
-
|
|
7939
8340
|
# Show / activate
|
|
7940
8341
|
sw.show()
|
|
7941
8342
|
sw.raise_()
|
|
7942
8343
|
self.mdi.setActiveSubWindow(sw)
|
|
7943
|
-
# (no second setWindowTitle() here)
|
|
7944
8344
|
|
|
7945
8345
|
# Optional minimize/restore interceptor
|
|
7946
8346
|
if hasattr(self, "_minimize_interceptor"):
|
|
@@ -8025,6 +8425,11 @@ class AstroSuiteProMainWindow(
|
|
|
8025
8425
|
except Exception:
|
|
8026
8426
|
pass
|
|
8027
8427
|
|
|
8428
|
+
try:
|
|
8429
|
+
self._fix_mdi_titlebar_emboss("#dcdcdc" if self._theme_mode()=="dark" else "#f0f0f0")
|
|
8430
|
+
except Exception:
|
|
8431
|
+
pass
|
|
8432
|
+
|
|
8028
8433
|
# -- 11) If this is the first window and it's an image, mimic "Cascade Views"
|
|
8029
8434
|
try:
|
|
8030
8435
|
if first_window and not is_table:
|
|
@@ -8119,25 +8524,57 @@ class AstroSuiteProMainWindow(
|
|
|
8119
8524
|
"autostretch_target": float(getattr(source_view, "autostretch_target", 0.25)),
|
|
8120
8525
|
}
|
|
8121
8526
|
|
|
8122
|
-
# 2) New name (
|
|
8123
|
-
base_name = ""
|
|
8527
|
+
# 2) New name (normalized: NO decorators like 🔗■●◆▲▪▫•◼◻◾◽)
|
|
8124
8528
|
try:
|
|
8125
|
-
base_name =
|
|
8529
|
+
base_name = self._doc_window_title(base_doc) # might include decorations
|
|
8126
8530
|
except Exception:
|
|
8127
8531
|
base_name = "Untitled"
|
|
8128
8532
|
|
|
8533
|
+
# Normalize it so uniqueness checks don't miss decorated titles
|
|
8129
8534
|
try:
|
|
8130
|
-
base_name =
|
|
8535
|
+
base_name = normalize_doc_title(base_name)
|
|
8536
|
+
except Exception:
|
|
8537
|
+
base_name = (base_name or "Untitled").strip()
|
|
8538
|
+
|
|
8539
|
+
# Build a set of existing document names (normalized)
|
|
8540
|
+
existing = set()
|
|
8541
|
+
try:
|
|
8542
|
+
dm = getattr(self, "doc_manager", None) or getattr(self, "docman", None)
|
|
8543
|
+
docs = []
|
|
8544
|
+
|
|
8545
|
+
# Prefer an official accessor if you have one
|
|
8546
|
+
if dm is not None:
|
|
8547
|
+
if hasattr(dm, "documents"):
|
|
8548
|
+
docs = list(dm.documents())
|
|
8549
|
+
elif hasattr(dm, "_docs"):
|
|
8550
|
+
docs = list(dm._docs)
|
|
8551
|
+
|
|
8552
|
+
for d in docs:
|
|
8553
|
+
try:
|
|
8554
|
+
dn = ""
|
|
8555
|
+
md = getattr(d, "metadata", {}) or {}
|
|
8556
|
+
dn = (md.get("display_name") or "").strip() or (d.display_name() or "").strip()
|
|
8557
|
+
dn = normalize_doc_title(dn)
|
|
8558
|
+
if dn:
|
|
8559
|
+
existing.add(dn)
|
|
8560
|
+
except Exception:
|
|
8561
|
+
pass
|
|
8131
8562
|
except Exception:
|
|
8132
|
-
|
|
8133
|
-
|
|
8134
|
-
|
|
8135
|
-
|
|
8136
|
-
|
|
8563
|
+
pass
|
|
8564
|
+
|
|
8565
|
+
# Pick a unique duplicate name: base_duplicate, base_duplicate2, ...
|
|
8566
|
+
candidate = f"{base_name}_duplicate"
|
|
8567
|
+
if candidate in existing:
|
|
8568
|
+
n = 2
|
|
8569
|
+
while True:
|
|
8570
|
+
cand = f"{base_name}_duplicate{n}"
|
|
8571
|
+
if cand not in existing:
|
|
8572
|
+
candidate = cand
|
|
8573
|
+
break
|
|
8574
|
+
n += 1
|
|
8137
8575
|
|
|
8138
8576
|
# 3) Duplicate the *base* document (not the ROI proxy)
|
|
8139
|
-
|
|
8140
|
-
new_doc = self.docman.duplicate_document(base_doc, new_name=f"{base_name}_duplicate")
|
|
8577
|
+
new_doc = self.docman.duplicate_document(base_doc, new_name=candidate)
|
|
8141
8578
|
print(f" Duplicated document ID {id(base_doc)} -> {id(new_doc)}")
|
|
8142
8579
|
|
|
8143
8580
|
# 4) Ensure the duplicate starts mask-free (so we don't inherit mask UI state)
|
|
@@ -8287,26 +8724,21 @@ class AstroSuiteProMainWindow(
|
|
|
8287
8724
|
|
|
8288
8725
|
|
|
8289
8726
|
def _activate_or_open_from_explorer(self, item):
|
|
8290
|
-
doc = item.data(Qt.ItemDataRole.UserRole)
|
|
8291
|
-
|
|
8292
|
-
|
|
8293
|
-
#
|
|
8294
|
-
|
|
8295
|
-
|
|
8296
|
-
|
|
8297
|
-
|
|
8298
|
-
|
|
8299
|
-
|
|
8300
|
-
|
|
8301
|
-
|
|
8302
|
-
|
|
8303
|
-
|
|
8304
|
-
|
|
8305
|
-
pass
|
|
8306
|
-
return
|
|
8307
|
-
|
|
8308
|
-
# 2) None exists -> open one
|
|
8309
|
-
self._open_subwindow_for_added_doc(base)
|
|
8727
|
+
doc = item.data(0, Qt.ItemDataRole.UserRole)
|
|
8728
|
+
if doc is None:
|
|
8729
|
+
return
|
|
8730
|
+
# you already have logic for this; typically:
|
|
8731
|
+
sw = self._find_subwindow_for_doc(doc)
|
|
8732
|
+
if sw:
|
|
8733
|
+
self.mdi.setActiveSubWindow(sw)
|
|
8734
|
+
sw.show()
|
|
8735
|
+
sw.raise_()
|
|
8736
|
+
return
|
|
8737
|
+
# else open it (if your app supports opening closed docs, otherwise no-op)
|
|
8738
|
+
try:
|
|
8739
|
+
self._open_subwindow_for_added_doc(doc)
|
|
8740
|
+
except Exception:
|
|
8741
|
+
pass
|
|
8310
8742
|
|
|
8311
8743
|
def _set_linked_stretch_from_action(self, checked: bool):
|
|
8312
8744
|
# persist as the default for *new* views
|
|
@@ -8432,7 +8864,10 @@ class AstroSuiteProMainWindow(
|
|
|
8432
8864
|
self._refresh_mask_action_states()
|
|
8433
8865
|
except Exception:
|
|
8434
8866
|
pass
|
|
8435
|
-
|
|
8867
|
+
try:
|
|
8868
|
+
self._fix_mdi_titlebar_emboss("#dcdcdc" if self._theme_mode()=="dark" else "#f0f0f0")
|
|
8869
|
+
except Exception:
|
|
8870
|
+
pass
|
|
8436
8871
|
|
|
8437
8872
|
def _sync_docman_active(self, doc):
|
|
8438
8873
|
dm = self.doc_manager
|
|
@@ -8686,6 +9121,13 @@ class AstroSuiteProMainWindow(
|
|
|
8686
9121
|
|
|
8687
9122
|
super().keyPressEvent(event)
|
|
8688
9123
|
|
|
9124
|
+
def _open_texture_clarity(self):
|
|
9125
|
+
try:
|
|
9126
|
+
from setiastro.saspro.texture_clarity import open_texture_clarity_dialog
|
|
9127
|
+
open_texture_clarity_dialog(self)
|
|
9128
|
+
except Exception as e:
|
|
9129
|
+
print(f"Error opening Texture & Clarity: {e}")
|
|
9130
|
+
|
|
8689
9131
|
def _update_usage_stats(self):
|
|
8690
9132
|
try:
|
|
8691
9133
|
now = time.time()
|