setiastrosuitepro 1.6.12__py3-none-any.whl → 1.7.3__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/3dplanet.png +0 -0
- setiastro/images/TextureClarity.svg +56 -0
- setiastro/images/narrowbandnormalization.png +0 -0
- setiastro/images/planetarystacker.png +0 -0
- setiastro/saspro/__init__.py +9 -8
- setiastro/saspro/__main__.py +326 -285
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/aberration_ai.py +128 -13
- setiastro/saspro/aberration_ai_preset.py +29 -3
- setiastro/saspro/astrospike_python.py +45 -3
- setiastro/saspro/blink_comparator_pro.py +116 -71
- setiastro/saspro/curve_editor_pro.py +72 -22
- setiastro/saspro/curves_preset.py +249 -47
- setiastro/saspro/doc_manager.py +4 -1
- setiastro/saspro/gui/main_window.py +326 -46
- setiastro/saspro/gui/mixins/file_mixin.py +41 -18
- setiastro/saspro/gui/mixins/menu_mixin.py +9 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +123 -7
- setiastro/saspro/histogram.py +179 -7
- setiastro/saspro/imageops/narrowband_normalization.py +816 -0
- setiastro/saspro/imageops/serloader.py +1429 -0
- setiastro/saspro/layers.py +186 -10
- setiastro/saspro/layers_dock.py +198 -5
- setiastro/saspro/legacy/image_manager.py +10 -4
- setiastro/saspro/legacy/numba_utils.py +1 -1
- setiastro/saspro/live_stacking.py +24 -4
- setiastro/saspro/multiscale_decomp.py +30 -17
- setiastro/saspro/narrowband_normalization.py +1618 -0
- setiastro/saspro/planetprojection.py +3854 -0
- setiastro/saspro/remove_green.py +1 -1
- setiastro/saspro/resources.py +8 -0
- setiastro/saspro/rgbalign.py +456 -12
- setiastro/saspro/save_options.py +45 -13
- setiastro/saspro/ser_stack_config.py +102 -0
- setiastro/saspro/ser_stacker.py +2327 -0
- setiastro/saspro/ser_stacker_dialog.py +1865 -0
- setiastro/saspro/ser_tracking.py +228 -0
- setiastro/saspro/serviewer.py +1773 -0
- setiastro/saspro/sfcc.py +298 -64
- setiastro/saspro/shortcuts.py +14 -7
- setiastro/saspro/stacking_suite.py +21 -6
- setiastro/saspro/stat_stretch.py +179 -31
- setiastro/saspro/subwindow.py +38 -5
- setiastro/saspro/texture_clarity.py +593 -0
- setiastro/saspro/widgets/resource_monitor.py +122 -74
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.3.dist-info}/METADATA +3 -2
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.3.dist-info}/RECORD +51 -37
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.3.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.3.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.3.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.3.dist-info}/licenses/license.txt +0 -0
|
@@ -129,7 +129,7 @@ from PyQt6.QtGui import (QPixmap, QColor, QIcon, QKeySequence, QShortcut,
|
|
|
129
129
|
|
|
130
130
|
# ----- QtCore -----
|
|
131
131
|
from PyQt6.QtCore import (Qt, pyqtSignal, QCoreApplication, QTimer, QSize, QSignalBlocker, QModelIndex, QThread, QUrl, QSettings, QEvent, QByteArray, QObject,
|
|
132
|
-
QPropertyAnimation, QEasingCurve, QElapsedTimer
|
|
132
|
+
QPropertyAnimation, QEasingCurve, QElapsedTimer, QPoint
|
|
133
133
|
)
|
|
134
134
|
|
|
135
135
|
from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
|
|
@@ -193,10 +193,10 @@ from setiastro.saspro.resources import (
|
|
|
193
193
|
nbtorgb_path, freqsep_path, contsub_path, halo_path, cosmic_path,
|
|
194
194
|
satellite_path, imagecombine_path, wrench_path, eye_icon_path,multiscale_decomp_path,
|
|
195
195
|
disk_icon_path, nuke_path, hubble_path, collage_path, annotated_path,
|
|
196
|
-
colorwheel_path, font_path, csv_icon_path, spinner_path, wims_path,
|
|
196
|
+
colorwheel_path, font_path, csv_icon_path, spinner_path, wims_path, narrowbandnormalization_path,
|
|
197
197
|
wimi_path, linearfit_path, debayer_path, aberration_path, acv_icon_path,
|
|
198
|
-
functionbundles_path, viewbundles_path, selectivecolor_path, rgbalign_path,
|
|
199
|
-
background_path, script_icon_path
|
|
198
|
+
functionbundles_path, viewbundles_path, selectivecolor_path, rgbalign_path, planetarystacker_path,
|
|
199
|
+
background_path, script_icon_path, planetprojection_path,
|
|
200
200
|
)
|
|
201
201
|
|
|
202
202
|
import faulthandler
|
|
@@ -435,7 +435,23 @@ class AstroSuiteProMainWindow(
|
|
|
435
435
|
version: str = "dev", build_timestamp: str = "dev"):
|
|
436
436
|
super().__init__(parent)
|
|
437
437
|
# Prevent white flash: start strictly transparent and force dark bg
|
|
438
|
-
|
|
438
|
+
from PyQt6.QtGui import QGuiApplication
|
|
439
|
+
|
|
440
|
+
def _is_wayland() -> bool:
|
|
441
|
+
try:
|
|
442
|
+
plat = (QGuiApplication.platformName() or "").lower()
|
|
443
|
+
if "wayland" in plat:
|
|
444
|
+
return True
|
|
445
|
+
except Exception:
|
|
446
|
+
pass
|
|
447
|
+
# fallback env checks
|
|
448
|
+
return bool(os.environ.get("WAYLAND_DISPLAY")) and not bool(os.environ.get("DISPLAY"))
|
|
449
|
+
|
|
450
|
+
# Prevent white flash: start strictly transparent and force dark bg
|
|
451
|
+
if not _is_wayland():
|
|
452
|
+
self.setWindowOpacity(0.0)
|
|
453
|
+
self.setStyleSheet("QMainWindow { background-color: #0F0F19; }")
|
|
454
|
+
|
|
439
455
|
self.setStyleSheet("QMainWindow { background-color: #0F0F19; }")
|
|
440
456
|
#self._stall = UiStallDetector(self, interval_ms=50, threshold_ms=250)
|
|
441
457
|
#self._stall.start()
|
|
@@ -604,7 +620,10 @@ class AstroSuiteProMainWindow(
|
|
|
604
620
|
self.docman.documentAdded.connect(self._on_document_added)
|
|
605
621
|
self.mdi.viewStateDropped.connect(self._on_mdi_viewstate_drop)
|
|
606
622
|
self.mdi.linkViewDropped.connect(self._on_linkview_drop)
|
|
607
|
-
|
|
623
|
+
self._mdi_open_batch = 0
|
|
624
|
+
self._mdi_place_mode = "cascade" # or "tile"
|
|
625
|
+
self._mdi_next_pos = None # QPoint in MDI coords
|
|
626
|
+
self._mdi_cascade_step = 28
|
|
608
627
|
self.doc_manager.set_mdi_area(self.mdi)
|
|
609
628
|
# Coalesce undo/redo label refreshes
|
|
610
629
|
self._undo_redo_refresh_pending = False
|
|
@@ -3905,6 +3924,19 @@ class AstroSuiteProMainWindow(
|
|
|
3905
3924
|
|
|
3906
3925
|
dlg.show()
|
|
3907
3926
|
|
|
3927
|
+
def _open_narrowband_normalization_tool(self):
|
|
3928
|
+
# Correct module import
|
|
3929
|
+
from setiastro.saspro.narrowband_normalization import NarrowbandNormalization
|
|
3930
|
+
|
|
3931
|
+
w = NarrowbandNormalization(doc_manager=self.docman, parent=self)
|
|
3932
|
+
w.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
3933
|
+
w.setWindowTitle("Narrowband Normalization")
|
|
3934
|
+
try:
|
|
3935
|
+
w.setWindowIcon(QIcon(narrowbandnormalization_path))
|
|
3936
|
+
except Exception:
|
|
3937
|
+
pass
|
|
3938
|
+
w.show()
|
|
3939
|
+
|
|
3908
3940
|
def _open_ppp_tool(self):
|
|
3909
3941
|
from setiastro.saspro.perfect_palette_picker import PerfectPalettePicker
|
|
3910
3942
|
w = PerfectPalettePicker(doc_manager=self.docman) # parent gives access to _spawn_subwindow_for
|
|
@@ -4256,6 +4288,37 @@ class AstroSuiteProMainWindow(
|
|
|
4256
4288
|
dlg.setWindowIcon(QIcon(livestacking_path))
|
|
4257
4289
|
dlg.show()
|
|
4258
4290
|
|
|
4291
|
+
def _open_planetary_stacker(self):
|
|
4292
|
+
# import locally to avoid startup cost / circular imports
|
|
4293
|
+
from setiastro.saspro.serviewer import SERViewer
|
|
4294
|
+
dlg = SERViewer(self)
|
|
4295
|
+
dlg.setWindowFlag(Qt.WindowType.Window, True)
|
|
4296
|
+
dlg.setWindowIcon(QIcon(planetarystacker_path))
|
|
4297
|
+
dlg.show()
|
|
4298
|
+
|
|
4299
|
+
def _open_planet_projection(self):
|
|
4300
|
+
from setiastro.saspro.planetprojection import PlanetProjectionDialog
|
|
4301
|
+
sw = self.mdi.activeSubWindow()
|
|
4302
|
+
if not sw:
|
|
4303
|
+
QMessageBox.information(self, "No image", "Open an image first.")
|
|
4304
|
+
return
|
|
4305
|
+
|
|
4306
|
+
view = sw.widget()
|
|
4307
|
+
doc = self.doc_manager.get_document_for_view(view)
|
|
4308
|
+
|
|
4309
|
+
dlg = PlanetProjectionDialog(self, doc)
|
|
4310
|
+
try:
|
|
4311
|
+
# dlg.setWindowIcon(QIcon(planetprojection_path))
|
|
4312
|
+
pass
|
|
4313
|
+
except Exception:
|
|
4314
|
+
pass
|
|
4315
|
+
dlg.resize(980, 720)
|
|
4316
|
+
dlg.setWindowFlag(Qt.WindowType.Window, True)
|
|
4317
|
+
dlg.setWindowIcon(QIcon(planetprojection_path))
|
|
4318
|
+
dlg.show()
|
|
4319
|
+
self._log("Functions: opened Planet Projection.")
|
|
4320
|
+
|
|
4321
|
+
|
|
4259
4322
|
def _open_stacking_suite(self):
|
|
4260
4323
|
# Reuse if we already have one
|
|
4261
4324
|
dlg = getattr(self, "_stacking_suite", None)
|
|
@@ -4299,6 +4362,202 @@ class AstroSuiteProMainWindow(
|
|
|
4299
4362
|
except Exception:
|
|
4300
4363
|
pass
|
|
4301
4364
|
|
|
4365
|
+
def _convert_mono_to_rgb_active(self):
|
|
4366
|
+
"""
|
|
4367
|
+
Convert active mono document to RGB by duplicating the channel.
|
|
4368
|
+
Updates the active document in-place (undoable).
|
|
4369
|
+
"""
|
|
4370
|
+
dm = getattr(self, "docman", None)
|
|
4371
|
+
if dm is None:
|
|
4372
|
+
return
|
|
4373
|
+
|
|
4374
|
+
try:
|
|
4375
|
+
doc = dm.get_active_document()
|
|
4376
|
+
except Exception:
|
|
4377
|
+
doc = None
|
|
4378
|
+
if doc is None:
|
|
4379
|
+
return
|
|
4380
|
+
|
|
4381
|
+
img = getattr(doc, "image", None)
|
|
4382
|
+
if img is None:
|
|
4383
|
+
return
|
|
4384
|
+
|
|
4385
|
+
import numpy as np
|
|
4386
|
+
|
|
4387
|
+
x = np.asarray(img)
|
|
4388
|
+
|
|
4389
|
+
# Already RGB?
|
|
4390
|
+
if x.ndim == 3 and x.shape[-1] == 3:
|
|
4391
|
+
try:
|
|
4392
|
+
name = getattr(doc, "display_name", lambda: None)() or getattr(doc, "name", "") or "Active"
|
|
4393
|
+
except Exception:
|
|
4394
|
+
name = "Active"
|
|
4395
|
+
if hasattr(self, "_log"):
|
|
4396
|
+
self._log(f"Mono → RGB: '{name}' is already RGB (shape={getattr(x,'shape',None)}).")
|
|
4397
|
+
return
|
|
4398
|
+
|
|
4399
|
+
# Determine what we're converting FROM
|
|
4400
|
+
src_desc = "unknown"
|
|
4401
|
+
if x.ndim == 2:
|
|
4402
|
+
mono = x
|
|
4403
|
+
src_desc = "mono (H×W)"
|
|
4404
|
+
elif x.ndim == 3 and x.shape[-1] == 1:
|
|
4405
|
+
mono = x[..., 0]
|
|
4406
|
+
src_desc = "mono (H×W×1)"
|
|
4407
|
+
else:
|
|
4408
|
+
# Unknown format (e.g., multi-channel >3)
|
|
4409
|
+
try:
|
|
4410
|
+
name = getattr(doc, "display_name", lambda: None)() or getattr(doc, "name", "") or "Active"
|
|
4411
|
+
except Exception:
|
|
4412
|
+
name = "Active"
|
|
4413
|
+
if hasattr(self, "_log"):
|
|
4414
|
+
self._log(f"Mono → RGB: '{name}' not convertible (shape={getattr(x,'shape',None)}).")
|
|
4415
|
+
return
|
|
4416
|
+
|
|
4417
|
+
before_shape = getattr(x, "shape", None)
|
|
4418
|
+
before_dtype = getattr(x, "dtype", None)
|
|
4419
|
+
|
|
4420
|
+
mono = mono.astype(np.float32, copy=False)
|
|
4421
|
+
rgb = np.stack([mono, mono, mono], axis=-1)
|
|
4422
|
+
|
|
4423
|
+
# metadata: preserve existing, but force "not mono"
|
|
4424
|
+
try:
|
|
4425
|
+
md = dict(getattr(doc, "metadata", None) or {})
|
|
4426
|
+
except Exception:
|
|
4427
|
+
md = {}
|
|
4428
|
+
|
|
4429
|
+
md["is_mono"] = False
|
|
4430
|
+
md["color_model"] = "RGB"
|
|
4431
|
+
md["channels"] = 3
|
|
4432
|
+
md["source"] = (md.get("source") or "Edit")
|
|
4433
|
+
|
|
4434
|
+
# If you track op params for history explorer
|
|
4435
|
+
md["__op_params__"] = {
|
|
4436
|
+
"op": "mono_to_rgb",
|
|
4437
|
+
"mode": "triplicate",
|
|
4438
|
+
"from": str(src_desc),
|
|
4439
|
+
"from_shape": tuple(before_shape) if before_shape is not None else None,
|
|
4440
|
+
"to_shape": tuple(rgb.shape),
|
|
4441
|
+
}
|
|
4442
|
+
|
|
4443
|
+
# name for logging
|
|
4444
|
+
try:
|
|
4445
|
+
name = getattr(doc, "display_name", lambda: None)() or getattr(doc, "name", "") or "Active"
|
|
4446
|
+
except Exception:
|
|
4447
|
+
name = "Active"
|
|
4448
|
+
|
|
4449
|
+
try:
|
|
4450
|
+
dm.update_active_document(
|
|
4451
|
+
rgb,
|
|
4452
|
+
metadata=md,
|
|
4453
|
+
step_name="Mono → RGB",
|
|
4454
|
+
doc=doc, # explicit is safer
|
|
4455
|
+
)
|
|
4456
|
+
|
|
4457
|
+
if hasattr(self, "_log"):
|
|
4458
|
+
self._log(
|
|
4459
|
+
f"Mono → RGB: '{name}' converted {src_desc} "
|
|
4460
|
+
f"(shape={before_shape}, dtype={before_dtype}) → "
|
|
4461
|
+
f"RGB (shape={rgb.shape}, dtype={rgb.dtype})."
|
|
4462
|
+
)
|
|
4463
|
+
|
|
4464
|
+
except Exception:
|
|
4465
|
+
import traceback
|
|
4466
|
+
try:
|
|
4467
|
+
from PyQt6.QtWidgets import QMessageBox
|
|
4468
|
+
QMessageBox.critical(self, "Mono → RGB", traceback.format_exc())
|
|
4469
|
+
except Exception:
|
|
4470
|
+
pass
|
|
4471
|
+
|
|
4472
|
+
def _swap_rb_active(self):
|
|
4473
|
+
"""
|
|
4474
|
+
Swap R and B channels in the active RGB document (undoable).
|
|
4475
|
+
Intended for debayer/channel-order mismatches.
|
|
4476
|
+
"""
|
|
4477
|
+
dm = getattr(self, "docman", None)
|
|
4478
|
+
if dm is None:
|
|
4479
|
+
return
|
|
4480
|
+
|
|
4481
|
+
try:
|
|
4482
|
+
doc = dm.get_active_document()
|
|
4483
|
+
except Exception:
|
|
4484
|
+
doc = None
|
|
4485
|
+
if doc is None:
|
|
4486
|
+
return
|
|
4487
|
+
|
|
4488
|
+
img = getattr(doc, "image", None)
|
|
4489
|
+
if img is None:
|
|
4490
|
+
return
|
|
4491
|
+
|
|
4492
|
+
import numpy as np
|
|
4493
|
+
x = np.asarray(img)
|
|
4494
|
+
|
|
4495
|
+
# Must be RGB
|
|
4496
|
+
if not (x.ndim == 3 and x.shape[-1] == 3):
|
|
4497
|
+
try:
|
|
4498
|
+
name = getattr(doc, "display_name", lambda: None)() or getattr(doc, "name", "") or "Active"
|
|
4499
|
+
except Exception:
|
|
4500
|
+
name = "Active"
|
|
4501
|
+
|
|
4502
|
+
if hasattr(self, "_log"):
|
|
4503
|
+
self._log(f"Swap R/B: '{name}' is not RGB (shape={getattr(x,'shape',None)}).")
|
|
4504
|
+
return
|
|
4505
|
+
|
|
4506
|
+
before_shape = x.shape
|
|
4507
|
+
before_dtype = x.dtype
|
|
4508
|
+
|
|
4509
|
+
# swap channels without changing dtype
|
|
4510
|
+
# (copy is safest so we don't mutate shared views)
|
|
4511
|
+
out = x.copy()
|
|
4512
|
+
out[..., 0], out[..., 2] = x[..., 2], x[..., 0]
|
|
4513
|
+
|
|
4514
|
+
# metadata: preserve existing, but annotate operation
|
|
4515
|
+
try:
|
|
4516
|
+
md = dict(getattr(doc, "metadata", None) or {})
|
|
4517
|
+
except Exception:
|
|
4518
|
+
md = {}
|
|
4519
|
+
|
|
4520
|
+
md["color_model"] = md.get("color_model", "RGB")
|
|
4521
|
+
md["channels"] = 3
|
|
4522
|
+
md["is_mono"] = False
|
|
4523
|
+
md["source"] = (md.get("source") or "Edit")
|
|
4524
|
+
|
|
4525
|
+
# If you track op params for history explorer
|
|
4526
|
+
md["__op_params__"] = {
|
|
4527
|
+
"op": "swap_rb",
|
|
4528
|
+
"from_shape": tuple(before_shape),
|
|
4529
|
+
"to_shape": tuple(out.shape),
|
|
4530
|
+
"dtype": str(before_dtype),
|
|
4531
|
+
}
|
|
4532
|
+
|
|
4533
|
+
try:
|
|
4534
|
+
name = getattr(doc, "display_name", lambda: None)() or getattr(doc, "name", "") or "Active"
|
|
4535
|
+
except Exception:
|
|
4536
|
+
name = "Active"
|
|
4537
|
+
|
|
4538
|
+
try:
|
|
4539
|
+
dm.update_active_document(
|
|
4540
|
+
out,
|
|
4541
|
+
metadata=md,
|
|
4542
|
+
step_name="Swap R ↔ B",
|
|
4543
|
+
doc=doc,
|
|
4544
|
+
)
|
|
4545
|
+
|
|
4546
|
+
if hasattr(self, "_log"):
|
|
4547
|
+
self._log(
|
|
4548
|
+
f"Swap R/B: '{name}' swapped channels "
|
|
4549
|
+
f"(shape={before_shape}, dtype={before_dtype})."
|
|
4550
|
+
)
|
|
4551
|
+
|
|
4552
|
+
except Exception:
|
|
4553
|
+
import traceback
|
|
4554
|
+
try:
|
|
4555
|
+
from PyQt6.QtWidgets import QMessageBox
|
|
4556
|
+
QMessageBox.critical(self, "Swap R/B", traceback.format_exc())
|
|
4557
|
+
except Exception:
|
|
4558
|
+
pass
|
|
4559
|
+
|
|
4560
|
+
|
|
4302
4561
|
def _on_stackingsuite_relaunch(self, old_dir: str, new_dir: str):
|
|
4303
4562
|
# Optional: respond to dialog's relaunch request
|
|
4304
4563
|
try:
|
|
@@ -4399,9 +4658,16 @@ class AstroSuiteProMainWindow(
|
|
|
4399
4658
|
# Create a callback to set the image back to the document
|
|
4400
4659
|
def set_image_callback(image_data, step_name):
|
|
4401
4660
|
"""Apply the result image back to the active document."""
|
|
4402
|
-
if active_doc and hasattr(active_doc, "
|
|
4661
|
+
if active_doc and hasattr(active_doc, "apply_edit"):
|
|
4403
4662
|
print(f"[AstroSpike] Setting image back to document, shape: {image_data.shape}")
|
|
4404
|
-
#
|
|
4663
|
+
# Use apply_edit for proper undo/redo integration
|
|
4664
|
+
meta = {
|
|
4665
|
+
"step_name": step_name,
|
|
4666
|
+
"astrospike": True
|
|
4667
|
+
}
|
|
4668
|
+
active_doc.apply_edit(image_data.astype(np.float32, copy=False), metadata=meta, step_name=step_name)
|
|
4669
|
+
elif active_doc and hasattr(active_doc, "set_image"):
|
|
4670
|
+
print(f"[AstroSpike] Setting image via set_image, shape: {image_data.shape}")
|
|
4405
4671
|
active_doc.set_image(image_data, metadata={}, step_name=step_name)
|
|
4406
4672
|
elif active_doc and hasattr(active_doc, "image"):
|
|
4407
4673
|
print(f"[AstroSpike] Setting image directly, shape: {image_data.shape}")
|
|
@@ -7888,6 +8154,43 @@ class AstroSuiteProMainWindow(
|
|
|
7888
8154
|
|
|
7889
8155
|
return t
|
|
7890
8156
|
|
|
8157
|
+
def _mdi_begin_open_batch(self, mode: str = "cascade"):
|
|
8158
|
+
self._mdi_open_batch += 1
|
|
8159
|
+
self._mdi_place_mode = mode or "cascade"
|
|
8160
|
+
self._mdi_next_pos = None
|
|
8161
|
+
|
|
8162
|
+
def _mdi_end_open_batch(self):
|
|
8163
|
+
self._mdi_open_batch = max(0, self._mdi_open_batch - 1)
|
|
8164
|
+
if self._mdi_open_batch == 0:
|
|
8165
|
+
self._mdi_next_pos = None
|
|
8166
|
+
|
|
8167
|
+
def _mdi_compute_initial_pos(self) -> QPoint:
|
|
8168
|
+
area = (self.mdi.viewport().geometry() if self.mdi.viewport() else self.mdi.contentsRect())
|
|
8169
|
+
# Put first window a bit inset so titlebars don’t clip
|
|
8170
|
+
return QPoint(area.left() + 18, area.top() + 18)
|
|
8171
|
+
|
|
8172
|
+
def _mdi_place_subwindow(self, sw, target_w: int, target_h: int):
|
|
8173
|
+
"""Deterministic placement. Uses a stable cursor during batch opens."""
|
|
8174
|
+
vp = self.mdi.viewport()
|
|
8175
|
+
area = vp.geometry() if vp else self.mdi.contentsRect()
|
|
8176
|
+
|
|
8177
|
+
if self._mdi_next_pos is None:
|
|
8178
|
+
self._mdi_next_pos = self._mdi_compute_initial_pos()
|
|
8179
|
+
|
|
8180
|
+
x = self._mdi_next_pos.x()
|
|
8181
|
+
y = self._mdi_next_pos.y()
|
|
8182
|
+
|
|
8183
|
+
# keep inside viewport; reset when we hit edge
|
|
8184
|
+
if (x + target_w > area.right() - 10) or (y + 40 > area.bottom() - 10):
|
|
8185
|
+
x = area.left() + 18
|
|
8186
|
+
y = area.top() + 18
|
|
8187
|
+
|
|
8188
|
+
sw.move(x, y)
|
|
8189
|
+
|
|
8190
|
+
# advance cursor
|
|
8191
|
+
step = int(self._mdi_cascade_step)
|
|
8192
|
+
self._mdi_next_pos = QPoint(x + step, y + step)
|
|
8193
|
+
|
|
7891
8194
|
def _spawn_subwindow_for(self, doc, *, force_new: bool = False):
|
|
7892
8195
|
"""
|
|
7893
8196
|
Open a subwindow for `doc`. If one already exists and force_new=False,
|
|
@@ -8061,52 +8364,22 @@ class AstroSuiteProMainWindow(
|
|
|
8061
8364
|
target_h = max(200, target_h)
|
|
8062
8365
|
|
|
8063
8366
|
sw.resize(target_w, target_h)
|
|
8064
|
-
sw.showNormal() #
|
|
8367
|
+
sw.showNormal() # clears any "maximized" flag from previous active window
|
|
8065
8368
|
|
|
8066
|
-
#
|
|
8067
|
-
|
|
8068
|
-
|
|
8069
|
-
|
|
8070
|
-
|
|
8071
|
-
|
|
8072
|
-
|
|
8073
|
-
active = self.mdi.activeSubWindow()
|
|
8074
|
-
if active and active.isVisible() and not (active.windowState() & Qt.WindowState.WindowMinimized):
|
|
8075
|
-
# Cascade from the active window
|
|
8076
|
-
geo = active.geometry()
|
|
8077
|
-
new_x = geo.x() + 30
|
|
8078
|
-
new_y = geo.y() + 30
|
|
8079
|
-
else:
|
|
8080
|
-
# Fallback: try to find the "last added" visible window to cascade from
|
|
8081
|
-
# (useful if active is None but windows exist)
|
|
8082
|
-
try:
|
|
8083
|
-
subs = [s for s in self.mdi.subWindowList() if s.isVisible() and s is not sw]
|
|
8084
|
-
if subs:
|
|
8085
|
-
# simplistic "last created" might be at end of list
|
|
8086
|
-
last = subs[-1]
|
|
8087
|
-
geo = last.geometry()
|
|
8088
|
-
new_x = geo.x() + 30
|
|
8089
|
-
new_y = geo.y() + 30
|
|
8369
|
+
# Deterministic placement (batch-aware)
|
|
8370
|
+
try:
|
|
8371
|
+
self._mdi_place_subwindow(sw, target_w, target_h)
|
|
8372
|
+
except Exception:
|
|
8373
|
+
# absolute fallback: top-left-ish
|
|
8374
|
+
try:
|
|
8375
|
+
sw.move(area.left() + 18, area.top() + 18)
|
|
8090
8376
|
except Exception:
|
|
8091
8377
|
pass
|
|
8092
8378
|
|
|
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)
|
|
8100
|
-
|
|
8101
|
-
sw.move(new_x, new_y)
|
|
8102
|
-
|
|
8103
|
-
# ⌠removed the "fill MDI viewport" block - we *don't* want full-monitor first window
|
|
8104
|
-
|
|
8105
8379
|
# Show / activate
|
|
8106
8380
|
sw.show()
|
|
8107
8381
|
sw.raise_()
|
|
8108
8382
|
self.mdi.setActiveSubWindow(sw)
|
|
8109
|
-
# (no second setWindowTitle() here)
|
|
8110
8383
|
|
|
8111
8384
|
# Optional minimize/restore interceptor
|
|
8112
8385
|
if hasattr(self, "_minimize_interceptor"):
|
|
@@ -8887,6 +9160,13 @@ class AstroSuiteProMainWindow(
|
|
|
8887
9160
|
|
|
8888
9161
|
super().keyPressEvent(event)
|
|
8889
9162
|
|
|
9163
|
+
def _open_texture_clarity(self):
|
|
9164
|
+
try:
|
|
9165
|
+
from setiastro.saspro.texture_clarity import open_texture_clarity_dialog
|
|
9166
|
+
open_texture_clarity_dialog(self)
|
|
9167
|
+
except Exception as e:
|
|
9168
|
+
print(f"Error opening Texture & Clarity: {e}")
|
|
9169
|
+
|
|
8890
9170
|
def _update_usage_stats(self):
|
|
8891
9171
|
try:
|
|
8892
9172
|
now = time.time()
|
|
@@ -7,7 +7,7 @@ import os
|
|
|
7
7
|
from typing import TYPE_CHECKING
|
|
8
8
|
|
|
9
9
|
from PyQt6.QtCore import Qt
|
|
10
|
-
from PyQt6.QtWidgets import QFileDialog, QMessageBox, QProgressDialog
|
|
10
|
+
from PyQt6.QtWidgets import QFileDialog, QMessageBox, QProgressDialog, QApplication
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
13
|
pass
|
|
@@ -85,7 +85,6 @@ class FileMixin:
|
|
|
85
85
|
self._save_recent_lists()
|
|
86
86
|
if hasattr(self, "_rebuild_recent_menus"):
|
|
87
87
|
self._rebuild_recent_menus()
|
|
88
|
-
# Extracted FILE methods
|
|
89
88
|
|
|
90
89
|
def open_files(self):
|
|
91
90
|
# One-stop "All Supported" plus focused groups the user can switch to
|
|
@@ -114,21 +113,41 @@ class FileMixin:
|
|
|
114
113
|
except Exception:
|
|
115
114
|
pass
|
|
116
115
|
|
|
117
|
-
#
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
116
|
+
# ---- BEGIN batch open (stable placement) ----
|
|
117
|
+
try:
|
|
118
|
+
self._mdi_begin_open_batch(mode="cascade")
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
|
|
123
|
+
try:
|
|
124
|
+
# open each path (doc_manager should emit documentAdded; no manual spawn)
|
|
125
|
+
for p in paths:
|
|
125
126
|
try:
|
|
126
|
-
|
|
127
|
-
self.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
127
|
+
_ = self.docman.open_path(p) # emits documentAdded; spawn will happen
|
|
128
|
+
self._log(f"Opened: {p}")
|
|
129
|
+
self._add_recent_image(p) # track MRU
|
|
130
|
+
|
|
131
|
+
# Increment statistics
|
|
132
|
+
try:
|
|
133
|
+
count = self.settings.value("stats/opened_images_count", 0, type=int)
|
|
134
|
+
self.settings.setValue("stats/opened_images_count", count + 1)
|
|
135
|
+
except Exception:
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
# Let Qt paint newly spawned subwindows as we go
|
|
139
|
+
QApplication.processEvents()
|
|
140
|
+
|
|
141
|
+
except Exception as e:
|
|
142
|
+
QMessageBox.warning(self, self.tr("Open failed"), f"{p}\n\n{e}")
|
|
143
|
+
QApplication.processEvents()
|
|
144
|
+
finally:
|
|
145
|
+
QApplication.restoreOverrideCursor()
|
|
146
|
+
try:
|
|
147
|
+
self._mdi_end_open_batch()
|
|
148
|
+
except Exception:
|
|
149
|
+
pass
|
|
150
|
+
|
|
132
151
|
|
|
133
152
|
def save_active(self):
|
|
134
153
|
from setiastro.saspro.main_helpers import (
|
|
@@ -197,14 +216,18 @@ class FileMixin:
|
|
|
197
216
|
# --- Bit depth selection ----------------------------------------
|
|
198
217
|
from setiastro.saspro.save_options import SaveOptionsDialog
|
|
199
218
|
current_bd = doc.metadata.get("bit_depth")
|
|
200
|
-
|
|
219
|
+
current_jq = (doc.metadata or {}).get("jpeg_quality", None)
|
|
220
|
+
|
|
221
|
+
dlg = SaveOptionsDialog(self, ext_norm, current_bd, current_jpeg_quality=current_jq)
|
|
201
222
|
if dlg.exec() != dlg.DialogCode.Accepted:
|
|
202
223
|
return
|
|
224
|
+
|
|
203
225
|
chosen_bd = dlg.selected_bit_depth()
|
|
226
|
+
chosen_jq = dlg.selected_jpeg_quality()
|
|
204
227
|
|
|
205
228
|
# --- Save & remember folder ----------------------------------------
|
|
206
229
|
try:
|
|
207
|
-
self.docman.save_document(doc, path, bit_depth_override=chosen_bd)
|
|
230
|
+
self.docman.save_document(doc, path, bit_depth_override=chosen_bd, jpeg_quality=chosen_jq)
|
|
208
231
|
self._log(f"Saved: {path} ({chosen_bd})")
|
|
209
232
|
self.settings.setValue("paths/last_save_dir", os.path.dirname(path))
|
|
210
233
|
except Exception as e:
|
|
@@ -121,6 +121,10 @@ class MenuMixin:
|
|
|
121
121
|
m_edit = mb.addMenu(self.tr("&Edit"))
|
|
122
122
|
m_edit.addAction(self.act_undo)
|
|
123
123
|
m_edit.addAction(self.act_redo)
|
|
124
|
+
m_edit.addSeparator()
|
|
125
|
+
m_edit.addAction(self.act_mono_to_rgb)
|
|
126
|
+
m_edit.addAction(self.act_swap_rb)
|
|
127
|
+
|
|
124
128
|
|
|
125
129
|
# Functions
|
|
126
130
|
m_fn = mb.addMenu(self.tr("&Functions"))
|
|
@@ -151,6 +155,7 @@ class MenuMixin:
|
|
|
151
155
|
m_fn.addAction(self.act_signature)
|
|
152
156
|
m_fn.addAction(self.act_star_stretch)
|
|
153
157
|
m_fn.addAction(self.act_stat_stretch)
|
|
158
|
+
m_fn.addAction(self.act_texture_clarity)
|
|
154
159
|
m_fn.addAction(self.act_wavescale_de)
|
|
155
160
|
m_fn.addAction(self.act_wavescale_hdr)
|
|
156
161
|
m_fn.addAction(self.act_white_balance)
|
|
@@ -170,8 +175,10 @@ class MenuMixin:
|
|
|
170
175
|
m_tools.addAction(self.act_freqsep)
|
|
171
176
|
m_tools.addAction(self.act_image_combine)
|
|
172
177
|
m_tools.addAction(self.act_multiscale_decomp)
|
|
178
|
+
m_tools.addAction(self.act_narrowband_normalization)
|
|
173
179
|
m_tools.addAction(self.act_nbtorgb)
|
|
174
180
|
m_tools.addAction(self.act_ppp)
|
|
181
|
+
|
|
175
182
|
m_tools.addAction(self.act_selective_color)
|
|
176
183
|
m_tools.addSeparator()
|
|
177
184
|
m_tools.addAction(self.act_view_bundles)
|
|
@@ -200,6 +207,8 @@ class MenuMixin:
|
|
|
200
207
|
m_star.addAction(self.act_isophote)
|
|
201
208
|
m_star.addAction(self.act_live_stacking)
|
|
202
209
|
m_star.addAction(self.act_mosaic_master)
|
|
210
|
+
m_star.addAction(self.act_planet_projection)
|
|
211
|
+
m_star.addAction(self.act_planetary_stacker)
|
|
203
212
|
m_star.addAction(self.act_plate_solve)
|
|
204
213
|
m_star.addAction(self.act_psf_viewer)
|
|
205
214
|
m_star.addAction(self.act_rgb_align)
|