setiastrosuitepro 1.6.0__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/__init__.py +2 -0
- setiastro/saspro/__init__.py +20 -0
- setiastro/saspro/__main__.py +784 -0
- setiastro/saspro/_generated/__init__.py +7 -0
- setiastro/saspro/_generated/build_info.py +2 -0
- setiastro/saspro/abe.py +1295 -0
- setiastro/saspro/abe_preset.py +196 -0
- setiastro/saspro/aberration_ai.py +694 -0
- setiastro/saspro/aberration_ai_preset.py +224 -0
- setiastro/saspro/accel_installer.py +218 -0
- setiastro/saspro/accel_workers.py +30 -0
- setiastro/saspro/add_stars.py +621 -0
- setiastro/saspro/astrobin_exporter.py +1007 -0
- setiastro/saspro/astrospike.py +153 -0
- setiastro/saspro/astrospike_python.py +1839 -0
- setiastro/saspro/autostretch.py +196 -0
- setiastro/saspro/backgroundneutral.py +560 -0
- setiastro/saspro/batch_convert.py +325 -0
- setiastro/saspro/batch_renamer.py +519 -0
- setiastro/saspro/blemish_blaster.py +488 -0
- setiastro/saspro/blink_comparator_pro.py +2923 -0
- setiastro/saspro/bundles.py +61 -0
- setiastro/saspro/bundles_dock.py +114 -0
- setiastro/saspro/cheat_sheet.py +168 -0
- setiastro/saspro/clahe.py +342 -0
- setiastro/saspro/comet_stacking.py +1377 -0
- setiastro/saspro/config.py +38 -0
- setiastro/saspro/config_bootstrap.py +40 -0
- setiastro/saspro/config_manager.py +316 -0
- setiastro/saspro/continuum_subtract.py +1617 -0
- setiastro/saspro/convo.py +1397 -0
- setiastro/saspro/convo_preset.py +414 -0
- setiastro/saspro/copyastro.py +187 -0
- setiastro/saspro/cosmicclarity.py +1564 -0
- setiastro/saspro/cosmicclarity_preset.py +407 -0
- setiastro/saspro/crop_dialog_pro.py +948 -0
- setiastro/saspro/crop_preset.py +189 -0
- setiastro/saspro/curve_editor_pro.py +2544 -0
- setiastro/saspro/curves_preset.py +375 -0
- setiastro/saspro/debayer.py +670 -0
- setiastro/saspro/debug_utils.py +29 -0
- setiastro/saspro/dnd_mime.py +35 -0
- setiastro/saspro/doc_manager.py +2634 -0
- setiastro/saspro/exoplanet_detector.py +2166 -0
- setiastro/saspro/file_utils.py +284 -0
- setiastro/saspro/fitsmodifier.py +744 -0
- setiastro/saspro/free_torch_memory.py +48 -0
- setiastro/saspro/frequency_separation.py +1343 -0
- setiastro/saspro/function_bundle.py +1594 -0
- setiastro/saspro/ghs_dialog_pro.py +660 -0
- setiastro/saspro/ghs_preset.py +284 -0
- setiastro/saspro/graxpert.py +634 -0
- setiastro/saspro/graxpert_preset.py +287 -0
- setiastro/saspro/gui/__init__.py +0 -0
- setiastro/saspro/gui/main_window.py +8494 -0
- setiastro/saspro/gui/mixins/__init__.py +33 -0
- setiastro/saspro/gui/mixins/dock_mixin.py +263 -0
- setiastro/saspro/gui/mixins/file_mixin.py +445 -0
- setiastro/saspro/gui/mixins/geometry_mixin.py +403 -0
- setiastro/saspro/gui/mixins/header_mixin.py +441 -0
- setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
- setiastro/saspro/gui/mixins/menu_mixin.py +361 -0
- setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +1324 -0
- setiastro/saspro/gui/mixins/update_mixin.py +309 -0
- setiastro/saspro/gui/mixins/view_mixin.py +435 -0
- setiastro/saspro/halobgon.py +462 -0
- setiastro/saspro/header_viewer.py +445 -0
- setiastro/saspro/headless_utils.py +88 -0
- setiastro/saspro/histogram.py +753 -0
- setiastro/saspro/history_explorer.py +939 -0
- setiastro/saspro/image_combine.py +414 -0
- setiastro/saspro/image_peeker_pro.py +1596 -0
- setiastro/saspro/imageops/__init__.py +37 -0
- setiastro/saspro/imageops/mdi_snap.py +292 -0
- setiastro/saspro/imageops/scnr.py +36 -0
- setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
- setiastro/saspro/imageops/stretch.py +244 -0
- setiastro/saspro/isophote.py +1179 -0
- setiastro/saspro/layers.py +208 -0
- setiastro/saspro/layers_dock.py +714 -0
- setiastro/saspro/lazy_imports.py +193 -0
- setiastro/saspro/legacy/__init__.py +2 -0
- setiastro/saspro/legacy/image_manager.py +2226 -0
- setiastro/saspro/legacy/numba_utils.py +3659 -0
- setiastro/saspro/legacy/xisf.py +1071 -0
- setiastro/saspro/linear_fit.py +534 -0
- setiastro/saspro/live_stacking.py +1830 -0
- setiastro/saspro/log_bus.py +5 -0
- setiastro/saspro/logging_config.py +460 -0
- setiastro/saspro/luminancerecombine.py +309 -0
- setiastro/saspro/main_helpers.py +201 -0
- setiastro/saspro/mask_creation.py +928 -0
- setiastro/saspro/masks_core.py +56 -0
- setiastro/saspro/mdi_widgets.py +353 -0
- setiastro/saspro/memory_utils.py +666 -0
- setiastro/saspro/metadata_patcher.py +75 -0
- setiastro/saspro/mfdeconv.py +3826 -0
- setiastro/saspro/mfdeconv_earlystop.py +71 -0
- setiastro/saspro/mfdeconvcudnn.py +3263 -0
- setiastro/saspro/mfdeconvsport.py +2382 -0
- setiastro/saspro/minorbodycatalog.py +567 -0
- setiastro/saspro/morphology.py +382 -0
- setiastro/saspro/multiscale_decomp.py +1290 -0
- setiastro/saspro/nbtorgb_stars.py +531 -0
- setiastro/saspro/numba_utils.py +3044 -0
- setiastro/saspro/numba_warmup.py +141 -0
- setiastro/saspro/ops/__init__.py +9 -0
- setiastro/saspro/ops/command_help_dialog.py +623 -0
- setiastro/saspro/ops/command_runner.py +217 -0
- setiastro/saspro/ops/commands.py +1594 -0
- setiastro/saspro/ops/script_editor.py +1102 -0
- setiastro/saspro/ops/scripts.py +1413 -0
- setiastro/saspro/ops/settings.py +560 -0
- setiastro/saspro/parallel_utils.py +554 -0
- setiastro/saspro/pedestal.py +121 -0
- setiastro/saspro/perfect_palette_picker.py +1053 -0
- setiastro/saspro/pipeline.py +110 -0
- setiastro/saspro/pixelmath.py +1600 -0
- setiastro/saspro/plate_solver.py +2435 -0
- setiastro/saspro/project_io.py +797 -0
- setiastro/saspro/psf_utils.py +136 -0
- setiastro/saspro/psf_viewer.py +549 -0
- setiastro/saspro/pyi_rthook_astroquery.py +95 -0
- setiastro/saspro/remove_green.py +314 -0
- setiastro/saspro/remove_stars.py +1625 -0
- setiastro/saspro/remove_stars_preset.py +404 -0
- setiastro/saspro/resources.py +472 -0
- setiastro/saspro/rgb_combination.py +207 -0
- setiastro/saspro/rgb_extract.py +19 -0
- setiastro/saspro/rgbalign.py +723 -0
- setiastro/saspro/runtime_imports.py +7 -0
- setiastro/saspro/runtime_torch.py +754 -0
- setiastro/saspro/save_options.py +72 -0
- setiastro/saspro/selective_color.py +1552 -0
- setiastro/saspro/sfcc.py +1425 -0
- setiastro/saspro/shortcuts.py +2807 -0
- setiastro/saspro/signature_insert.py +1099 -0
- setiastro/saspro/stacking_suite.py +17712 -0
- setiastro/saspro/star_alignment.py +7420 -0
- setiastro/saspro/star_alignment_preset.py +329 -0
- setiastro/saspro/star_metrics.py +49 -0
- setiastro/saspro/star_spikes.py +681 -0
- setiastro/saspro/star_stretch.py +470 -0
- setiastro/saspro/stat_stretch.py +502 -0
- setiastro/saspro/status_log_dock.py +78 -0
- setiastro/saspro/subwindow.py +3267 -0
- setiastro/saspro/supernovaasteroidhunter.py +1712 -0
- setiastro/saspro/swap_manager.py +99 -0
- setiastro/saspro/torch_backend.py +89 -0
- setiastro/saspro/torch_rejection.py +434 -0
- setiastro/saspro/view_bundle.py +1555 -0
- setiastro/saspro/wavescale_hdr.py +624 -0
- setiastro/saspro/wavescale_hdr_preset.py +100 -0
- setiastro/saspro/wavescalede.py +657 -0
- setiastro/saspro/wavescalede_preset.py +228 -0
- setiastro/saspro/wcs_update.py +374 -0
- setiastro/saspro/whitebalance.py +456 -0
- setiastro/saspro/widgets/__init__.py +48 -0
- setiastro/saspro/widgets/common_utilities.py +305 -0
- setiastro/saspro/widgets/graphics_views.py +122 -0
- setiastro/saspro/widgets/image_utils.py +518 -0
- setiastro/saspro/widgets/preview_dialogs.py +280 -0
- setiastro/saspro/widgets/spinboxes.py +275 -0
- setiastro/saspro/widgets/themed_buttons.py +13 -0
- setiastro/saspro/widgets/wavelet_utils.py +299 -0
- setiastro/saspro/window_shelf.py +185 -0
- setiastro/saspro/xisf.py +1123 -0
- setiastrosuitepro-1.6.0.dist-info/METADATA +266 -0
- setiastrosuitepro-1.6.0.dist-info/RECORD +174 -0
- setiastrosuitepro-1.6.0.dist-info/WHEEL +4 -0
- setiastrosuitepro-1.6.0.dist-info/entry_points.txt +6 -0
- setiastrosuitepro-1.6.0.dist-info/licenses/LICENSE +674 -0
- setiastrosuitepro-1.6.0.dist-info/licenses/license.txt +2580 -0
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
# ops.settings.py
|
|
2
|
+
from PyQt6.QtWidgets import (
|
|
3
|
+
QLineEdit, QDialogButtonBox, QFileDialog, QDialog, QPushButton, QFormLayout,QApplication,
|
|
4
|
+
QHBoxLayout, QVBoxLayout, QWidget, QCheckBox, QComboBox, QSpinBox, QDoubleSpinBox, QLabel, QColorDialog, QFontDialog, QSlider)
|
|
5
|
+
from PyQt6.QtCore import QSettings, Qt
|
|
6
|
+
import pytz # for timezone list
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SettingsDialog(QDialog):
|
|
10
|
+
"""
|
|
11
|
+
Simple settings UI for external executable paths + WIMS defaults.
|
|
12
|
+
Values are persisted via the provided QSettings instance.
|
|
13
|
+
"""
|
|
14
|
+
def __init__(self, parent, settings: QSettings):
|
|
15
|
+
super().__init__(parent)
|
|
16
|
+
self.setWindowTitle("Preferences")
|
|
17
|
+
self.settings = settings
|
|
18
|
+
|
|
19
|
+
# ---- Existing fields (paths, checkboxes, etc.) ----
|
|
20
|
+
self.le_graxpert = QLineEdit()
|
|
21
|
+
self.le_cosmic = QLineEdit()
|
|
22
|
+
self.le_starnet = QLineEdit()
|
|
23
|
+
self.le_astap = QLineEdit()
|
|
24
|
+
|
|
25
|
+
self.chk_updates_startup = QCheckBox("Check for updates on startup")
|
|
26
|
+
self.chk_updates_startup.setChecked(
|
|
27
|
+
self.settings.value("updates/check_on_startup", True, type=bool)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
self.le_updates_url = QLineEdit()
|
|
31
|
+
self.le_updates_url.setPlaceholderText("Raw JSON URL (advanced)")
|
|
32
|
+
self.le_updates_url.setText(
|
|
33
|
+
self.settings.value(
|
|
34
|
+
"updates/url",
|
|
35
|
+
"https://raw.githubusercontent.com/setiastro/setiastrosuitepro/main/updates.json",
|
|
36
|
+
type=str
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
btn_reset_updates_url = QPushButton("Reset")
|
|
41
|
+
btn_reset_updates_url.setToolTip("Restore default updates URL")
|
|
42
|
+
btn_reset_updates_url.clicked.connect(
|
|
43
|
+
lambda: self.le_updates_url.setText(
|
|
44
|
+
"https://raw.githubusercontent.com/setiastro/setiastrosuitepro/main/updates.json"
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Optional: “Check Now…” button
|
|
49
|
+
self.btn_check_now = QPushButton("Check Now…")
|
|
50
|
+
self.btn_check_now.setToolTip("Run an update check immediately")
|
|
51
|
+
self.btn_check_now.setVisible(hasattr(parent, "_check_for_updates_async"))
|
|
52
|
+
self.btn_check_now.clicked.connect(self._check_updates_now_clicked)
|
|
53
|
+
|
|
54
|
+
# Build the updates URL row ONCE (we'll insert it later on the right column)
|
|
55
|
+
row_updates_url = QHBoxLayout()
|
|
56
|
+
row_updates_url.addWidget(self.le_updates_url, 1)
|
|
57
|
+
row_updates_url.addWidget(btn_reset_updates_url)
|
|
58
|
+
row_updates_url.addWidget(self.btn_check_now)
|
|
59
|
+
|
|
60
|
+
self.chk_save_shortcuts = QCheckBox("Save desktop shortcuts on exit")
|
|
61
|
+
self.chk_save_shortcuts.setChecked(
|
|
62
|
+
self.settings.value("shortcuts/save_on_exit", True, type=bool)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
self.cb_theme = QComboBox()
|
|
66
|
+
# Order: Dark, Gray, Light, System, Custom
|
|
67
|
+
self.cb_theme.addItems(["Dark", "Gray", "Light", "System", "Custom"])
|
|
68
|
+
|
|
69
|
+
theme_val = (self.settings.value("ui/theme", "system", type=str) or "system").lower()
|
|
70
|
+
index_map = {"dark": 0, "gray": 1, "light": 2, "system": 3, "custom": 4}
|
|
71
|
+
self.cb_theme.setCurrentIndex(index_map.get(theme_val, 2))
|
|
72
|
+
|
|
73
|
+
# "Customize…" button for custom theme
|
|
74
|
+
self.btn_theme_custom = QPushButton("Customize…")
|
|
75
|
+
self.btn_theme_custom.setToolTip("Edit custom colors and font")
|
|
76
|
+
self.btn_theme_custom.setEnabled(theme_val == "custom")
|
|
77
|
+
self.btn_theme_custom.clicked.connect(self._open_theme_editor)
|
|
78
|
+
|
|
79
|
+
# Keep button enabled state in sync with combo
|
|
80
|
+
self.cb_theme.currentIndexChanged.connect(self._on_theme_changed)
|
|
81
|
+
|
|
82
|
+
self.le_graxpert.setText(self.settings.value("paths/graxpert", "", type=str))
|
|
83
|
+
self.le_cosmic.setText(self.settings.value("paths/cosmic_clarity", "", type=str))
|
|
84
|
+
self.le_starnet.setText(self.settings.value("paths/starnet", "", type=str))
|
|
85
|
+
self.le_astap.setText(self.settings.value("paths/astap", "", type=str))
|
|
86
|
+
|
|
87
|
+
btn_grax = QPushButton("Browse…"); btn_grax.clicked.connect(lambda: self._browse_into(self.le_graxpert))
|
|
88
|
+
btn_ccl = QPushButton("Browse…"); btn_ccl.clicked.connect(lambda: self._browse_dir(self.le_cosmic))
|
|
89
|
+
btn_star = QPushButton("Browse…"); btn_star.clicked.connect(lambda: self._browse_into(self.le_starnet))
|
|
90
|
+
btn_astap = QPushButton("Browse…"); btn_astap.clicked.connect(lambda: self._browse_into(self.le_astap))
|
|
91
|
+
|
|
92
|
+
row_grax = QHBoxLayout(); row_grax.addWidget(self.le_graxpert); row_grax.addWidget(btn_grax)
|
|
93
|
+
row_ccl = QHBoxLayout(); row_ccl.addWidget(self.le_cosmic); row_ccl.addWidget(btn_ccl)
|
|
94
|
+
row_star = QHBoxLayout(); row_star.addWidget(self.le_starnet); row_star.addWidget(btn_star)
|
|
95
|
+
row_astap = QHBoxLayout(); row_astap.addWidget(self.le_astap); row_astap.addWidget(btn_astap)
|
|
96
|
+
|
|
97
|
+
self.le_astrometry = QLineEdit()
|
|
98
|
+
self.le_astrometry.setEchoMode(QLineEdit.EchoMode.Password)
|
|
99
|
+
self.le_astrometry.setText(self.settings.value("api/astrometry_key", "", type=str))
|
|
100
|
+
|
|
101
|
+
# ---- WIMS defaults ----
|
|
102
|
+
self.sp_lat = QDoubleSpinBox(); self.sp_lat.setRange(-90.0, 90.0); self.sp_lat.setDecimals(6)
|
|
103
|
+
self.sp_lon = QDoubleSpinBox(); self.sp_lon.setRange(-180.0, 180.0); self.sp_lon.setDecimals(6)
|
|
104
|
+
self.le_date = QLineEdit() # YYYY-MM-DD
|
|
105
|
+
self.le_time = QLineEdit() # HH:MM
|
|
106
|
+
self.cb_tz = QComboBox(); self.cb_tz.addItems(pytz.all_timezones)
|
|
107
|
+
self.sp_min_alt = QDoubleSpinBox(); self.sp_min_alt.setRange(0.0, 90.0); self.sp_min_alt.setDecimals(1)
|
|
108
|
+
self.sp_obj_limit = QSpinBox(); self.sp_obj_limit.setRange(1, 1000)
|
|
109
|
+
|
|
110
|
+
self.sp_lat.setValue(self.settings.value("latitude", 0.0, type=float))
|
|
111
|
+
self.sp_lon.setValue(self.settings.value("longitude", 0.0, type=float))
|
|
112
|
+
self.le_date.setText(self.settings.value("date", "", type=str) or "")
|
|
113
|
+
self.le_time.setText(self.settings.value("time", "", type=str) or "")
|
|
114
|
+
tz_val = self.settings.value("timezone", "UTC", type=str) or "UTC"
|
|
115
|
+
idx = max(0, self.cb_tz.findText(tz_val)); self.cb_tz.setCurrentIndex(idx)
|
|
116
|
+
self.sp_min_alt.setValue(self.settings.value("min_altitude", 0.0, type=float))
|
|
117
|
+
self.sp_obj_limit.setValue(self.settings.value("object_limit", 100, type=int))
|
|
118
|
+
|
|
119
|
+
self.chk_autostretch_16bit = QCheckBox("High-quality autostretch (16-bit; better gradients)")
|
|
120
|
+
self.chk_autostretch_16bit.setToolTip("Compute autostretch on a 16-bit histogram (smoother gradients).")
|
|
121
|
+
self.chk_autostretch_16bit.setChecked(
|
|
122
|
+
self.settings.value("display/autostretch_16bit", True, type=bool)
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
self.slider_bg_opacity = QSlider(Qt.Orientation.Horizontal)
|
|
126
|
+
self.slider_bg_opacity.setRange(0, 100)
|
|
127
|
+
current_opacity = self.settings.value("display/bg_opacity", 50, type=int)
|
|
128
|
+
self.slider_bg_opacity.setValue(current_opacity)
|
|
129
|
+
# Keep a copy of the original opacity so we can restore it if the user cancels
|
|
130
|
+
self._initial_bg_opacity = int(current_opacity)
|
|
131
|
+
|
|
132
|
+
self.lbl_bg_opacity_val = QLabel(f"{current_opacity}%")
|
|
133
|
+
self.lbl_bg_opacity_val.setFixedWidth(40)
|
|
134
|
+
|
|
135
|
+
def _on_opacity_changed(val):
|
|
136
|
+
self.lbl_bg_opacity_val.setText(f"{val}%")
|
|
137
|
+
# Aggiorna in tempo reale il valore nei settings
|
|
138
|
+
self.settings.setValue("display/bg_opacity", val)
|
|
139
|
+
self.settings.sync()
|
|
140
|
+
# Richiedi al parent (main window) di aggiornare il rendering della MDI
|
|
141
|
+
parent = self.parent()
|
|
142
|
+
if parent and hasattr(parent, "mdi") and hasattr(parent.mdi, "viewport"):
|
|
143
|
+
parent.mdi.viewport().update()
|
|
144
|
+
|
|
145
|
+
self.slider_bg_opacity.valueChanged.connect(_on_opacity_changed)
|
|
146
|
+
|
|
147
|
+
row_bg_opacity = QHBoxLayout()
|
|
148
|
+
row_bg_opacity.addWidget(self.slider_bg_opacity)
|
|
149
|
+
row_bg_opacity.addWidget(self.lbl_bg_opacity_val)
|
|
150
|
+
w_bg_opacity = QWidget()
|
|
151
|
+
w_bg_opacity.setLayout(row_bg_opacity)
|
|
152
|
+
|
|
153
|
+
# ---- Custom background: choose/clear preview ----
|
|
154
|
+
self.le_bg_path = QLineEdit()
|
|
155
|
+
self.le_bg_path.setReadOnly(True)
|
|
156
|
+
# remember initial custom background so Cancel can restore it
|
|
157
|
+
self._initial_bg_path = self.settings.value("ui/custom_background", "", type=str) or ""
|
|
158
|
+
self.le_bg_path.setText(self._initial_bg_path)
|
|
159
|
+
btn_choose_bg = QPushButton("Choose Background…")
|
|
160
|
+
btn_choose_bg.setToolTip("Pick a PNG or JPG to use as the application background")
|
|
161
|
+
btn_choose_bg.clicked.connect(self._choose_background_clicked)
|
|
162
|
+
btn_clear_bg = QPushButton("Clear")
|
|
163
|
+
btn_clear_bg.setToolTip("Remove custom background and restore default")
|
|
164
|
+
btn_clear_bg.clicked.connect(self._clear_background_clicked)
|
|
165
|
+
|
|
166
|
+
row_bg_image = QHBoxLayout()
|
|
167
|
+
row_bg_image.addWidget(self.le_bg_path, 1)
|
|
168
|
+
row_bg_image.addWidget(btn_choose_bg)
|
|
169
|
+
row_bg_image.addWidget(btn_clear_bg)
|
|
170
|
+
w_bg_image = QWidget()
|
|
171
|
+
w_bg_image.setLayout(row_bg_image)
|
|
172
|
+
|
|
173
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
174
|
+
# LAYOUT MUST EXIST BEFORE ANY addRow(...) — build it here
|
|
175
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
176
|
+
root = QVBoxLayout(self)
|
|
177
|
+
cols = QHBoxLayout(); root.addLayout(cols)
|
|
178
|
+
|
|
179
|
+
left_col = QFormLayout()
|
|
180
|
+
right_col = QFormLayout()
|
|
181
|
+
for f in (left_col, right_col):
|
|
182
|
+
f.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
|
|
183
|
+
f.setRowWrapPolicy(QFormLayout.RowWrapPolicy.DontWrapRows)
|
|
184
|
+
f.setFormAlignment(Qt.AlignmentFlag.AlignTop)
|
|
185
|
+
|
|
186
|
+
cols.addLayout(left_col, 1)
|
|
187
|
+
cols.addSpacing(16)
|
|
188
|
+
cols.addLayout(right_col, 1)
|
|
189
|
+
|
|
190
|
+
# ---- Left column: Paths & Integrations ----
|
|
191
|
+
left_col.addRow(QLabel("<b>Paths & Integrations</b>"))
|
|
192
|
+
w = QWidget(); w.setLayout(row_grax); left_col.addRow("GraXpert executable:", w)
|
|
193
|
+
w = QWidget(); w.setLayout(row_ccl); left_col.addRow("Cosmic Clarity folder:", w)
|
|
194
|
+
w = QWidget(); w.setLayout(row_star); left_col.addRow("StarNet executable:", w)
|
|
195
|
+
w = QWidget(); w.setLayout(row_astap); left_col.addRow("ASTAP executable:", w)
|
|
196
|
+
left_col.addRow("Astrometry.net API key:", self.le_astrometry)
|
|
197
|
+
left_col.addRow(self.chk_save_shortcuts)
|
|
198
|
+
row_theme = QHBoxLayout()
|
|
199
|
+
row_theme.addWidget(self.cb_theme, 1)
|
|
200
|
+
row_theme.addWidget(self.btn_theme_custom)
|
|
201
|
+
w_theme = QWidget()
|
|
202
|
+
w_theme.setLayout(row_theme)
|
|
203
|
+
left_col.addRow("Theme:", w_theme)
|
|
204
|
+
|
|
205
|
+
# ---- Display (moved under Theme) ----
|
|
206
|
+
left_col.addRow(QLabel("<b>Display</b>"))
|
|
207
|
+
left_col.addRow(self.chk_autostretch_16bit)
|
|
208
|
+
left_col.addRow("Background Opacity:", w_bg_opacity)
|
|
209
|
+
left_col.addRow("Background Image:", w_bg_image)
|
|
210
|
+
|
|
211
|
+
# ---- Right column: WIMS + RA/Dec + Updates + Display ----
|
|
212
|
+
right_col.addRow(QLabel("<b>What's In My Sky — Defaults</b>"))
|
|
213
|
+
right_col.addRow("Latitude (°):", self.sp_lat)
|
|
214
|
+
right_col.addRow("Longitude (°):", self.sp_lon)
|
|
215
|
+
right_col.addRow("Date (YYYY-MM-DD):", self.le_date)
|
|
216
|
+
right_col.addRow("Time (HH:MM):", self.le_time)
|
|
217
|
+
right_col.addRow("Time Zone:", self.cb_tz)
|
|
218
|
+
right_col.addRow("Min Altitude (°):", self.sp_min_alt)
|
|
219
|
+
right_col.addRow("Object Limit:", self.sp_obj_limit)
|
|
220
|
+
|
|
221
|
+
# ---- RA/Dec Overlay ----
|
|
222
|
+
right_col.addRow(QLabel("<b>RA/Dec Overlay</b>"))
|
|
223
|
+
self.chk_wcs_enabled = QCheckBox("Show RA/Dec grid")
|
|
224
|
+
self.chk_wcs_enabled.setChecked(self.settings.value("wcs_grid/enabled", True, type=bool))
|
|
225
|
+
right_col.addRow(self.chk_wcs_enabled)
|
|
226
|
+
|
|
227
|
+
self.cb_wcs_mode = QComboBox(); self.cb_wcs_mode.addItems(["Auto", "Fixed spacing"])
|
|
228
|
+
self.cb_wcs_mode.setCurrentIndex(
|
|
229
|
+
0 if (self.settings.value("wcs_grid/mode", "auto", type=str) == "auto") else 1
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
self.cb_wcs_unit = QComboBox(); self.cb_wcs_unit.addItems(["deg", "arcmin"])
|
|
233
|
+
self.cb_wcs_unit.setCurrentIndex(
|
|
234
|
+
0 if (self.settings.value("wcs_grid/step_unit", "deg", type=str) == "deg") else 1
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
self.sp_wcs_step = QDoubleSpinBox()
|
|
238
|
+
self.sp_wcs_step.setDecimals(3); self.sp_wcs_step.setRange(0.001, 90.0)
|
|
239
|
+
self.sp_wcs_step.setValue(self.settings.value("wcs_grid/step_value", 1.0, type=float))
|
|
240
|
+
self.sp_wcs_step.setEnabled(self.cb_wcs_mode.currentIndex() == 1)
|
|
241
|
+
|
|
242
|
+
def _sync_suffix():
|
|
243
|
+
self.sp_wcs_step.setSuffix(" °" if self.cb_wcs_unit.currentIndex() == 0 else " arcmin")
|
|
244
|
+
_sync_suffix()
|
|
245
|
+
self.cb_wcs_unit.currentIndexChanged.connect(_sync_suffix)
|
|
246
|
+
self.cb_wcs_mode.currentIndexChanged.connect(lambda i: self.sp_wcs_step.setEnabled(i == 1))
|
|
247
|
+
|
|
248
|
+
row_wcs = QHBoxLayout()
|
|
249
|
+
row_wcs.addWidget(QLabel("Mode:")); row_wcs.addWidget(self.cb_wcs_mode)
|
|
250
|
+
row_wcs.addSpacing(8)
|
|
251
|
+
row_wcs.addWidget(QLabel("Step:")); row_wcs.addWidget(self.sp_wcs_step, 1); row_wcs.addWidget(self.cb_wcs_unit)
|
|
252
|
+
_w = QWidget(); _w.setLayout(row_wcs)
|
|
253
|
+
right_col.addRow(_w)
|
|
254
|
+
|
|
255
|
+
# ---- Updates ----
|
|
256
|
+
right_col.addRow(QLabel("<b>Updates</b>"))
|
|
257
|
+
right_col.addRow(self.chk_updates_startup)
|
|
258
|
+
w = QWidget(); w.setLayout(row_updates_url)
|
|
259
|
+
right_col.addRow("Updates JSON URL:", w)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# ---- Buttons ----
|
|
264
|
+
btns = QDialogButtonBox(
|
|
265
|
+
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, parent=self
|
|
266
|
+
)
|
|
267
|
+
btns.accepted.connect(self._save_and_accept)
|
|
268
|
+
btns.rejected.connect(self.reject)
|
|
269
|
+
root.addWidget(btns)
|
|
270
|
+
|
|
271
|
+
def reject(self):
|
|
272
|
+
"""User cancelled: restore the original background opacity (revert live changes)."""
|
|
273
|
+
try:
|
|
274
|
+
# Restore saved original value
|
|
275
|
+
self.settings.setValue("display/bg_opacity", int(self._initial_bg_opacity))
|
|
276
|
+
self.settings.sync()
|
|
277
|
+
# Ask parent to redraw with restored value
|
|
278
|
+
parent = self.parent()
|
|
279
|
+
if parent:
|
|
280
|
+
# restore original custom background (may be empty)
|
|
281
|
+
try:
|
|
282
|
+
# If there was an initial custom background, restore it; otherwise clear.
|
|
283
|
+
if self._initial_bg_path:
|
|
284
|
+
if hasattr(parent, "_apply_custom_background"):
|
|
285
|
+
parent._apply_custom_background(self._initial_bg_path)
|
|
286
|
+
else:
|
|
287
|
+
# Avoid calling _apply_custom_background("") which shows a warning
|
|
288
|
+
if hasattr(parent, "_clear_custom_background"):
|
|
289
|
+
parent._clear_custom_background()
|
|
290
|
+
elif hasattr(parent, "_apply_custom_background"):
|
|
291
|
+
parent._apply_custom_background("")
|
|
292
|
+
except Exception:
|
|
293
|
+
pass
|
|
294
|
+
# update MDI viewport/redraw
|
|
295
|
+
try:
|
|
296
|
+
if hasattr(parent, "mdi") and hasattr(parent.mdi, "viewport"):
|
|
297
|
+
parent.mdi.viewport().update()
|
|
298
|
+
except Exception:
|
|
299
|
+
pass
|
|
300
|
+
except Exception:
|
|
301
|
+
pass
|
|
302
|
+
super().reject()
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# ----------------- helpers -----------------
|
|
306
|
+
def _browse_into(self, lineedit: QLineEdit):
|
|
307
|
+
path, _ = QFileDialog.getOpenFileName(self, "Select Executable", "", "Executables (*)")
|
|
308
|
+
if path:
|
|
309
|
+
lineedit.setText(path)
|
|
310
|
+
|
|
311
|
+
def _browse_dir(self, lineedit: QLineEdit):
|
|
312
|
+
path = QFileDialog.getExistingDirectory(self, "Select Folder", "")
|
|
313
|
+
if path:
|
|
314
|
+
lineedit.setText(path)
|
|
315
|
+
|
|
316
|
+
def _check_updates_now_clicked(self):
|
|
317
|
+
"""Persist update settings, then ask the main window to run an interactive check (if available)."""
|
|
318
|
+
self.settings.setValue("updates/check_on_startup", self.chk_updates_startup.isChecked())
|
|
319
|
+
self.settings.setValue("updates/url", self.le_updates_url.text().strip())
|
|
320
|
+
self.settings.sync()
|
|
321
|
+
|
|
322
|
+
parent = self.parent()
|
|
323
|
+
if parent and hasattr(parent, "_check_for_updates_async"):
|
|
324
|
+
try:
|
|
325
|
+
parent._check_for_updates_async(interactive=True)
|
|
326
|
+
except Exception:
|
|
327
|
+
pass
|
|
328
|
+
|
|
329
|
+
def _choose_background_clicked(self):
|
|
330
|
+
"""Open a file picker and apply a custom background image for the app."""
|
|
331
|
+
path, _ = QFileDialog.getOpenFileName(self, "Select background image", "", "Images (*.png *.jpg *.jpeg)")
|
|
332
|
+
if not path:
|
|
333
|
+
return
|
|
334
|
+
try:
|
|
335
|
+
# Do NOT persist yet — just update UI and preview via parent.
|
|
336
|
+
self.le_bg_path.setText(path)
|
|
337
|
+
parent = self.parent()
|
|
338
|
+
if parent and hasattr(parent, "_apply_custom_background"):
|
|
339
|
+
try:
|
|
340
|
+
parent._apply_custom_background(path)
|
|
341
|
+
except Exception:
|
|
342
|
+
pass
|
|
343
|
+
except Exception:
|
|
344
|
+
pass
|
|
345
|
+
|
|
346
|
+
def _clear_background_clicked(self):
|
|
347
|
+
"""Clear persisted custom background and ask main window to restore defaults."""
|
|
348
|
+
try:
|
|
349
|
+
# Do NOT modify settings yet — clear preview and let Save apply
|
|
350
|
+
self.le_bg_path.setText("")
|
|
351
|
+
parent = self.parent()
|
|
352
|
+
if parent:
|
|
353
|
+
# request parent to clear preview/background for now
|
|
354
|
+
try:
|
|
355
|
+
if hasattr(parent, "_clear_custom_background"):
|
|
356
|
+
parent._clear_custom_background()
|
|
357
|
+
elif hasattr(parent, "_apply_custom_background"):
|
|
358
|
+
parent._apply_custom_background("")
|
|
359
|
+
except Exception:
|
|
360
|
+
pass
|
|
361
|
+
except Exception:
|
|
362
|
+
pass
|
|
363
|
+
|
|
364
|
+
def _on_theme_changed(self, idx: int):
|
|
365
|
+
# Enable the "Customize…" button only when Custom is selected
|
|
366
|
+
text = self.cb_theme.currentText().lower()
|
|
367
|
+
self.btn_theme_custom.setEnabled(text == "custom")
|
|
368
|
+
|
|
369
|
+
def _open_theme_editor(self):
|
|
370
|
+
from PyQt6.QtWidgets import QDialog
|
|
371
|
+
dlg = ThemeEditorDialog(self, self.settings)
|
|
372
|
+
if dlg.exec() == QDialog.DialogCode.Accepted:
|
|
373
|
+
# If user saved a custom theme, make sure "Custom" is selected
|
|
374
|
+
self.cb_theme.setCurrentIndex(4) # Custom
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _save_and_accept(self):
|
|
378
|
+
# Paths / Integrations
|
|
379
|
+
self.settings.setValue("paths/graxpert", self.le_graxpert.text().strip())
|
|
380
|
+
self.settings.setValue("paths/cosmic_clarity", self.le_cosmic.text().strip())
|
|
381
|
+
self.settings.setValue("paths/starnet", self.le_starnet.text().strip())
|
|
382
|
+
self.settings.setValue("paths/astap", self.le_astap.text().strip())
|
|
383
|
+
self.settings.setValue("shortcuts/save_on_exit", self.chk_save_shortcuts.isChecked())
|
|
384
|
+
self.settings.setValue("api/astrometry_key", self.le_astrometry.text().strip())
|
|
385
|
+
|
|
386
|
+
# WIMS defaults
|
|
387
|
+
self.settings.setValue("latitude", float(self.sp_lat.value()))
|
|
388
|
+
self.settings.setValue("longitude", float(self.sp_lon.value()))
|
|
389
|
+
self.settings.setValue("date", self.le_date.text().strip())
|
|
390
|
+
self.settings.setValue("time", self.le_time.text().strip())
|
|
391
|
+
self.settings.setValue("timezone", self.cb_tz.currentText())
|
|
392
|
+
self.settings.setValue("min_altitude", float(self.sp_min_alt.value()))
|
|
393
|
+
self.settings.setValue("object_limit", int(self.sp_obj_limit.value()))
|
|
394
|
+
|
|
395
|
+
# RA/Dec Overlay
|
|
396
|
+
self.settings.setValue("wcs_grid/enabled", self.chk_wcs_enabled.isChecked())
|
|
397
|
+
self.settings.setValue("wcs_grid/mode", "auto" if self.cb_wcs_mode.currentIndex() == 0 else "fixed")
|
|
398
|
+
self.settings.setValue("wcs_grid/step_unit", "deg" if self.cb_wcs_unit.currentIndex() == 0 else "arcmin")
|
|
399
|
+
self.settings.setValue("wcs_grid/step_value", float(self.sp_wcs_step.value()))
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
# Updates + Display
|
|
403
|
+
self.settings.setValue("updates/check_on_startup", self.chk_updates_startup.isChecked())
|
|
404
|
+
self.settings.setValue("updates/url", self.le_updates_url.text().strip())
|
|
405
|
+
self.settings.setValue("display/autostretch_16bit", self.chk_autostretch_16bit.isChecked())
|
|
406
|
+
|
|
407
|
+
# Custom background: persist the chosen path (empty -> remove)
|
|
408
|
+
bg_path = (self.le_bg_path.text() or "").strip()
|
|
409
|
+
if bg_path:
|
|
410
|
+
self.settings.setValue("ui/custom_background", bg_path)
|
|
411
|
+
else:
|
|
412
|
+
try:
|
|
413
|
+
self.settings.remove("ui/custom_background")
|
|
414
|
+
except Exception:
|
|
415
|
+
self.settings.setValue("ui/custom_background", "")
|
|
416
|
+
|
|
417
|
+
# bg_opacity is already saved in real-time by _on_opacity_changed()
|
|
418
|
+
|
|
419
|
+
# Theme
|
|
420
|
+
idx = max(0, self.cb_theme.currentIndex())
|
|
421
|
+
if idx == 0:
|
|
422
|
+
theme_val = "dark"
|
|
423
|
+
elif idx == 1:
|
|
424
|
+
theme_val = "gray"
|
|
425
|
+
elif idx == 2:
|
|
426
|
+
theme_val = "light"
|
|
427
|
+
elif idx == 3:
|
|
428
|
+
theme_val = "system"
|
|
429
|
+
else:
|
|
430
|
+
theme_val = "custom"
|
|
431
|
+
self.settings.setValue("ui/theme", theme_val)
|
|
432
|
+
|
|
433
|
+
self.settings.sync()
|
|
434
|
+
|
|
435
|
+
# Apply now if the parent knows how
|
|
436
|
+
p = self.parent()
|
|
437
|
+
if p and hasattr(p, "apply_theme_from_settings"):
|
|
438
|
+
try:
|
|
439
|
+
p.apply_theme_from_settings()
|
|
440
|
+
except Exception:
|
|
441
|
+
pass
|
|
442
|
+
|
|
443
|
+
if hasattr(p, "mdi") and hasattr(p.mdi, "viewport"):
|
|
444
|
+
p.mdi.viewport().update()
|
|
445
|
+
|
|
446
|
+
self.accept()
|
|
447
|
+
|
|
448
|
+
from PyQt6.QtGui import QColor, QFont
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
class ThemeEditorDialog(QDialog):
|
|
452
|
+
"""
|
|
453
|
+
Simple "Custom Theme" editor: lets the user pick main colors and a UI font.
|
|
454
|
+
Colors are stored in QSettings as hex strings (e.g. '#404040').
|
|
455
|
+
"""
|
|
456
|
+
def __init__(self, parent, settings: QSettings):
|
|
457
|
+
super().__init__(parent)
|
|
458
|
+
self.settings = settings
|
|
459
|
+
self.setWindowTitle("Custom Theme")
|
|
460
|
+
self.colors: dict[str, QColor] = {}
|
|
461
|
+
self.font_str: str = self.settings.value("ui/custom/font", "", type=str) or ""
|
|
462
|
+
|
|
463
|
+
form = QFormLayout(self)
|
|
464
|
+
|
|
465
|
+
# Helper: add color pickers for key roles
|
|
466
|
+
self._add_color_picker(form, "Window / Panels", "ui/custom/window", QColor(40, 40, 40))
|
|
467
|
+
self._add_color_picker(form, "Base (Editors)", "ui/custom/base", QColor(24, 24, 24))
|
|
468
|
+
self._add_color_picker(form, "Alternate Base", "ui/custom/altbase", QColor(32, 32, 32))
|
|
469
|
+
self._add_color_picker(form, "Text", "ui/custom/text", QColor(230, 230, 230))
|
|
470
|
+
self._add_color_picker(form, "Buttons", "ui/custom/button", QColor(40, 40, 40))
|
|
471
|
+
self._add_color_picker(form, "Highlight / Accent","ui/custom/highlight",QColor(30, 144, 255))
|
|
472
|
+
self._add_color_picker(form, "Link", "ui/custom/link", QColor(120, 170, 255))
|
|
473
|
+
self._add_color_picker(form, "Visited Link", "ui/custom/link_visited", QColor(180, 150, 255))
|
|
474
|
+
|
|
475
|
+
# Font picker
|
|
476
|
+
self.btn_font = QPushButton("Choose…")
|
|
477
|
+
self.btn_font.clicked.connect(self._pick_font)
|
|
478
|
+
form.addRow("UI Font:", self.btn_font)
|
|
479
|
+
|
|
480
|
+
# Buttons
|
|
481
|
+
btns = QDialogButtonBox(
|
|
482
|
+
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel,
|
|
483
|
+
parent=self
|
|
484
|
+
)
|
|
485
|
+
btns.accepted.connect(self._save_and_accept)
|
|
486
|
+
btns.rejected.connect(self.reject)
|
|
487
|
+
form.addRow(btns)
|
|
488
|
+
|
|
489
|
+
# ---------- helpers ----------
|
|
490
|
+
|
|
491
|
+
def _add_color_picker(self, form: QFormLayout, label_text: str,
|
|
492
|
+
key: str, default: QColor):
|
|
493
|
+
# Load from settings or default
|
|
494
|
+
stored = self.settings.value(key, default.name(), type=str)
|
|
495
|
+
color = QColor(stored) if stored else default
|
|
496
|
+
self.colors[key] = color
|
|
497
|
+
|
|
498
|
+
btn = QPushButton(color.name())
|
|
499
|
+
btn.setMinimumWidth(90)
|
|
500
|
+
btn.setStyleSheet(f"background-color: {color.name()}; color: #ffffff;")
|
|
501
|
+
btn.clicked.connect(lambda _=False, k=key, b=btn: self._pick_color(k, b))
|
|
502
|
+
|
|
503
|
+
form.addRow(label_text + ":", btn)
|
|
504
|
+
|
|
505
|
+
def _pick_color(self, key: str, button: QPushButton):
|
|
506
|
+
initial = self.colors.get(key, QColor("#404040"))
|
|
507
|
+
col = QColorDialog.getColor(initial, self, "Select Color")
|
|
508
|
+
if col.isValid():
|
|
509
|
+
self.colors[key] = col
|
|
510
|
+
button.setText(col.name())
|
|
511
|
+
button.setStyleSheet(f"background-color: {col.name()}; color: #ffffff;")
|
|
512
|
+
|
|
513
|
+
from PyQt6.QtGui import QFont
|
|
514
|
+
from PyQt6.QtWidgets import QFontDialog
|
|
515
|
+
|
|
516
|
+
def _pick_font(self):
|
|
517
|
+
# Load previous font if we have one
|
|
518
|
+
base_str = self.settings.value("ui/custom_font", "", type=str)
|
|
519
|
+
base_font = QFont()
|
|
520
|
+
if base_str:
|
|
521
|
+
try:
|
|
522
|
+
base_font.fromString(base_str)
|
|
523
|
+
except Exception:
|
|
524
|
+
pass
|
|
525
|
+
|
|
526
|
+
# ✅ NOTE: (font, ok) — NOT (ok, font)
|
|
527
|
+
font, ok = QFontDialog.getFont(base_font, self, "Select UI Font")
|
|
528
|
+
if not ok:
|
|
529
|
+
return # user cancelled
|
|
530
|
+
|
|
531
|
+
# Store and update preview
|
|
532
|
+
self.font_str = font.toString()
|
|
533
|
+
self.settings.setValue("ui/custom_font", self.font_str)
|
|
534
|
+
self.settings.sync()
|
|
535
|
+
|
|
536
|
+
# If you have a label/button to show the chosen font:
|
|
537
|
+
try:
|
|
538
|
+
self.font_button.setText(f"{font.family()}, {font.pointSize()} pt")
|
|
539
|
+
except Exception:
|
|
540
|
+
pass
|
|
541
|
+
|
|
542
|
+
# Re-apply theme so the new font takes effect
|
|
543
|
+
parent = self.parent()
|
|
544
|
+
if parent and hasattr(parent, "apply_theme_from_settings"):
|
|
545
|
+
try:
|
|
546
|
+
parent.apply_theme_from_settings()
|
|
547
|
+
except Exception:
|
|
548
|
+
pass
|
|
549
|
+
|
|
550
|
+
def _save_and_accept(self):
|
|
551
|
+
# Persist colors
|
|
552
|
+
for key, col in self.colors.items():
|
|
553
|
+
self.settings.setValue(key, col.name())
|
|
554
|
+
|
|
555
|
+
# Persist font if chosen
|
|
556
|
+
if self.font_str:
|
|
557
|
+
self.settings.setValue("ui/custom/font", self.font_str)
|
|
558
|
+
|
|
559
|
+
self.settings.sync()
|
|
560
|
+
self.accept()
|