setiastrosuitepro 1.6.2__py3-none-any.whl → 1.6.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of setiastrosuitepro might be problematic. Click here for more details.
- setiastro/images/abeicon.svg +16 -0
- setiastro/images/acv_icon.png +0 -0
- setiastro/images/colorwheel.svg +97 -0
- setiastro/images/cosmic.svg +40 -0
- setiastro/images/cosmicsat.svg +24 -0
- setiastro/images/first_quarter.png +0 -0
- setiastro/images/full_moon.png +0 -0
- setiastro/images/graxpert.svg +19 -0
- setiastro/images/last_quarter.png +0 -0
- setiastro/images/linearfit.svg +32 -0
- setiastro/images/new_moon.png +0 -0
- setiastro/images/pixelmath.svg +42 -0
- setiastro/images/rotatearbitrary.png +0 -0
- setiastro/images/waning_crescent_1.png +0 -0
- setiastro/images/waning_crescent_2.png +0 -0
- setiastro/images/waning_crescent_3.png +0 -0
- setiastro/images/waning_crescent_4.png +0 -0
- setiastro/images/waning_crescent_5.png +0 -0
- setiastro/images/waning_gibbous_1.png +0 -0
- setiastro/images/waning_gibbous_2.png +0 -0
- setiastro/images/waning_gibbous_3.png +0 -0
- setiastro/images/waning_gibbous_4.png +0 -0
- setiastro/images/waning_gibbous_5.png +0 -0
- setiastro/images/waxing_crescent_1.png +0 -0
- setiastro/images/waxing_crescent_2.png +0 -0
- setiastro/images/waxing_crescent_3.png +0 -0
- setiastro/images/waxing_crescent_4.png +0 -0
- setiastro/images/waxing_crescent_5.png +0 -0
- setiastro/images/waxing_gibbous_1.png +0 -0
- setiastro/images/waxing_gibbous_2.png +0 -0
- setiastro/images/waxing_gibbous_3.png +0 -0
- setiastro/images/waxing_gibbous_4.png +0 -0
- setiastro/images/waxing_gibbous_5.png +0 -0
- setiastro/qml/ResourceMonitor.qml +84 -82
- setiastro/saspro/__main__.py +20 -1
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/abe.py +37 -4
- setiastro/saspro/aberration_ai.py +237 -21
- setiastro/saspro/acv_exporter.py +379 -0
- setiastro/saspro/add_stars.py +33 -6
- setiastro/saspro/backgroundneutral.py +114 -37
- setiastro/saspro/blemish_blaster.py +4 -1
- setiastro/saspro/blink_comparator_pro.py +548 -275
- setiastro/saspro/clahe.py +4 -1
- setiastro/saspro/continuum_subtract.py +4 -1
- setiastro/saspro/convo.py +13 -7
- setiastro/saspro/cosmicclarity.py +129 -18
- setiastro/saspro/crop_dialog_pro.py +134 -8
- setiastro/saspro/curve_editor_pro.py +109 -42
- setiastro/saspro/doc_manager.py +246 -16
- setiastro/saspro/exoplanet_detector.py +120 -28
- setiastro/saspro/frequency_separation.py +1158 -204
- setiastro/saspro/function_bundle.py +16 -16
- setiastro/saspro/ghs_dialog_pro.py +81 -16
- setiastro/saspro/graxpert.py +1 -0
- setiastro/saspro/gui/main_window.py +519 -289
- setiastro/saspro/gui/mixins/dock_mixin.py +276 -42
- setiastro/saspro/gui/mixins/geometry_mixin.py +105 -5
- setiastro/saspro/gui/mixins/menu_mixin.py +28 -1
- setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
- setiastro/saspro/gui/mixins/toolbar_mixin.py +416 -27
- setiastro/saspro/gui/mixins/update_mixin.py +138 -36
- setiastro/saspro/gui/mixins/view_mixin.py +42 -0
- setiastro/saspro/halobgon.py +4 -0
- setiastro/saspro/histogram.py +5 -1
- setiastro/saspro/image_combine.py +4 -0
- setiastro/saspro/image_peeker_pro.py +4 -0
- setiastro/saspro/imageops/starbasedwhitebalance.py +23 -52
- setiastro/saspro/imageops/stretch.py +582 -62
- setiastro/saspro/isophote.py +4 -0
- setiastro/saspro/layers.py +13 -9
- setiastro/saspro/layers_dock.py +183 -3
- setiastro/saspro/legacy/image_manager.py +154 -20
- setiastro/saspro/legacy/numba_utils.py +67 -47
- setiastro/saspro/legacy/xisf.py +240 -98
- setiastro/saspro/live_stacking.py +180 -79
- setiastro/saspro/luminancerecombine.py +228 -27
- setiastro/saspro/mask_creation.py +174 -15
- setiastro/saspro/mfdeconv.py +113 -35
- setiastro/saspro/mfdeconvcudnn.py +119 -70
- setiastro/saspro/mfdeconvsport.py +112 -35
- setiastro/saspro/morphology.py +4 -0
- setiastro/saspro/multiscale_decomp.py +748 -255
- setiastro/saspro/numba_utils.py +72 -57
- setiastro/saspro/ops/commands.py +18 -18
- setiastro/saspro/ops/script_editor.py +10 -2
- setiastro/saspro/ops/scripts.py +122 -0
- setiastro/saspro/perfect_palette_picker.py +37 -3
- setiastro/saspro/plate_solver.py +84 -49
- setiastro/saspro/psf_viewer.py +119 -37
- setiastro/saspro/remove_stars_preset.py +55 -13
- setiastro/saspro/resources.py +97 -11
- setiastro/saspro/rgbalign.py +4 -0
- setiastro/saspro/selective_color.py +83 -21
- setiastro/saspro/sfcc.py +364 -152
- setiastro/saspro/shortcuts.py +253 -49
- setiastro/saspro/signature_insert.py +692 -33
- setiastro/saspro/stacking_suite.py +1610 -574
- setiastro/saspro/star_alignment.py +522 -453
- setiastro/saspro/star_spikes.py +4 -0
- setiastro/saspro/star_stretch.py +38 -3
- setiastro/saspro/stat_stretch.py +743 -128
- setiastro/saspro/status_log_dock.py +1 -1
- setiastro/saspro/subwindow.py +786 -360
- setiastro/saspro/supernovaasteroidhunter.py +1 -1
- setiastro/saspro/swap_manager.py +77 -42
- setiastro/saspro/translations/all_source_strings.json +1588 -516
- setiastro/saspro/translations/ar_translations.py +915 -684
- setiastro/saspro/translations/de_translations.py +442 -463
- setiastro/saspro/translations/es_translations.py +277 -47
- setiastro/saspro/translations/fr_translations.py +279 -47
- setiastro/saspro/translations/hi_translations.py +253 -21
- setiastro/saspro/translations/integrate_translations.py +3 -2
- setiastro/saspro/translations/it_translations.py +1211 -161
- setiastro/saspro/translations/ja_translations.py +3340 -3107
- setiastro/saspro/translations/pt_translations.py +3315 -3337
- setiastro/saspro/translations/ru_translations.py +351 -117
- setiastro/saspro/translations/saspro_ar.qm +0 -0
- setiastro/saspro/translations/saspro_ar.ts +15902 -138
- setiastro/saspro/translations/saspro_de.qm +0 -0
- setiastro/saspro/translations/saspro_de.ts +14428 -133
- setiastro/saspro/translations/saspro_es.qm +0 -0
- setiastro/saspro/translations/saspro_es.ts +11503 -7821
- setiastro/saspro/translations/saspro_fr.qm +0 -0
- setiastro/saspro/translations/saspro_fr.ts +11168 -7812
- setiastro/saspro/translations/saspro_hi.qm +0 -0
- setiastro/saspro/translations/saspro_hi.ts +14733 -135
- setiastro/saspro/translations/saspro_it.qm +0 -0
- setiastro/saspro/translations/saspro_it.ts +14347 -7821
- setiastro/saspro/translations/saspro_ja.qm +0 -0
- setiastro/saspro/translations/saspro_ja.ts +14860 -137
- setiastro/saspro/translations/saspro_pt.qm +0 -0
- setiastro/saspro/translations/saspro_pt.ts +14904 -137
- setiastro/saspro/translations/saspro_ru.qm +0 -0
- setiastro/saspro/translations/saspro_ru.ts +11766 -168
- setiastro/saspro/translations/saspro_sw.qm +0 -0
- setiastro/saspro/translations/saspro_sw.ts +15115 -135
- setiastro/saspro/translations/saspro_uk.qm +0 -0
- setiastro/saspro/translations/saspro_uk.ts +11206 -6729
- setiastro/saspro/translations/saspro_zh.qm +0 -0
- setiastro/saspro/translations/saspro_zh.ts +10581 -7812
- setiastro/saspro/translations/sw_translations.py +282 -56
- setiastro/saspro/translations/uk_translations.py +264 -35
- setiastro/saspro/translations/zh_translations.py +282 -47
- setiastro/saspro/view_bundle.py +17 -17
- setiastro/saspro/wavescale_hdr.py +4 -1
- setiastro/saspro/wavescalede.py +4 -1
- setiastro/saspro/whitebalance.py +84 -12
- setiastro/saspro/widgets/common_utilities.py +28 -21
- setiastro/saspro/widgets/minigame/game.js +11 -6
- setiastro/saspro/widgets/resource_monitor.py +133 -57
- setiastro/saspro/widgets/spinboxes.py +28 -13
- setiastro/saspro/wimi.py +92 -721
- setiastro/saspro/wims.py +46 -36
- setiastro/saspro/window_shelf.py +2 -2
- setiastro/saspro/xisf.py +101 -11
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/METADATA +8 -7
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/RECORD +162 -128
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/licenses/license.txt +0 -0
|
@@ -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
|
|
132
|
+
QPropertyAnimation, QEasingCurve, QElapsedTimer
|
|
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
|
|
|
@@ -187,14 +187,14 @@ from setiastro.saspro.resources import (
|
|
|
187
187
|
platesolve_path, psf_path, supernova_path, starregistration_path,
|
|
188
188
|
stacking_path, pedestal_icon_path, starspike_path, aperture_path,
|
|
189
189
|
jwstpupil_path, signature_icon_path, livestacking_path, hrdiagram_path,
|
|
190
|
-
convoicon_path, spcc_icon_path, sasp_data_path, exoicon_path, peeker_icon,
|
|
190
|
+
convoicon_path, spcc_icon_path, sasp_data_path, exoicon_path, peeker_icon,rotatearbitrary_path,
|
|
191
191
|
dse_icon_path, astrobin_filters_csv_path, isophote_path, statstretch_path,
|
|
192
192
|
starstretch_path, curves_path, disk_path, uhs_path, blink_path, ppp_path,
|
|
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
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)
|
|
@@ -310,7 +449,7 @@ class AstroSuiteProMainWindow(
|
|
|
310
449
|
from setiastro.saspro.window_shelf import WindowShelf, MinimizeInterceptor
|
|
311
450
|
from setiastro.saspro.imageops.mdi_snap import MdiSnapController
|
|
312
451
|
from setiastro.saspro.ops.scripts import ScriptManager
|
|
313
|
-
self._version =
|
|
452
|
+
self._version = version
|
|
314
453
|
self._build_timestamp = build_timestamp
|
|
315
454
|
self.setWindowTitle(f"Seti Astro Suite Pro v{self._version}")
|
|
316
455
|
self.resize(1400, 900)
|
|
@@ -467,26 +606,23 @@ class AstroSuiteProMainWindow(
|
|
|
467
606
|
self.mdi.linkViewDropped.connect(self._on_linkview_drop)
|
|
468
607
|
|
|
469
608
|
self.doc_manager.set_mdi_area(self.mdi)
|
|
470
|
-
|
|
609
|
+
# Coalesce undo/redo label refreshes
|
|
610
|
+
self._undo_redo_refresh_pending = False
|
|
611
|
+
self._undo_redo_refresh_timer = QTimer(self)
|
|
612
|
+
self._undo_redo_refresh_timer.setSingleShot(True)
|
|
613
|
+
self._undo_redo_refresh_timer.timeout.connect(self._do_undo_redo_label_refresh)
|
|
471
614
|
# Keep the toolbar in sync whenever anything relevant changes
|
|
472
|
-
self.doc_manager.documentAdded.connect(lambda *_: self.
|
|
473
|
-
self.doc_manager.documentRemoved.connect(lambda *_: self.
|
|
474
|
-
self.doc_manager.imageRegionUpdated.connect(lambda *_: self.
|
|
475
|
-
self.doc_manager.previewRepaintRequested.connect(lambda *_: self.
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
try:
|
|
484
|
-
QApplication.instance().focusChanged.connect(
|
|
485
|
-
lambda *_: QTimer.singleShot(0, self.update_undo_redo_action_labels)
|
|
486
|
-
)
|
|
487
|
-
except Exception:
|
|
488
|
-
pass
|
|
489
|
-
|
|
615
|
+
self.doc_manager.documentAdded.connect(lambda *_: self._schedule_undo_redo_label_refresh())
|
|
616
|
+
self.doc_manager.documentRemoved.connect(lambda *_: self._schedule_undo_redo_label_refresh())
|
|
617
|
+
self.doc_manager.imageRegionUpdated.connect(lambda *_: self._schedule_undo_redo_label_refresh())
|
|
618
|
+
self.doc_manager.previewRepaintRequested.connect(lambda *_: self._schedule_undo_redo_label_refresh())
|
|
619
|
+
self.mdi.subWindowActivated.connect(lambda *_: self._schedule_undo_redo_label_refresh())
|
|
620
|
+
|
|
621
|
+
# optional: keep, but schedule (or remove entirely)
|
|
622
|
+
#try:
|
|
623
|
+
# QApplication.instance().focusChanged.connect(lambda *_: self._schedule_undo_redo_label_refresh())
|
|
624
|
+
#except Exception:
|
|
625
|
+
# pass
|
|
490
626
|
self.shortcuts.load_shortcuts()
|
|
491
627
|
self._ensure_persistent_names()
|
|
492
628
|
self._restore_window_placement()
|
|
@@ -570,6 +706,22 @@ class AstroSuiteProMainWindow(
|
|
|
570
706
|
|
|
571
707
|
# _init_log_dock, _hook_stdout_stderr, and _append_log_text are now in DockMixin
|
|
572
708
|
|
|
709
|
+
def _schedule_undo_redo_label_refresh(self):
|
|
710
|
+
# Coalesce many triggers into one UI update
|
|
711
|
+
if getattr(self, "_undo_redo_refresh_pending", False):
|
|
712
|
+
return
|
|
713
|
+
self._undo_redo_refresh_pending = True
|
|
714
|
+
# 0ms is fine *if* it’s a real attribute timer (not a local)
|
|
715
|
+
self._undo_redo_refresh_timer.start(0)
|
|
716
|
+
|
|
717
|
+
def _do_undo_redo_label_refresh(self):
|
|
718
|
+
self._undo_redo_refresh_pending = False
|
|
719
|
+
try:
|
|
720
|
+
self.update_undo_redo_action_labels()
|
|
721
|
+
except Exception:
|
|
722
|
+
pass
|
|
723
|
+
|
|
724
|
+
|
|
573
725
|
def _rebuild_menus_for_language(self):
|
|
574
726
|
"""Rebuild menus after language change to apply new translations."""
|
|
575
727
|
try:
|
|
@@ -608,7 +760,7 @@ class AstroSuiteProMainWindow(
|
|
|
608
760
|
doc.changed.connect(self.update_undo_redo_action_labels)
|
|
609
761
|
except Exception:
|
|
610
762
|
pass
|
|
611
|
-
self.
|
|
763
|
+
self._schedule_undo_redo_label_refresh()
|
|
612
764
|
|
|
613
765
|
def _promote_roi_preview_to_real_doc(self, st: dict, preview_doc) -> None:
|
|
614
766
|
"""
|
|
@@ -843,7 +995,18 @@ class AstroSuiteProMainWindow(
|
|
|
843
995
|
dm = self.doc_manager
|
|
844
996
|
doc = None
|
|
845
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)
|
|
846
1007
|
|
|
1008
|
+
# sanity: show which fields are present
|
|
1009
|
+
print("[DNDDBG:DROP_ENTER] has source_view_title?", "source_view_title" in st)
|
|
847
1010
|
|
|
848
1011
|
# Prefer *stable* identifiers over the proxy pointer
|
|
849
1012
|
uid = st.get("doc_uid")
|
|
@@ -946,7 +1109,21 @@ class AstroSuiteProMainWindow(
|
|
|
946
1109
|
print("[VIEWSTATE_DROP] EXIT (no doc)")
|
|
947
1110
|
return
|
|
948
1111
|
|
|
949
|
-
|
|
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"))
|
|
950
1127
|
|
|
951
1128
|
# ----------------------------------------
|
|
952
1129
|
# 4) Peek at metadata to see if this is a
|
|
@@ -1037,34 +1214,35 @@ class AstroSuiteProMainWindow(
|
|
|
1037
1214
|
# copy the view transform.
|
|
1038
1215
|
# ----------------------------------------
|
|
1039
1216
|
if force_new:
|
|
1040
|
-
# We're here only if:
|
|
1041
|
-
# - it's NOT a preview (normal full or promoted ROI), or
|
|
1042
|
-
# - ROI promotion didn't apply and we fell through.
|
|
1043
1217
|
base_doc = doc
|
|
1044
1218
|
|
|
1045
|
-
# 1)
|
|
1046
|
-
|
|
1047
|
-
|
|
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:
|
|
1048
1224
|
try:
|
|
1049
1225
|
base_name = base_doc.display_name() or "Untitled"
|
|
1050
1226
|
except Exception:
|
|
1051
1227
|
base_name = "Untitled"
|
|
1052
1228
|
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
)
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
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
|
+
)
|
|
1068
1246
|
|
|
1069
1247
|
# 2) Let doc_manager's documentAdded handler create the subwindow.
|
|
1070
1248
|
# We just wait for it to show up and then apply the view state.
|
|
@@ -1190,14 +1368,6 @@ class AstroSuiteProMainWindow(
|
|
|
1190
1368
|
return False
|
|
1191
1369
|
|
|
1192
1370
|
def _on_document_added(self, doc):
|
|
1193
|
-
# Helpful debug:
|
|
1194
|
-
try:
|
|
1195
|
-
is_table = (getattr(doc, "metadata", {}).get("doc_type") == "table") or \
|
|
1196
|
-
(hasattr(doc, "rows") and hasattr(doc, "headers"))
|
|
1197
|
-
self._log(f"[documentAdded] {type(doc).__name__} table={is_table} name={getattr(doc, 'display_name', lambda:'?')()}")
|
|
1198
|
-
except Exception:
|
|
1199
|
-
pass
|
|
1200
|
-
|
|
1201
1371
|
self._spawn_subwindow_for(doc)
|
|
1202
1372
|
|
|
1203
1373
|
# --- UI scaffolding ---
|
|
@@ -1206,11 +1376,11 @@ class AstroSuiteProMainWindow(
|
|
|
1206
1376
|
global_pos = lw.viewport().mapToGlobal(pos)
|
|
1207
1377
|
|
|
1208
1378
|
menu = QMenu(lw)
|
|
1209
|
-
act_copy_selected = menu.addAction("Copy Selected")
|
|
1210
|
-
act_copy_all = menu.addAction("Copy All")
|
|
1379
|
+
act_copy_selected = menu.addAction(self.tr("Copy Selected"))
|
|
1380
|
+
act_copy_all = menu.addAction(self.tr("Copy All"))
|
|
1211
1381
|
menu.addSeparator()
|
|
1212
|
-
act_select_all = menu.addAction("Select All Lines")
|
|
1213
|
-
act_clear = menu.addAction("Clear Console")
|
|
1382
|
+
act_select_all = menu.addAction(self.tr("Select All Lines"))
|
|
1383
|
+
act_clear = menu.addAction(self.tr("Clear Console"))
|
|
1214
1384
|
|
|
1215
1385
|
action = menu.exec(global_pos)
|
|
1216
1386
|
if action is None:
|
|
@@ -1804,7 +1974,7 @@ class AstroSuiteProMainWindow(
|
|
|
1804
1974
|
|
|
1805
1975
|
show_view_bundles(self)
|
|
1806
1976
|
except Exception as e:
|
|
1807
|
-
QMessageBox.warning(self, "View Bundles", f"Open failed:\n{e}")
|
|
1977
|
+
QMessageBox.warning(self, self.tr("View Bundles"), f"Open failed:\n{e}")
|
|
1808
1978
|
|
|
1809
1979
|
def _open_function_bundles(self):
|
|
1810
1980
|
from setiastro.saspro.function_bundle import show_function_bundles
|
|
@@ -1812,7 +1982,7 @@ class AstroSuiteProMainWindow(
|
|
|
1812
1982
|
|
|
1813
1983
|
show_function_bundles(self)
|
|
1814
1984
|
except Exception as e:
|
|
1815
|
-
QMessageBox.warning(self, "Function Bundles", f"Open failed:\n{e}")
|
|
1985
|
+
QMessageBox.warning(self, self.tr("Function Bundles"), f"Open failed:\n{e}")
|
|
1816
1986
|
|
|
1817
1987
|
def _open_scripts_folder(self):
|
|
1818
1988
|
if hasattr(self, "scriptman"):
|
|
@@ -1874,43 +2044,43 @@ class AstroSuiteProMainWindow(
|
|
|
1874
2044
|
# Manual list (extend anytime). Format: (Gesture, Context, Effect)
|
|
1875
2045
|
rows = [
|
|
1876
2046
|
# Command search
|
|
1877
|
-
("A", "Display Stretch", "Toggle Display Auto-Stretch"),
|
|
1878
|
-
("Ctrl+I", "Invert", "Invert the Image"),
|
|
1879
|
-
("Ctrl+Shift+P", "Command Search", "Focus the command search bar; Enter runs first match"),
|
|
2047
|
+
("A", "Display Stretch", self.tr("Toggle Display Auto-Stretch")),
|
|
2048
|
+
("Ctrl+I", "Invert", self.tr("Invert the Image")),
|
|
2049
|
+
("Ctrl+Shift+P", "Command Search", self.tr("Focus the command search bar; Enter runs first match")),
|
|
1880
2050
|
|
|
1881
2051
|
# View Icon
|
|
1882
|
-
("Drag view -> Off to Canvas", "View", "Duplicate Image"),
|
|
1883
|
-
("Drag view -> On to Other Image", "View", "Copy Zoom and Pan"),
|
|
1884
|
-
("Shift+Drag -> On to Other Image", "View", "Apply that image to the other as a mask"),
|
|
1885
|
-
("Ctrl+Drag -> On to Other Image", "View", "Copy Astrometric Solution"),
|
|
2052
|
+
("Drag view -> Off to Canvas", "View", self.tr("Duplicate Image")),
|
|
2053
|
+
("Drag view -> On to Other Image", "View", self.tr("Copy Zoom and Pan")),
|
|
2054
|
+
("Shift+Drag -> On to Other Image", "View", self.tr("Apply that image to the other as a mask")),
|
|
2055
|
+
("Ctrl+Drag -> On to Other Image", "View", self.tr("Copy Astrometric Solution")),
|
|
1886
2056
|
|
|
1887
2057
|
# View zoom
|
|
1888
|
-
("Ctrl+1", "View", "Zoom to 100% (1:1)"),
|
|
1889
|
-
("Ctrl+0", "View", "Fit image to current window"),
|
|
1890
|
-
("Ctrl++", "View", "Zoom In"),
|
|
1891
|
-
("Ctrl+-", "View", "Zoom Out"),
|
|
2058
|
+
("Ctrl+1", "View", self.tr("Zoom to 100% (1:1)")),
|
|
2059
|
+
("Ctrl+0", "View", self.tr("Fit image to current window")),
|
|
2060
|
+
("Ctrl++", "View", self.tr("Zoom In")),
|
|
2061
|
+
("Ctrl+-", "View", self.tr("Zoom Out")),
|
|
1892
2062
|
|
|
1893
2063
|
# Window switching
|
|
1894
|
-
("Ctrl+PgDown", "MDI", "Switch to previously active view"),
|
|
1895
|
-
("Ctrl+PgUp", "MDI", "Switch to next active view"),
|
|
2064
|
+
("Ctrl+PgDown", "MDI", self.tr("Switch to previously active view")),
|
|
2065
|
+
("Ctrl+PgUp", "MDI", self.tr("Switch to next active view")),
|
|
1896
2066
|
|
|
1897
2067
|
# Shortcuts canvas + buttons
|
|
1898
|
-
("Alt+Drag (toolbar button)", "Toolbar", "Create a desktop shortcut for that action"),
|
|
1899
|
-
("Alt+Drag (shortcut button -> view)", "Shortcuts", "Headless apply the shortcut's command/preset to a view"),
|
|
1900
|
-
("Ctrl/Shift+Click", "Shortcuts", "Multi-select shortcut buttons"),
|
|
1901
|
-
("Drag (selection)", "Shortcuts", "Move selected shortcut buttons"),
|
|
1902
|
-
("Delete / Backspace", "Shortcuts", "Delete selected shortcut buttons"),
|
|
1903
|
-
("Ctrl+A", "Shortcuts", "Select all shortcut buttons"),
|
|
1904
|
-
("Double-click empty area", "MDI background", "Open files dialog"),
|
|
2068
|
+
("Alt+Drag (toolbar button)", "Toolbar", self.tr("Create a desktop shortcut for that action")),
|
|
2069
|
+
("Alt+Drag (shortcut button -> view)", "Shortcuts", self.tr("Headless apply the shortcut's command/preset to a view")),
|
|
2070
|
+
("Ctrl/Shift+Click", "Shortcuts", self.tr("Multi-select shortcut buttons")),
|
|
2071
|
+
("Drag (selection)", "Shortcuts", self.tr("Move selected shortcut buttons")),
|
|
2072
|
+
("Delete / Backspace", "Shortcuts", self.tr("Delete selected shortcut buttons")),
|
|
2073
|
+
("Ctrl+A", "Shortcuts", self.tr("Select all shortcut buttons")),
|
|
2074
|
+
("Double-click empty area", "MDI background", self.tr("Open files dialog")),
|
|
1905
2075
|
|
|
1906
2076
|
# Layers dock
|
|
1907
|
-
("Drag view -> Layers list", "Layers", "Add dragged view as a new layer (on top)"),
|
|
1908
|
-
("Shift+Drag mask -> Layers list", "Layers", "Attach dragged image as mask to the selected layer"),
|
|
2077
|
+
("Drag view -> Layers list", "Layers", self.tr("Add dragged view as a new layer (on top)")),
|
|
2078
|
+
("Shift+Drag mask -> Layers list", "Layers", self.tr("Attach dragged image as mask to the selected layer")),
|
|
1909
2079
|
|
|
1910
2080
|
# Crop tool
|
|
1911
|
-
("Click-drag", "Crop Tool", "Draw a crop rectangle"),
|
|
1912
|
-
("Drag corner handles", "Crop Tool", "Resize crop rectangle"),
|
|
1913
|
-
("Shift+Drag on box", "Crop Tool", "Rotate crop rectangle"),
|
|
2081
|
+
("Click-drag", "Crop Tool", self.tr("Draw a crop rectangle")),
|
|
2082
|
+
("Drag corner handles", "Crop Tool", self.tr("Resize crop rectangle")),
|
|
2083
|
+
("Shift+Drag on box", "Crop Tool", self.tr("Rotate crop rectangle")),
|
|
1914
2084
|
]
|
|
1915
2085
|
return rows
|
|
1916
2086
|
|
|
@@ -2027,7 +2197,7 @@ class AstroSuiteProMainWindow(
|
|
|
2027
2197
|
if getattr(self, "doc_manager", None) and self.doc_manager._docs:
|
|
2028
2198
|
if not self._confirm_discard(
|
|
2029
2199
|
title=title,
|
|
2030
|
-
msg=(
|
|
2200
|
+
msg=self.tr(
|
|
2031
2201
|
"Loading a project will close current views and replace desktop shortcuts.\n"
|
|
2032
2202
|
"Continue?"
|
|
2033
2203
|
),
|
|
@@ -2490,10 +2660,27 @@ class AstroSuiteProMainWindow(
|
|
|
2490
2660
|
return f"{name}{dims}"
|
|
2491
2661
|
|
|
2492
2662
|
def _update_explorer_item_for_doc(self, doc):
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
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
|
|
2497
2684
|
return
|
|
2498
2685
|
#-----------FUNCTIONS----------------
|
|
2499
2686
|
|
|
@@ -3002,17 +3189,17 @@ class AstroSuiteProMainWindow(
|
|
|
3002
3189
|
|
|
3003
3190
|
self.convo_window.show()
|
|
3004
3191
|
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
3192
|
def _apply_extract_luminance_preset_to_doc(self, doc, preset=None):
|
|
3008
|
-
|
|
3009
3193
|
from PyQt6.QtWidgets import QMessageBox
|
|
3010
|
-
from setiastro.saspro.luminancerecombine import
|
|
3011
|
-
|
|
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
|
|
3012
3200
|
|
|
3013
3201
|
doc = unwrap_docproxy(doc)
|
|
3014
3202
|
p = dict(preset or {})
|
|
3015
|
-
mode = (p.get("mode") or "rec709").lower()
|
|
3016
3203
|
|
|
3017
3204
|
if doc is None or getattr(doc, "image", None) is None:
|
|
3018
3205
|
QMessageBox.information(self, "Extract Luminance", "No target image.")
|
|
@@ -3020,52 +3207,43 @@ class AstroSuiteProMainWindow(
|
|
|
3020
3207
|
|
|
3021
3208
|
img = np.asarray(doc.image)
|
|
3022
3209
|
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
w = _LUMA_REC601
|
|
3026
|
-
elif mode == "rec2020":
|
|
3027
|
-
w = _LUMA_REC2020
|
|
3028
|
-
elif mode == "max":
|
|
3029
|
-
w = None
|
|
3030
|
-
else:
|
|
3031
|
-
w = _LUMA_REC709
|
|
3210
|
+
mode = str(p.get("mode", "rec709")).strip()
|
|
3211
|
+
resolved_method, w, profile_name = resolve_luma_profile_weights(mode)
|
|
3032
3212
|
|
|
3033
|
-
L = compute_luminance(img, method=
|
|
3213
|
+
L = compute_luminance(img, method=resolved_method, weights=w)
|
|
3034
3214
|
|
|
3035
3215
|
dm = getattr(self, "doc_manager", None)
|
|
3036
3216
|
if dm is None:
|
|
3037
|
-
# headless fallback: just overwrite active doc
|
|
3038
3217
|
doc.apply_edit(L.astype(np.float32), step_name="Extract Luminance")
|
|
3039
3218
|
return
|
|
3040
3219
|
|
|
3041
|
-
|
|
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
|
+
|
|
3042
3229
|
try:
|
|
3230
|
+
suffix = f"{profile_name}" if profile_name else resolved_method
|
|
3043
3231
|
new_doc = dm.create_document_from_array(
|
|
3044
3232
|
L.astype(np.float32),
|
|
3045
|
-
name=f"{doc.display_name()} -- Luminance ({
|
|
3233
|
+
name=f"{doc.display_name()} -- Luminance ({suffix})",
|
|
3046
3234
|
is_mono=True,
|
|
3047
|
-
metadata=
|
|
3235
|
+
metadata=meta,
|
|
3048
3236
|
)
|
|
3049
3237
|
dm.add_document(new_doc)
|
|
3050
3238
|
except Exception:
|
|
3051
|
-
# safe fallback
|
|
3052
3239
|
doc.apply_edit(L.astype(np.float32), step_name="Extract Luminance")
|
|
3053
3240
|
|
|
3054
|
-
|
|
3055
3241
|
def _extract_luminance(self, doc=None, preset: dict | None = None):
|
|
3056
|
-
from
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
Preset schema:
|
|
3063
|
-
{
|
|
3064
|
-
"mode": "rec709" | "rec601" | "rec2020" | "max" | "snr" | "equal" | "median",
|
|
3065
|
-
# aliases accepted: method, luma_method, nb_max -> "max", snr_unequal -> "snr"
|
|
3066
|
-
}
|
|
3067
|
-
"""
|
|
3068
|
-
# 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
|
+
|
|
3069
3247
|
sw = None
|
|
3070
3248
|
if doc is None:
|
|
3071
3249
|
sw = self.mdi.activeSubWindow()
|
|
@@ -3084,70 +3262,19 @@ class AstroSuiteProMainWindow(
|
|
|
3084
3262
|
QMessageBox.information(self, "Extract Luminance", "Luminance extraction requires an RGB image.")
|
|
3085
3263
|
return
|
|
3086
3264
|
|
|
3087
|
-
# 2) normalize to [0,1] float32
|
|
3088
|
-
a = img.astype(np.float32, copy=False)
|
|
3089
|
-
if a.size:
|
|
3090
|
-
m = float(np.nanmax(a))
|
|
3091
|
-
if np.isfinite(m) and m > 1.0:
|
|
3092
|
-
a = a / m
|
|
3093
|
-
a = np.clip(a, 0.0, 1.0)
|
|
3094
|
-
|
|
3095
|
-
# 3) choose luminance method
|
|
3096
3265
|
p = dict(preset or {})
|
|
3097
|
-
|
|
3266
|
+
mode = str(
|
|
3098
3267
|
p.get("mode",
|
|
3099
3268
|
p.get("method",
|
|
3100
3269
|
p.get("luma_method",
|
|
3101
3270
|
getattr(self, "luma_method", "rec709"))))
|
|
3102
|
-
).strip()
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
"k": "rec709",
|
|
3110
|
-
"rec.601": "rec601",
|
|
3111
|
-
"rec-601": "rec601",
|
|
3112
|
-
"rec.2020": "rec2020",
|
|
3113
|
-
"rec-2020": "rec2020",
|
|
3114
|
-
"nb_max": "max",
|
|
3115
|
-
"narrowband": "max",
|
|
3116
|
-
"snr_unequal": "snr",
|
|
3117
|
-
"unequal_noise": "snr",
|
|
3118
|
-
}
|
|
3119
|
-
method = alias.get(method, method)
|
|
3120
|
-
|
|
3121
|
-
# 4) compute luminance per selected method
|
|
3122
|
-
luma_weights = None
|
|
3123
|
-
if method == "rec601":
|
|
3124
|
-
luma_weights = _LUMA_REC601
|
|
3125
|
-
y = np.tensordot(a, _LUMA_REC601, axes=([2],[0]))
|
|
3126
|
-
elif method == "rec2020":
|
|
3127
|
-
luma_weights = _LUMA_REC2020
|
|
3128
|
-
y = np.tensordot(a, _LUMA_REC2020, axes=([2],[0]))
|
|
3129
|
-
elif method == "max":
|
|
3130
|
-
y = a.max(axis=2)
|
|
3131
|
-
elif method == "median":
|
|
3132
|
-
y = np.median(a, axis=2)
|
|
3133
|
-
elif method == "equal":
|
|
3134
|
-
luma_weights = np.array([1/3, 1/3, 1/3], dtype=np.float32)
|
|
3135
|
-
y = a.mean(axis=2)
|
|
3136
|
-
elif method == "snr":
|
|
3137
|
-
from setiastro.saspro.luminancerecombine import _estimate_noise_sigma_per_channel
|
|
3138
|
-
sigma = _estimate_noise_sigma_per_channel(a)
|
|
3139
|
-
w = 1.0 / (sigma[:3]**2 + 1e-12)
|
|
3140
|
-
w = w / w.sum()
|
|
3141
|
-
luma_weights = w.astype(np.float32)
|
|
3142
|
-
y = np.tensordot(a[..., :3], luma_weights, axes=([2],[0]))
|
|
3143
|
-
else: # "rec709" default
|
|
3144
|
-
method = "rec709"
|
|
3145
|
-
luma_weights = _LUMA_REC709
|
|
3146
|
-
y = np.tensordot(a, _LUMA_REC709, axes=([2],[0]))
|
|
3147
|
-
|
|
3148
|
-
y = np.clip(y.astype(np.float32, copy=False), 0.0, 1.0)
|
|
3149
|
-
|
|
3150
|
-
# 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 ----
|
|
3151
3278
|
base_meta = {}
|
|
3152
3279
|
try:
|
|
3153
3280
|
base_meta = dict(getattr(doc, "metadata", {}) or {})
|
|
@@ -3159,13 +3286,16 @@ class AstroSuiteProMainWindow(
|
|
|
3159
3286
|
"source": "ExtractLuminance",
|
|
3160
3287
|
"is_mono": True,
|
|
3161
3288
|
"bit_depth": "32f",
|
|
3162
|
-
"luma_method":
|
|
3289
|
+
"luma_method": resolved_method,
|
|
3163
3290
|
}
|
|
3164
|
-
if
|
|
3165
|
-
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)
|
|
3166
3295
|
|
|
3167
3296
|
base_title = sw.windowTitle() if sw else (getattr(doc, "title", getattr(doc, "name", "")) or "Untitled")
|
|
3168
|
-
|
|
3297
|
+
suffix = f"{profile_name}" if profile_name else resolved_method
|
|
3298
|
+
title = f"{base_title} -- Luminance ({suffix})"
|
|
3169
3299
|
|
|
3170
3300
|
dm = getattr(self, "docman", None)
|
|
3171
3301
|
if dm is None:
|
|
@@ -3193,19 +3323,19 @@ class AstroSuiteProMainWindow(
|
|
|
3193
3323
|
except Exception:
|
|
3194
3324
|
pass
|
|
3195
3325
|
|
|
3196
|
-
# ðŸ" Remember for Replay (optional but consistent)
|
|
3197
3326
|
try:
|
|
3198
3327
|
remember = getattr(self, "remember_last_headless_command", None) or getattr(self, "_remember_last_headless_command", None)
|
|
3199
3328
|
if callable(remember):
|
|
3200
|
-
remember("extract_luminance", {"mode":
|
|
3329
|
+
remember("extract_luminance", {"mode": mode}, description="Extract Luminance")
|
|
3201
3330
|
except Exception:
|
|
3202
3331
|
pass
|
|
3203
3332
|
|
|
3204
3333
|
if hasattr(self, "_log"):
|
|
3205
|
-
self._log(f"Extract Luminance ({
|
|
3334
|
+
self._log(f"Extract Luminance ({suffix}) -> new mono document created.")
|
|
3206
3335
|
|
|
3207
3336
|
return new_doc
|
|
3208
3337
|
|
|
3338
|
+
|
|
3209
3339
|
def _subwindow_docs(self):
|
|
3210
3340
|
docs = []
|
|
3211
3341
|
for sw in self.mdi.subWindowList():
|
|
@@ -4456,6 +4586,48 @@ class AstroSuiteProMainWindow(
|
|
|
4456
4586
|
dlg.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
4457
4587
|
dlg.show()
|
|
4458
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
|
+
|
|
4459
4631
|
def _open_linear_fit(self):
|
|
4460
4632
|
from setiastro.saspro.linear_fit import LinearFitDialog
|
|
4461
4633
|
dm = getattr(self, "doc_manager", None) or getattr(self, "docman", None)
|
|
@@ -4572,12 +4744,6 @@ class AstroSuiteProMainWindow(
|
|
|
4572
4744
|
if max_len and len(hist) > max_len:
|
|
4573
4745
|
del hist[:-max_len]
|
|
4574
4746
|
|
|
4575
|
-
# Logging as before
|
|
4576
|
-
try:
|
|
4577
|
-
self._log(f"[Replay] Last action stored: {desc} (command_id={command_id})")
|
|
4578
|
-
except Exception:
|
|
4579
|
-
print(f"[Replay] Last action stored: {desc} (command_id={command_id})")
|
|
4580
|
-
|
|
4581
4747
|
|
|
4582
4748
|
|
|
4583
4749
|
def _remember_last_headless_command(self, command_id: str, preset: dict | None = None, description: str = ""):
|
|
@@ -4633,17 +4799,6 @@ class AstroSuiteProMainWindow(
|
|
|
4633
4799
|
"""
|
|
4634
4800
|
payload = getattr(self, "_last_headless_command", None)
|
|
4635
4801
|
|
|
4636
|
-
# DEBUG
|
|
4637
|
-
try:
|
|
4638
|
-
self._log(
|
|
4639
|
-
f"[Replay] replay_last_action_on_subwindow: payload={bool(payload)}, "
|
|
4640
|
-
f"target_sw={id(target_sw) if target_sw else None}"
|
|
4641
|
-
)
|
|
4642
|
-
except Exception:
|
|
4643
|
-
print(
|
|
4644
|
-
f"[Replay] replay_last_action_on_subwindow: payload={bool(payload)}, "
|
|
4645
|
-
f"target_sw={id(target_sw) if target_sw else None}"
|
|
4646
|
-
)
|
|
4647
4802
|
|
|
4648
4803
|
if not payload:
|
|
4649
4804
|
QMessageBox.information(
|
|
@@ -4675,17 +4830,6 @@ class AstroSuiteProMainWindow(
|
|
|
4675
4830
|
"""
|
|
4676
4831
|
payload = getattr(self, "_last_headless_command", None) or {}
|
|
4677
4832
|
|
|
4678
|
-
# DEBUG
|
|
4679
|
-
try:
|
|
4680
|
-
self._log(
|
|
4681
|
-
f"[Replay] replay_last_action_on_base: payload={bool(payload)}, "
|
|
4682
|
-
f"target_sw={id(target_sw) if target_sw else None}"
|
|
4683
|
-
)
|
|
4684
|
-
except Exception:
|
|
4685
|
-
print(
|
|
4686
|
-
f"[Replay] replay_last_action_on_base: payload={bool(payload)}, "
|
|
4687
|
-
f"target_sw={id(target_sw) if target_sw else None}"
|
|
4688
|
-
)
|
|
4689
4833
|
|
|
4690
4834
|
if not payload:
|
|
4691
4835
|
QMessageBox.information(
|
|
@@ -4711,17 +4855,6 @@ class AstroSuiteProMainWindow(
|
|
|
4711
4855
|
QMessageBox.information(self, "Replay Last Action", "No base image to apply the action to.")
|
|
4712
4856
|
return
|
|
4713
4857
|
|
|
4714
|
-
# Small debug about which doc we're hitting
|
|
4715
|
-
try:
|
|
4716
|
-
view = target_sw.widget()
|
|
4717
|
-
cur_doc = getattr(view, "document", None)
|
|
4718
|
-
self._log(
|
|
4719
|
-
f"[Replay] base_doc id={id(base_doc)}, "
|
|
4720
|
-
f"view.document id={id(cur_doc)}, "
|
|
4721
|
-
f"same={base_doc is cur_doc}"
|
|
4722
|
-
)
|
|
4723
|
-
except Exception:
|
|
4724
|
-
pass
|
|
4725
4858
|
|
|
4726
4859
|
# ---- Extract cid + preset from payload (support both old + new schemas) ----
|
|
4727
4860
|
cid_raw = payload.get("command_id")
|
|
@@ -5542,6 +5675,10 @@ class AstroSuiteProMainWindow(
|
|
|
5542
5675
|
"rotate_180": "geom_rotate_180",
|
|
5543
5676
|
"geom_rotate_180": "geom_rotate_180",
|
|
5544
5677
|
|
|
5678
|
+
"rotate_any": "geom_rotate_any",
|
|
5679
|
+
"rotate_arbitrary": "geom_rotate_any",
|
|
5680
|
+
"geom_rotate_any": "geom_rotate_any",
|
|
5681
|
+
|
|
5545
5682
|
"invert": "geom_invert",
|
|
5546
5683
|
"geom_invert": "geom_invert",
|
|
5547
5684
|
|
|
@@ -6542,6 +6679,17 @@ class AstroSuiteProMainWindow(
|
|
|
6542
6679
|
QMessageBox.warning(self, "Rotate 180Â deg", str(e))
|
|
6543
6680
|
return
|
|
6544
6681
|
|
|
6682
|
+
if cid == "geom_rotate_any":
|
|
6683
|
+
try:
|
|
6684
|
+
angle = float(preset.get("angle_deg", preset.get("angle", 0.0)))
|
|
6685
|
+
called = _call_any(["_apply_geom_rot_any_to_doc"], doc, angle_deg=angle)
|
|
6686
|
+
if not called:
|
|
6687
|
+
raise RuntimeError("No rotate-any apply method found")
|
|
6688
|
+
self._log(f"Rotate ({angle:g}°) applied to '{target_sw.windowTitle()}'")
|
|
6689
|
+
except Exception as e:
|
|
6690
|
+
QMessageBox.warning(self, "Rotate...", str(e))
|
|
6691
|
+
return
|
|
6692
|
+
|
|
6545
6693
|
if cid == "geom_rescale":
|
|
6546
6694
|
try:
|
|
6547
6695
|
factor = float(preset.get("factor", 1.0))
|
|
@@ -7347,7 +7495,7 @@ class AstroSuiteProMainWindow(
|
|
|
7347
7495
|
self._search_dock = None
|
|
7348
7496
|
|
|
7349
7497
|
# --- Right-side mini dock with the search box ---
|
|
7350
|
-
self._search_dock = QDockWidget("Command Search", self)
|
|
7498
|
+
self._search_dock = QDockWidget(self.tr("Command Search"), self)
|
|
7351
7499
|
self._search_dock.setObjectName("CommandSearchDock")
|
|
7352
7500
|
# âœ... Allow moving/closing like other panels
|
|
7353
7501
|
self._search_dock.setAllowedAreas(
|
|
@@ -7562,7 +7710,7 @@ class AstroSuiteProMainWindow(
|
|
|
7562
7710
|
except Exception:
|
|
7563
7711
|
pass
|
|
7564
7712
|
|
|
7565
|
-
try: self.
|
|
7713
|
+
try: self._schedule_undo_redo_label_refresh()
|
|
7566
7714
|
except Exception as e:
|
|
7567
7715
|
import logging
|
|
7568
7716
|
logging.debug(f"Exception suppressed: {type(e).__name__}: {e}")
|
|
@@ -7617,12 +7765,33 @@ class AstroSuiteProMainWindow(
|
|
|
7617
7765
|
pass
|
|
7618
7766
|
|
|
7619
7767
|
def _pretty_title(self, doc, *, linked: bool | None = None) -> str:
|
|
7620
|
-
|
|
7621
|
-
|
|
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
|
|
7622
7790
|
if linked is None:
|
|
7623
|
-
linked = hasattr(doc, "_parent_doc")
|
|
7791
|
+
linked = hasattr(doc, "_parent_doc")
|
|
7624
7792
|
return f"[LINK] {name}" if linked else name
|
|
7625
7793
|
|
|
7794
|
+
|
|
7626
7795
|
def _build_subwindow_title_for_doc(self, doc) -> str:
|
|
7627
7796
|
"""
|
|
7628
7797
|
Build a unique, human-friendly title for a QMdiSubWindow
|
|
@@ -7690,6 +7859,34 @@ class AstroSuiteProMainWindow(
|
|
|
7690
7859
|
return cand
|
|
7691
7860
|
n += 1
|
|
7692
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
|
|
7693
7890
|
|
|
7694
7891
|
def _spawn_subwindow_for(self, doc, *, force_new: bool = False):
|
|
7695
7892
|
"""
|
|
@@ -7793,10 +7990,7 @@ class AstroSuiteProMainWindow(
|
|
|
7793
7990
|
if replay_sig is not None:
|
|
7794
7991
|
try:
|
|
7795
7992
|
replay_sig.connect(self._on_view_replay_last_requested)
|
|
7796
|
-
|
|
7797
|
-
self._log(f"[Replay] Connected {sig_name_used} for view id={id(view)}")
|
|
7798
|
-
except Exception:
|
|
7799
|
-
print(f"[Replay] Connected {sig_name_used} for view id={id(view)}")
|
|
7993
|
+
|
|
7800
7994
|
except Exception as e:
|
|
7801
7995
|
try:
|
|
7802
7996
|
self._log(f"[Replay] FAILED to connect {sig_name_used} for view id={id(view)}: {e}")
|
|
@@ -7804,7 +7998,8 @@ class AstroSuiteProMainWindow(
|
|
|
7804
7998
|
print(f"[Replay] FAILED to connect {sig_name_used} for view id={id(view)}: {e}")
|
|
7805
7999
|
|
|
7806
8000
|
self._hook_preview_awareness(view)
|
|
7807
|
-
|
|
8001
|
+
|
|
8002
|
+
base_title = self._doc_window_title(doc) # ✅ use metadata display_name
|
|
7808
8003
|
final_title = self._unique_window_title(base_title)
|
|
7809
8004
|
|
|
7810
8005
|
# -- 6) Add subwindow and set chrome
|
|
@@ -7828,7 +8023,8 @@ class AstroSuiteProMainWindow(
|
|
|
7828
8023
|
# We target ~60% of the viewport height, clamped to sane bounds.
|
|
7829
8024
|
# -------------------------------------------------------------------------
|
|
7830
8025
|
vp = self.mdi.viewport()
|
|
7831
|
-
|
|
8026
|
+
# Use viewport geometry in MDI coordinates (NOT viewport-local rect)
|
|
8027
|
+
area = vp.geometry() if vp else self.mdi.contentsRect()
|
|
7832
8028
|
|
|
7833
8029
|
# Determine aspect ratio
|
|
7834
8030
|
img_w = img_h = None
|
|
@@ -7871,7 +8067,7 @@ class AstroSuiteProMainWindow(
|
|
|
7871
8067
|
# Smart Cascade: Position relative to the *currently active* window
|
|
7872
8068
|
# (before we make the new one active).
|
|
7873
8069
|
# -------------------------------------------------------------------------
|
|
7874
|
-
new_x, new_y =
|
|
8070
|
+
new_x, new_y = area.left(), area.top()
|
|
7875
8071
|
|
|
7876
8072
|
# Get dominant/active window *before* we activate the new one
|
|
7877
8073
|
active = self.mdi.activeSubWindow()
|
|
@@ -7894,15 +8090,13 @@ class AstroSuiteProMainWindow(
|
|
|
7894
8090
|
except Exception:
|
|
7895
8091
|
pass
|
|
7896
8092
|
|
|
7897
|
-
# Bounds check:
|
|
7898
|
-
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
|
|
7902
|
-
|
|
7903
|
-
|
|
7904
|
-
new_x = max(0, new_x)
|
|
7905
|
-
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)
|
|
7906
8100
|
|
|
7907
8101
|
sw.move(new_x, new_y)
|
|
7908
8102
|
|
|
@@ -7997,6 +8191,11 @@ class AstroSuiteProMainWindow(
|
|
|
7997
8191
|
except Exception:
|
|
7998
8192
|
pass
|
|
7999
8193
|
|
|
8194
|
+
try:
|
|
8195
|
+
self._fix_mdi_titlebar_emboss("#dcdcdc" if self._theme_mode()=="dark" else "#f0f0f0")
|
|
8196
|
+
except Exception:
|
|
8197
|
+
pass
|
|
8198
|
+
|
|
8000
8199
|
# -- 11) If this is the first window and it's an image, mimic "Cascade Views"
|
|
8001
8200
|
try:
|
|
8002
8201
|
if first_window and not is_table:
|
|
@@ -8050,7 +8249,7 @@ class AstroSuiteProMainWindow(
|
|
|
8050
8249
|
# If no subwindows remain, clear all "active doc" UI bits, including header
|
|
8051
8250
|
if not self.mdi.subWindowList():
|
|
8052
8251
|
self.currentDocumentChanged.emit(None) # drives HeaderViewerDock.set_document(None)
|
|
8053
|
-
self.
|
|
8252
|
+
self._schedule_undo_redo_label_refresh()
|
|
8054
8253
|
self._hdr_refresh_timer.start(0) # belt-and-suspenders for manual widgets
|
|
8055
8254
|
# If your dock has its own set_document, call it explicitly too
|
|
8056
8255
|
hv = getattr(self, "header_viewer", None)
|
|
@@ -8091,25 +8290,57 @@ class AstroSuiteProMainWindow(
|
|
|
8091
8290
|
"autostretch_target": float(getattr(source_view, "autostretch_target", 0.25)),
|
|
8092
8291
|
}
|
|
8093
8292
|
|
|
8094
|
-
# 2) New name (
|
|
8095
|
-
base_name = ""
|
|
8293
|
+
# 2) New name (normalized: NO decorators like 🔗■●◆▲▪▫•◼◻◾◽)
|
|
8096
8294
|
try:
|
|
8097
|
-
base_name =
|
|
8295
|
+
base_name = self._doc_window_title(base_doc) # might include decorations
|
|
8098
8296
|
except Exception:
|
|
8099
8297
|
base_name = "Untitled"
|
|
8100
8298
|
|
|
8299
|
+
# Normalize it so uniqueness checks don't miss decorated titles
|
|
8101
8300
|
try:
|
|
8102
|
-
base_name =
|
|
8301
|
+
base_name = normalize_doc_title(base_name)
|
|
8302
|
+
except Exception:
|
|
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
|
|
8103
8328
|
except Exception:
|
|
8104
|
-
|
|
8105
|
-
|
|
8106
|
-
|
|
8107
|
-
|
|
8108
|
-
|
|
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
|
|
8109
8341
|
|
|
8110
8342
|
# 3) Duplicate the *base* document (not the ROI proxy)
|
|
8111
|
-
|
|
8112
|
-
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)
|
|
8113
8344
|
print(f" Duplicated document ID {id(base_doc)} -> {id(new_doc)}")
|
|
8114
8345
|
|
|
8115
8346
|
# 4) Ensure the duplicate starts mask-free (so we don't inherit mask UI state)
|
|
@@ -8259,26 +8490,21 @@ class AstroSuiteProMainWindow(
|
|
|
8259
8490
|
|
|
8260
8491
|
|
|
8261
8492
|
def _activate_or_open_from_explorer(self, item):
|
|
8262
|
-
doc = item.data(Qt.ItemDataRole.UserRole)
|
|
8263
|
-
|
|
8264
|
-
|
|
8265
|
-
#
|
|
8266
|
-
|
|
8267
|
-
|
|
8268
|
-
|
|
8269
|
-
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
|
|
8274
|
-
|
|
8275
|
-
|
|
8276
|
-
|
|
8277
|
-
pass
|
|
8278
|
-
return
|
|
8279
|
-
|
|
8280
|
-
# 2) None exists -> open one
|
|
8281
|
-
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
|
|
8282
8508
|
|
|
8283
8509
|
def _set_linked_stretch_from_action(self, checked: bool):
|
|
8284
8510
|
# persist as the default for *new* views
|
|
@@ -8392,16 +8618,20 @@ class AstroSuiteProMainWindow(
|
|
|
8392
8618
|
|
|
8393
8619
|
# Misc UI refreshes (guarded)
|
|
8394
8620
|
try:
|
|
8395
|
-
self.
|
|
8621
|
+
self._schedule_undo_redo_label_refresh()
|
|
8396
8622
|
except Exception:
|
|
8397
8623
|
pass
|
|
8624
|
+
#try:
|
|
8625
|
+
# if hasattr(self, "_hdr_refresh_timer") and self._hdr_refresh_timer is not None:
|
|
8626
|
+
# self._hdr_refresh_timer.start(0)
|
|
8627
|
+
#except Exception:
|
|
8628
|
+
# pass
|
|
8398
8629
|
try:
|
|
8399
|
-
|
|
8400
|
-
self._hdr_refresh_timer.start(0)
|
|
8630
|
+
self._refresh_mask_action_states()
|
|
8401
8631
|
except Exception:
|
|
8402
8632
|
pass
|
|
8403
8633
|
try:
|
|
8404
|
-
self.
|
|
8634
|
+
self._fix_mdi_titlebar_emboss("#dcdcdc" if self._theme_mode()=="dark" else "#f0f0f0")
|
|
8405
8635
|
except Exception:
|
|
8406
8636
|
pass
|
|
8407
8637
|
|