setiastrosuitepro 1.6.4__py3-none-any.whl → 1.6.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- setiastro/images/abeicon.svg +16 -0
- setiastro/images/acv_icon.png +0 -0
- setiastro/images/cosmic.svg +40 -0
- setiastro/images/cosmicsat.svg +24 -0
- setiastro/images/first_quarter.png +0 -0
- setiastro/images/full_moon.png +0 -0
- setiastro/images/graxpert.svg +19 -0
- setiastro/images/last_quarter.png +0 -0
- setiastro/images/linearfit.svg +32 -0
- setiastro/images/new_moon.png +0 -0
- setiastro/images/pixelmath.svg +42 -0
- setiastro/images/waning_crescent_1.png +0 -0
- setiastro/images/waning_crescent_2.png +0 -0
- setiastro/images/waning_crescent_3.png +0 -0
- setiastro/images/waning_crescent_4.png +0 -0
- setiastro/images/waning_crescent_5.png +0 -0
- setiastro/images/waning_gibbous_1.png +0 -0
- setiastro/images/waning_gibbous_2.png +0 -0
- setiastro/images/waning_gibbous_3.png +0 -0
- setiastro/images/waning_gibbous_4.png +0 -0
- setiastro/images/waning_gibbous_5.png +0 -0
- setiastro/images/waxing_crescent_1.png +0 -0
- setiastro/images/waxing_crescent_2.png +0 -0
- setiastro/images/waxing_crescent_3.png +0 -0
- setiastro/images/waxing_crescent_4.png +0 -0
- setiastro/images/waxing_crescent_5.png +0 -0
- setiastro/images/waxing_gibbous_1.png +0 -0
- setiastro/images/waxing_gibbous_2.png +0 -0
- setiastro/images/waxing_gibbous_3.png +0 -0
- setiastro/images/waxing_gibbous_4.png +0 -0
- setiastro/images/waxing_gibbous_5.png +0 -0
- setiastro/qml/ResourceMonitor.qml +84 -82
- setiastro/saspro/__main__.py +19 -0
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/abe.py +37 -4
- setiastro/saspro/aberration_ai.py +237 -21
- setiastro/saspro/acv_exporter.py +379 -0
- setiastro/saspro/add_stars.py +33 -6
- setiastro/saspro/backgroundneutral.py +35 -7
- setiastro/saspro/blemish_blaster.py +4 -1
- setiastro/saspro/blink_comparator_pro.py +74 -24
- setiastro/saspro/clahe.py +4 -1
- setiastro/saspro/continuum_subtract.py +4 -1
- setiastro/saspro/convo.py +4 -1
- setiastro/saspro/cosmicclarity.py +129 -18
- setiastro/saspro/crop_dialog_pro.py +123 -7
- setiastro/saspro/curve_editor_pro.py +109 -42
- setiastro/saspro/doc_manager.py +67 -4
- setiastro/saspro/exoplanet_detector.py +120 -28
- setiastro/saspro/frequency_separation.py +1158 -204
- setiastro/saspro/ghs_dialog_pro.py +81 -16
- setiastro/saspro/graxpert.py +1 -0
- setiastro/saspro/gui/main_window.py +393 -204
- setiastro/saspro/gui/mixins/menu_mixin.py +1 -0
- setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
- setiastro/saspro/gui/mixins/toolbar_mixin.py +356 -12
- setiastro/saspro/gui/mixins/update_mixin.py +138 -36
- setiastro/saspro/gui/mixins/view_mixin.py +42 -0
- setiastro/saspro/halobgon.py +4 -0
- setiastro/saspro/histogram.py +5 -1
- setiastro/saspro/image_combine.py +4 -0
- setiastro/saspro/image_peeker_pro.py +4 -0
- setiastro/saspro/imageops/stretch.py +531 -62
- setiastro/saspro/isophote.py +4 -0
- setiastro/saspro/layers.py +13 -9
- setiastro/saspro/layers_dock.py +183 -3
- setiastro/saspro/legacy/image_manager.py +154 -20
- setiastro/saspro/legacy/numba_utils.py +43 -0
- setiastro/saspro/legacy/xisf.py +240 -98
- setiastro/saspro/live_stacking.py +180 -79
- setiastro/saspro/luminancerecombine.py +228 -27
- setiastro/saspro/mask_creation.py +174 -15
- setiastro/saspro/mfdeconv.py +113 -35
- setiastro/saspro/mfdeconvcudnn.py +119 -70
- setiastro/saspro/mfdeconvsport.py +112 -35
- setiastro/saspro/morphology.py +4 -0
- setiastro/saspro/multiscale_decomp.py +51 -12
- setiastro/saspro/numba_utils.py +72 -2
- setiastro/saspro/ops/commands.py +18 -18
- setiastro/saspro/ops/script_editor.py +5 -2
- setiastro/saspro/ops/scripts.py +3 -0
- setiastro/saspro/perfect_palette_picker.py +37 -3
- setiastro/saspro/plate_solver.py +84 -49
- setiastro/saspro/psf_viewer.py +119 -37
- setiastro/saspro/resources.py +67 -0
- setiastro/saspro/rgbalign.py +4 -0
- setiastro/saspro/selective_color.py +4 -1
- setiastro/saspro/sfcc.py +60 -2
- setiastro/saspro/shortcuts.py +142 -23
- setiastro/saspro/signature_insert.py +692 -33
- setiastro/saspro/stacking_suite.py +1017 -400
- setiastro/saspro/star_alignment.py +4 -1
- setiastro/saspro/star_spikes.py +4 -0
- setiastro/saspro/star_stretch.py +38 -3
- setiastro/saspro/stat_stretch.py +702 -128
- setiastro/saspro/subwindow.py +786 -360
- setiastro/saspro/supernovaasteroidhunter.py +1 -1
- setiastro/saspro/wavescale_hdr.py +4 -1
- setiastro/saspro/wavescalede.py +4 -1
- setiastro/saspro/whitebalance.py +60 -12
- setiastro/saspro/widgets/common_utilities.py +28 -21
- setiastro/saspro/widgets/resource_monitor.py +109 -59
- setiastro/saspro/widgets/spinboxes.py +10 -13
- setiastro/saspro/wimi.py +27 -656
- setiastro/saspro/wims.py +13 -3
- setiastro/saspro/xisf.py +101 -11
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/METADATA +2 -1
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/RECORD +112 -80
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/licenses/license.txt +0 -0
|
@@ -229,6 +229,7 @@ class MenuMixin:
|
|
|
229
229
|
|
|
230
230
|
|
|
231
231
|
m_header = mb.addMenu(self.tr("&Header Mods && Misc"))
|
|
232
|
+
m_header.addAction(self.act_acv_exporter)
|
|
232
233
|
m_header.addAction(self.act_astrobin_exporter)
|
|
233
234
|
m_header.addAction(self.act_batch_convert)
|
|
234
235
|
m_header.addAction(self.act_batch_renamer)
|
|
@@ -9,12 +9,30 @@ from __future__ import annotations
|
|
|
9
9
|
from typing import TYPE_CHECKING
|
|
10
10
|
|
|
11
11
|
from PyQt6.QtCore import Qt, QTimer
|
|
12
|
-
from PyQt6.QtGui import QBrush, QColor, QFont, QPalette
|
|
13
|
-
from PyQt6.QtWidgets import QApplication
|
|
12
|
+
from PyQt6.QtGui import QBrush, QColor, QFont, QPalette, QPainter, QPixmap, QIcon
|
|
13
|
+
from PyQt6.QtWidgets import QApplication, QLabel, QWidget
|
|
14
14
|
|
|
15
15
|
if TYPE_CHECKING:
|
|
16
16
|
pass
|
|
17
17
|
|
|
18
|
+
def _force_mdi_subwindow_flags(sw):
|
|
19
|
+
f = sw.windowFlags()
|
|
20
|
+
|
|
21
|
+
# Clear only the *window type* bits (NOT random low bits)
|
|
22
|
+
f &= ~Qt.WindowType.WindowType_Mask
|
|
23
|
+
|
|
24
|
+
# Force true MDI child type
|
|
25
|
+
f |= Qt.WindowType.SubWindow
|
|
26
|
+
|
|
27
|
+
# Add desired buttons/hints
|
|
28
|
+
f |= (Qt.WindowType.CustomizeWindowHint |
|
|
29
|
+
Qt.WindowType.WindowTitleHint |
|
|
30
|
+
Qt.WindowType.WindowSystemMenuHint |
|
|
31
|
+
Qt.WindowType.WindowMinimizeButtonHint |
|
|
32
|
+
Qt.WindowType.WindowMaximizeButtonHint |
|
|
33
|
+
Qt.WindowType.WindowCloseButtonHint)
|
|
34
|
+
|
|
35
|
+
sw.setWindowFlags(f)
|
|
18
36
|
|
|
19
37
|
class ThemeMixin:
|
|
20
38
|
"""
|
|
@@ -88,7 +106,7 @@ class ThemeMixin:
|
|
|
88
106
|
app.setPalette(self._gray_palette())
|
|
89
107
|
app.setStyleSheet(
|
|
90
108
|
"QToolTip { color: #f0f0f0; background-color: #3a3a3a; border: 1px solid #5a5a5a; }"
|
|
91
|
-
)
|
|
109
|
+
)
|
|
92
110
|
elif mode == "light":
|
|
93
111
|
app.setPalette(self._light_palette())
|
|
94
112
|
app.setStyleSheet(
|
|
@@ -119,6 +137,11 @@ class ThemeMixin:
|
|
|
119
137
|
self._repolish_top_levels()
|
|
120
138
|
self._apply_workspace_theme()
|
|
121
139
|
self._style_mdi_titlebars()
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
self._retint_zoom_icons()
|
|
143
|
+
except Exception:
|
|
144
|
+
pass
|
|
122
145
|
self._menu_view_panels = None
|
|
123
146
|
|
|
124
147
|
try:
|
|
@@ -139,26 +162,149 @@ class ThemeMixin:
|
|
|
139
162
|
w.setUpdatesEnabled(True)
|
|
140
163
|
|
|
141
164
|
def _style_mdi_titlebars(self):
|
|
142
|
-
"""Apply theme-specific styles to MDI subwindow titlebars."""
|
|
143
165
|
mode = self._theme_mode()
|
|
166
|
+
|
|
144
167
|
if mode == "dark":
|
|
145
|
-
base = "#1b1b1b"
|
|
146
|
-
active = "#242424"
|
|
168
|
+
base = "#1b1b1b"
|
|
169
|
+
active = "#242424"
|
|
147
170
|
fg = "#dcdcdc"
|
|
148
171
|
elif mode in ("gray", "custom"):
|
|
149
172
|
base = "#3a3a3a"
|
|
150
173
|
active = "#454545"
|
|
151
174
|
fg = "#f0f0f0"
|
|
152
175
|
else:
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
176
|
+
base = "#eaeaea"
|
|
177
|
+
active = "#ffffff"
|
|
178
|
+
fg = "#141414"
|
|
156
179
|
|
|
180
|
+
# style *our* titlebar only
|
|
157
181
|
self.mdi.setStyleSheet(f"""
|
|
158
|
-
|
|
159
|
-
|
|
182
|
+
QWidget#sas_mdi_titlebar {{
|
|
183
|
+
background: {base};
|
|
184
|
+
}}
|
|
185
|
+
QWidget#sas_mdi_titlebar[active="true"] {{
|
|
186
|
+
background: {active};
|
|
187
|
+
}}
|
|
188
|
+
QLabel#sas_mdi_title_label {{
|
|
189
|
+
color: {fg};
|
|
190
|
+
background: transparent;
|
|
191
|
+
}}
|
|
192
|
+
QWidget#sas_mdi_titlebar QToolButton {{
|
|
193
|
+
color: {fg};
|
|
194
|
+
background: transparent;
|
|
195
|
+
}}
|
|
196
|
+
QWidget#sas_mdi_titlebar QToolButton:hover {{
|
|
197
|
+
background: rgba(255,255,255,0.10);
|
|
198
|
+
}}
|
|
160
199
|
""")
|
|
161
200
|
|
|
201
|
+
|
|
202
|
+
def _fix_mdi_titlebar_emboss(self, fg_hex: str):
|
|
203
|
+
"""
|
|
204
|
+
Fusion/Windows style can draw embossed title text (two-pass).
|
|
205
|
+
In dark themes that can become white-on-white -> 'double text'.
|
|
206
|
+
Force the shadow/emboss colors on the *titlebar widget only*.
|
|
207
|
+
"""
|
|
208
|
+
try:
|
|
209
|
+
fg = QColor(fg_hex)
|
|
210
|
+
except Exception:
|
|
211
|
+
fg = QColor(240, 240, 240)
|
|
212
|
+
|
|
213
|
+
for sw in self.mdi.subWindowList():
|
|
214
|
+
try:
|
|
215
|
+
tb = sw.findChild(QWidget, "qt_mdi_titlebar")
|
|
216
|
+
if tb is None:
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
pal = tb.palette()
|
|
220
|
+
|
|
221
|
+
# Main text
|
|
222
|
+
pal.setColor(QPalette.ColorRole.WindowText, fg)
|
|
223
|
+
pal.setColor(QPalette.ColorRole.Text, fg)
|
|
224
|
+
pal.setColor(QPalette.ColorRole.ButtonText, fg)
|
|
225
|
+
|
|
226
|
+
# Critical: make the embossed/shadow pass dark
|
|
227
|
+
dark = QColor(0, 0, 0)
|
|
228
|
+
pal.setColor(QPalette.ColorRole.Light, dark)
|
|
229
|
+
pal.setColor(QPalette.ColorRole.Midlight, dark)
|
|
230
|
+
pal.setColor(QPalette.ColorRole.Dark, dark)
|
|
231
|
+
pal.setColor(QPalette.ColorRole.Shadow, dark)
|
|
232
|
+
|
|
233
|
+
tb.setPalette(pal)
|
|
234
|
+
|
|
235
|
+
# Also push to the label if present (some styles read it from label)
|
|
236
|
+
lbl = tb.findChild(QLabel)
|
|
237
|
+
if lbl is not None:
|
|
238
|
+
lbl.setPalette(pal)
|
|
239
|
+
except Exception:
|
|
240
|
+
pass
|
|
241
|
+
|
|
242
|
+
def _tint_icon(self, icon: QIcon, color: QColor) -> QIcon:
|
|
243
|
+
"""
|
|
244
|
+
Take an existing icon (often fromTheme) and force a single-color glyph.
|
|
245
|
+
Sets Normal and Active to the same tinted pixmaps to prevent hover flipping.
|
|
246
|
+
"""
|
|
247
|
+
if icon.isNull():
|
|
248
|
+
return icon
|
|
249
|
+
|
|
250
|
+
out = QIcon()
|
|
251
|
+
sizes = [16, 20, 24, 32, 48, 64]
|
|
252
|
+
|
|
253
|
+
for sz in sizes:
|
|
254
|
+
pm = icon.pixmap(sz, sz, QIcon.Mode.Normal, QIcon.State.Off)
|
|
255
|
+
if pm.isNull():
|
|
256
|
+
continue
|
|
257
|
+
|
|
258
|
+
tinted = QPixmap(pm.size())
|
|
259
|
+
tinted.fill(Qt.GlobalColor.transparent)
|
|
260
|
+
|
|
261
|
+
p = QPainter(tinted)
|
|
262
|
+
p.setRenderHint(QPainter.RenderHint.Antialiasing, True)
|
|
263
|
+
p.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform, True)
|
|
264
|
+
|
|
265
|
+
# Use the original alpha as a mask, fill with our color
|
|
266
|
+
p.drawPixmap(0, 0, pm)
|
|
267
|
+
p.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceIn)
|
|
268
|
+
p.fillRect(tinted.rect(), color)
|
|
269
|
+
p.end()
|
|
270
|
+
|
|
271
|
+
# Normal + Active -> same pixmap (prevents hover flip)
|
|
272
|
+
out.addPixmap(tinted, QIcon.Mode.Normal, QIcon.State.Off)
|
|
273
|
+
out.addPixmap(tinted, QIcon.Mode.Active, QIcon.State.Off)
|
|
274
|
+
|
|
275
|
+
# Disabled: slightly dimmer (optional)
|
|
276
|
+
dis = QColor(color)
|
|
277
|
+
dis.setAlphaF(0.45)
|
|
278
|
+
dispm = QPixmap(tinted.size())
|
|
279
|
+
dispm.fill(Qt.GlobalColor.transparent)
|
|
280
|
+
p2 = QPainter(dispm)
|
|
281
|
+
p2.drawPixmap(0, 0, tinted)
|
|
282
|
+
p2.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceIn)
|
|
283
|
+
p2.fillRect(dispm.rect(), dis)
|
|
284
|
+
p2.end()
|
|
285
|
+
out.addPixmap(dispm, QIcon.Mode.Disabled, QIcon.State.Off)
|
|
286
|
+
|
|
287
|
+
return out
|
|
288
|
+
|
|
289
|
+
def _retint_zoom_icons(self):
|
|
290
|
+
"""
|
|
291
|
+
Retint only the zoom actions (the ones built from QIcon.fromTheme).
|
|
292
|
+
Call after app palette is applied.
|
|
293
|
+
"""
|
|
294
|
+
pal = QApplication.palette()
|
|
295
|
+
glyph = pal.color(QPalette.ColorRole.ButtonText) # or Text; ButtonText tends to match toolbars well
|
|
296
|
+
|
|
297
|
+
for name in ("act_zoom_out", "act_zoom_in", "act_zoom_1_1", "act_zoom_fit"):
|
|
298
|
+
act = getattr(self, name, None)
|
|
299
|
+
if act is None:
|
|
300
|
+
continue
|
|
301
|
+
|
|
302
|
+
# stash original once so repeated theme flips don't re-tint a tinted icon
|
|
303
|
+
if not hasattr(act, "_base_icon"):
|
|
304
|
+
act._base_icon = act.icon()
|
|
305
|
+
|
|
306
|
+
act.setIcon(self._tint_icon(act._base_icon, glyph))
|
|
307
|
+
|
|
162
308
|
def _dark_palette(self) -> QPalette:
|
|
163
309
|
"""Create a dark theme palette."""
|
|
164
310
|
p = QPalette()
|
|
@@ -172,7 +318,7 @@ class ThemeMixin:
|
|
|
172
318
|
hi = QColor(30, 144, 255) # highlight (dodger blue)
|
|
173
319
|
|
|
174
320
|
p.setColor(QPalette.ColorRole.Window, panel)
|
|
175
|
-
p.setColor(QPalette.ColorRole.WindowText,
|
|
321
|
+
p.setColor(QPalette.ColorRole.WindowText, QColor(255, 255, 255))
|
|
176
322
|
p.setColor(QPalette.ColorRole.Base, bg)
|
|
177
323
|
p.setColor(QPalette.ColorRole.AlternateBase, altbase)
|
|
178
324
|
p.setColor(QPalette.ColorRole.ToolTipBase, panel)
|
|
@@ -270,7 +416,7 @@ class ThemeMixin:
|
|
|
270
416
|
link = QColor(120, 170, 255)
|
|
271
417
|
linkv = QColor(180, 150, 255)
|
|
272
418
|
hi = QColor(95, 145, 230)
|
|
273
|
-
hitxt = QColor(
|
|
419
|
+
hitxt = QColor(20, 20, 20)
|
|
274
420
|
|
|
275
421
|
# Core roles
|
|
276
422
|
p.setColor(QPalette.ColorRole.Window, window)
|
|
@@ -300,7 +446,7 @@ class ThemeMixin:
|
|
|
300
446
|
p.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.WindowText, dis)
|
|
301
447
|
p.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Base, QColor(58, 58, 58))
|
|
302
448
|
p.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Highlight, QColor(80, 80, 80))
|
|
303
|
-
p.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.HighlightedText, QColor(
|
|
449
|
+
p.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.HighlightedText, QColor(20, 20, 20))
|
|
304
450
|
|
|
305
451
|
return p
|
|
306
452
|
|
|
@@ -203,16 +203,16 @@ class ToolbarMixin:
|
|
|
203
203
|
tb_fn.addAction(self.act_convo)
|
|
204
204
|
tb_fn.addAction(self.act_extract_luma)
|
|
205
205
|
|
|
206
|
-
btn_luma = tb_fn.widgetForAction(self.act_extract_luma)
|
|
207
|
-
if isinstance(btn_luma, QToolButton):
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
206
|
+
#btn_luma = tb_fn.widgetForAction(self.act_extract_luma)
|
|
207
|
+
#if isinstance(btn_luma, QToolButton):
|
|
208
|
+
# luma_menu = QMenu(btn_luma)
|
|
209
|
+
# luma_menu.addActions(self._luma_group.actions())
|
|
210
|
+
# btn_luma.setMenu(luma_menu)
|
|
211
|
+
# btn_luma.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
|
|
212
|
+
# btn_luma.setStyleSheet("""
|
|
213
|
+
# QToolButton { color: #dcdcdc; }
|
|
214
|
+
# QToolButton:pressed, QToolButton:checked { color: #DAA520; font-weight: 600; }
|
|
215
|
+
# """)
|
|
216
216
|
|
|
217
217
|
tb_fn.addAction(self.act_recombine_luma)
|
|
218
218
|
tb_fn.addAction(self.act_rgb_extract)
|
|
@@ -361,9 +361,25 @@ class ToolbarMixin:
|
|
|
361
361
|
except Exception:
|
|
362
362
|
pass
|
|
363
363
|
|
|
364
|
+
|
|
365
|
+
tb_hidden = DraggableToolBar(self.tr("Hidden"), self)
|
|
366
|
+
tb_hidden.setObjectName("Hidden")
|
|
367
|
+
tb_hidden.setSettingsKey("Toolbar/Hidden")
|
|
368
|
+
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_hidden)
|
|
369
|
+
tb_hidden.setVisible(False) # <- always hidden
|
|
370
|
+
|
|
364
371
|
# This can move actions between toolbars, so do it after each toolbar has its base order restored.
|
|
365
372
|
self._restore_toolbar_memberships()
|
|
366
373
|
|
|
374
|
+
# Migrate legacy per-toolbar Hidden lists into the new Hidden toolbar
|
|
375
|
+
self._migrate_old_hidden_sets_to_hidden_toolbar()
|
|
376
|
+
|
|
377
|
+
# (Optional) Re-apply per-toolbar order after migration (since we removed actions)
|
|
378
|
+
for _tb in self.findChildren(DraggableToolBar):
|
|
379
|
+
key = getattr(_tb, "_settings_key", None)
|
|
380
|
+
if key:
|
|
381
|
+
self._restore_toolbar_order(_tb, str(key))
|
|
382
|
+
|
|
367
383
|
# Re-apply hidden state AFTER memberships (actions may have moved toolbars).
|
|
368
384
|
# This also guarantees correctness even if any toolbar was rebuilt/adjusted internally.
|
|
369
385
|
for _tb in self.findChildren(DraggableToolBar):
|
|
@@ -372,7 +388,9 @@ class ToolbarMixin:
|
|
|
372
388
|
except Exception:
|
|
373
389
|
pass
|
|
374
390
|
|
|
391
|
+
# Rebind ALL dropdowns after reorder/membership moves
|
|
375
392
|
self._rebind_view_dropdowns()
|
|
393
|
+
self._rebind_extract_luma_dropdown()
|
|
376
394
|
|
|
377
395
|
def _toolbar_containing_action(self, action: QAction):
|
|
378
396
|
from setiastro.saspro.shortcuts import DraggableToolBar
|
|
@@ -381,6 +399,140 @@ class ToolbarMixin:
|
|
|
381
399
|
return tb
|
|
382
400
|
return None
|
|
383
401
|
|
|
402
|
+
def _rebind_extract_luma_dropdown(self):
|
|
403
|
+
from PyQt6.QtWidgets import QMenu, QToolButton
|
|
404
|
+
from setiastro.saspro.luminancerecombine import LUMA_PROFILES
|
|
405
|
+
|
|
406
|
+
tb = self._toolbar_containing_action(self.act_extract_luma)
|
|
407
|
+
if not tb:
|
|
408
|
+
return
|
|
409
|
+
|
|
410
|
+
btn = tb.widgetForAction(self.act_extract_luma)
|
|
411
|
+
if not isinstance(btn, QToolButton):
|
|
412
|
+
return
|
|
413
|
+
|
|
414
|
+
menu = QMenu(btn)
|
|
415
|
+
|
|
416
|
+
# Ensure cache exists (created in _create_actions(), but be defensive)
|
|
417
|
+
if not hasattr(self, "_luma_sensor_actions"):
|
|
418
|
+
self._luma_sensor_actions = {} # key -> QAction
|
|
419
|
+
|
|
420
|
+
cur_method = str(getattr(self, "luma_method", "rec709"))
|
|
421
|
+
|
|
422
|
+
# ============================================================
|
|
423
|
+
# PATCH SITE #1 (Standard menu) <-- THIS IS WHERE "C" GOES
|
|
424
|
+
# Find your old block:
|
|
425
|
+
#
|
|
426
|
+
# # ---- Standard (use your QActionGroup, keep it simple) ----
|
|
427
|
+
# if getattr(self, "_luma_group", None) is not None:
|
|
428
|
+
# std_menu = QMenu(self.tr("Standard"), menu)
|
|
429
|
+
# std_menu.addActions(self._luma_group.actions())
|
|
430
|
+
# menu.addMenu(std_menu)
|
|
431
|
+
#
|
|
432
|
+
# Replace it with the block below.
|
|
433
|
+
# ============================================================
|
|
434
|
+
if getattr(self, "_luma_group", None) is not None:
|
|
435
|
+
# Sync standard checked states from self.luma_method
|
|
436
|
+
for a in self._luma_group.actions():
|
|
437
|
+
data = str(a.data() or "")
|
|
438
|
+
if data.startswith("sensor:"):
|
|
439
|
+
continue # standards only in Standard menu
|
|
440
|
+
a.blockSignals(True)
|
|
441
|
+
try:
|
|
442
|
+
a.setChecked(data == cur_method)
|
|
443
|
+
finally:
|
|
444
|
+
a.blockSignals(False)
|
|
445
|
+
|
|
446
|
+
# Add ONLY standard actions to the Standard menu (exclude sensors)
|
|
447
|
+
std_menu = QMenu(self.tr("Standard"), menu)
|
|
448
|
+
for a in self._luma_group.actions():
|
|
449
|
+
data = str(a.data() or "")
|
|
450
|
+
if data.startswith("sensor:"):
|
|
451
|
+
continue
|
|
452
|
+
std_menu.addAction(a)
|
|
453
|
+
menu.addMenu(std_menu)
|
|
454
|
+
|
|
455
|
+
# ---- Sensors (nested by category path) ----
|
|
456
|
+
sensors_root = QMenu(self.tr("Sensors"), menu)
|
|
457
|
+
|
|
458
|
+
# Build tree of submenus keyed by "Sensors/<path>"
|
|
459
|
+
submenu_cache: dict[str, QMenu] = {}
|
|
460
|
+
|
|
461
|
+
def get_or_make_path(root_menu: QMenu, path: str) -> QMenu:
|
|
462
|
+
parts = [p for p in path.split("/") if p.strip()]
|
|
463
|
+
cur = root_menu
|
|
464
|
+
acc = ""
|
|
465
|
+
for part in parts:
|
|
466
|
+
acc = f"{acc}/{part}" if acc else part
|
|
467
|
+
if acc not in submenu_cache:
|
|
468
|
+
sm = QMenu(self.tr(part), cur)
|
|
469
|
+
cur.addMenu(sm)
|
|
470
|
+
submenu_cache[acc] = sm
|
|
471
|
+
cur = submenu_cache[acc]
|
|
472
|
+
return cur
|
|
473
|
+
|
|
474
|
+
any_sensor = False
|
|
475
|
+
for key, prof in LUMA_PROFILES.items():
|
|
476
|
+
if not str(key).startswith("sensor:"):
|
|
477
|
+
continue
|
|
478
|
+
|
|
479
|
+
cat = str(prof.get("category", "Sensors/Other"))
|
|
480
|
+
group_path = cat.split("Sensors/", 1)[1] if "Sensors/" in cat else cat
|
|
481
|
+
parent_menu = get_or_make_path(sensors_root, group_path)
|
|
482
|
+
|
|
483
|
+
display_name = key.split("sensor:", 1)[1].strip()
|
|
484
|
+
desc = str(prof.get("description", display_name))
|
|
485
|
+
info = str(prof.get("info", "")).strip()
|
|
486
|
+
|
|
487
|
+
# ============================================================
|
|
488
|
+
# PATCH SITE #2 (Sensors become mutually exclusive with standards)
|
|
489
|
+
# - Cache the QAction so we don't pile up connections/actions
|
|
490
|
+
# - Add it to the SAME QActionGroup (exclusive) as standards
|
|
491
|
+
# - Do NOT use _pick_sensor; rely on QActionGroup.triggered
|
|
492
|
+
# ============================================================
|
|
493
|
+
act = self._luma_sensor_actions.get(key)
|
|
494
|
+
if act is None:
|
|
495
|
+
act = parent_menu.addAction(self.tr(display_name))
|
|
496
|
+
act.setCheckable(True)
|
|
497
|
+
act.setData(key) # IMPORTANT: enables group.triggered to set luma_method
|
|
498
|
+
|
|
499
|
+
# Put sensors into the SAME exclusive group so selecting one
|
|
500
|
+
# deselects everything else (standards and other sensors).
|
|
501
|
+
if getattr(self, "_luma_group", None) is not None:
|
|
502
|
+
self._luma_group.addAction(act)
|
|
503
|
+
|
|
504
|
+
self._luma_sensor_actions[key] = act
|
|
505
|
+
else:
|
|
506
|
+
# Reuse action, but re-add it to the correct submenu location
|
|
507
|
+
parent_menu.addAction(act)
|
|
508
|
+
act.setText(self.tr(display_name))
|
|
509
|
+
|
|
510
|
+
# Update UI info each bind (safe if translations change)
|
|
511
|
+
if info:
|
|
512
|
+
act.setStatusTip(info)
|
|
513
|
+
act.setToolTip(f"{desc}\n{info}")
|
|
514
|
+
else:
|
|
515
|
+
act.setToolTip(desc)
|
|
516
|
+
|
|
517
|
+
# Checked state reflects current selection
|
|
518
|
+
act.blockSignals(True)
|
|
519
|
+
try:
|
|
520
|
+
act.setChecked(cur_method == str(key))
|
|
521
|
+
finally:
|
|
522
|
+
act.blockSignals(False)
|
|
523
|
+
|
|
524
|
+
any_sensor = True
|
|
525
|
+
|
|
526
|
+
if any_sensor:
|
|
527
|
+
menu.addMenu(sensors_root)
|
|
528
|
+
|
|
529
|
+
btn.setMenu(menu)
|
|
530
|
+
btn.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
|
|
531
|
+
btn.setStyleSheet("""
|
|
532
|
+
QToolButton { color: #dcdcdc; }
|
|
533
|
+
QToolButton:pressed, QToolButton:checked { color: #DAA520; font-weight: 600; }
|
|
534
|
+
""")
|
|
535
|
+
|
|
384
536
|
|
|
385
537
|
def _rebind_view_dropdowns(self):
|
|
386
538
|
"""
|
|
@@ -767,6 +919,7 @@ class ToolbarMixin:
|
|
|
767
919
|
self.luma_method = getattr(self, "luma_method", "rec709") # default
|
|
768
920
|
self._luma_group = QActionGroup(self)
|
|
769
921
|
self._luma_group.setExclusive(True)
|
|
922
|
+
self._luma_sensor_actions = {} # key -> QAction
|
|
770
923
|
|
|
771
924
|
def _mk(method_key, text):
|
|
772
925
|
act = QAction(text, self, checkable=True)
|
|
@@ -786,8 +939,10 @@ class ToolbarMixin:
|
|
|
786
939
|
|
|
787
940
|
# update method when user picks from the menu
|
|
788
941
|
def _on_luma_pick(act):
|
|
789
|
-
|
|
790
|
-
|
|
942
|
+
key = act.data()
|
|
943
|
+
if key is None:
|
|
944
|
+
return
|
|
945
|
+
self.luma_method = str(key)
|
|
791
946
|
try:
|
|
792
947
|
self.settings.setValue("ui/luminance_method", self.luma_method)
|
|
793
948
|
except Exception:
|
|
@@ -1105,6 +1260,13 @@ class ToolbarMixin:
|
|
|
1105
1260
|
self.act_copy_astrometry = QAction(self.tr("Copy Astrometric Solution..."), self)
|
|
1106
1261
|
self.act_copy_astrometry.triggered.connect(self._open_copy_astrometry)
|
|
1107
1262
|
|
|
1263
|
+
self.act_acv_exporter = QAction(self.tr("Astro Catalogue Viewer Exporter..."), self)
|
|
1264
|
+
self.act_acv_exporter.setIconVisibleInMenu(True)
|
|
1265
|
+
self.act_acv_exporter.setStatusTip(self.tr("Export current view into Astro Catalogue Viewer folders"))
|
|
1266
|
+
self.act_acv_exporter.triggered.connect(self._open_acv_exporter)
|
|
1267
|
+
|
|
1268
|
+
|
|
1269
|
+
|
|
1108
1270
|
# Create Mask
|
|
1109
1271
|
self.act_create_mask = QAction(QIcon(maskcreate_path), self.tr("Create Mask..."), self)
|
|
1110
1272
|
self.act_create_mask.setIconVisibleInMenu(True)
|
|
@@ -1356,6 +1518,188 @@ class ToolbarMixin:
|
|
|
1356
1518
|
if key:
|
|
1357
1519
|
self._restore_toolbar_order(tb, str(key))
|
|
1358
1520
|
|
|
1521
|
+
def _hidden_toolbar(self):
|
|
1522
|
+
from setiastro.saspro.shortcuts import DraggableToolBar
|
|
1523
|
+
for tb in self.findChildren(DraggableToolBar):
|
|
1524
|
+
if getattr(tb, "_settings_key", None) == "Toolbar/Hidden" or tb.objectName() == "Hidden":
|
|
1525
|
+
return tb
|
|
1526
|
+
return None
|
|
1527
|
+
|
|
1528
|
+
def _toolbar_by_settings_key(self, key: str):
|
|
1529
|
+
from setiastro.saspro.shortcuts import DraggableToolBar
|
|
1530
|
+
for tb in self.findChildren(DraggableToolBar):
|
|
1531
|
+
if getattr(tb, "_settings_key", None) == key:
|
|
1532
|
+
return tb
|
|
1533
|
+
return None
|
|
1534
|
+
|
|
1535
|
+
def _action_cid(self, act):
|
|
1536
|
+
return str(act.property("command_id") or act.objectName() or "")
|
|
1537
|
+
|
|
1538
|
+
def _hide_action_to_hidden_toolbar(self, act):
|
|
1539
|
+
"""
|
|
1540
|
+
Move action to the Hidden toolbar, remembering where it came from.
|
|
1541
|
+
"""
|
|
1542
|
+
tb_hidden = self._hidden_toolbar()
|
|
1543
|
+
if not tb_hidden:
|
|
1544
|
+
return
|
|
1545
|
+
|
|
1546
|
+
cid = self._action_cid(act)
|
|
1547
|
+
if not cid:
|
|
1548
|
+
return
|
|
1549
|
+
|
|
1550
|
+
# Find current toolbar (if any) and remember it
|
|
1551
|
+
cur_tb = self._toolbar_containing_action(act)
|
|
1552
|
+
if cur_tb and getattr(cur_tb, "_settings_key", None) and cur_tb is not tb_hidden:
|
|
1553
|
+
prev_key = str(cur_tb._settings_key)
|
|
1554
|
+
|
|
1555
|
+
# Persist "previous toolbar" so unhide can restore
|
|
1556
|
+
try:
|
|
1557
|
+
raw = self.settings.value("Toolbar/HiddenPrev", "", type=str) or ""
|
|
1558
|
+
except Exception:
|
|
1559
|
+
raw = ""
|
|
1560
|
+
try:
|
|
1561
|
+
prev = json.loads(raw) if raw else {}
|
|
1562
|
+
except Exception:
|
|
1563
|
+
prev = {}
|
|
1564
|
+
prev[cid] = prev_key
|
|
1565
|
+
self.settings.setValue("Toolbar/HiddenPrev", json.dumps(prev))
|
|
1566
|
+
|
|
1567
|
+
# Remove from all toolbars, add to hidden
|
|
1568
|
+
from setiastro.saspro.shortcuts import DraggableToolBar
|
|
1569
|
+
for t in self.findChildren(DraggableToolBar):
|
|
1570
|
+
if act in t.actions():
|
|
1571
|
+
t.removeAction(act)
|
|
1572
|
+
|
|
1573
|
+
tb_hidden.addAction(act)
|
|
1574
|
+
|
|
1575
|
+
# Update assignment mapping so restore works on next launch
|
|
1576
|
+
tb_hidden._update_assignment_for_action(act)
|
|
1577
|
+
|
|
1578
|
+
# Persist ordering (optional but nice)
|
|
1579
|
+
try:
|
|
1580
|
+
tb_hidden._persist_order()
|
|
1581
|
+
except Exception:
|
|
1582
|
+
pass
|
|
1583
|
+
if cur_tb:
|
|
1584
|
+
try:
|
|
1585
|
+
cur_tb._persist_order()
|
|
1586
|
+
except Exception:
|
|
1587
|
+
pass
|
|
1588
|
+
|
|
1589
|
+
def _unhide_action_from_hidden_toolbar(self, act):
|
|
1590
|
+
"""
|
|
1591
|
+
Move action out of Hidden toolbar back to its remembered toolbar.
|
|
1592
|
+
Falls back to Toolbar/View if missing.
|
|
1593
|
+
"""
|
|
1594
|
+
tb_hidden = self._hidden_toolbar()
|
|
1595
|
+
if not tb_hidden:
|
|
1596
|
+
return
|
|
1597
|
+
|
|
1598
|
+
cid = self._action_cid(act)
|
|
1599
|
+
if not cid:
|
|
1600
|
+
return
|
|
1601
|
+
|
|
1602
|
+
# Load prev mapping
|
|
1603
|
+
try:
|
|
1604
|
+
raw = self.settings.value("Toolbar/HiddenPrev", "", type=str) or ""
|
|
1605
|
+
except Exception:
|
|
1606
|
+
raw = ""
|
|
1607
|
+
try:
|
|
1608
|
+
prev = json.loads(raw) if raw else {}
|
|
1609
|
+
except Exception:
|
|
1610
|
+
prev = {}
|
|
1611
|
+
|
|
1612
|
+
target_key = prev.get(cid) or "Toolbar/View"
|
|
1613
|
+
tb_target = self._toolbar_by_settings_key(target_key) or self._toolbar_by_settings_key("Toolbar/View")
|
|
1614
|
+
if not tb_target:
|
|
1615
|
+
return
|
|
1616
|
+
|
|
1617
|
+
tb_hidden.removeAction(act)
|
|
1618
|
+
tb_target.addAction(act)
|
|
1619
|
+
|
|
1620
|
+
# Update assignment mapping
|
|
1621
|
+
tb_target._update_assignment_for_action(act)
|
|
1622
|
+
|
|
1623
|
+
# Cleanup prev mapping (optional)
|
|
1624
|
+
if cid in prev:
|
|
1625
|
+
prev.pop(cid, None)
|
|
1626
|
+
self.settings.setValue("Toolbar/HiddenPrev", json.dumps(prev))
|
|
1627
|
+
|
|
1628
|
+
try:
|
|
1629
|
+
tb_target._persist_order()
|
|
1630
|
+
tb_hidden._persist_order()
|
|
1631
|
+
except Exception:
|
|
1632
|
+
pass
|
|
1633
|
+
|
|
1634
|
+
def _migrate_old_hidden_sets_to_hidden_toolbar(self):
|
|
1635
|
+
"""
|
|
1636
|
+
One-time migration:
|
|
1637
|
+
Old system: per-toolbar QSettings lists at "<ToolbarKey>/Hidden" storing action IDs
|
|
1638
|
+
New system: move those actions into the dedicated Hidden toolbar (Toolbar/Hidden)
|
|
1639
|
+
and persist membership via Toolbar/Assignments.
|
|
1640
|
+
"""
|
|
1641
|
+
if not hasattr(self, "settings"):
|
|
1642
|
+
return
|
|
1643
|
+
|
|
1644
|
+
# Run once
|
|
1645
|
+
if self.settings.value("Toolbar/HiddenMigrationDone", False, type=bool):
|
|
1646
|
+
return
|
|
1647
|
+
|
|
1648
|
+
tb_hidden = self._hidden_toolbar()
|
|
1649
|
+
if not tb_hidden:
|
|
1650
|
+
return
|
|
1651
|
+
|
|
1652
|
+
from setiastro.saspro.shortcuts import DraggableToolBar
|
|
1653
|
+
|
|
1654
|
+
# If Hidden toolbar already has actions, assume user is already on new system
|
|
1655
|
+
# but we still can migrate any remaining old keys.
|
|
1656
|
+
# (We won't early-return.)
|
|
1657
|
+
|
|
1658
|
+
for tb in self.findChildren(DraggableToolBar):
|
|
1659
|
+
k = getattr(tb, "_settings_key", None)
|
|
1660
|
+
if not k or str(k) == "Toolbar/Hidden":
|
|
1661
|
+
continue
|
|
1662
|
+
|
|
1663
|
+
# Old storage key
|
|
1664
|
+
old_key = f"{k}/Hidden"
|
|
1665
|
+
|
|
1666
|
+
# Read old hidden list (Qt backend may return list OR str)
|
|
1667
|
+
try:
|
|
1668
|
+
raw = self.settings.value(old_key, [], type=list)
|
|
1669
|
+
except Exception:
|
|
1670
|
+
raw = self.settings.value(old_key, []) # fallback
|
|
1671
|
+
|
|
1672
|
+
if not raw:
|
|
1673
|
+
continue
|
|
1674
|
+
|
|
1675
|
+
if isinstance(raw, str):
|
|
1676
|
+
raw_list = [raw]
|
|
1677
|
+
else:
|
|
1678
|
+
try:
|
|
1679
|
+
raw_list = list(raw)
|
|
1680
|
+
except Exception:
|
|
1681
|
+
raw_list = []
|
|
1682
|
+
|
|
1683
|
+
hidden_ids = {str(x) for x in raw_list if str(x).strip()}
|
|
1684
|
+
if not hidden_ids:
|
|
1685
|
+
# Clear junk so we don’t keep trying
|
|
1686
|
+
self.settings.setValue(old_key, [])
|
|
1687
|
+
continue
|
|
1688
|
+
|
|
1689
|
+
# Move matching actions into Hidden toolbar
|
|
1690
|
+
for act in list(tb.actions()):
|
|
1691
|
+
if act is None or act.isSeparator():
|
|
1692
|
+
continue
|
|
1693
|
+
cid = str(act.property("command_id") or act.objectName() or "")
|
|
1694
|
+
if cid and cid in hidden_ids:
|
|
1695
|
+
self._hide_action_to_hidden_toolbar(act)
|
|
1696
|
+
|
|
1697
|
+
# Clear old storage so you don't keep re-migrating
|
|
1698
|
+
self.settings.setValue(old_key, [])
|
|
1699
|
+
|
|
1700
|
+
# Mark migration complete
|
|
1701
|
+
self.settings.setValue("Toolbar/HiddenMigrationDone", True)
|
|
1702
|
+
|
|
1359
1703
|
|
|
1360
1704
|
def update_undo_redo_action_labels(self):
|
|
1361
1705
|
if not hasattr(self, "act_undo"): # not built yet
|