setiastrosuitepro 1.6.1__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/data/SASP_data.fits +0 -0
- setiastro/data/catalogs/List_of_Galaxies_with_Distances_Gly.csv +488 -0
- setiastro/data/catalogs/astrobin_filters.csv +890 -0
- setiastro/data/catalogs/astrobin_filters_page1_local.csv +51 -0
- setiastro/data/catalogs/cali2.csv +63 -0
- setiastro/data/catalogs/cali2color.csv +65 -0
- setiastro/data/catalogs/celestial_catalog - original.csv +16471 -0
- setiastro/data/catalogs/celestial_catalog.csv +24031 -0
- setiastro/data/catalogs/detected_stars.csv +24784 -0
- setiastro/data/catalogs/fits_header_data.csv +46 -0
- setiastro/data/catalogs/test.csv +8 -0
- setiastro/data/catalogs/updated_celestial_catalog.csv +16471 -0
- setiastro/images/Astro_Spikes.png +0 -0
- setiastro/images/HRDiagram.png +0 -0
- setiastro/images/LExtract.png +0 -0
- setiastro/images/LInsert.png +0 -0
- setiastro/images/Oxygenation-atm-2.svg.png +0 -0
- setiastro/images/RGB080604.png +0 -0
- setiastro/images/abeicon.png +0 -0
- setiastro/images/aberration.png +0 -0
- setiastro/images/andromedatry.png +0 -0
- setiastro/images/andromedatry_satellited.png +0 -0
- setiastro/images/annotated.png +0 -0
- setiastro/images/aperture.png +0 -0
- setiastro/images/astrosuite.ico +0 -0
- setiastro/images/astrosuite.png +0 -0
- setiastro/images/astrosuitepro.icns +0 -0
- setiastro/images/astrosuitepro.ico +0 -0
- setiastro/images/astrosuitepro.png +0 -0
- setiastro/images/background.png +0 -0
- setiastro/images/background2.png +0 -0
- setiastro/images/benchmark.png +0 -0
- setiastro/images/big_moon_stabilizer_timeline.png +0 -0
- setiastro/images/big_moon_stabilizer_timeline_clean.png +0 -0
- setiastro/images/blaster.png +0 -0
- setiastro/images/blink.png +0 -0
- setiastro/images/clahe.png +0 -0
- setiastro/images/collage.png +0 -0
- setiastro/images/colorwheel.png +0 -0
- setiastro/images/contsub.png +0 -0
- setiastro/images/convo.png +0 -0
- setiastro/images/copyslot.png +0 -0
- setiastro/images/cosmic.png +0 -0
- setiastro/images/cosmicsat.png +0 -0
- setiastro/images/crop1.png +0 -0
- setiastro/images/cropicon.png +0 -0
- setiastro/images/curves.png +0 -0
- setiastro/images/cvs.png +0 -0
- setiastro/images/debayer.png +0 -0
- setiastro/images/denoise_cnn_custom.png +0 -0
- setiastro/images/denoise_cnn_graph.png +0 -0
- setiastro/images/disk.png +0 -0
- setiastro/images/dse.png +0 -0
- setiastro/images/exoicon.png +0 -0
- setiastro/images/eye.png +0 -0
- setiastro/images/fliphorizontal.png +0 -0
- setiastro/images/flipvertical.png +0 -0
- setiastro/images/font.png +0 -0
- setiastro/images/freqsep.png +0 -0
- setiastro/images/functionbundle.png +0 -0
- setiastro/images/graxpert.png +0 -0
- setiastro/images/green.png +0 -0
- setiastro/images/gridicon.png +0 -0
- setiastro/images/halo.png +0 -0
- setiastro/images/hdr.png +0 -0
- setiastro/images/histogram.png +0 -0
- setiastro/images/hubble.png +0 -0
- setiastro/images/imagecombine.png +0 -0
- setiastro/images/invert.png +0 -0
- setiastro/images/isophote.png +0 -0
- setiastro/images/isophote_demo_figure.png +0 -0
- setiastro/images/isophote_demo_image.png +0 -0
- setiastro/images/isophote_demo_model.png +0 -0
- setiastro/images/isophote_demo_residual.png +0 -0
- setiastro/images/jwstpupil.png +0 -0
- setiastro/images/linearfit.png +0 -0
- setiastro/images/livestacking.png +0 -0
- setiastro/images/mask.png +0 -0
- setiastro/images/maskapply.png +0 -0
- setiastro/images/maskcreate.png +0 -0
- setiastro/images/maskremove.png +0 -0
- setiastro/images/morpho.png +0 -0
- setiastro/images/mosaic.png +0 -0
- setiastro/images/multiscale_decomp.png +0 -0
- setiastro/images/nbtorgb.png +0 -0
- setiastro/images/neutral.png +0 -0
- setiastro/images/nuke.png +0 -0
- setiastro/images/openfile.png +0 -0
- setiastro/images/pedestal.png +0 -0
- setiastro/images/pen.png +0 -0
- setiastro/images/pixelmath.png +0 -0
- setiastro/images/platesolve.png +0 -0
- setiastro/images/ppp.png +0 -0
- setiastro/images/pro.png +0 -0
- setiastro/images/project.png +0 -0
- setiastro/images/psf.png +0 -0
- setiastro/images/redo.png +0 -0
- setiastro/images/redoicon.png +0 -0
- setiastro/images/rescale.png +0 -0
- setiastro/images/rgbalign.png +0 -0
- setiastro/images/rgbcombo.png +0 -0
- setiastro/images/rgbextract.png +0 -0
- setiastro/images/rotate180.png +0 -0
- setiastro/images/rotateclockwise.png +0 -0
- setiastro/images/rotatecounterclockwise.png +0 -0
- setiastro/images/satellite.png +0 -0
- setiastro/images/script.png +0 -0
- setiastro/images/selectivecolor.png +0 -0
- setiastro/images/simbad.png +0 -0
- setiastro/images/slot0.png +0 -0
- setiastro/images/slot1.png +0 -0
- setiastro/images/slot2.png +0 -0
- setiastro/images/slot3.png +0 -0
- setiastro/images/slot4.png +0 -0
- setiastro/images/slot5.png +0 -0
- setiastro/images/slot6.png +0 -0
- setiastro/images/slot7.png +0 -0
- setiastro/images/slot8.png +0 -0
- setiastro/images/slot9.png +0 -0
- setiastro/images/spcc.png +0 -0
- setiastro/images/spin_precession_vs_lunar_distance.png +0 -0
- setiastro/images/spinner.gif +0 -0
- setiastro/images/stacking.png +0 -0
- setiastro/images/staradd.png +0 -0
- setiastro/images/staralign.png +0 -0
- setiastro/images/starnet.png +0 -0
- setiastro/images/starregistration.png +0 -0
- setiastro/images/starspike.png +0 -0
- setiastro/images/starstretch.png +0 -0
- setiastro/images/statstretch.png +0 -0
- setiastro/images/supernova.png +0 -0
- setiastro/images/uhs.png +0 -0
- setiastro/images/undoicon.png +0 -0
- setiastro/images/upscale.png +0 -0
- setiastro/images/viewbundle.png +0 -0
- setiastro/images/whitebalance.png +0 -0
- setiastro/images/wimi_icon_256x256.png +0 -0
- setiastro/images/wimilogo.png +0 -0
- setiastro/images/wims.png +0 -0
- setiastro/images/wrench_icon.png +0 -0
- setiastro/images/xisfliberator.png +0 -0
- setiastro/saspro/__init__.py +20 -0
- setiastro/saspro/__main__.py +809 -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 +2926 -0
- setiastro/saspro/bundles.py +61 -0
- setiastro/saspro/bundles_dock.py +114 -0
- setiastro/saspro/cheat_sheet.py +178 -0
- setiastro/saspro/clahe.py +342 -0
- setiastro/saspro/comet_stacking.py +1377 -0
- setiastro/saspro/common_tr.py +107 -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 +956 -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 +2641 -0
- setiastro/saspro/exoplanet_detector.py +2166 -0
- setiastro/saspro/file_utils.py +284 -0
- setiastro/saspro/fitsmodifier.py +745 -0
- setiastro/saspro/fix_bom.py +32 -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/generate_translations.py +2378 -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 +8567 -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 +443 -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 +1457 -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 +448 -0
- setiastro/saspro/headless_utils.py +88 -0
- setiastro/saspro/histogram.py +753 -0
- setiastro/saspro/history_explorer.py +939 -0
- setiastro/saspro/i18n.py +156 -0
- setiastro/saspro/image_combine.py +414 -0
- setiastro/saspro/image_peeker_pro.py +1601 -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 +679 -0
- setiastro/saspro/parallel_utils.py +554 -0
- setiastro/saspro/pedestal.py +121 -0
- setiastro/saspro/perfect_palette_picker.py +1070 -0
- setiastro/saspro/pipeline.py +110 -0
- setiastro/saspro/pixelmath.py +1600 -0
- setiastro/saspro/plate_solver.py +2444 -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 +477 -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 +1430 -0
- setiastro/saspro/shortcuts.py +3043 -0
- setiastro/saspro/signature_insert.py +1099 -0
- setiastro/saspro/stacking_suite.py +18181 -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 +506 -0
- setiastro/saspro/status_log_dock.py +78 -0
- setiastro/saspro/subwindow.py +3267 -0
- setiastro/saspro/supernovaasteroidhunter.py +1716 -0
- setiastro/saspro/swap_manager.py +99 -0
- setiastro/saspro/torch_backend.py +89 -0
- setiastro/saspro/torch_rejection.py +434 -0
- setiastro/saspro/translations/de_translations.py +3733 -0
- setiastro/saspro/translations/es_translations.py +3923 -0
- setiastro/saspro/translations/fr_translations.py +3842 -0
- setiastro/saspro/translations/integrate_translations.py +234 -0
- setiastro/saspro/translations/it_translations.py +3662 -0
- setiastro/saspro/translations/ja_translations.py +3585 -0
- setiastro/saspro/translations/pt_translations.py +3853 -0
- setiastro/saspro/translations/saspro_de.qm +0 -0
- setiastro/saspro/translations/saspro_de.ts +253 -0
- setiastro/saspro/translations/saspro_es.qm +0 -0
- setiastro/saspro/translations/saspro_es.ts +12520 -0
- setiastro/saspro/translations/saspro_fr.qm +0 -0
- setiastro/saspro/translations/saspro_fr.ts +12514 -0
- setiastro/saspro/translations/saspro_it.qm +0 -0
- setiastro/saspro/translations/saspro_it.ts +12520 -0
- setiastro/saspro/translations/saspro_ja.qm +0 -0
- setiastro/saspro/translations/saspro_ja.ts +257 -0
- setiastro/saspro/translations/saspro_pt.qm +0 -0
- setiastro/saspro/translations/saspro_pt.ts +257 -0
- setiastro/saspro/translations/saspro_zh.qm +0 -0
- setiastro/saspro/translations/saspro_zh.ts +12520 -0
- setiastro/saspro/translations/zh_translations.py +3659 -0
- setiastro/saspro/versioning.py +71 -0
- setiastro/saspro/view_bundle.py +1555 -0
- setiastro/saspro/wavescale_hdr.py +624 -0
- setiastro/saspro/wavescale_hdr_preset.py +101 -0
- setiastro/saspro/wavescalede.py +658 -0
- setiastro/saspro/wavescalede_preset.py +230 -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 +306 -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.1.dist-info/METADATA +267 -0
- setiastrosuitepro-1.6.1.dist-info/RECORD +342 -0
- setiastrosuitepro-1.6.1.dist-info/WHEEL +4 -0
- setiastrosuitepro-1.6.1.dist-info/entry_points.txt +6 -0
- setiastrosuitepro-1.6.1.dist-info/licenses/LICENSE +674 -0
- setiastrosuitepro-1.6.1.dist-info/licenses/license.txt +2580 -0
|
@@ -0,0 +1,679 @@
|
|
|
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
|
+
# i18n support
|
|
9
|
+
from setiastro.saspro.i18n import get_available_languages, get_saved_language, save_language
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SettingsDialog(QDialog):
|
|
13
|
+
"""
|
|
14
|
+
Simple settings UI for external executable paths + WIMS defaults.
|
|
15
|
+
Values are persisted via the provided QSettings instance.
|
|
16
|
+
"""
|
|
17
|
+
def __init__(self, parent, settings: QSettings):
|
|
18
|
+
super().__init__(parent)
|
|
19
|
+
self.setWindowTitle(self.tr("Preferences"))
|
|
20
|
+
self.settings = settings
|
|
21
|
+
|
|
22
|
+
# Ensure we don't delete on close, so we can cache it
|
|
23
|
+
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, False)
|
|
24
|
+
|
|
25
|
+
# ---- Existing fields (paths, checkboxes, etc.) ----
|
|
26
|
+
self.le_graxpert = QLineEdit()
|
|
27
|
+
self.le_cosmic = QLineEdit()
|
|
28
|
+
self.le_starnet = QLineEdit()
|
|
29
|
+
self.le_astap = QLineEdit()
|
|
30
|
+
|
|
31
|
+
self.chk_updates_startup = QCheckBox(self.tr("Check for updates on startup"))
|
|
32
|
+
|
|
33
|
+
self.le_updates_url = QLineEdit()
|
|
34
|
+
self.le_updates_url.setPlaceholderText("Raw JSON URL (advanced)")
|
|
35
|
+
|
|
36
|
+
btn_reset_updates_url = QPushButton(self.tr("Reset"))
|
|
37
|
+
btn_reset_updates_url.setToolTip(self.tr("Restore default updates URL"))
|
|
38
|
+
btn_reset_updates_url.clicked.connect(
|
|
39
|
+
lambda: self.le_updates_url.setText(
|
|
40
|
+
"https://raw.githubusercontent.com/setiastro/setiastrosuitepro/main/updates.json"
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Optional: “Check Now…” button
|
|
45
|
+
self.btn_check_now = QPushButton(self.tr("Check Now…"))
|
|
46
|
+
self.btn_check_now.setToolTip(self.tr("Run an update check immediately"))
|
|
47
|
+
self.btn_check_now.setVisible(hasattr(parent, "_check_for_updates_async"))
|
|
48
|
+
self.btn_check_now.clicked.connect(self._check_updates_now_clicked)
|
|
49
|
+
|
|
50
|
+
# Build the updates URL row ONCE (we'll insert it later on the right column)
|
|
51
|
+
row_updates_url = QHBoxLayout()
|
|
52
|
+
row_updates_url.addWidget(self.le_updates_url, 1)
|
|
53
|
+
row_updates_url.addWidget(btn_reset_updates_url)
|
|
54
|
+
row_updates_url.addWidget(self.btn_check_now)
|
|
55
|
+
|
|
56
|
+
self.chk_save_shortcuts = QCheckBox(self.tr("Save desktop shortcuts on exit"))
|
|
57
|
+
|
|
58
|
+
self.cb_theme = QComboBox()
|
|
59
|
+
# Order: Dark, Gray, Light, System, Custom
|
|
60
|
+
self.cb_theme.addItems(["Dark", "Gray", "Light", "System", "Custom"])
|
|
61
|
+
|
|
62
|
+
# "Customize…" button for custom theme
|
|
63
|
+
self.btn_theme_custom = QPushButton(self.tr("Customize…"))
|
|
64
|
+
self.btn_theme_custom.setToolTip(self.tr("Edit custom colors and font"))
|
|
65
|
+
self.btn_theme_custom.clicked.connect(self._open_theme_editor)
|
|
66
|
+
|
|
67
|
+
# Keep button enabled state in sync with combo
|
|
68
|
+
self.cb_theme.currentIndexChanged.connect(self._on_theme_changed)
|
|
69
|
+
|
|
70
|
+
# ---- Language selector ----
|
|
71
|
+
self.cb_language = QComboBox()
|
|
72
|
+
self._lang_codes = list(get_available_languages().keys()) # ["en", "it", "fr", "es"]
|
|
73
|
+
self._lang_names = list(get_available_languages().values()) # ["English", "Italiano", ...]
|
|
74
|
+
self.cb_language.addItems(self._lang_names)
|
|
75
|
+
self._initial_language = "en" # placeholder, set in refresh_ui
|
|
76
|
+
|
|
77
|
+
btn_grax = QPushButton(self.tr("Browse…")); btn_grax.clicked.connect(lambda: self._browse_into(self.le_graxpert))
|
|
78
|
+
btn_ccl = QPushButton(self.tr("Browse…")); btn_ccl.clicked.connect(lambda: self._browse_dir(self.le_cosmic))
|
|
79
|
+
btn_star = QPushButton(self.tr("Browse…")); btn_star.clicked.connect(lambda: self._browse_into(self.le_starnet))
|
|
80
|
+
btn_astap = QPushButton(self.tr("Browse…")); btn_astap.clicked.connect(lambda: self._browse_into(self.le_astap))
|
|
81
|
+
|
|
82
|
+
row_grax = QHBoxLayout(); row_grax.addWidget(self.le_graxpert); row_grax.addWidget(btn_grax)
|
|
83
|
+
row_ccl = QHBoxLayout(); row_ccl.addWidget(self.le_cosmic); row_ccl.addWidget(btn_ccl)
|
|
84
|
+
row_star = QHBoxLayout(); row_star.addWidget(self.le_starnet); row_star.addWidget(btn_star)
|
|
85
|
+
row_astap = QHBoxLayout(); row_astap.addWidget(self.le_astap); row_astap.addWidget(btn_astap)
|
|
86
|
+
|
|
87
|
+
self.le_astrometry = QLineEdit()
|
|
88
|
+
self.le_astrometry.setEchoMode(QLineEdit.EchoMode.Password)
|
|
89
|
+
|
|
90
|
+
# ---- WIMS defaults ----
|
|
91
|
+
self.sp_lat = QDoubleSpinBox(); self.sp_lat.setRange(-90.0, 90.0); self.sp_lat.setDecimals(6)
|
|
92
|
+
self.sp_lon = QDoubleSpinBox(); self.sp_lon.setRange(-180.0, 180.0); self.sp_lon.setDecimals(6)
|
|
93
|
+
self.le_date = QLineEdit() # YYYY-MM-DD
|
|
94
|
+
self.le_time = QLineEdit() # HH:MM
|
|
95
|
+
self.cb_tz = QComboBox(); self.cb_tz.addItems(pytz.all_timezones)
|
|
96
|
+
self.sp_min_alt = QDoubleSpinBox(); self.sp_min_alt.setRange(0.0, 90.0); self.sp_min_alt.setDecimals(1)
|
|
97
|
+
self.sp_obj_limit = QSpinBox(); self.sp_obj_limit.setRange(1, 1000)
|
|
98
|
+
|
|
99
|
+
self.chk_autostretch_16bit = QCheckBox(self.tr("High-quality autostretch (16-bit; better gradients)"))
|
|
100
|
+
self.chk_autostretch_16bit.setToolTip(self.tr("Compute autostretch on a 16-bit histogram (smoother gradients)."))
|
|
101
|
+
|
|
102
|
+
self.slider_bg_opacity = QSlider(Qt.Orientation.Horizontal)
|
|
103
|
+
self.slider_bg_opacity.setRange(0, 100)
|
|
104
|
+
self._initial_bg_opacity = 50
|
|
105
|
+
|
|
106
|
+
self.lbl_bg_opacity_val = QLabel("50%")
|
|
107
|
+
self.lbl_bg_opacity_val.setFixedWidth(40)
|
|
108
|
+
|
|
109
|
+
def _on_opacity_changed(val):
|
|
110
|
+
self.lbl_bg_opacity_val.setText(f"{val}%")
|
|
111
|
+
# Aggiorna in tempo reale il valore nei settings
|
|
112
|
+
self.settings.setValue("display/bg_opacity", val)
|
|
113
|
+
self.settings.sync()
|
|
114
|
+
# Richiedi al parent (main window) di aggiornare il rendering della MDI
|
|
115
|
+
parent = self.parent()
|
|
116
|
+
if parent and hasattr(parent, "mdi") and hasattr(parent.mdi, "viewport"):
|
|
117
|
+
parent.mdi.viewport().update()
|
|
118
|
+
|
|
119
|
+
self.slider_bg_opacity.valueChanged.connect(_on_opacity_changed)
|
|
120
|
+
|
|
121
|
+
row_bg_opacity = QHBoxLayout()
|
|
122
|
+
row_bg_opacity.addWidget(self.slider_bg_opacity)
|
|
123
|
+
row_bg_opacity.addWidget(self.lbl_bg_opacity_val)
|
|
124
|
+
w_bg_opacity = QWidget()
|
|
125
|
+
w_bg_opacity.setLayout(row_bg_opacity)
|
|
126
|
+
|
|
127
|
+
# ---- Custom background: choose/clear preview ----
|
|
128
|
+
self.le_bg_path = QLineEdit()
|
|
129
|
+
self.le_bg_path.setReadOnly(True)
|
|
130
|
+
self._initial_bg_path = ""
|
|
131
|
+
|
|
132
|
+
btn_choose_bg = QPushButton(self.tr("Choose Background…"))
|
|
133
|
+
btn_choose_bg.setToolTip(self.tr("Pick a PNG or JPG to use as the application background"))
|
|
134
|
+
btn_choose_bg.clicked.connect(self._choose_background_clicked)
|
|
135
|
+
btn_clear_bg = QPushButton(self.tr("Clear"))
|
|
136
|
+
btn_clear_bg.setToolTip(self.tr("Remove custom background and restore default"))
|
|
137
|
+
btn_clear_bg.clicked.connect(self._clear_background_clicked)
|
|
138
|
+
|
|
139
|
+
row_bg_image = QHBoxLayout()
|
|
140
|
+
row_bg_image.addWidget(self.le_bg_path, 1)
|
|
141
|
+
row_bg_image.addWidget(btn_choose_bg)
|
|
142
|
+
row_bg_image.addWidget(btn_clear_bg)
|
|
143
|
+
w_bg_image = QWidget()
|
|
144
|
+
w_bg_image.setLayout(row_bg_image)
|
|
145
|
+
|
|
146
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
147
|
+
# LAYOUT MUST EXIST BEFORE ANY addRow(...) — build it here
|
|
148
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
149
|
+
root = QVBoxLayout(self)
|
|
150
|
+
cols = QHBoxLayout(); root.addLayout(cols)
|
|
151
|
+
|
|
152
|
+
left_col = QFormLayout()
|
|
153
|
+
right_col = QFormLayout()
|
|
154
|
+
for f in (left_col, right_col):
|
|
155
|
+
f.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
|
|
156
|
+
f.setRowWrapPolicy(QFormLayout.RowWrapPolicy.DontWrapRows)
|
|
157
|
+
f.setFormAlignment(Qt.AlignmentFlag.AlignTop)
|
|
158
|
+
|
|
159
|
+
cols.addLayout(left_col, 1)
|
|
160
|
+
cols.addSpacing(16)
|
|
161
|
+
cols.addLayout(right_col, 1)
|
|
162
|
+
|
|
163
|
+
# ---- Left column: Paths & Integrations ----
|
|
164
|
+
left_col.addRow(QLabel(self.tr("<b>Paths & Integrations</b>")))
|
|
165
|
+
w = QWidget(); w.setLayout(row_grax); left_col.addRow(self.tr("GraXpert executable:"), w)
|
|
166
|
+
w = QWidget(); w.setLayout(row_ccl); left_col.addRow(self.tr("Cosmic Clarity folder:"), w)
|
|
167
|
+
w = QWidget(); w.setLayout(row_star); left_col.addRow(self.tr("StarNet executable:"), w)
|
|
168
|
+
w = QWidget(); w.setLayout(row_astap); left_col.addRow(self.tr("ASTAP executable:"), w)
|
|
169
|
+
left_col.addRow(self.tr("Astrometry.net API key:"), self.le_astrometry)
|
|
170
|
+
left_col.addRow(self.chk_save_shortcuts)
|
|
171
|
+
row_theme = QHBoxLayout()
|
|
172
|
+
row_theme.addWidget(self.cb_theme, 1)
|
|
173
|
+
row_theme.addWidget(self.btn_theme_custom)
|
|
174
|
+
w_theme = QWidget()
|
|
175
|
+
w_theme.setLayout(row_theme)
|
|
176
|
+
left_col.addRow(self.tr("Theme:"), w_theme)
|
|
177
|
+
left_col.addRow(self.tr("Language:"), self.cb_language)
|
|
178
|
+
|
|
179
|
+
# ---- Display (moved under Theme) ----
|
|
180
|
+
left_col.addRow(QLabel(self.tr("<b>Display</b>")))
|
|
181
|
+
left_col.addRow(self.chk_autostretch_16bit)
|
|
182
|
+
left_col.addRow(self.tr("Background Opacity:"), w_bg_opacity)
|
|
183
|
+
left_col.addRow(self.tr("Background Image:"), w_bg_image)
|
|
184
|
+
|
|
185
|
+
# ---- Right column: WIMS + RA/Dec + Updates + Display ----
|
|
186
|
+
right_col.addRow(QLabel(self.tr("<b>What's In My Sky — Defaults</b>")))
|
|
187
|
+
right_col.addRow(self.tr("Latitude (°):"), self.sp_lat)
|
|
188
|
+
right_col.addRow(self.tr("Longitude (°):"), self.sp_lon)
|
|
189
|
+
right_col.addRow(self.tr("Date (YYYY-MM-DD):"), self.le_date)
|
|
190
|
+
right_col.addRow(self.tr("Time (HH:MM):"), self.le_time)
|
|
191
|
+
right_col.addRow(self.tr("Time Zone:"), self.cb_tz)
|
|
192
|
+
right_col.addRow(self.tr("Min Altitude (°):"), self.sp_min_alt)
|
|
193
|
+
right_col.addRow(self.tr("Object Limit:"), self.sp_obj_limit)
|
|
194
|
+
|
|
195
|
+
# ---- RA/Dec Overlay ----
|
|
196
|
+
right_col.addRow(QLabel(self.tr("<b>RA/Dec Overlay</b>")))
|
|
197
|
+
self.chk_wcs_enabled = QCheckBox(self.tr("Show RA/Dec grid"))
|
|
198
|
+
|
|
199
|
+
right_col.addRow(self.chk_wcs_enabled)
|
|
200
|
+
|
|
201
|
+
self.cb_wcs_mode = QComboBox(); self.cb_wcs_mode.addItems(["Auto", "Fixed spacing"])
|
|
202
|
+
self.cb_wcs_unit = QComboBox(); self.cb_wcs_unit.addItems(["deg", "arcmin"])
|
|
203
|
+
|
|
204
|
+
self.sp_wcs_step = QDoubleSpinBox()
|
|
205
|
+
self.sp_wcs_step.setDecimals(3); self.sp_wcs_step.setRange(0.001, 90.0)
|
|
206
|
+
|
|
207
|
+
def _sync_suffix():
|
|
208
|
+
self.sp_wcs_step.setSuffix(" °" if self.cb_wcs_unit.currentIndex() == 0 else " arcmin")
|
|
209
|
+
_sync_suffix()
|
|
210
|
+
self.cb_wcs_unit.currentIndexChanged.connect(_sync_suffix)
|
|
211
|
+
self.cb_wcs_mode.currentIndexChanged.connect(lambda i: self.sp_wcs_step.setEnabled(i == 1))
|
|
212
|
+
|
|
213
|
+
row_wcs = QHBoxLayout()
|
|
214
|
+
row_wcs.addWidget(QLabel(self.tr("Mode:"))); row_wcs.addWidget(self.cb_wcs_mode)
|
|
215
|
+
row_wcs.addSpacing(8)
|
|
216
|
+
row_wcs.addWidget(QLabel(self.tr("Step:"))); row_wcs.addWidget(self.sp_wcs_step, 1); row_wcs.addWidget(self.cb_wcs_unit)
|
|
217
|
+
_w = QWidget(); _w.setLayout(row_wcs)
|
|
218
|
+
right_col.addRow(_w)
|
|
219
|
+
|
|
220
|
+
# ---- Updates ----
|
|
221
|
+
right_col.addRow(QLabel(self.tr("<b>Updates</b>")))
|
|
222
|
+
right_col.addRow(self.chk_updates_startup)
|
|
223
|
+
w = QWidget(); w.setLayout(row_updates_url)
|
|
224
|
+
right_col.addRow(self.tr("Updates JSON URL:"), w)
|
|
225
|
+
|
|
226
|
+
# ---- Buttons ----
|
|
227
|
+
btns = QDialogButtonBox(
|
|
228
|
+
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, parent=self
|
|
229
|
+
)
|
|
230
|
+
btns.accepted.connect(self._save_and_accept)
|
|
231
|
+
btns.rejected.connect(self.reject)
|
|
232
|
+
root.addWidget(btns)
|
|
233
|
+
|
|
234
|
+
# Initial Load:
|
|
235
|
+
self.refresh_ui()
|
|
236
|
+
|
|
237
|
+
def refresh_ui(self):
|
|
238
|
+
"""
|
|
239
|
+
Reloads all settings from self.settings and updates the UI widgets.
|
|
240
|
+
Call this before showing the cached dialog to ensure it matches current state.
|
|
241
|
+
"""
|
|
242
|
+
# Updates
|
|
243
|
+
self.chk_updates_startup.setChecked(
|
|
244
|
+
self.settings.value("updates/check_on_startup", True, type=bool)
|
|
245
|
+
)
|
|
246
|
+
self.le_updates_url.setText(
|
|
247
|
+
self.settings.value(
|
|
248
|
+
"updates/url",
|
|
249
|
+
"https://raw.githubusercontent.com/setiastro/setiastrosuitepro/main/updates.json",
|
|
250
|
+
type=str
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# Shortcuts
|
|
255
|
+
self.chk_save_shortcuts.setChecked(
|
|
256
|
+
self.settings.value("shortcuts/save_on_exit", True, type=bool)
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Theme
|
|
260
|
+
theme_val = (self.settings.value("ui/theme", "system", type=str) or "system").lower()
|
|
261
|
+
index_map = {"dark": 0, "gray": 1, "light": 2, "system": 3, "custom": 4}
|
|
262
|
+
self.cb_theme.setCurrentIndex(index_map.get(theme_val, 2))
|
|
263
|
+
self.btn_theme_custom.setEnabled(theme_val == "custom")
|
|
264
|
+
|
|
265
|
+
# Language
|
|
266
|
+
current_lang = get_saved_language()
|
|
267
|
+
try:
|
|
268
|
+
lang_idx = self._lang_codes.index(current_lang)
|
|
269
|
+
except ValueError:
|
|
270
|
+
lang_idx = 0
|
|
271
|
+
# fallback to whatever is first or default
|
|
272
|
+
if "en" in self._lang_codes:
|
|
273
|
+
lang_idx = self._lang_codes.index("en")
|
|
274
|
+
|
|
275
|
+
self.cb_language.blockSignals(True)
|
|
276
|
+
self.cb_language.setCurrentIndex(lang_idx)
|
|
277
|
+
self.cb_language.blockSignals(False)
|
|
278
|
+
self._initial_language = current_lang # Track for restart notification
|
|
279
|
+
|
|
280
|
+
# Path fields
|
|
281
|
+
self.le_graxpert.setText(self.settings.value("paths/graxpert", "", type=str))
|
|
282
|
+
self.le_cosmic.setText(self.settings.value("paths/cosmic_clarity", "", type=str))
|
|
283
|
+
self.le_starnet.setText(self.settings.value("paths/starnet", "", type=str))
|
|
284
|
+
self.le_astap.setText(self.settings.value("paths/astap", "", type=str))
|
|
285
|
+
self.le_astrometry.setText(self.settings.value("api/astrometry_key", "", type=str))
|
|
286
|
+
|
|
287
|
+
# WIMS
|
|
288
|
+
self.sp_lat.setValue(self.settings.value("latitude", 0.0, type=float))
|
|
289
|
+
self.sp_lon.setValue(self.settings.value("longitude", 0.0, type=float))
|
|
290
|
+
self.le_date.setText(self.settings.value("date", "", type=str) or "")
|
|
291
|
+
self.le_time.setText(self.settings.value("time", "", type=str) or "")
|
|
292
|
+
tz_val = self.settings.value("timezone", "UTC", type=str) or "UTC"
|
|
293
|
+
idx = max(0, self.cb_tz.findText(tz_val))
|
|
294
|
+
self.cb_tz.setCurrentIndex(idx)
|
|
295
|
+
self.sp_min_alt.setValue(self.settings.value("min_altitude", 0.0, type=float))
|
|
296
|
+
self.sp_obj_limit.setValue(self.settings.value("object_limit", 100, type=int))
|
|
297
|
+
|
|
298
|
+
# Display
|
|
299
|
+
self.chk_autostretch_16bit.setChecked(
|
|
300
|
+
self.settings.value("display/autostretch_16bit", True, type=bool)
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
current_opacity = self.settings.value("display/bg_opacity", 50, type=int)
|
|
304
|
+
self.slider_bg_opacity.blockSignals(True)
|
|
305
|
+
self.slider_bg_opacity.setValue(current_opacity)
|
|
306
|
+
self.slider_bg_opacity.blockSignals(False)
|
|
307
|
+
self.lbl_bg_opacity_val.setText(f"{current_opacity}%")
|
|
308
|
+
self._initial_bg_opacity = int(current_opacity) # For cancel/revert
|
|
309
|
+
|
|
310
|
+
# Custom background
|
|
311
|
+
self._initial_bg_path = self.settings.value("ui/custom_background", "", type=str) or ""
|
|
312
|
+
self.le_bg_path.setText(self._initial_bg_path)
|
|
313
|
+
|
|
314
|
+
# RA/Dec Overlay
|
|
315
|
+
self.chk_wcs_enabled.setChecked(self.settings.value("wcs_grid/enabled", True, type=bool))
|
|
316
|
+
|
|
317
|
+
self.cb_wcs_mode.blockSignals(True)
|
|
318
|
+
self.cb_wcs_mode.setCurrentIndex(
|
|
319
|
+
0 if (self.settings.value("wcs_grid/mode", "auto", type=str) == "auto") else 1
|
|
320
|
+
)
|
|
321
|
+
self.cb_wcs_mode.blockSignals(False)
|
|
322
|
+
|
|
323
|
+
self.cb_wcs_unit.blockSignals(True)
|
|
324
|
+
self.cb_wcs_unit.setCurrentIndex(
|
|
325
|
+
0 if (self.settings.value("wcs_grid/step_unit", "deg", type=str) == "deg") else 1
|
|
326
|
+
)
|
|
327
|
+
self.cb_wcs_unit.blockSignals(False)
|
|
328
|
+
|
|
329
|
+
self.sp_wcs_step.setValue(self.settings.value("wcs_grid/step_value", 1.0, type=float))
|
|
330
|
+
self.sp_wcs_step.setEnabled(self.cb_wcs_mode.currentIndex() == 1)
|
|
331
|
+
self.sp_wcs_step.setSuffix(" °" if self.cb_wcs_unit.currentIndex() == 0 else " arcmin")
|
|
332
|
+
|
|
333
|
+
def reject(self):
|
|
334
|
+
"""User cancelled: restore the original background opacity (revert live changes)."""
|
|
335
|
+
try:
|
|
336
|
+
# Restore saved original value
|
|
337
|
+
self.settings.setValue("display/bg_opacity", int(self._initial_bg_opacity))
|
|
338
|
+
self.settings.sync()
|
|
339
|
+
# Ask parent to redraw with restored value
|
|
340
|
+
parent = self.parent()
|
|
341
|
+
if parent:
|
|
342
|
+
# restore original custom background (may be empty)
|
|
343
|
+
try:
|
|
344
|
+
# If there was an initial custom background, restore it; otherwise clear.
|
|
345
|
+
if self._initial_bg_path:
|
|
346
|
+
if hasattr(parent, "_apply_custom_background"):
|
|
347
|
+
parent._apply_custom_background(self._initial_bg_path)
|
|
348
|
+
else:
|
|
349
|
+
# Avoid calling _apply_custom_background("") which shows a warning
|
|
350
|
+
if hasattr(parent, "_clear_custom_background"):
|
|
351
|
+
parent._clear_custom_background()
|
|
352
|
+
elif hasattr(parent, "_apply_custom_background"):
|
|
353
|
+
parent._apply_custom_background("")
|
|
354
|
+
except Exception:
|
|
355
|
+
pass
|
|
356
|
+
# update MDI viewport/redraw
|
|
357
|
+
try:
|
|
358
|
+
if hasattr(parent, "mdi") and hasattr(parent.mdi, "viewport"):
|
|
359
|
+
parent.mdi.viewport().update()
|
|
360
|
+
except Exception:
|
|
361
|
+
pass
|
|
362
|
+
except Exception:
|
|
363
|
+
pass
|
|
364
|
+
super().reject()
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
# ----------------- helpers -----------------
|
|
368
|
+
def _browse_into(self, lineedit: QLineEdit):
|
|
369
|
+
path, _ = QFileDialog.getOpenFileName(self, "Select Executable", "", "Executables (*)")
|
|
370
|
+
if path:
|
|
371
|
+
lineedit.setText(path)
|
|
372
|
+
|
|
373
|
+
def _browse_dir(self, lineedit: QLineEdit):
|
|
374
|
+
path = QFileDialog.getExistingDirectory(self, "Select Folder", "")
|
|
375
|
+
if path:
|
|
376
|
+
lineedit.setText(path)
|
|
377
|
+
|
|
378
|
+
def _check_updates_now_clicked(self):
|
|
379
|
+
"""Persist update settings, then ask the main window to run an interactive check (if available)."""
|
|
380
|
+
self.settings.setValue("updates/check_on_startup", self.chk_updates_startup.isChecked())
|
|
381
|
+
self.settings.setValue("updates/url", self.le_updates_url.text().strip())
|
|
382
|
+
self.settings.sync()
|
|
383
|
+
|
|
384
|
+
parent = self.parent()
|
|
385
|
+
if parent and hasattr(parent, "_check_for_updates_async"):
|
|
386
|
+
try:
|
|
387
|
+
parent._check_for_updates_async(interactive=True)
|
|
388
|
+
except Exception:
|
|
389
|
+
pass
|
|
390
|
+
|
|
391
|
+
def _choose_background_clicked(self):
|
|
392
|
+
"""Open a file picker and apply a custom background image for the app."""
|
|
393
|
+
path, _ = QFileDialog.getOpenFileName(self, "Select background image", "", "Images (*.png *.jpg *.jpeg)")
|
|
394
|
+
if not path:
|
|
395
|
+
return
|
|
396
|
+
try:
|
|
397
|
+
# Do NOT persist yet — just update UI and preview via parent.
|
|
398
|
+
self.le_bg_path.setText(path)
|
|
399
|
+
parent = self.parent()
|
|
400
|
+
if parent and hasattr(parent, "_apply_custom_background"):
|
|
401
|
+
try:
|
|
402
|
+
parent._apply_custom_background(path)
|
|
403
|
+
except Exception:
|
|
404
|
+
pass
|
|
405
|
+
except Exception:
|
|
406
|
+
pass
|
|
407
|
+
|
|
408
|
+
def _clear_background_clicked(self):
|
|
409
|
+
"""Clear persisted custom background and ask main window to restore defaults."""
|
|
410
|
+
try:
|
|
411
|
+
# Do NOT modify settings yet — clear preview and let Save apply
|
|
412
|
+
self.le_bg_path.setText("")
|
|
413
|
+
parent = self.parent()
|
|
414
|
+
if parent:
|
|
415
|
+
# request parent to clear preview/background for now
|
|
416
|
+
try:
|
|
417
|
+
if hasattr(parent, "_clear_custom_background"):
|
|
418
|
+
parent._clear_custom_background()
|
|
419
|
+
elif hasattr(parent, "_apply_custom_background"):
|
|
420
|
+
parent._apply_custom_background("")
|
|
421
|
+
except Exception:
|
|
422
|
+
pass
|
|
423
|
+
except Exception:
|
|
424
|
+
pass
|
|
425
|
+
|
|
426
|
+
def _on_theme_changed(self, idx: int):
|
|
427
|
+
# Enable the "Customize…" button only when Custom is selected
|
|
428
|
+
text = self.cb_theme.currentText().lower()
|
|
429
|
+
self.btn_theme_custom.setEnabled(text == "custom")
|
|
430
|
+
|
|
431
|
+
def _open_theme_editor(self):
|
|
432
|
+
from PyQt6.QtWidgets import QDialog
|
|
433
|
+
dlg = ThemeEditorDialog(self, self.settings)
|
|
434
|
+
if dlg.exec() == QDialog.DialogCode.Accepted:
|
|
435
|
+
# If user saved a custom theme, make sure "Custom" is selected
|
|
436
|
+
self.cb_theme.setCurrentIndex(4) # Custom
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _save_and_accept(self):
|
|
440
|
+
# Paths / Integrations
|
|
441
|
+
self.settings.setValue("paths/graxpert", self.le_graxpert.text().strip())
|
|
442
|
+
self.settings.setValue("paths/cosmic_clarity", self.le_cosmic.text().strip())
|
|
443
|
+
self.settings.setValue("paths/starnet", self.le_starnet.text().strip())
|
|
444
|
+
self.settings.setValue("paths/astap", self.le_astap.text().strip())
|
|
445
|
+
self.settings.setValue("shortcuts/save_on_exit", self.chk_save_shortcuts.isChecked())
|
|
446
|
+
self.settings.setValue("api/astrometry_key", self.le_astrometry.text().strip())
|
|
447
|
+
|
|
448
|
+
# WIMS defaults
|
|
449
|
+
self.settings.setValue("latitude", float(self.sp_lat.value()))
|
|
450
|
+
self.settings.setValue("longitude", float(self.sp_lon.value()))
|
|
451
|
+
self.settings.setValue("date", self.le_date.text().strip())
|
|
452
|
+
self.settings.setValue("time", self.le_time.text().strip())
|
|
453
|
+
self.settings.setValue("timezone", self.cb_tz.currentText())
|
|
454
|
+
self.settings.setValue("min_altitude", float(self.sp_min_alt.value()))
|
|
455
|
+
self.settings.setValue("object_limit", int(self.sp_obj_limit.value()))
|
|
456
|
+
|
|
457
|
+
# RA/Dec Overlay
|
|
458
|
+
self.settings.setValue("wcs_grid/enabled", self.chk_wcs_enabled.isChecked())
|
|
459
|
+
self.settings.setValue("wcs_grid/mode", "auto" if self.cb_wcs_mode.currentIndex() == 0 else "fixed")
|
|
460
|
+
self.settings.setValue("wcs_grid/step_unit", "deg" if self.cb_wcs_unit.currentIndex() == 0 else "arcmin")
|
|
461
|
+
self.settings.setValue("wcs_grid/step_value", float(self.sp_wcs_step.value()))
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
# Updates + Display
|
|
465
|
+
self.settings.setValue("updates/check_on_startup", self.chk_updates_startup.isChecked())
|
|
466
|
+
self.settings.setValue("updates/url", self.le_updates_url.text().strip())
|
|
467
|
+
self.settings.setValue("display/autostretch_16bit", self.chk_autostretch_16bit.isChecked())
|
|
468
|
+
|
|
469
|
+
# Custom background: persist the chosen path (empty -> remove)
|
|
470
|
+
bg_path = (self.le_bg_path.text() or "").strip()
|
|
471
|
+
if bg_path:
|
|
472
|
+
self.settings.setValue("ui/custom_background", bg_path)
|
|
473
|
+
else:
|
|
474
|
+
try:
|
|
475
|
+
self.settings.remove("ui/custom_background")
|
|
476
|
+
except Exception:
|
|
477
|
+
self.settings.setValue("ui/custom_background", "")
|
|
478
|
+
|
|
479
|
+
# bg_opacity is already saved in real-time by _on_opacity_changed()
|
|
480
|
+
|
|
481
|
+
# Theme
|
|
482
|
+
idx = max(0, self.cb_theme.currentIndex())
|
|
483
|
+
if idx == 0:
|
|
484
|
+
theme_val = "dark"
|
|
485
|
+
elif idx == 1:
|
|
486
|
+
theme_val = "gray"
|
|
487
|
+
elif idx == 2:
|
|
488
|
+
theme_val = "light"
|
|
489
|
+
elif idx == 3:
|
|
490
|
+
theme_val = "system"
|
|
491
|
+
else:
|
|
492
|
+
theme_val = "custom"
|
|
493
|
+
self.settings.setValue("ui/theme", theme_val)
|
|
494
|
+
|
|
495
|
+
# Language
|
|
496
|
+
lang_idx = self.cb_language.currentIndex()
|
|
497
|
+
new_lang = self._lang_codes[lang_idx] if 0 <= lang_idx < len(self._lang_codes) else "en"
|
|
498
|
+
save_language(new_lang)
|
|
499
|
+
|
|
500
|
+
# Apply language change immediately if changed
|
|
501
|
+
if new_lang != self._initial_language:
|
|
502
|
+
from PyQt6.QtWidgets import QMessageBox
|
|
503
|
+
import sys
|
|
504
|
+
import os
|
|
505
|
+
|
|
506
|
+
# Save UI state before restart to avoid losing toolbar/window changes
|
|
507
|
+
p = self.parent()
|
|
508
|
+
if p and hasattr(p, "save_ui_state"):
|
|
509
|
+
try:
|
|
510
|
+
p.save_ui_state()
|
|
511
|
+
except Exception:
|
|
512
|
+
pass
|
|
513
|
+
|
|
514
|
+
# Set restart flag on parent to bypass exit confirmation
|
|
515
|
+
if p:
|
|
516
|
+
p._is_restarting = True
|
|
517
|
+
|
|
518
|
+
self.settings.sync()
|
|
519
|
+
|
|
520
|
+
QMessageBox.information(
|
|
521
|
+
self,
|
|
522
|
+
self.tr("Restart required"),
|
|
523
|
+
self.tr("The application will now restart to apply the language change.")
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
# Restart logic
|
|
527
|
+
import subprocess
|
|
528
|
+
try:
|
|
529
|
+
# Prepare arguments for restart
|
|
530
|
+
args = sys.argv[:]
|
|
531
|
+
if getattr(sys, 'frozen', False):
|
|
532
|
+
# If frozen, sys.executable is the app, and args[0] is also the app
|
|
533
|
+
cmd = [sys.executable] + args[1:]
|
|
534
|
+
else:
|
|
535
|
+
# If running from source, sys.executable is python, args[0] is the script
|
|
536
|
+
cmd = [sys.executable] + args
|
|
537
|
+
|
|
538
|
+
# Start new process and exit current one
|
|
539
|
+
if os.name == 'nt':
|
|
540
|
+
# On Windows, use DETACHED_PROCESS to break the process tree connection to the terminal
|
|
541
|
+
# and close_fds to ensure no handles are inherited.
|
|
542
|
+
subprocess.Popen(cmd, creationflags=subprocess.DETACHED_PROCESS, close_fds=True)
|
|
543
|
+
else:
|
|
544
|
+
subprocess.Popen(cmd)
|
|
545
|
+
|
|
546
|
+
QApplication.instance().quit()
|
|
547
|
+
sys.exit(0)
|
|
548
|
+
except Exception:
|
|
549
|
+
# Fallback to execl if Popen fails
|
|
550
|
+
os.execl(sys.executable, sys.executable, *sys.argv)
|
|
551
|
+
|
|
552
|
+
self.settings.sync()
|
|
553
|
+
|
|
554
|
+
# Apply now if the parent knows how
|
|
555
|
+
p = self.parent()
|
|
556
|
+
if p and hasattr(p, "apply_theme_from_settings"):
|
|
557
|
+
try:
|
|
558
|
+
p.apply_theme_from_settings()
|
|
559
|
+
except Exception:
|
|
560
|
+
pass
|
|
561
|
+
|
|
562
|
+
if hasattr(p, "mdi") and hasattr(p.mdi, "viewport"):
|
|
563
|
+
p.mdi.viewport().update()
|
|
564
|
+
|
|
565
|
+
self.accept()
|
|
566
|
+
|
|
567
|
+
from PyQt6.QtGui import QColor, QFont
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
class ThemeEditorDialog(QDialog):
|
|
571
|
+
"""
|
|
572
|
+
Simple "Custom Theme" editor: lets the user pick main colors and a UI font.
|
|
573
|
+
Colors are stored in QSettings as hex strings (e.g. '#404040').
|
|
574
|
+
"""
|
|
575
|
+
def __init__(self, parent, settings: QSettings):
|
|
576
|
+
super().__init__(parent)
|
|
577
|
+
self.settings = settings
|
|
578
|
+
self.setWindowTitle("Custom Theme")
|
|
579
|
+
self.colors: dict[str, QColor] = {}
|
|
580
|
+
self.font_str: str = self.settings.value("ui/custom/font", "", type=str) or ""
|
|
581
|
+
|
|
582
|
+
form = QFormLayout(self)
|
|
583
|
+
|
|
584
|
+
# Helper: add color pickers for key roles
|
|
585
|
+
self._add_color_picker(form, "Window / Panels", "ui/custom/window", QColor(40, 40, 40))
|
|
586
|
+
self._add_color_picker(form, "Base (Editors)", "ui/custom/base", QColor(24, 24, 24))
|
|
587
|
+
self._add_color_picker(form, "Alternate Base", "ui/custom/altbase", QColor(32, 32, 32))
|
|
588
|
+
self._add_color_picker(form, "Text", "ui/custom/text", QColor(230, 230, 230))
|
|
589
|
+
self._add_color_picker(form, "Buttons", "ui/custom/button", QColor(40, 40, 40))
|
|
590
|
+
self._add_color_picker(form, "Highlight / Accent","ui/custom/highlight",QColor(30, 144, 255))
|
|
591
|
+
self._add_color_picker(form, "Link", "ui/custom/link", QColor(120, 170, 255))
|
|
592
|
+
self._add_color_picker(form, "Visited Link", "ui/custom/link_visited", QColor(180, 150, 255))
|
|
593
|
+
|
|
594
|
+
# Font picker
|
|
595
|
+
self.btn_font = QPushButton("Choose…")
|
|
596
|
+
self.btn_font.clicked.connect(self._pick_font)
|
|
597
|
+
form.addRow("UI Font:", self.btn_font)
|
|
598
|
+
|
|
599
|
+
# Buttons
|
|
600
|
+
btns = QDialogButtonBox(
|
|
601
|
+
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel,
|
|
602
|
+
parent=self
|
|
603
|
+
)
|
|
604
|
+
btns.accepted.connect(self._save_and_accept)
|
|
605
|
+
btns.rejected.connect(self.reject)
|
|
606
|
+
form.addRow(btns)
|
|
607
|
+
|
|
608
|
+
# ---------- helpers ----------
|
|
609
|
+
|
|
610
|
+
def _add_color_picker(self, form: QFormLayout, label_text: str,
|
|
611
|
+
key: str, default: QColor):
|
|
612
|
+
# Load from settings or default
|
|
613
|
+
stored = self.settings.value(key, default.name(), type=str)
|
|
614
|
+
color = QColor(stored) if stored else default
|
|
615
|
+
self.colors[key] = color
|
|
616
|
+
|
|
617
|
+
btn = QPushButton(color.name())
|
|
618
|
+
btn.setMinimumWidth(90)
|
|
619
|
+
btn.setStyleSheet(f"background-color: {color.name()}; color: #ffffff;")
|
|
620
|
+
btn.clicked.connect(lambda _=False, k=key, b=btn: self._pick_color(k, b))
|
|
621
|
+
|
|
622
|
+
form.addRow(label_text + ":", btn)
|
|
623
|
+
|
|
624
|
+
def _pick_color(self, key: str, button: QPushButton):
|
|
625
|
+
initial = self.colors.get(key, QColor("#404040"))
|
|
626
|
+
col = QColorDialog.getColor(initial, self, "Select Color")
|
|
627
|
+
if col.isValid():
|
|
628
|
+
self.colors[key] = col
|
|
629
|
+
button.setText(col.name())
|
|
630
|
+
button.setStyleSheet(f"background-color: {col.name()}; color: #ffffff;")
|
|
631
|
+
|
|
632
|
+
from PyQt6.QtGui import QFont
|
|
633
|
+
from PyQt6.QtWidgets import QFontDialog
|
|
634
|
+
|
|
635
|
+
def _pick_font(self):
|
|
636
|
+
# Load previous font if we have one
|
|
637
|
+
base_str = self.settings.value("ui/custom_font", "", type=str)
|
|
638
|
+
base_font = QFont()
|
|
639
|
+
if base_str:
|
|
640
|
+
try:
|
|
641
|
+
base_font.fromString(base_str)
|
|
642
|
+
except Exception:
|
|
643
|
+
pass
|
|
644
|
+
|
|
645
|
+
# ✅ NOTE: (font, ok) — NOT (ok, font)
|
|
646
|
+
font, ok = QFontDialog.getFont(base_font, self, "Select UI Font")
|
|
647
|
+
if not ok:
|
|
648
|
+
return # user cancelled
|
|
649
|
+
|
|
650
|
+
# Store and update preview
|
|
651
|
+
self.font_str = font.toString()
|
|
652
|
+
self.settings.setValue("ui/custom_font", self.font_str)
|
|
653
|
+
self.settings.sync()
|
|
654
|
+
|
|
655
|
+
# If you have a label/button to show the chosen font:
|
|
656
|
+
try:
|
|
657
|
+
self.font_button.setText(f"{font.family()}, {font.pointSize()} pt")
|
|
658
|
+
except Exception:
|
|
659
|
+
pass
|
|
660
|
+
|
|
661
|
+
# Re-apply theme so the new font takes effect
|
|
662
|
+
parent = self.parent()
|
|
663
|
+
if parent and hasattr(parent, "apply_theme_from_settings"):
|
|
664
|
+
try:
|
|
665
|
+
parent.apply_theme_from_settings()
|
|
666
|
+
except Exception:
|
|
667
|
+
pass
|
|
668
|
+
|
|
669
|
+
def _save_and_accept(self):
|
|
670
|
+
# Persist colors
|
|
671
|
+
for key, col in self.colors.items():
|
|
672
|
+
self.settings.setValue(key, col.name())
|
|
673
|
+
|
|
674
|
+
# Persist font if chosen
|
|
675
|
+
if self.font_str:
|
|
676
|
+
self.settings.setValue("ui/custom/font", self.font_str)
|
|
677
|
+
|
|
678
|
+
self.settings.sync()
|
|
679
|
+
self.accept()
|