setiastrosuitepro 1.6.4__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/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 +108 -40
- setiastro/saspro/blemish_blaster.py +4 -1
- setiastro/saspro/blink_comparator_pro.py +74 -24
- 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 +109 -42
- 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 +429 -228
- setiastro/saspro/gui/mixins/dock_mixin.py +245 -24
- setiastro/saspro/gui/mixins/menu_mixin.py +27 -1
- setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
- setiastro/saspro/gui/mixins/toolbar_mixin.py +384 -18
- 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 +51 -12
- 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/resources.py +67 -0
- setiastro/saspro/rgbalign.py +4 -0
- setiastro/saspro/selective_color.py +4 -1
- setiastro/saspro/sfcc.py +364 -152
- setiastro/saspro/shortcuts.py +160 -29
- setiastro/saspro/signature_insert.py +692 -33
- setiastro/saspro/stacking_suite.py +1331 -484
- 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 +743 -128
- setiastro/saspro/subwindow.py +786 -360
- setiastro/saspro/supernovaasteroidhunter.py +1 -1
- 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 +109 -59
- 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.6.12.dist-info}/METADATA +2 -1
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/RECORD +115 -82
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/licenses/license.txt +0 -0
|
@@ -168,7 +168,7 @@ except Exception:
|
|
|
168
168
|
|
|
169
169
|
|
|
170
170
|
|
|
171
|
-
|
|
171
|
+
_DEBUG_DND_DUP = False
|
|
172
172
|
|
|
173
173
|
|
|
174
174
|
|
|
@@ -194,7 +194,7 @@ from setiastro.saspro.resources import (
|
|
|
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
196
|
colorwheel_path, font_path, csv_icon_path, spinner_path, wims_path,
|
|
197
|
-
wimi_path, linearfit_path, debayer_path, aberration_path,
|
|
197
|
+
wimi_path, linearfit_path, debayer_path, aberration_path, acv_icon_path,
|
|
198
198
|
functionbundles_path, viewbundles_path, selectivecolor_path, rgbalign_path,
|
|
199
199
|
background_path, script_icon_path
|
|
200
200
|
)
|
|
@@ -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)
|
|
@@ -856,7 +995,18 @@ class AstroSuiteProMainWindow(
|
|
|
856
995
|
dm = self.doc_manager
|
|
857
996
|
doc = None
|
|
858
997
|
|
|
998
|
+
if _DEBUG_DND_DUP:
|
|
999
|
+
import json
|
|
1000
|
+
print("\n[DNDDBG:DROP_ENTER] raw st dict:")
|
|
1001
|
+
try:
|
|
1002
|
+
# st is already a dict here
|
|
1003
|
+
for k in sorted(st.keys()):
|
|
1004
|
+
print(f" {k}: {st.get(k)!r}")
|
|
1005
|
+
except Exception as e:
|
|
1006
|
+
print("[DNDDBG:DROP_ENTER] failed printing st:", e)
|
|
859
1007
|
|
|
1008
|
+
# sanity: show which fields are present
|
|
1009
|
+
print("[DNDDBG:DROP_ENTER] has source_view_title?", "source_view_title" in st)
|
|
860
1010
|
|
|
861
1011
|
# Prefer *stable* identifiers over the proxy pointer
|
|
862
1012
|
uid = st.get("doc_uid")
|
|
@@ -959,7 +1109,21 @@ class AstroSuiteProMainWindow(
|
|
|
959
1109
|
print("[VIEWSTATE_DROP] EXIT (no doc)")
|
|
960
1110
|
return
|
|
961
1111
|
|
|
962
|
-
|
|
1112
|
+
if _DEBUG_DND_DUP:
|
|
1113
|
+
try:
|
|
1114
|
+
dname = doc.display_name() if hasattr(doc, "display_name") else None
|
|
1115
|
+
except Exception:
|
|
1116
|
+
dname = None
|
|
1117
|
+
try:
|
|
1118
|
+
meta = getattr(doc, "metadata", {}) or {}
|
|
1119
|
+
except Exception:
|
|
1120
|
+
meta = {}
|
|
1121
|
+
print("\n[DNDDBG:DOC_RESOLVED]")
|
|
1122
|
+
print(" doc_obj:", doc, "type:", type(doc).__name__, "id:", id(doc))
|
|
1123
|
+
print(" doc.uid:", getattr(doc, "uid", None))
|
|
1124
|
+
print(" doc.display_name():", dname)
|
|
1125
|
+
print(" meta.display_name:", meta.get("display_name"))
|
|
1126
|
+
print(" meta.file_path:", meta.get("file_path"))
|
|
963
1127
|
|
|
964
1128
|
# ----------------------------------------
|
|
965
1129
|
# 4) Peek at metadata to see if this is a
|
|
@@ -1050,34 +1214,35 @@ class AstroSuiteProMainWindow(
|
|
|
1050
1214
|
# copy the view transform.
|
|
1051
1215
|
# ----------------------------------------
|
|
1052
1216
|
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
1217
|
base_doc = doc
|
|
1057
1218
|
|
|
1058
|
-
# 1)
|
|
1059
|
-
|
|
1060
|
-
|
|
1219
|
+
# 1) Prefer the dragged view's title
|
|
1220
|
+
base_name = (st.get("source_view_title") or "").strip()
|
|
1221
|
+
|
|
1222
|
+
# 2) Fallback to document display name
|
|
1223
|
+
if not base_name:
|
|
1061
1224
|
try:
|
|
1062
1225
|
base_name = base_doc.display_name() or "Untitled"
|
|
1063
1226
|
except Exception:
|
|
1064
1227
|
base_name = "Untitled"
|
|
1065
1228
|
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
)
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1229
|
+
# 3) Clean it (strip glyphs / "Active View" / etc.)
|
|
1230
|
+
try:
|
|
1231
|
+
base_name = _strip_ui_decorations(base_name)
|
|
1232
|
+
except Exception:
|
|
1233
|
+
if base_name.startswith("Active View: "):
|
|
1234
|
+
base_name = base_name[len("Active View: "):]
|
|
1235
|
+
|
|
1236
|
+
if _DEBUG_DND_DUP:
|
|
1237
|
+
print("\n[DNDDBG:NAME_COMPUTE]")
|
|
1238
|
+
print(" st.source_view_title:", (st.get("source_view_title") or "").strip())
|
|
1239
|
+
print(" base_doc.display_name():", (base_doc.display_name() if hasattr(base_doc,"display_name") else None))
|
|
1240
|
+
print(" base_name(after fallbacks/strip):", base_name)
|
|
1241
|
+
print(" new_name passed:", f"{base_name}_duplicate")
|
|
1242
|
+
|
|
1243
|
+
new_doc = self.docman.duplicate_document(
|
|
1244
|
+
base_doc, new_name=f"{base_name}_duplicate"
|
|
1245
|
+
)
|
|
1081
1246
|
|
|
1082
1247
|
# 2) Let doc_manager's documentAdded handler create the subwindow.
|
|
1083
1248
|
# We just wait for it to show up and then apply the view state.
|
|
@@ -1203,14 +1368,6 @@ class AstroSuiteProMainWindow(
|
|
|
1203
1368
|
return False
|
|
1204
1369
|
|
|
1205
1370
|
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
1371
|
self._spawn_subwindow_for(doc)
|
|
1215
1372
|
|
|
1216
1373
|
# --- UI scaffolding ---
|
|
@@ -2503,10 +2660,27 @@ class AstroSuiteProMainWindow(
|
|
|
2503
2660
|
return f"{name}{dims}"
|
|
2504
2661
|
|
|
2505
2662
|
def _update_explorer_item_for_doc(self, doc):
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2663
|
+
# Delegate to DockMixin implementation if present
|
|
2664
|
+
try:
|
|
2665
|
+
return super()._update_explorer_item_for_doc(doc)
|
|
2666
|
+
except Exception:
|
|
2667
|
+
pass
|
|
2668
|
+
|
|
2669
|
+
# Fallback: tree-safe implementation
|
|
2670
|
+
if not hasattr(self, "explorer") or self.explorer is None:
|
|
2671
|
+
return
|
|
2672
|
+
try:
|
|
2673
|
+
n = self.explorer.topLevelItemCount()
|
|
2674
|
+
except Exception:
|
|
2675
|
+
return
|
|
2676
|
+
|
|
2677
|
+
for i in range(n):
|
|
2678
|
+
it = self.explorer.topLevelItem(i)
|
|
2679
|
+
if it.data(0, Qt.ItemDataRole.UserRole) is doc:
|
|
2680
|
+
try:
|
|
2681
|
+
self._refresh_explorer_row(it, doc)
|
|
2682
|
+
except Exception:
|
|
2683
|
+
pass
|
|
2510
2684
|
return
|
|
2511
2685
|
#-----------FUNCTIONS----------------
|
|
2512
2686
|
|
|
@@ -3015,17 +3189,17 @@ class AstroSuiteProMainWindow(
|
|
|
3015
3189
|
|
|
3016
3190
|
self.convo_window.show()
|
|
3017
3191
|
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
3192
|
def _apply_extract_luminance_preset_to_doc(self, doc, preset=None):
|
|
3021
|
-
|
|
3022
3193
|
from PyQt6.QtWidgets import QMessageBox
|
|
3023
|
-
from setiastro.saspro.luminancerecombine import
|
|
3024
|
-
|
|
3194
|
+
from setiastro.saspro.luminancerecombine import (
|
|
3195
|
+
compute_luminance,
|
|
3196
|
+
resolve_luma_profile_weights,
|
|
3197
|
+
)
|
|
3198
|
+
from setiastro.saspro.headless_utils import unwrap_docproxy
|
|
3199
|
+
import numpy as np
|
|
3025
3200
|
|
|
3026
3201
|
doc = unwrap_docproxy(doc)
|
|
3027
3202
|
p = dict(preset or {})
|
|
3028
|
-
mode = (p.get("mode") or "rec709").lower()
|
|
3029
3203
|
|
|
3030
3204
|
if doc is None or getattr(doc, "image", None) is None:
|
|
3031
3205
|
QMessageBox.information(self, "Extract Luminance", "No target image.")
|
|
@@ -3033,52 +3207,43 @@ class AstroSuiteProMainWindow(
|
|
|
3033
3207
|
|
|
3034
3208
|
img = np.asarray(doc.image)
|
|
3035
3209
|
|
|
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
|
|
3210
|
+
mode = str(p.get("mode", "rec709")).strip()
|
|
3211
|
+
resolved_method, w, profile_name = resolve_luma_profile_weights(mode)
|
|
3045
3212
|
|
|
3046
|
-
L = compute_luminance(img, method=
|
|
3213
|
+
L = compute_luminance(img, method=resolved_method, weights=w)
|
|
3047
3214
|
|
|
3048
3215
|
dm = getattr(self, "doc_manager", None)
|
|
3049
3216
|
if dm is None:
|
|
3050
|
-
# headless fallback: just overwrite active doc
|
|
3051
3217
|
doc.apply_edit(L.astype(np.float32), step_name="Extract Luminance")
|
|
3052
3218
|
return
|
|
3053
3219
|
|
|
3054
|
-
|
|
3220
|
+
meta = {
|
|
3221
|
+
"step_name": "Extract Luminance",
|
|
3222
|
+
"luma_method": resolved_method,
|
|
3223
|
+
}
|
|
3224
|
+
if w is not None:
|
|
3225
|
+
meta["luma_weights"] = np.asarray(w, dtype=np.float32).tolist()
|
|
3226
|
+
if profile_name:
|
|
3227
|
+
meta["luma_profile"] = str(profile_name)
|
|
3228
|
+
|
|
3055
3229
|
try:
|
|
3230
|
+
suffix = f"{profile_name}" if profile_name else resolved_method
|
|
3056
3231
|
new_doc = dm.create_document_from_array(
|
|
3057
3232
|
L.astype(np.float32),
|
|
3058
|
-
name=f"{doc.display_name()} -- Luminance ({
|
|
3233
|
+
name=f"{doc.display_name()} -- Luminance ({suffix})",
|
|
3059
3234
|
is_mono=True,
|
|
3060
|
-
metadata=
|
|
3235
|
+
metadata=meta,
|
|
3061
3236
|
)
|
|
3062
3237
|
dm.add_document(new_doc)
|
|
3063
3238
|
except Exception:
|
|
3064
|
-
# safe fallback
|
|
3065
3239
|
doc.apply_edit(L.astype(np.float32), step_name="Extract Luminance")
|
|
3066
3240
|
|
|
3067
|
-
|
|
3068
3241
|
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
|
|
3242
|
+
from PyQt6.QtWidgets import QMessageBox
|
|
3243
|
+
from PyQt6.QtGui import QIcon
|
|
3244
|
+
from setiastro.saspro.luminancerecombine import compute_luminance, resolve_luma_profile_weights
|
|
3245
|
+
|
|
3246
|
+
|
|
3082
3247
|
sw = None
|
|
3083
3248
|
if doc is None:
|
|
3084
3249
|
sw = self.mdi.activeSubWindow()
|
|
@@ -3097,70 +3262,19 @@ class AstroSuiteProMainWindow(
|
|
|
3097
3262
|
QMessageBox.information(self, "Extract Luminance", "Luminance extraction requires an RGB image.")
|
|
3098
3263
|
return
|
|
3099
3264
|
|
|
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
3265
|
p = dict(preset or {})
|
|
3110
|
-
|
|
3266
|
+
mode = str(
|
|
3111
3267
|
p.get("mode",
|
|
3112
3268
|
p.get("method",
|
|
3113
3269
|
p.get("luma_method",
|
|
3114
3270
|
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
|
|
3271
|
+
).strip()
|
|
3272
|
+
|
|
3273
|
+
resolved_method, w, profile_name = resolve_luma_profile_weights(mode)
|
|
3274
|
+
|
|
3275
|
+
y = compute_luminance(img, method=resolved_method, weights=w)
|
|
3276
|
+
|
|
3277
|
+
# ---- metadata & title ----
|
|
3164
3278
|
base_meta = {}
|
|
3165
3279
|
try:
|
|
3166
3280
|
base_meta = dict(getattr(doc, "metadata", {}) or {})
|
|
@@ -3172,13 +3286,16 @@ class AstroSuiteProMainWindow(
|
|
|
3172
3286
|
"source": "ExtractLuminance",
|
|
3173
3287
|
"is_mono": True,
|
|
3174
3288
|
"bit_depth": "32f",
|
|
3175
|
-
"luma_method":
|
|
3289
|
+
"luma_method": resolved_method,
|
|
3176
3290
|
}
|
|
3177
|
-
if
|
|
3178
|
-
meta["luma_weights"] = np.asarray(
|
|
3291
|
+
if w is not None:
|
|
3292
|
+
meta["luma_weights"] = np.asarray(w, dtype=np.float32).tolist()
|
|
3293
|
+
if profile_name:
|
|
3294
|
+
meta["luma_profile"] = str(profile_name)
|
|
3179
3295
|
|
|
3180
3296
|
base_title = sw.windowTitle() if sw else (getattr(doc, "title", getattr(doc, "name", "")) or "Untitled")
|
|
3181
|
-
|
|
3297
|
+
suffix = f"{profile_name}" if profile_name else resolved_method
|
|
3298
|
+
title = f"{base_title} -- Luminance ({suffix})"
|
|
3182
3299
|
|
|
3183
3300
|
dm = getattr(self, "docman", None)
|
|
3184
3301
|
if dm is None:
|
|
@@ -3206,19 +3323,19 @@ class AstroSuiteProMainWindow(
|
|
|
3206
3323
|
except Exception:
|
|
3207
3324
|
pass
|
|
3208
3325
|
|
|
3209
|
-
# ðŸ" Remember for Replay (optional but consistent)
|
|
3210
3326
|
try:
|
|
3211
3327
|
remember = getattr(self, "remember_last_headless_command", None) or getattr(self, "_remember_last_headless_command", None)
|
|
3212
3328
|
if callable(remember):
|
|
3213
|
-
remember("extract_luminance", {"mode":
|
|
3329
|
+
remember("extract_luminance", {"mode": mode}, description="Extract Luminance")
|
|
3214
3330
|
except Exception:
|
|
3215
3331
|
pass
|
|
3216
3332
|
|
|
3217
3333
|
if hasattr(self, "_log"):
|
|
3218
|
-
self._log(f"Extract Luminance ({
|
|
3334
|
+
self._log(f"Extract Luminance ({suffix}) -> new mono document created.")
|
|
3219
3335
|
|
|
3220
3336
|
return new_doc
|
|
3221
3337
|
|
|
3338
|
+
|
|
3222
3339
|
def _subwindow_docs(self):
|
|
3223
3340
|
docs = []
|
|
3224
3341
|
for sw in self.mdi.subWindowList():
|
|
@@ -4469,6 +4586,48 @@ class AstroSuiteProMainWindow(
|
|
|
4469
4586
|
dlg.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
4470
4587
|
dlg.show()
|
|
4471
4588
|
|
|
4589
|
+
def _open_acv_exporter(self):
|
|
4590
|
+
from setiastro.saspro.acv_exporter import AstroCatalogueViewerExporterDialog
|
|
4591
|
+
|
|
4592
|
+
dm = getattr(self, "doc_manager", None) or getattr(self, "docman", None)
|
|
4593
|
+
if dm is None:
|
|
4594
|
+
QMessageBox.information(self, "Astro Catalogue Viewer Exporter", "No document manager available.")
|
|
4595
|
+
return
|
|
4596
|
+
|
|
4597
|
+
sw = self.mdi.activeSubWindow()
|
|
4598
|
+
if not sw:
|
|
4599
|
+
QMessageBox.information(self, "Astro Catalogue Viewer Exporter", "Open an image first.")
|
|
4600
|
+
return
|
|
4601
|
+
|
|
4602
|
+
view = sw.widget()
|
|
4603
|
+
active_doc = None
|
|
4604
|
+
|
|
4605
|
+
# Prefer ROI-aware resolution
|
|
4606
|
+
try:
|
|
4607
|
+
if hasattr(dm, "get_document_for_view"):
|
|
4608
|
+
active_doc = dm.get_document_for_view(view)
|
|
4609
|
+
except Exception:
|
|
4610
|
+
active_doc = None
|
|
4611
|
+
|
|
4612
|
+
# Fallback
|
|
4613
|
+
if active_doc is None:
|
|
4614
|
+
try:
|
|
4615
|
+
active_doc = getattr(view, "document", None)
|
|
4616
|
+
except Exception:
|
|
4617
|
+
active_doc = None
|
|
4618
|
+
|
|
4619
|
+
if active_doc is None or getattr(active_doc, "image", None) is None:
|
|
4620
|
+
QMessageBox.information(self, "Astro Catalogue Viewer Exporter", "No active image.")
|
|
4621
|
+
return
|
|
4622
|
+
|
|
4623
|
+
dlg = AstroCatalogueViewerExporterDialog(self, dm, active_doc)
|
|
4624
|
+
try:
|
|
4625
|
+
dlg.setWindowIcon(QIcon(acv_icon_path))
|
|
4626
|
+
except Exception:
|
|
4627
|
+
pass
|
|
4628
|
+
dlg.show()
|
|
4629
|
+
|
|
4630
|
+
|
|
4472
4631
|
def _open_linear_fit(self):
|
|
4473
4632
|
from setiastro.saspro.linear_fit import LinearFitDialog
|
|
4474
4633
|
dm = getattr(self, "doc_manager", None) or getattr(self, "docman", None)
|
|
@@ -4585,12 +4744,6 @@ class AstroSuiteProMainWindow(
|
|
|
4585
4744
|
if max_len and len(hist) > max_len:
|
|
4586
4745
|
del hist[:-max_len]
|
|
4587
4746
|
|
|
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
4747
|
|
|
4595
4748
|
|
|
4596
4749
|
def _remember_last_headless_command(self, command_id: str, preset: dict | None = None, description: str = ""):
|
|
@@ -4646,17 +4799,6 @@ class AstroSuiteProMainWindow(
|
|
|
4646
4799
|
"""
|
|
4647
4800
|
payload = getattr(self, "_last_headless_command", None)
|
|
4648
4801
|
|
|
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
4802
|
|
|
4661
4803
|
if not payload:
|
|
4662
4804
|
QMessageBox.information(
|
|
@@ -4688,17 +4830,6 @@ class AstroSuiteProMainWindow(
|
|
|
4688
4830
|
"""
|
|
4689
4831
|
payload = getattr(self, "_last_headless_command", None) or {}
|
|
4690
4832
|
|
|
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
4833
|
|
|
4703
4834
|
if not payload:
|
|
4704
4835
|
QMessageBox.information(
|
|
@@ -4724,17 +4855,6 @@ class AstroSuiteProMainWindow(
|
|
|
4724
4855
|
QMessageBox.information(self, "Replay Last Action", "No base image to apply the action to.")
|
|
4725
4856
|
return
|
|
4726
4857
|
|
|
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
4858
|
|
|
4739
4859
|
# ---- Extract cid + preset from payload (support both old + new schemas) ----
|
|
4740
4860
|
cid_raw = payload.get("command_id")
|
|
@@ -7645,12 +7765,33 @@ class AstroSuiteProMainWindow(
|
|
|
7645
7765
|
pass
|
|
7646
7766
|
|
|
7647
7767
|
def _pretty_title(self, doc, *, linked: bool | None = None) -> str:
|
|
7648
|
-
|
|
7649
|
-
|
|
7768
|
+
md = (getattr(doc, "metadata", {}) or {})
|
|
7769
|
+
|
|
7770
|
+
# ✅ 1) Prefer explicit display_name (what duplicate/rename intends)
|
|
7771
|
+
name = (md.get("display_name") or "").strip()
|
|
7772
|
+
|
|
7773
|
+
# 2) Fallback to file_path (but only if display_name is missing)
|
|
7774
|
+
if not name:
|
|
7775
|
+
fp = (md.get("file_path") or "").strip()
|
|
7776
|
+
if fp:
|
|
7777
|
+
name = os.path.splitext(os.path.basename(fp))[0]
|
|
7778
|
+
|
|
7779
|
+
# 3) Fallback to doc.display_name()
|
|
7780
|
+
if not name:
|
|
7781
|
+
name = getattr(doc, "display_name", lambda: "Untitled")()
|
|
7782
|
+
name = (name or "Untitled").replace("[LINK] ", "").strip()
|
|
7783
|
+
|
|
7784
|
+
# If it looks like a filename, drop extension
|
|
7785
|
+
base, ext = os.path.splitext(name)
|
|
7786
|
+
if ext and len(ext) <= 10:
|
|
7787
|
+
name = base
|
|
7788
|
+
|
|
7789
|
+
# linked marker logic
|
|
7650
7790
|
if linked is None:
|
|
7651
|
-
linked = hasattr(doc, "_parent_doc")
|
|
7791
|
+
linked = hasattr(doc, "_parent_doc")
|
|
7652
7792
|
return f"[LINK] {name}" if linked else name
|
|
7653
7793
|
|
|
7794
|
+
|
|
7654
7795
|
def _build_subwindow_title_for_doc(self, doc) -> str:
|
|
7655
7796
|
"""
|
|
7656
7797
|
Build a unique, human-friendly title for a QMdiSubWindow
|
|
@@ -7718,6 +7859,34 @@ class AstroSuiteProMainWindow(
|
|
|
7718
7859
|
return cand
|
|
7719
7860
|
n += 1
|
|
7720
7861
|
|
|
7862
|
+
|
|
7863
|
+
def _doc_window_title(self, doc) -> str:
|
|
7864
|
+
md = getattr(doc, "metadata", {}) or {}
|
|
7865
|
+
|
|
7866
|
+
t = (md.get("display_name") or "").strip()
|
|
7867
|
+
if not t:
|
|
7868
|
+
try:
|
|
7869
|
+
t = (doc.display_name() or "").strip()
|
|
7870
|
+
except Exception:
|
|
7871
|
+
t = ""
|
|
7872
|
+
|
|
7873
|
+
if not t:
|
|
7874
|
+
fp = (md.get("file_path") or "").strip()
|
|
7875
|
+
if fp:
|
|
7876
|
+
t = os.path.splitext(os.path.basename(fp))[0] # ✅ strip ext here too
|
|
7877
|
+
|
|
7878
|
+
t = t or "Untitled"
|
|
7879
|
+
|
|
7880
|
+
# strip glyphs etc
|
|
7881
|
+
try:
|
|
7882
|
+
t = _strip_ui_decorations(t)
|
|
7883
|
+
except Exception:
|
|
7884
|
+
pass
|
|
7885
|
+
|
|
7886
|
+
# ✅ ALWAYS strip filename-like extension at the very end
|
|
7887
|
+
t = _strip_filename_ext(t)
|
|
7888
|
+
|
|
7889
|
+
return t
|
|
7721
7890
|
|
|
7722
7891
|
def _spawn_subwindow_for(self, doc, *, force_new: bool = False):
|
|
7723
7892
|
"""
|
|
@@ -7821,10 +7990,7 @@ class AstroSuiteProMainWindow(
|
|
|
7821
7990
|
if replay_sig is not None:
|
|
7822
7991
|
try:
|
|
7823
7992
|
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)}")
|
|
7993
|
+
|
|
7828
7994
|
except Exception as e:
|
|
7829
7995
|
try:
|
|
7830
7996
|
self._log(f"[Replay] FAILED to connect {sig_name_used} for view id={id(view)}: {e}")
|
|
@@ -7832,7 +7998,8 @@ class AstroSuiteProMainWindow(
|
|
|
7832
7998
|
print(f"[Replay] FAILED to connect {sig_name_used} for view id={id(view)}: {e}")
|
|
7833
7999
|
|
|
7834
8000
|
self._hook_preview_awareness(view)
|
|
7835
|
-
|
|
8001
|
+
|
|
8002
|
+
base_title = self._doc_window_title(doc) # ✅ use metadata display_name
|
|
7836
8003
|
final_title = self._unique_window_title(base_title)
|
|
7837
8004
|
|
|
7838
8005
|
# -- 6) Add subwindow and set chrome
|
|
@@ -7856,7 +8023,8 @@ class AstroSuiteProMainWindow(
|
|
|
7856
8023
|
# We target ~60% of the viewport height, clamped to sane bounds.
|
|
7857
8024
|
# -------------------------------------------------------------------------
|
|
7858
8025
|
vp = self.mdi.viewport()
|
|
7859
|
-
|
|
8026
|
+
# Use viewport geometry in MDI coordinates (NOT viewport-local rect)
|
|
8027
|
+
area = vp.geometry() if vp else self.mdi.contentsRect()
|
|
7860
8028
|
|
|
7861
8029
|
# Determine aspect ratio
|
|
7862
8030
|
img_w = img_h = None
|
|
@@ -7899,7 +8067,7 @@ class AstroSuiteProMainWindow(
|
|
|
7899
8067
|
# Smart Cascade: Position relative to the *currently active* window
|
|
7900
8068
|
# (before we make the new one active).
|
|
7901
8069
|
# -------------------------------------------------------------------------
|
|
7902
|
-
new_x, new_y =
|
|
8070
|
+
new_x, new_y = area.left(), area.top()
|
|
7903
8071
|
|
|
7904
8072
|
# Get dominant/active window *before* we activate the new one
|
|
7905
8073
|
active = self.mdi.activeSubWindow()
|
|
@@ -7922,15 +8090,13 @@ class AstroSuiteProMainWindow(
|
|
|
7922
8090
|
except Exception:
|
|
7923
8091
|
pass
|
|
7924
8092
|
|
|
7925
|
-
# Bounds check:
|
|
7926
|
-
|
|
7927
|
-
|
|
7928
|
-
|
|
7929
|
-
|
|
7930
|
-
|
|
7931
|
-
|
|
7932
|
-
new_x = max(0, new_x)
|
|
7933
|
-
new_y = max(0, new_y)
|
|
8093
|
+
# Bounds check: keep titlebar visible and stay inside viewport
|
|
8094
|
+
if (new_x + target_w > area.right() - 10) or (new_y + 40 > area.bottom() - 10):
|
|
8095
|
+
new_x = area.left()
|
|
8096
|
+
new_y = area.top()
|
|
8097
|
+
|
|
8098
|
+
new_x = max(area.left(), new_x)
|
|
8099
|
+
new_y = max(area.top(), new_y)
|
|
7934
8100
|
|
|
7935
8101
|
sw.move(new_x, new_y)
|
|
7936
8102
|
|
|
@@ -8025,6 +8191,11 @@ class AstroSuiteProMainWindow(
|
|
|
8025
8191
|
except Exception:
|
|
8026
8192
|
pass
|
|
8027
8193
|
|
|
8194
|
+
try:
|
|
8195
|
+
self._fix_mdi_titlebar_emboss("#dcdcdc" if self._theme_mode()=="dark" else "#f0f0f0")
|
|
8196
|
+
except Exception:
|
|
8197
|
+
pass
|
|
8198
|
+
|
|
8028
8199
|
# -- 11) If this is the first window and it's an image, mimic "Cascade Views"
|
|
8029
8200
|
try:
|
|
8030
8201
|
if first_window and not is_table:
|
|
@@ -8119,25 +8290,57 @@ class AstroSuiteProMainWindow(
|
|
|
8119
8290
|
"autostretch_target": float(getattr(source_view, "autostretch_target", 0.25)),
|
|
8120
8291
|
}
|
|
8121
8292
|
|
|
8122
|
-
# 2) New name (
|
|
8123
|
-
base_name = ""
|
|
8293
|
+
# 2) New name (normalized: NO decorators like 🔗■●◆▲▪▫•◼◻◾◽)
|
|
8124
8294
|
try:
|
|
8125
|
-
base_name =
|
|
8295
|
+
base_name = self._doc_window_title(base_doc) # might include decorations
|
|
8126
8296
|
except Exception:
|
|
8127
8297
|
base_name = "Untitled"
|
|
8128
8298
|
|
|
8299
|
+
# Normalize it so uniqueness checks don't miss decorated titles
|
|
8129
8300
|
try:
|
|
8130
|
-
base_name =
|
|
8301
|
+
base_name = normalize_doc_title(base_name)
|
|
8131
8302
|
except Exception:
|
|
8132
|
-
|
|
8133
|
-
|
|
8134
|
-
|
|
8135
|
-
|
|
8136
|
-
|
|
8303
|
+
base_name = (base_name or "Untitled").strip()
|
|
8304
|
+
|
|
8305
|
+
# Build a set of existing document names (normalized)
|
|
8306
|
+
existing = set()
|
|
8307
|
+
try:
|
|
8308
|
+
dm = getattr(self, "doc_manager", None) or getattr(self, "docman", None)
|
|
8309
|
+
docs = []
|
|
8310
|
+
|
|
8311
|
+
# Prefer an official accessor if you have one
|
|
8312
|
+
if dm is not None:
|
|
8313
|
+
if hasattr(dm, "documents"):
|
|
8314
|
+
docs = list(dm.documents())
|
|
8315
|
+
elif hasattr(dm, "_docs"):
|
|
8316
|
+
docs = list(dm._docs)
|
|
8317
|
+
|
|
8318
|
+
for d in docs:
|
|
8319
|
+
try:
|
|
8320
|
+
dn = ""
|
|
8321
|
+
md = getattr(d, "metadata", {}) or {}
|
|
8322
|
+
dn = (md.get("display_name") or "").strip() or (d.display_name() or "").strip()
|
|
8323
|
+
dn = normalize_doc_title(dn)
|
|
8324
|
+
if dn:
|
|
8325
|
+
existing.add(dn)
|
|
8326
|
+
except Exception:
|
|
8327
|
+
pass
|
|
8328
|
+
except Exception:
|
|
8329
|
+
pass
|
|
8330
|
+
|
|
8331
|
+
# Pick a unique duplicate name: base_duplicate, base_duplicate2, ...
|
|
8332
|
+
candidate = f"{base_name}_duplicate"
|
|
8333
|
+
if candidate in existing:
|
|
8334
|
+
n = 2
|
|
8335
|
+
while True:
|
|
8336
|
+
cand = f"{base_name}_duplicate{n}"
|
|
8337
|
+
if cand not in existing:
|
|
8338
|
+
candidate = cand
|
|
8339
|
+
break
|
|
8340
|
+
n += 1
|
|
8137
8341
|
|
|
8138
8342
|
# 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")
|
|
8343
|
+
new_doc = self.docman.duplicate_document(base_doc, new_name=candidate)
|
|
8141
8344
|
print(f" Duplicated document ID {id(base_doc)} -> {id(new_doc)}")
|
|
8142
8345
|
|
|
8143
8346
|
# 4) Ensure the duplicate starts mask-free (so we don't inherit mask UI state)
|
|
@@ -8287,26 +8490,21 @@ class AstroSuiteProMainWindow(
|
|
|
8287
8490
|
|
|
8288
8491
|
|
|
8289
8492
|
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)
|
|
8493
|
+
doc = item.data(0, Qt.ItemDataRole.UserRole)
|
|
8494
|
+
if doc is None:
|
|
8495
|
+
return
|
|
8496
|
+
# you already have logic for this; typically:
|
|
8497
|
+
sw = self._find_subwindow_for_doc(doc)
|
|
8498
|
+
if sw:
|
|
8499
|
+
self.mdi.setActiveSubWindow(sw)
|
|
8500
|
+
sw.show()
|
|
8501
|
+
sw.raise_()
|
|
8502
|
+
return
|
|
8503
|
+
# else open it (if your app supports opening closed docs, otherwise no-op)
|
|
8504
|
+
try:
|
|
8505
|
+
self._open_subwindow_for_added_doc(doc)
|
|
8506
|
+
except Exception:
|
|
8507
|
+
pass
|
|
8310
8508
|
|
|
8311
8509
|
def _set_linked_stretch_from_action(self, checked: bool):
|
|
8312
8510
|
# persist as the default for *new* views
|
|
@@ -8432,7 +8630,10 @@ class AstroSuiteProMainWindow(
|
|
|
8432
8630
|
self._refresh_mask_action_states()
|
|
8433
8631
|
except Exception:
|
|
8434
8632
|
pass
|
|
8435
|
-
|
|
8633
|
+
try:
|
|
8634
|
+
self._fix_mdi_titlebar_emboss("#dcdcdc" if self._theme_mode()=="dark" else "#f0f0f0")
|
|
8635
|
+
except Exception:
|
|
8636
|
+
pass
|
|
8436
8637
|
|
|
8437
8638
|
def _sync_docman_active(self, doc):
|
|
8438
8639
|
dm = self.doc_manager
|