setiastrosuitepro 1.8.0.post3__py3-none-any.whl → 1.8.1.post2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of setiastrosuitepro might be problematic. Click here for more details.

@@ -5,6 +5,104 @@ import os
5
5
  import math
6
6
  import random
7
7
  import sys
8
+ from PyQt6.QtCore import QByteArray
9
+
10
+ def _qs_raw(settings, key, default=None):
11
+ try:
12
+ return settings.value(key, default)
13
+ except Exception:
14
+ return default
15
+
16
+ def _qs_text(v):
17
+ """Best-effort: convert QByteArray/bytes -> str, preserve strings, else None."""
18
+ try:
19
+ if isinstance(v, QByteArray):
20
+ # PyQt6-safe
21
+ v = bytes(v.data()).decode("utf-8", "ignore")
22
+ elif isinstance(v, (bytes, bytearray)):
23
+ v = bytes(v).decode("utf-8", "ignore")
24
+ if isinstance(v, str):
25
+ return v.strip()
26
+ except Exception:
27
+ pass
28
+ return None
29
+
30
+ def _purge(settings, key):
31
+ try:
32
+ settings.remove(key)
33
+ except Exception:
34
+ pass
35
+
36
+ def qs_int(settings, key, default=0, *, purge_bad=True):
37
+ v = _qs_raw(settings, key, default)
38
+ try:
39
+ if isinstance(v, bool):
40
+ return int(v)
41
+ if isinstance(v, int):
42
+ return v
43
+ if isinstance(v, float):
44
+ return int(v)
45
+
46
+ s = _qs_text(v)
47
+ if s is not None:
48
+ if s == "":
49
+ return default
50
+ s = s.replace(",", ".") # locale fix
51
+ return int(float(s)) # handles "3.0"
52
+
53
+ # Last resort: try numeric conversion, but don't guess on random objects
54
+ return int(v)
55
+ except Exception:
56
+ if purge_bad:
57
+ _purge(settings, key)
58
+ return default
59
+
60
+ def qs_float(settings, key, default=0.0, *, purge_bad=True):
61
+ v = _qs_raw(settings, key, default)
62
+ try:
63
+ if isinstance(v, bool):
64
+ return float(int(v))
65
+ if isinstance(v, (int, float)):
66
+ return float(v)
67
+
68
+ s = _qs_text(v)
69
+ if s is not None:
70
+ if s == "":
71
+ return default
72
+ s = s.replace(",", ".") # locale fix
73
+ return float(s)
74
+
75
+ return float(v)
76
+ except Exception:
77
+ if purge_bad:
78
+ _purge(settings, key)
79
+ return default
80
+
81
+ def qs_bool(settings, key, default=False, *, purge_bad=True):
82
+ v = _qs_raw(settings, key, default)
83
+ try:
84
+ if isinstance(v, bool):
85
+ return v
86
+ if isinstance(v, (int, float)):
87
+ return bool(int(v))
88
+
89
+ s = _qs_text(v)
90
+ if s is not None:
91
+ s = s.lower()
92
+ if s in ("1", "true", "yes", "on"):
93
+ return True
94
+ if s in ("0", "false", "no", "off"):
95
+ return False
96
+ return default
97
+
98
+ # Do NOT guess truthiness of random objects; that's how weirdness spreads
99
+ return default
100
+ except Exception:
101
+ if purge_bad:
102
+ _purge(settings, key)
103
+ return default
104
+
105
+
8
106
  # ---------------------------------------------------------------------
9
107
  # Executor helper: avoid ProcessPool in frozen (PyInstaller) builds
10
108
  # ---------------------------------------------------------------------
@@ -637,6 +735,7 @@ class StellarAlignmentDialog(QDialog):
637
735
  pass # older PyQt6 versions
638
736
 
639
737
  self.settings = settings
738
+
640
739
  self.parent_window = parent
641
740
  self._docman = doc_manager or getattr(parent, "doc_manager", None)
642
741
 
@@ -4169,65 +4268,98 @@ class MosaicSettingsDialog(QDialog):
4169
4268
  self.setWindowTitle("Mosaic Master Settings")
4170
4269
  self.initUI()
4171
4270
 
4271
+ def _set_2dec(self, spin):
4272
+ """Safely force 2-dec display regardless of CustomDoubleSpinBox implementation."""
4273
+ try:
4274
+ # Standard QAbstractSpinBox API
4275
+ le = spin.lineEdit()
4276
+ if le is not None:
4277
+ le.setText(f"{spin.value():.2f}")
4278
+ return
4279
+ except Exception:
4280
+ pass
4281
+
4282
+ # Fallback: your custom class might expose .lineEdit as an attribute
4283
+ try:
4284
+ le = getattr(spin, "lineEdit", None)
4285
+ if le is not None:
4286
+ le.setText(f"{spin.value():.2f}")
4287
+ except Exception:
4288
+ pass
4289
+
4172
4290
  def initUI(self):
4173
4291
  layout = QFormLayout(self)
4174
4292
 
4175
4293
  # Number of Stars to Attempt to Use
4176
- self.starCountSpin = CustomSpinBox(minimum=1, maximum=1000,
4177
- initial=self.settings.value("mosaic/num_stars", 150, type=int),
4178
- step=1)
4294
+ self.starCountSpin = CustomSpinBox(
4295
+ minimum=1, maximum=1000,
4296
+ initial=qs_int(self.settings, "mosaic/num_stars", 150),
4297
+ step=1
4298
+ )
4179
4299
  layout.addRow("Number of Stars:", self.starCountSpin)
4180
4300
 
4181
4301
  # Translation Max Tolerance
4182
- self.transTolSpin = CustomDoubleSpinBox(minimum=0.0, maximum=10.0,
4183
- initial=self.settings.value("mosaic/translation_max_tolerance", 3.0, type=float),
4184
- step=0.1)
4302
+ self.transTolSpin = CustomDoubleSpinBox(
4303
+ minimum=0.0, maximum=10.0,
4304
+ initial=qs_float(self.settings, "mosaic/translation_max_tolerance", 3.0),
4305
+ step=0.1
4306
+ )
4185
4307
  layout.addRow("Translation Max Tolerance:", self.transTolSpin)
4186
4308
 
4187
4309
  # Scale Min Tolerance
4188
- self.scaleMinSpin = CustomDoubleSpinBox(minimum=0.0, maximum=10.0,
4189
- initial=self.settings.value("mosaic/scale_min_tolerance", 0.8, type=float),
4190
- step=0.1)
4310
+ self.scaleMinSpin = CustomDoubleSpinBox(
4311
+ minimum=0.0, maximum=10.0,
4312
+ initial=qs_float(self.settings, "mosaic/scale_min_tolerance", 0.8),
4313
+ step=0.1
4314
+ )
4191
4315
  layout.addRow("Scale Min Tolerance:", self.scaleMinSpin)
4192
4316
 
4193
4317
  # Scale Max Tolerance
4194
- self.scaleMaxSpin = CustomDoubleSpinBox(minimum=0.0, maximum=10.0,
4195
- initial=self.settings.value("mosaic/scale_max_tolerance", 1.25, type=float),
4196
- step=0.1)
4318
+ self.scaleMaxSpin = CustomDoubleSpinBox(
4319
+ minimum=0.0, maximum=10.0,
4320
+ initial=qs_float(self.settings, "mosaic/scale_max_tolerance", 1.25),
4321
+ step=0.1
4322
+ )
4197
4323
  layout.addRow("Scale Max Tolerance:", self.scaleMaxSpin)
4198
4324
 
4199
4325
  # Rotation Max Tolerance
4200
- self.rotationMaxSpin = CustomDoubleSpinBox(minimum=0.0, maximum=180.0,
4201
- initial=self.settings.value("mosaic/rotation_max_tolerance", 45.0, type=float),
4202
- step=0.1)
4203
- # Force two decimals in display
4204
- self.rotationMaxSpin.lineEdit.setText(f"{self.rotationMaxSpin.value():.2f}")
4326
+ self.rotationMaxSpin = CustomDoubleSpinBox(
4327
+ minimum=0.0, maximum=180.0,
4328
+ initial=qs_float(self.settings, "mosaic/rotation_max_tolerance", 45.0),
4329
+ step=0.1, decimals=2
4330
+ )
4205
4331
  layout.addRow("Rotation Max Tolerance (°):", self.rotationMaxSpin)
4206
4332
 
4207
4333
  # Skew Max Tolerance
4208
- self.skewMaxSpin = CustomDoubleSpinBox(minimum=0.0, maximum=1.0,
4209
- initial=self.settings.value("mosaic/skew_max_tolerance", 0.1, type=float),
4210
- step=0.01)
4334
+ self.skewMaxSpin = CustomDoubleSpinBox(
4335
+ minimum=0.0, maximum=1.0,
4336
+ initial=qs_float(self.settings, "mosaic/skew_max_tolerance", 0.1),
4337
+ step=0.01
4338
+ )
4211
4339
  layout.addRow("Skew Max Tolerance:", self.skewMaxSpin)
4212
4340
 
4213
4341
  # FWHM for Star Detection
4214
- self.fwhmSpin = CustomDoubleSpinBox(minimum=0.0, maximum=20.0,
4215
- initial=self.settings.value("mosaic/star_fwhm", 3.0, type=float),
4216
- step=0.1)
4217
- self.fwhmSpin.lineEdit.setText(f"{self.fwhmSpin.value():.2f}")
4342
+ self.fwhmSpin = CustomDoubleSpinBox(
4343
+ minimum=0.0, maximum=20.0,
4344
+ initial=qs_float(self.settings, "mosaic/star_fwhm", 3.0),
4345
+ step=0.1, decimals=2
4346
+ )
4218
4347
  layout.addRow("FWHM for Star Detection:", self.fwhmSpin)
4219
4348
 
4220
4349
  # Sigma for Star Detection
4221
- self.sigmaSpin = CustomDoubleSpinBox(minimum=0.0, maximum=10.0,
4222
- initial=self.settings.value("mosaic/star_sigma", 3.0, type=float),
4223
- step=0.1)
4224
- self.sigmaSpin.lineEdit.setText(f"{self.sigmaSpin.value():.2f}")
4350
+ self.sigmaSpin = CustomDoubleSpinBox(
4351
+ minimum=0.0, maximum=10.0,
4352
+ initial=qs_float(self.settings, "mosaic/star_sigma", 3.0),
4353
+ step=0.1, decimals=2
4354
+ )
4225
4355
  layout.addRow("Sigma for Star Detection:", self.sigmaSpin)
4226
4356
 
4227
4357
  # Polynomial Degree
4228
- self.polyDegreeSpin = CustomSpinBox(minimum=1, maximum=6,
4229
- initial=self.settings.value("mosaic/poly_degree", 3, type=int),
4230
- step=1)
4358
+ self.polyDegreeSpin = CustomSpinBox(
4359
+ minimum=1, maximum=6,
4360
+ initial=qs_int(self.settings, "mosaic/poly_degree", 3),
4361
+ step=1
4362
+ )
4231
4363
  layout.addRow("Polynomial Degree:", self.polyDegreeSpin)
4232
4364
 
4233
4365
  buttons = QDialogButtonBox(
@@ -4239,16 +4371,18 @@ class MosaicSettingsDialog(QDialog):
4239
4371
  layout.addRow(buttons)
4240
4372
 
4241
4373
  def accept(self):
4242
- # Save the values to QSettings
4243
- self.settings.setValue("mosaic/num_stars", self.starCountSpin.value)
4244
- self.settings.setValue("mosaic/translation_max_tolerance", self.transTolSpin.value())
4245
- self.settings.setValue("mosaic/scale_min_tolerance", self.scaleMinSpin.value())
4246
- self.settings.setValue("mosaic/scale_max_tolerance", self.scaleMaxSpin.value())
4247
- self.settings.setValue("mosaic/rotation_max_tolerance", self.rotationMaxSpin.value())
4248
- self.settings.setValue("mosaic/skew_max_tolerance", self.skewMaxSpin.value())
4249
- self.settings.setValue("mosaic/star_fwhm", self.fwhmSpin.value())
4250
- self.settings.setValue("mosaic/star_sigma", self.sigmaSpin.value())
4251
- self.settings.setValue("mosaic/poly_degree", self.polyDegreeSpin.value)
4374
+ # Save the values to QSettings (IMPORTANT: call value() not value)
4375
+ self.settings.setValue("mosaic/num_stars", int(self.starCountSpin.value()))
4376
+ self.settings.setValue("mosaic/translation_max_tolerance", float(self.transTolSpin.value()))
4377
+ self.settings.setValue("mosaic/scale_min_tolerance", float(self.scaleMinSpin.value()))
4378
+ self.settings.setValue("mosaic/scale_max_tolerance", float(self.scaleMaxSpin.value()))
4379
+ self.settings.setValue("mosaic/rotation_max_tolerance", float(self.rotationMaxSpin.value()))
4380
+ self.settings.setValue("mosaic/skew_max_tolerance", float(self.skewMaxSpin.value()))
4381
+ self.settings.setValue("mosaic/star_fwhm", float(self.fwhmSpin.value()))
4382
+ self.settings.setValue("mosaic/star_sigma", float(self.sigmaSpin.value()))
4383
+ self.settings.setValue("mosaic/poly_degree", int(self.polyDegreeSpin.value()))
4384
+
4385
+ self.settings.sync()
4252
4386
  super().accept()
4253
4387
 
4254
4388
  class PolyGradientRemoval:
@@ -4618,6 +4752,7 @@ class MosaicMasterDialog(QDialog):
4618
4752
  self.stretch_original_mins = []
4619
4753
  self.stretch_original_medians = []
4620
4754
  self.was_single_channel = False
4755
+ self._migrate_mosaic_settings()
4621
4756
 
4622
4757
  self.initUI()
4623
4758
 
@@ -4699,8 +4834,9 @@ class MosaicMasterDialog(QDialog):
4699
4834
  layout.addLayout(checkbox_layout)
4700
4835
 
4701
4836
  # Persisted WCS-only
4702
- _settings = QSettings("SetiAstro", "SASpro")
4703
- self.wcsOnlyCheckBox.setChecked(_settings.value("mosaic/wcs_only", False, type=bool))
4837
+ self.wcsOnlyCheckBox.setChecked(qs_bool(self.settings, "mosaic/wcs_only", False))
4838
+
4839
+
4704
4840
 
4705
4841
  # Helpers ----------------------------------------------------------
4706
4842
  def _set_checked(cb: QCheckBox, checked: bool):
@@ -4725,7 +4861,8 @@ class MosaicMasterDialog(QDialog):
4725
4861
 
4726
4862
  def _on_wcs_only_changed(state: int):
4727
4863
  # Persist setting first (this one is user-visible preference)
4728
- QSettings("SetiAstro", "SASpro").setValue("mosaic/wcs_only", self.wcsOnlyCheckBox.isChecked())
4864
+ self.settings.setValue("mosaic/wcs_only", self.wcsOnlyCheckBox.isChecked())
4865
+ self.settings.sync()
4729
4866
 
4730
4867
  # If WCS-only is turned ON, force Seestar OFF
4731
4868
  if self.wcsOnlyCheckBox.isChecked():
@@ -4762,14 +4899,15 @@ class MosaicMasterDialog(QDialog):
4762
4899
  "Precise — Full WCS: astropy.reproject per channel; slowest, most exact."
4763
4900
  )
4764
4901
 
4765
- _default_mode = _settings.value("mosaic/reproject_mode", "Fast — SIP-aware (Exact Remap)")
4902
+ _default_mode = self.settings.value("mosaic/reproject_mode", "Fast — SIP-aware (Exact Remap)", type=str)
4903
+
4766
4904
  valid_modes = [self.reprojectModeCombo.itemText(i) for i in range(self.reprojectModeCombo.count())]
4767
4905
  if _default_mode not in valid_modes:
4768
4906
  _default_mode = "Fast — SIP-aware (Exact Remap)"
4769
4907
  self.reprojectModeCombo.setCurrentText(_default_mode)
4770
- self.reprojectModeCombo.currentTextChanged.connect(
4771
- lambda t: QSettings("SetiAstro", "SASpro").setValue("mosaic/reproject_mode", t)
4772
- )
4908
+ self.reprojectModeCombo.currentTextChanged.connect(self._on_reproject_mode_changed)
4909
+
4910
+ self.settings.sync()
4773
4911
 
4774
4912
  row = QHBoxLayout()
4775
4913
  row.addWidget(self.reprojectModeLabel)
@@ -4809,6 +4947,18 @@ class MosaicMasterDialog(QDialog):
4809
4947
 
4810
4948
  self.setLayout(layout)
4811
4949
 
4950
+ def _on_reproject_mode_changed(self, t: str):
4951
+ self.settings.setValue("mosaic/reproject_mode", t)
4952
+ self.settings.sync()
4953
+
4954
+
4955
+ def _migrate_mosaic_settings(self):
4956
+ s = self.settings
4957
+ _ = qs_int(s, "mosaic/poly_degree", 3)
4958
+ _ = qs_float(s, "mosaic/star_sigma", 3.0)
4959
+ _ = qs_float(s, "mosaic/star_fwhm", 3.0)
4960
+ # etc...
4961
+ s.sync()
4812
4962
 
4813
4963
  def _target_median_from_first(self, items):
4814
4964
  # Pick a stable target (median of first image after safe clipping)
@@ -5043,82 +5193,6 @@ class MosaicMasterDialog(QDialog):
5043
5193
  return [(self._title_for_doc(d), d) for d in docs]
5044
5194
 
5045
5195
 
5046
- # --- title helpers -------------------------------------------------
5047
- def _callmaybe(self, obj, attr):
5048
- """Return obj.attr() if callable, else obj.attr; else None."""
5049
- try:
5050
- v = getattr(obj, attr, None)
5051
- return v() if callable(v) else v
5052
- except Exception:
5053
- return None
5054
-
5055
- def _best_title_from_obj(self, o):
5056
- """Try common title/name providers on a QWidget/ImageDocument."""
5057
- if o is None:
5058
- return None
5059
- # 1) display_name (method or attr)
5060
- t = self._callmaybe(o, "display_name")
5061
- if isinstance(t, str) and t.strip():
5062
- return t
5063
- # 2) windowTitle / objectName (Qt)
5064
- for a in ("windowTitle", "objectName", "title", "name"):
5065
- t = self._callmaybe(o, a)
5066
- if isinstance(t, str) and t.strip():
5067
- return t
5068
- # 3) metadata.display_name
5069
- try:
5070
- md = getattr(o, "metadata", {}) or {}
5071
- t = md.get("display_name")
5072
- if isinstance(t, str) and t.strip():
5073
- return t
5074
- except Exception:
5075
- pass
5076
- return None
5077
-
5078
- def _resolve_view_title(self, view, doc, title_hint=None):
5079
- """Prefer the host-provided title, then view, then doc, then a safe default."""
5080
- # host-provided title from _list_open_docs()
5081
- if isinstance(title_hint, str) and title_hint.strip():
5082
- return title_hint.strip()
5083
- # view wins over doc
5084
- t = self._best_title_from_obj(view) or self._best_title_from_obj(doc)
5085
- return t if t else "Untitled View"
5086
-
5087
-
5088
- def _pick_view_dialog(self):
5089
- items = self._iter_docs()
5090
- if not items:
5091
- QMessageBox.information(self, "Add from View", "No open views found.")
5092
- return None
5093
-
5094
- dlg = QDialog(self)
5095
- dlg.setWindowTitle("Select View")
5096
- v = QVBoxLayout(dlg)
5097
- v.addWidget(QLabel("Choose a view to add:"))
5098
-
5099
- combo = QComboBox()
5100
- for (_, doc) in items:
5101
- title = self._fmt_doc_title(doc)
5102
- # append dims if available
5103
- try:
5104
- img = _doc_image(doc) # same utility used by Stellar Alignment
5105
- if img is not None:
5106
- h, w = img.shape[:2]
5107
- title = f"{title} — {w}×{h}"
5108
- except Exception:
5109
- pass
5110
- combo.addItem(title, userData=doc)
5111
- v.addWidget(combo)
5112
-
5113
- bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
5114
- QDialogButtonBox.StandardButton.Cancel)
5115
- bb.accepted.connect(dlg.accept)
5116
- bb.rejected.connect(dlg.reject)
5117
- v.addWidget(bb)
5118
-
5119
- return combo.currentData() if dlg.exec() else None
5120
-
5121
-
5122
5196
  def openSettings(self):
5123
5197
  dlg = MosaicSettingsDialog(self.settings, self)
5124
5198
  if dlg.exec():
@@ -5130,31 +5204,37 @@ class MosaicMasterDialog(QDialog):
5130
5204
  self,
5131
5205
  "Add Image(s)",
5132
5206
  "",
5133
- "Images (*.png *.jpg *.jpeg *.tif *.tiff *.fits *.fit *.fz *.fz *.xisf *.cr2 *.nef *.arw *.dng *.raf *.orf *.rw2 *.pef)"
5207
+ "Images (*.png *.jpg *.jpeg *.tif *.tiff *.fits *.fit *.fz *.xisf *.cr2 *.nef *.arw *.dng *.raf *.orf *.rw2 *.pef)"
5134
5208
  )
5135
- if paths:
5136
- for path in paths:
5137
- arr, header, bitdepth, ismono = load_image(path)
5138
- header = sanitize_wcs_header(header) if header else None
5139
- wcs_obj = get_wcs_from_header(header) if header else None
5140
- d = {
5141
- "path": path,
5142
- "image": arr,
5143
- "header": header,
5144
- "wcs": wcs_obj,
5145
- "bit_depth": bitdepth,
5146
- "is_mono": ismono,
5147
- "transform": None
5148
- }
5149
- self.loaded_images.append(d)
5150
-
5151
- text = os.path.basename(path)
5152
- if wcs_obj is not None:
5153
- text += " [WCS]"
5154
- item = QListWidgetItem(text)
5155
- item.setToolTip(path)
5156
- self.images_list.addItem(item)
5157
- self.update_status()
5209
+ if not paths:
5210
+ return
5211
+
5212
+ for path in paths:
5213
+ arr, header, bitdepth, ismono = load_image(path)
5214
+ header = sanitize_wcs_header(header) if header else None
5215
+ wcs_obj = get_wcs_from_header(header) if header else None
5216
+
5217
+ base_name = os.path.basename(path) # <- what you want in status strings
5218
+
5219
+ d = {
5220
+ "path": path, # internal identity
5221
+ "display": base_name, # user-facing label
5222
+ "image": arr,
5223
+ "header": header,
5224
+ "wcs": wcs_obj,
5225
+ "bit_depth": bitdepth,
5226
+ "is_mono": ismono,
5227
+ "transform": None
5228
+ }
5229
+ self.loaded_images.append(d)
5230
+
5231
+ # list text can include decorations like [WCS]
5232
+ list_text = base_name + (" [WCS]" if wcs_obj is not None else "")
5233
+ item = QListWidgetItem(list_text)
5234
+ item.setToolTip(path) # still used for removal
5235
+ self.images_list.addItem(item)
5236
+
5237
+ self.update_status()
5158
5238
 
5159
5239
  def add_image_from_view(self):
5160
5240
  items = self._iter_docs()
@@ -5165,38 +5245,31 @@ class MosaicMasterDialog(QDialog):
5165
5245
  candidates = []
5166
5246
  for title, doc in items:
5167
5247
  # image
5168
- img = None
5169
5248
  try:
5170
- if hasattr(doc, "get_image") and callable(doc.get_image):
5171
- img = doc.get_image()
5172
- else:
5173
- img = getattr(doc, "image", None)
5249
+ img = doc.get_image() if hasattr(doc, "get_image") and callable(doc.get_image) else getattr(doc, "image", None)
5174
5250
  except Exception:
5175
5251
  img = None
5176
5252
  if not isinstance(img, np.ndarray):
5177
5253
  continue
5178
5254
 
5179
5255
  # metadata/header
5180
- meta = {}
5181
5256
  try:
5182
- if hasattr(doc, "get_metadata") and callable(doc.get_metadata):
5183
- meta = doc.get_metadata() or {}
5184
- else:
5185
- meta = getattr(doc, "metadata", {}) or {}
5257
+ meta = doc.get_metadata() if hasattr(doc, "get_metadata") and callable(doc.get_metadata) else getattr(doc, "metadata", {}) or {}
5186
5258
  except Exception:
5187
5259
  meta = {}
5188
5260
 
5189
- header = (meta.get("original_header")
5190
- or meta.get("header")
5191
- or meta.get("fits_header"))
5261
+ header = (meta.get("original_header") or meta.get("header") or meta.get("fits_header"))
5192
5262
  header = sanitize_wcs_header(header) if header else None
5193
5263
  wcs_obj = get_wcs_from_header(header) if header else None
5194
5264
 
5195
5265
  h, w = img.shape[:2]
5196
- label = f"{title} — {w}×{h}"
5197
- key = f"view://{id(doc)}" # stable pseudo-path for removal
5198
5266
 
5199
- candidates.append((label, img, meta, key, header, wcs_obj))
5267
+ display_name = str(title).strip() if title else "View" # <- clean
5268
+ list_label = f"{display_name} — {w}×{h}" # <- fancy UI
5269
+
5270
+ key = f"view://{id(doc)}"
5271
+
5272
+ candidates.append((list_label, display_name, img, meta, key, header, wcs_obj))
5200
5273
 
5201
5274
  if not candidates:
5202
5275
  QMessageBox.information(self, "Add from View", "No views with image data are open.")
@@ -5207,10 +5280,11 @@ class MosaicMasterDialog(QDialog):
5207
5280
  if not ok or not choice:
5208
5281
  return
5209
5282
 
5210
- label, image, metadata, path_key, header, wcs_obj = candidates[labels.index(choice)]
5283
+ list_label, display_name, image, metadata, path_key, header, wcs_obj = candidates[labels.index(choice)]
5211
5284
 
5212
5285
  d = {
5213
- "path": path_key,
5286
+ "path": path_key, # internal id for removal
5287
+ "display": display_name, # <- used by status strings / logs
5214
5288
  "image": image,
5215
5289
  "header": header,
5216
5290
  "wcs": wcs_obj,
@@ -5220,13 +5294,25 @@ class MosaicMasterDialog(QDialog):
5220
5294
  }
5221
5295
  self.loaded_images.append(d)
5222
5296
 
5223
- txt = label + (" [WCS]" if wcs_obj is not None else "")
5297
+ txt = list_label + (" [WCS]" if wcs_obj is not None else "")
5224
5298
  item = QListWidgetItem(txt)
5225
5299
  item.setToolTip(path_key)
5226
5300
  self.images_list.addItem(item)
5227
5301
  self.update_status()
5228
5302
 
5229
5303
 
5304
+ def _item_label(self, item: dict) -> str:
5305
+ # 1) explicit display label wins
5306
+ d = item.get("display")
5307
+ if isinstance(d, str) and d.strip():
5308
+ return d.strip()
5309
+
5310
+ # 2) fallback from path (disk or view://)
5311
+ p = item.get("path", "")
5312
+ if isinstance(p, str) and p.startswith("view://"):
5313
+ return "View"
5314
+ return os.path.basename(str(p)) if p else "Item"
5315
+
5230
5316
  def remove_selected(self):
5231
5317
  s = self.images_list.selectedItems()
5232
5318
  if not s:
@@ -5258,42 +5344,6 @@ class MosaicMasterDialog(QDialog):
5258
5344
  push_cb=self._push_mosaic_to_new_doc).show()
5259
5345
  break
5260
5346
 
5261
- def _resolve_view_title(self, *objs) -> str:
5262
- """
5263
- Try hard to get a meaningful title from any of the passed objects
5264
- (view, subwindow, doc, etc.). Returns a non-empty string or 'Untitled View'.
5265
- """
5266
- def first_str(x):
5267
- return str(x).strip() if x is not None and str(x).strip() else None
5268
-
5269
- for o in objs:
5270
- if o is None:
5271
- continue
5272
- # Methods that return a title
5273
- for meth in ("windowTitle", "title", "displayTitle", "text"):
5274
- fn = getattr(o, meth, None)
5275
- if callable(fn):
5276
- s = first_str(fn())
5277
- if s: return s
5278
- # Properties/attributes that might hold a title
5279
- for attr in ("title", "display_title", "name", "objectName", "display_name"):
5280
- s = first_str(getattr(o, attr, None))
5281
- if s: return s
5282
- # From metadata
5283
- meta = getattr(o, "metadata", None)
5284
- if isinstance(meta, dict):
5285
- s = first_str(meta.get("display_name") or meta.get("title") or meta.get("name"))
5286
- if s: return s
5287
- # From file path-ish things
5288
- for attr in ("file_path", "filepath", "path", "filename"):
5289
- p = getattr(o, attr, None)
5290
- if p:
5291
- base = os.path.basename(str(p))
5292
- s = first_str(base)
5293
- if s: return s
5294
-
5295
- return "Untitled View"
5296
-
5297
5347
  # --- WCS/SIP helpers ---------------------------------------------------------
5298
5348
  def _ensure_image_naxis(self, hdr: dict | fits.Header, shape):
5299
5349
  try:
@@ -5390,7 +5440,7 @@ class MosaicMasterDialog(QDialog):
5390
5440
  self.astap_exe = new_path
5391
5441
  QMessageBox.information(self, "Mosaic Master", "ASTAP path updated successfully.")
5392
5442
  else:
5393
- self.status_label.setText(f"No ASTAP path; blind solving {item['path']}...")
5443
+ self.status_label.setText(f"No ASTAP path; blind solving {self._item_label(item)}...")
5394
5444
  QApplication.processEvents()
5395
5445
  solved_header = self.perform_blind_solve(item)
5396
5446
  if solved_header:
@@ -5399,12 +5449,13 @@ class MosaicMasterDialog(QDialog):
5399
5449
  continue
5400
5450
 
5401
5451
  # Attempt ASTAP solve.
5402
- self.status_label.setText(f"Attempting ASTAP solve for {item['path']}...")
5452
+ self.status_label.setText(f"Attempting ASTAP solve for {self._item_label(item)}...")
5453
+
5403
5454
  QApplication.processEvents()
5404
5455
  solved_header = self.attempt_astap_solve(item)
5405
5456
 
5406
5457
  if solved_header is None:
5407
- self.status_label.setText(f"ASTAP failed for {item['path']}. Falling back to blind solve...")
5458
+ self.status_label.setText(f"ASTAP failed for {self._item_label(item)}. Falling back to blind solve...")
5408
5459
  QApplication.processEvents()
5409
5460
  solved_header = self.perform_blind_solve(item)
5410
5461
 
@@ -5412,7 +5463,8 @@ class MosaicMasterDialog(QDialog):
5412
5463
  solved_header = self._normalize_wcs_header(solved_header, item["image"].shape)
5413
5464
  item["wcs"] = self._build_wcs(solved_header, item["image"].shape)
5414
5465
  else:
5415
- print(f"[Mosaic] Plate solving failed for {item['path']}.")
5466
+ print(f"[Mosaic] Plate solving failed for {self._item_label(item)} ({item.get('path','')}).")
5467
+
5416
5468
 
5417
5469
  # ------------------------------------------------------------
5418
5470
  # 2) Gather WCS-valid panels
@@ -5502,7 +5554,7 @@ class MosaicMasterDialog(QDialog):
5502
5554
  for idx, itm in enumerate(wcs_items):
5503
5555
  arr = itm["image"]
5504
5556
 
5505
- self.status_label.setText(f"Mapping {itm['path']} into mosaic frame...")
5557
+ self.status_label.setText(f"Mapping {self._item_label(itm)} into mosaic frame...")
5506
5558
  QApplication.processEvents()
5507
5559
 
5508
5560
  img_lin = arr.astype(np.float32, copy=False)
@@ -5607,7 +5659,7 @@ class MosaicMasterDialog(QDialog):
5607
5659
  self.final_mosaic += gray_aligned * smooth_mask
5608
5660
  self.weight_mosaic += smooth_mask
5609
5661
 
5610
- self.status_label.setText(f"Processed: {itm['path']}")
5662
+ self.status_label.setText(f"Processed: {self._item_label(itm)}")
5611
5663
  QApplication.processEvents()
5612
5664
 
5613
5665
  # ------------------------------------------------------------
@@ -6497,7 +6549,7 @@ class MosaicMasterDialog(QDialog):
6497
6549
  - If refinement fails, returns None.
6498
6550
  """
6499
6551
  print("\n--- Starting Refined Alignment ---")
6500
- poly_degree = self.settings.value("mosaic/poly_degree", 3, type=int)
6552
+ poly_degree = qs_int(self.settings, "mosaic/poly_degree", 3)
6501
6553
  self.status_label.setText("Refinement: Converting images to grayscale...")
6502
6554
  QApplication.processEvents()
6503
6555