setiastrosuitepro 1.6.4__py3-none-any.whl → 1.6.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- setiastro/images/abeicon.svg +16 -0
- setiastro/images/acv_icon.png +0 -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 +19 -0
- 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 +35 -7
- 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 +4 -1
- 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 +67 -4
- 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 +393 -204
- setiastro/saspro/gui/mixins/menu_mixin.py +1 -0
- setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
- setiastro/saspro/gui/mixins/toolbar_mixin.py +356 -12
- 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/stretch.py +531 -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 +43 -0
- 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 -2
- setiastro/saspro/ops/commands.py +18 -18
- setiastro/saspro/ops/script_editor.py +5 -2
- setiastro/saspro/ops/scripts.py +3 -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 +60 -2
- setiastro/saspro/shortcuts.py +142 -23
- setiastro/saspro/signature_insert.py +692 -33
- setiastro/saspro/stacking_suite.py +1017 -400
- setiastro/saspro/star_alignment.py +4 -1
- setiastro/saspro/star_spikes.py +4 -0
- setiastro/saspro/star_stretch.py +38 -3
- setiastro/saspro/stat_stretch.py +702 -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 +60 -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.10.dist-info}/METADATA +2 -1
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/RECORD +112 -80
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.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 ---
|
|
@@ -3015,17 +3172,17 @@ class AstroSuiteProMainWindow(
|
|
|
3015
3172
|
|
|
3016
3173
|
self.convo_window.show()
|
|
3017
3174
|
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
3175
|
def _apply_extract_luminance_preset_to_doc(self, doc, preset=None):
|
|
3021
|
-
|
|
3022
3176
|
from PyQt6.QtWidgets import QMessageBox
|
|
3023
|
-
from setiastro.saspro.luminancerecombine import
|
|
3024
|
-
|
|
3177
|
+
from setiastro.saspro.luminancerecombine import (
|
|
3178
|
+
compute_luminance,
|
|
3179
|
+
resolve_luma_profile_weights,
|
|
3180
|
+
)
|
|
3181
|
+
from setiastro.saspro.headless_utils import unwrap_docproxy
|
|
3182
|
+
import numpy as np
|
|
3025
3183
|
|
|
3026
3184
|
doc = unwrap_docproxy(doc)
|
|
3027
3185
|
p = dict(preset or {})
|
|
3028
|
-
mode = (p.get("mode") or "rec709").lower()
|
|
3029
3186
|
|
|
3030
3187
|
if doc is None or getattr(doc, "image", None) is None:
|
|
3031
3188
|
QMessageBox.information(self, "Extract Luminance", "No target image.")
|
|
@@ -3033,52 +3190,43 @@ class AstroSuiteProMainWindow(
|
|
|
3033
3190
|
|
|
3034
3191
|
img = np.asarray(doc.image)
|
|
3035
3192
|
|
|
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
|
|
3193
|
+
mode = str(p.get("mode", "rec709")).strip()
|
|
3194
|
+
resolved_method, w, profile_name = resolve_luma_profile_weights(mode)
|
|
3045
3195
|
|
|
3046
|
-
L = compute_luminance(img, method=
|
|
3196
|
+
L = compute_luminance(img, method=resolved_method, weights=w)
|
|
3047
3197
|
|
|
3048
3198
|
dm = getattr(self, "doc_manager", None)
|
|
3049
3199
|
if dm is None:
|
|
3050
|
-
# headless fallback: just overwrite active doc
|
|
3051
3200
|
doc.apply_edit(L.astype(np.float32), step_name="Extract Luminance")
|
|
3052
3201
|
return
|
|
3053
3202
|
|
|
3054
|
-
|
|
3203
|
+
meta = {
|
|
3204
|
+
"step_name": "Extract Luminance",
|
|
3205
|
+
"luma_method": resolved_method,
|
|
3206
|
+
}
|
|
3207
|
+
if w is not None:
|
|
3208
|
+
meta["luma_weights"] = np.asarray(w, dtype=np.float32).tolist()
|
|
3209
|
+
if profile_name:
|
|
3210
|
+
meta["luma_profile"] = str(profile_name)
|
|
3211
|
+
|
|
3055
3212
|
try:
|
|
3213
|
+
suffix = f"{profile_name}" if profile_name else resolved_method
|
|
3056
3214
|
new_doc = dm.create_document_from_array(
|
|
3057
3215
|
L.astype(np.float32),
|
|
3058
|
-
name=f"{doc.display_name()} -- Luminance ({
|
|
3216
|
+
name=f"{doc.display_name()} -- Luminance ({suffix})",
|
|
3059
3217
|
is_mono=True,
|
|
3060
|
-
metadata=
|
|
3218
|
+
metadata=meta,
|
|
3061
3219
|
)
|
|
3062
3220
|
dm.add_document(new_doc)
|
|
3063
3221
|
except Exception:
|
|
3064
|
-
# safe fallback
|
|
3065
3222
|
doc.apply_edit(L.astype(np.float32), step_name="Extract Luminance")
|
|
3066
3223
|
|
|
3067
|
-
|
|
3068
3224
|
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
|
|
3225
|
+
from PyQt6.QtWidgets import QMessageBox
|
|
3226
|
+
from PyQt6.QtGui import QIcon
|
|
3227
|
+
from setiastro.saspro.luminancerecombine import compute_luminance, resolve_luma_profile_weights
|
|
3228
|
+
|
|
3229
|
+
|
|
3082
3230
|
sw = None
|
|
3083
3231
|
if doc is None:
|
|
3084
3232
|
sw = self.mdi.activeSubWindow()
|
|
@@ -3097,70 +3245,19 @@ class AstroSuiteProMainWindow(
|
|
|
3097
3245
|
QMessageBox.information(self, "Extract Luminance", "Luminance extraction requires an RGB image.")
|
|
3098
3246
|
return
|
|
3099
3247
|
|
|
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
3248
|
p = dict(preset or {})
|
|
3110
|
-
|
|
3249
|
+
mode = str(
|
|
3111
3250
|
p.get("mode",
|
|
3112
3251
|
p.get("method",
|
|
3113
3252
|
p.get("luma_method",
|
|
3114
3253
|
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
|
|
3254
|
+
).strip()
|
|
3255
|
+
|
|
3256
|
+
resolved_method, w, profile_name = resolve_luma_profile_weights(mode)
|
|
3257
|
+
|
|
3258
|
+
y = compute_luminance(img, method=resolved_method, weights=w)
|
|
3259
|
+
|
|
3260
|
+
# ---- metadata & title ----
|
|
3164
3261
|
base_meta = {}
|
|
3165
3262
|
try:
|
|
3166
3263
|
base_meta = dict(getattr(doc, "metadata", {}) or {})
|
|
@@ -3172,13 +3269,16 @@ class AstroSuiteProMainWindow(
|
|
|
3172
3269
|
"source": "ExtractLuminance",
|
|
3173
3270
|
"is_mono": True,
|
|
3174
3271
|
"bit_depth": "32f",
|
|
3175
|
-
"luma_method":
|
|
3272
|
+
"luma_method": resolved_method,
|
|
3176
3273
|
}
|
|
3177
|
-
if
|
|
3178
|
-
meta["luma_weights"] = np.asarray(
|
|
3274
|
+
if w is not None:
|
|
3275
|
+
meta["luma_weights"] = np.asarray(w, dtype=np.float32).tolist()
|
|
3276
|
+
if profile_name:
|
|
3277
|
+
meta["luma_profile"] = str(profile_name)
|
|
3179
3278
|
|
|
3180
3279
|
base_title = sw.windowTitle() if sw else (getattr(doc, "title", getattr(doc, "name", "")) or "Untitled")
|
|
3181
|
-
|
|
3280
|
+
suffix = f"{profile_name}" if profile_name else resolved_method
|
|
3281
|
+
title = f"{base_title} -- Luminance ({suffix})"
|
|
3182
3282
|
|
|
3183
3283
|
dm = getattr(self, "docman", None)
|
|
3184
3284
|
if dm is None:
|
|
@@ -3206,19 +3306,19 @@ class AstroSuiteProMainWindow(
|
|
|
3206
3306
|
except Exception:
|
|
3207
3307
|
pass
|
|
3208
3308
|
|
|
3209
|
-
# ðŸ" Remember for Replay (optional but consistent)
|
|
3210
3309
|
try:
|
|
3211
3310
|
remember = getattr(self, "remember_last_headless_command", None) or getattr(self, "_remember_last_headless_command", None)
|
|
3212
3311
|
if callable(remember):
|
|
3213
|
-
remember("extract_luminance", {"mode":
|
|
3312
|
+
remember("extract_luminance", {"mode": mode}, description="Extract Luminance")
|
|
3214
3313
|
except Exception:
|
|
3215
3314
|
pass
|
|
3216
3315
|
|
|
3217
3316
|
if hasattr(self, "_log"):
|
|
3218
|
-
self._log(f"Extract Luminance ({
|
|
3317
|
+
self._log(f"Extract Luminance ({suffix}) -> new mono document created.")
|
|
3219
3318
|
|
|
3220
3319
|
return new_doc
|
|
3221
3320
|
|
|
3321
|
+
|
|
3222
3322
|
def _subwindow_docs(self):
|
|
3223
3323
|
docs = []
|
|
3224
3324
|
for sw in self.mdi.subWindowList():
|
|
@@ -4469,6 +4569,48 @@ class AstroSuiteProMainWindow(
|
|
|
4469
4569
|
dlg.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
4470
4570
|
dlg.show()
|
|
4471
4571
|
|
|
4572
|
+
def _open_acv_exporter(self):
|
|
4573
|
+
from setiastro.saspro.acv_exporter import AstroCatalogueViewerExporterDialog
|
|
4574
|
+
|
|
4575
|
+
dm = getattr(self, "doc_manager", None) or getattr(self, "docman", None)
|
|
4576
|
+
if dm is None:
|
|
4577
|
+
QMessageBox.information(self, "Astro Catalogue Viewer Exporter", "No document manager available.")
|
|
4578
|
+
return
|
|
4579
|
+
|
|
4580
|
+
sw = self.mdi.activeSubWindow()
|
|
4581
|
+
if not sw:
|
|
4582
|
+
QMessageBox.information(self, "Astro Catalogue Viewer Exporter", "Open an image first.")
|
|
4583
|
+
return
|
|
4584
|
+
|
|
4585
|
+
view = sw.widget()
|
|
4586
|
+
active_doc = None
|
|
4587
|
+
|
|
4588
|
+
# Prefer ROI-aware resolution
|
|
4589
|
+
try:
|
|
4590
|
+
if hasattr(dm, "get_document_for_view"):
|
|
4591
|
+
active_doc = dm.get_document_for_view(view)
|
|
4592
|
+
except Exception:
|
|
4593
|
+
active_doc = None
|
|
4594
|
+
|
|
4595
|
+
# Fallback
|
|
4596
|
+
if active_doc is None:
|
|
4597
|
+
try:
|
|
4598
|
+
active_doc = getattr(view, "document", None)
|
|
4599
|
+
except Exception:
|
|
4600
|
+
active_doc = None
|
|
4601
|
+
|
|
4602
|
+
if active_doc is None or getattr(active_doc, "image", None) is None:
|
|
4603
|
+
QMessageBox.information(self, "Astro Catalogue Viewer Exporter", "No active image.")
|
|
4604
|
+
return
|
|
4605
|
+
|
|
4606
|
+
dlg = AstroCatalogueViewerExporterDialog(self, dm, active_doc)
|
|
4607
|
+
try:
|
|
4608
|
+
dlg.setWindowIcon(QIcon(acv_icon_path))
|
|
4609
|
+
except Exception:
|
|
4610
|
+
pass
|
|
4611
|
+
dlg.show()
|
|
4612
|
+
|
|
4613
|
+
|
|
4472
4614
|
def _open_linear_fit(self):
|
|
4473
4615
|
from setiastro.saspro.linear_fit import LinearFitDialog
|
|
4474
4616
|
dm = getattr(self, "doc_manager", None) or getattr(self, "docman", None)
|
|
@@ -4585,12 +4727,6 @@ class AstroSuiteProMainWindow(
|
|
|
4585
4727
|
if max_len and len(hist) > max_len:
|
|
4586
4728
|
del hist[:-max_len]
|
|
4587
4729
|
|
|
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
4730
|
|
|
4595
4731
|
|
|
4596
4732
|
def _remember_last_headless_command(self, command_id: str, preset: dict | None = None, description: str = ""):
|
|
@@ -4646,17 +4782,6 @@ class AstroSuiteProMainWindow(
|
|
|
4646
4782
|
"""
|
|
4647
4783
|
payload = getattr(self, "_last_headless_command", None)
|
|
4648
4784
|
|
|
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
4785
|
|
|
4661
4786
|
if not payload:
|
|
4662
4787
|
QMessageBox.information(
|
|
@@ -4688,17 +4813,6 @@ class AstroSuiteProMainWindow(
|
|
|
4688
4813
|
"""
|
|
4689
4814
|
payload = getattr(self, "_last_headless_command", None) or {}
|
|
4690
4815
|
|
|
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
4816
|
|
|
4703
4817
|
if not payload:
|
|
4704
4818
|
QMessageBox.information(
|
|
@@ -4724,17 +4838,6 @@ class AstroSuiteProMainWindow(
|
|
|
4724
4838
|
QMessageBox.information(self, "Replay Last Action", "No base image to apply the action to.")
|
|
4725
4839
|
return
|
|
4726
4840
|
|
|
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
4841
|
|
|
4739
4842
|
# ---- Extract cid + preset from payload (support both old + new schemas) ----
|
|
4740
4843
|
cid_raw = payload.get("command_id")
|
|
@@ -7645,12 +7748,33 @@ class AstroSuiteProMainWindow(
|
|
|
7645
7748
|
pass
|
|
7646
7749
|
|
|
7647
7750
|
def _pretty_title(self, doc, *, linked: bool | None = None) -> str:
|
|
7648
|
-
|
|
7649
|
-
|
|
7751
|
+
md = (getattr(doc, "metadata", {}) or {})
|
|
7752
|
+
|
|
7753
|
+
# ✅ 1) Prefer explicit display_name (what duplicate/rename intends)
|
|
7754
|
+
name = (md.get("display_name") or "").strip()
|
|
7755
|
+
|
|
7756
|
+
# 2) Fallback to file_path (but only if display_name is missing)
|
|
7757
|
+
if not name:
|
|
7758
|
+
fp = (md.get("file_path") or "").strip()
|
|
7759
|
+
if fp:
|
|
7760
|
+
name = os.path.splitext(os.path.basename(fp))[0]
|
|
7761
|
+
|
|
7762
|
+
# 3) Fallback to doc.display_name()
|
|
7763
|
+
if not name:
|
|
7764
|
+
name = getattr(doc, "display_name", lambda: "Untitled")()
|
|
7765
|
+
name = (name or "Untitled").replace("[LINK] ", "").strip()
|
|
7766
|
+
|
|
7767
|
+
# If it looks like a filename, drop extension
|
|
7768
|
+
base, ext = os.path.splitext(name)
|
|
7769
|
+
if ext and len(ext) <= 10:
|
|
7770
|
+
name = base
|
|
7771
|
+
|
|
7772
|
+
# linked marker logic
|
|
7650
7773
|
if linked is None:
|
|
7651
|
-
linked = hasattr(doc, "_parent_doc")
|
|
7774
|
+
linked = hasattr(doc, "_parent_doc")
|
|
7652
7775
|
return f"[LINK] {name}" if linked else name
|
|
7653
7776
|
|
|
7777
|
+
|
|
7654
7778
|
def _build_subwindow_title_for_doc(self, doc) -> str:
|
|
7655
7779
|
"""
|
|
7656
7780
|
Build a unique, human-friendly title for a QMdiSubWindow
|
|
@@ -7718,6 +7842,34 @@ class AstroSuiteProMainWindow(
|
|
|
7718
7842
|
return cand
|
|
7719
7843
|
n += 1
|
|
7720
7844
|
|
|
7845
|
+
|
|
7846
|
+
def _doc_window_title(self, doc) -> str:
|
|
7847
|
+
md = getattr(doc, "metadata", {}) or {}
|
|
7848
|
+
|
|
7849
|
+
t = (md.get("display_name") or "").strip()
|
|
7850
|
+
if not t:
|
|
7851
|
+
try:
|
|
7852
|
+
t = (doc.display_name() or "").strip()
|
|
7853
|
+
except Exception:
|
|
7854
|
+
t = ""
|
|
7855
|
+
|
|
7856
|
+
if not t:
|
|
7857
|
+
fp = (md.get("file_path") or "").strip()
|
|
7858
|
+
if fp:
|
|
7859
|
+
t = os.path.splitext(os.path.basename(fp))[0] # ✅ strip ext here too
|
|
7860
|
+
|
|
7861
|
+
t = t or "Untitled"
|
|
7862
|
+
|
|
7863
|
+
# strip glyphs etc
|
|
7864
|
+
try:
|
|
7865
|
+
t = _strip_ui_decorations(t)
|
|
7866
|
+
except Exception:
|
|
7867
|
+
pass
|
|
7868
|
+
|
|
7869
|
+
# ✅ ALWAYS strip filename-like extension at the very end
|
|
7870
|
+
t = _strip_filename_ext(t)
|
|
7871
|
+
|
|
7872
|
+
return t
|
|
7721
7873
|
|
|
7722
7874
|
def _spawn_subwindow_for(self, doc, *, force_new: bool = False):
|
|
7723
7875
|
"""
|
|
@@ -7821,10 +7973,7 @@ class AstroSuiteProMainWindow(
|
|
|
7821
7973
|
if replay_sig is not None:
|
|
7822
7974
|
try:
|
|
7823
7975
|
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)}")
|
|
7976
|
+
|
|
7828
7977
|
except Exception as e:
|
|
7829
7978
|
try:
|
|
7830
7979
|
self._log(f"[Replay] FAILED to connect {sig_name_used} for view id={id(view)}: {e}")
|
|
@@ -7832,7 +7981,8 @@ class AstroSuiteProMainWindow(
|
|
|
7832
7981
|
print(f"[Replay] FAILED to connect {sig_name_used} for view id={id(view)}: {e}")
|
|
7833
7982
|
|
|
7834
7983
|
self._hook_preview_awareness(view)
|
|
7835
|
-
|
|
7984
|
+
|
|
7985
|
+
base_title = self._doc_window_title(doc) # ✅ use metadata display_name
|
|
7836
7986
|
final_title = self._unique_window_title(base_title)
|
|
7837
7987
|
|
|
7838
7988
|
# -- 6) Add subwindow and set chrome
|
|
@@ -7856,7 +8006,8 @@ class AstroSuiteProMainWindow(
|
|
|
7856
8006
|
# We target ~60% of the viewport height, clamped to sane bounds.
|
|
7857
8007
|
# -------------------------------------------------------------------------
|
|
7858
8008
|
vp = self.mdi.viewport()
|
|
7859
|
-
|
|
8009
|
+
# Use viewport geometry in MDI coordinates (NOT viewport-local rect)
|
|
8010
|
+
area = vp.geometry() if vp else self.mdi.contentsRect()
|
|
7860
8011
|
|
|
7861
8012
|
# Determine aspect ratio
|
|
7862
8013
|
img_w = img_h = None
|
|
@@ -7899,7 +8050,7 @@ class AstroSuiteProMainWindow(
|
|
|
7899
8050
|
# Smart Cascade: Position relative to the *currently active* window
|
|
7900
8051
|
# (before we make the new one active).
|
|
7901
8052
|
# -------------------------------------------------------------------------
|
|
7902
|
-
new_x, new_y =
|
|
8053
|
+
new_x, new_y = area.left(), area.top()
|
|
7903
8054
|
|
|
7904
8055
|
# Get dominant/active window *before* we activate the new one
|
|
7905
8056
|
active = self.mdi.activeSubWindow()
|
|
@@ -7922,15 +8073,13 @@ class AstroSuiteProMainWindow(
|
|
|
7922
8073
|
except Exception:
|
|
7923
8074
|
pass
|
|
7924
8075
|
|
|
7925
|
-
# Bounds check:
|
|
7926
|
-
|
|
7927
|
-
|
|
7928
|
-
|
|
7929
|
-
|
|
7930
|
-
|
|
7931
|
-
|
|
7932
|
-
new_x = max(0, new_x)
|
|
7933
|
-
new_y = max(0, new_y)
|
|
8076
|
+
# Bounds check: keep titlebar visible and stay inside viewport
|
|
8077
|
+
if (new_x + target_w > area.right() - 10) or (new_y + 40 > area.bottom() - 10):
|
|
8078
|
+
new_x = area.left()
|
|
8079
|
+
new_y = area.top()
|
|
8080
|
+
|
|
8081
|
+
new_x = max(area.left(), new_x)
|
|
8082
|
+
new_y = max(area.top(), new_y)
|
|
7934
8083
|
|
|
7935
8084
|
sw.move(new_x, new_y)
|
|
7936
8085
|
|
|
@@ -8025,6 +8174,11 @@ class AstroSuiteProMainWindow(
|
|
|
8025
8174
|
except Exception:
|
|
8026
8175
|
pass
|
|
8027
8176
|
|
|
8177
|
+
try:
|
|
8178
|
+
self._fix_mdi_titlebar_emboss("#dcdcdc" if self._theme_mode()=="dark" else "#f0f0f0")
|
|
8179
|
+
except Exception:
|
|
8180
|
+
pass
|
|
8181
|
+
|
|
8028
8182
|
# -- 11) If this is the first window and it's an image, mimic "Cascade Views"
|
|
8029
8183
|
try:
|
|
8030
8184
|
if first_window and not is_table:
|
|
@@ -8119,25 +8273,57 @@ class AstroSuiteProMainWindow(
|
|
|
8119
8273
|
"autostretch_target": float(getattr(source_view, "autostretch_target", 0.25)),
|
|
8120
8274
|
}
|
|
8121
8275
|
|
|
8122
|
-
# 2) New name (
|
|
8123
|
-
base_name = ""
|
|
8276
|
+
# 2) New name (normalized: NO decorators like 🔗■●◆▲▪▫•◼◻◾◽)
|
|
8124
8277
|
try:
|
|
8125
|
-
base_name =
|
|
8278
|
+
base_name = self._doc_window_title(base_doc) # might include decorations
|
|
8126
8279
|
except Exception:
|
|
8127
8280
|
base_name = "Untitled"
|
|
8128
8281
|
|
|
8282
|
+
# Normalize it so uniqueness checks don't miss decorated titles
|
|
8129
8283
|
try:
|
|
8130
|
-
base_name =
|
|
8284
|
+
base_name = normalize_doc_title(base_name)
|
|
8131
8285
|
except Exception:
|
|
8132
|
-
|
|
8133
|
-
|
|
8134
|
-
|
|
8135
|
-
|
|
8136
|
-
|
|
8286
|
+
base_name = (base_name or "Untitled").strip()
|
|
8287
|
+
|
|
8288
|
+
# Build a set of existing document names (normalized)
|
|
8289
|
+
existing = set()
|
|
8290
|
+
try:
|
|
8291
|
+
dm = getattr(self, "doc_manager", None) or getattr(self, "docman", None)
|
|
8292
|
+
docs = []
|
|
8293
|
+
|
|
8294
|
+
# Prefer an official accessor if you have one
|
|
8295
|
+
if dm is not None:
|
|
8296
|
+
if hasattr(dm, "documents"):
|
|
8297
|
+
docs = list(dm.documents())
|
|
8298
|
+
elif hasattr(dm, "_docs"):
|
|
8299
|
+
docs = list(dm._docs)
|
|
8300
|
+
|
|
8301
|
+
for d in docs:
|
|
8302
|
+
try:
|
|
8303
|
+
dn = ""
|
|
8304
|
+
md = getattr(d, "metadata", {}) or {}
|
|
8305
|
+
dn = (md.get("display_name") or "").strip() or (d.display_name() or "").strip()
|
|
8306
|
+
dn = normalize_doc_title(dn)
|
|
8307
|
+
if dn:
|
|
8308
|
+
existing.add(dn)
|
|
8309
|
+
except Exception:
|
|
8310
|
+
pass
|
|
8311
|
+
except Exception:
|
|
8312
|
+
pass
|
|
8313
|
+
|
|
8314
|
+
# Pick a unique duplicate name: base_duplicate, base_duplicate2, ...
|
|
8315
|
+
candidate = f"{base_name}_duplicate"
|
|
8316
|
+
if candidate in existing:
|
|
8317
|
+
n = 2
|
|
8318
|
+
while True:
|
|
8319
|
+
cand = f"{base_name}_duplicate{n}"
|
|
8320
|
+
if cand not in existing:
|
|
8321
|
+
candidate = cand
|
|
8322
|
+
break
|
|
8323
|
+
n += 1
|
|
8137
8324
|
|
|
8138
8325
|
# 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")
|
|
8326
|
+
new_doc = self.docman.duplicate_document(base_doc, new_name=candidate)
|
|
8141
8327
|
print(f" Duplicated document ID {id(base_doc)} -> {id(new_doc)}")
|
|
8142
8328
|
|
|
8143
8329
|
# 4) Ensure the duplicate starts mask-free (so we don't inherit mask UI state)
|
|
@@ -8432,7 +8618,10 @@ class AstroSuiteProMainWindow(
|
|
|
8432
8618
|
self._refresh_mask_action_states()
|
|
8433
8619
|
except Exception:
|
|
8434
8620
|
pass
|
|
8435
|
-
|
|
8621
|
+
try:
|
|
8622
|
+
self._fix_mdi_titlebar_emboss("#dcdcdc" if self._theme_mode()=="dark" else "#f0f0f0")
|
|
8623
|
+
except Exception:
|
|
8624
|
+
pass
|
|
8436
8625
|
|
|
8437
8626
|
def _sync_docman_active(self, doc):
|
|
8438
8627
|
dm = self.doc_manager
|