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.

Files changed (342) hide show
  1. setiastro/__init__.py +2 -0
  2. setiastro/data/SASP_data.fits +0 -0
  3. setiastro/data/catalogs/List_of_Galaxies_with_Distances_Gly.csv +488 -0
  4. setiastro/data/catalogs/astrobin_filters.csv +890 -0
  5. setiastro/data/catalogs/astrobin_filters_page1_local.csv +51 -0
  6. setiastro/data/catalogs/cali2.csv +63 -0
  7. setiastro/data/catalogs/cali2color.csv +65 -0
  8. setiastro/data/catalogs/celestial_catalog - original.csv +16471 -0
  9. setiastro/data/catalogs/celestial_catalog.csv +24031 -0
  10. setiastro/data/catalogs/detected_stars.csv +24784 -0
  11. setiastro/data/catalogs/fits_header_data.csv +46 -0
  12. setiastro/data/catalogs/test.csv +8 -0
  13. setiastro/data/catalogs/updated_celestial_catalog.csv +16471 -0
  14. setiastro/images/Astro_Spikes.png +0 -0
  15. setiastro/images/HRDiagram.png +0 -0
  16. setiastro/images/LExtract.png +0 -0
  17. setiastro/images/LInsert.png +0 -0
  18. setiastro/images/Oxygenation-atm-2.svg.png +0 -0
  19. setiastro/images/RGB080604.png +0 -0
  20. setiastro/images/abeicon.png +0 -0
  21. setiastro/images/aberration.png +0 -0
  22. setiastro/images/andromedatry.png +0 -0
  23. setiastro/images/andromedatry_satellited.png +0 -0
  24. setiastro/images/annotated.png +0 -0
  25. setiastro/images/aperture.png +0 -0
  26. setiastro/images/astrosuite.ico +0 -0
  27. setiastro/images/astrosuite.png +0 -0
  28. setiastro/images/astrosuitepro.icns +0 -0
  29. setiastro/images/astrosuitepro.ico +0 -0
  30. setiastro/images/astrosuitepro.png +0 -0
  31. setiastro/images/background.png +0 -0
  32. setiastro/images/background2.png +0 -0
  33. setiastro/images/benchmark.png +0 -0
  34. setiastro/images/big_moon_stabilizer_timeline.png +0 -0
  35. setiastro/images/big_moon_stabilizer_timeline_clean.png +0 -0
  36. setiastro/images/blaster.png +0 -0
  37. setiastro/images/blink.png +0 -0
  38. setiastro/images/clahe.png +0 -0
  39. setiastro/images/collage.png +0 -0
  40. setiastro/images/colorwheel.png +0 -0
  41. setiastro/images/contsub.png +0 -0
  42. setiastro/images/convo.png +0 -0
  43. setiastro/images/copyslot.png +0 -0
  44. setiastro/images/cosmic.png +0 -0
  45. setiastro/images/cosmicsat.png +0 -0
  46. setiastro/images/crop1.png +0 -0
  47. setiastro/images/cropicon.png +0 -0
  48. setiastro/images/curves.png +0 -0
  49. setiastro/images/cvs.png +0 -0
  50. setiastro/images/debayer.png +0 -0
  51. setiastro/images/denoise_cnn_custom.png +0 -0
  52. setiastro/images/denoise_cnn_graph.png +0 -0
  53. setiastro/images/disk.png +0 -0
  54. setiastro/images/dse.png +0 -0
  55. setiastro/images/exoicon.png +0 -0
  56. setiastro/images/eye.png +0 -0
  57. setiastro/images/fliphorizontal.png +0 -0
  58. setiastro/images/flipvertical.png +0 -0
  59. setiastro/images/font.png +0 -0
  60. setiastro/images/freqsep.png +0 -0
  61. setiastro/images/functionbundle.png +0 -0
  62. setiastro/images/graxpert.png +0 -0
  63. setiastro/images/green.png +0 -0
  64. setiastro/images/gridicon.png +0 -0
  65. setiastro/images/halo.png +0 -0
  66. setiastro/images/hdr.png +0 -0
  67. setiastro/images/histogram.png +0 -0
  68. setiastro/images/hubble.png +0 -0
  69. setiastro/images/imagecombine.png +0 -0
  70. setiastro/images/invert.png +0 -0
  71. setiastro/images/isophote.png +0 -0
  72. setiastro/images/isophote_demo_figure.png +0 -0
  73. setiastro/images/isophote_demo_image.png +0 -0
  74. setiastro/images/isophote_demo_model.png +0 -0
  75. setiastro/images/isophote_demo_residual.png +0 -0
  76. setiastro/images/jwstpupil.png +0 -0
  77. setiastro/images/linearfit.png +0 -0
  78. setiastro/images/livestacking.png +0 -0
  79. setiastro/images/mask.png +0 -0
  80. setiastro/images/maskapply.png +0 -0
  81. setiastro/images/maskcreate.png +0 -0
  82. setiastro/images/maskremove.png +0 -0
  83. setiastro/images/morpho.png +0 -0
  84. setiastro/images/mosaic.png +0 -0
  85. setiastro/images/multiscale_decomp.png +0 -0
  86. setiastro/images/nbtorgb.png +0 -0
  87. setiastro/images/neutral.png +0 -0
  88. setiastro/images/nuke.png +0 -0
  89. setiastro/images/openfile.png +0 -0
  90. setiastro/images/pedestal.png +0 -0
  91. setiastro/images/pen.png +0 -0
  92. setiastro/images/pixelmath.png +0 -0
  93. setiastro/images/platesolve.png +0 -0
  94. setiastro/images/ppp.png +0 -0
  95. setiastro/images/pro.png +0 -0
  96. setiastro/images/project.png +0 -0
  97. setiastro/images/psf.png +0 -0
  98. setiastro/images/redo.png +0 -0
  99. setiastro/images/redoicon.png +0 -0
  100. setiastro/images/rescale.png +0 -0
  101. setiastro/images/rgbalign.png +0 -0
  102. setiastro/images/rgbcombo.png +0 -0
  103. setiastro/images/rgbextract.png +0 -0
  104. setiastro/images/rotate180.png +0 -0
  105. setiastro/images/rotateclockwise.png +0 -0
  106. setiastro/images/rotatecounterclockwise.png +0 -0
  107. setiastro/images/satellite.png +0 -0
  108. setiastro/images/script.png +0 -0
  109. setiastro/images/selectivecolor.png +0 -0
  110. setiastro/images/simbad.png +0 -0
  111. setiastro/images/slot0.png +0 -0
  112. setiastro/images/slot1.png +0 -0
  113. setiastro/images/slot2.png +0 -0
  114. setiastro/images/slot3.png +0 -0
  115. setiastro/images/slot4.png +0 -0
  116. setiastro/images/slot5.png +0 -0
  117. setiastro/images/slot6.png +0 -0
  118. setiastro/images/slot7.png +0 -0
  119. setiastro/images/slot8.png +0 -0
  120. setiastro/images/slot9.png +0 -0
  121. setiastro/images/spcc.png +0 -0
  122. setiastro/images/spin_precession_vs_lunar_distance.png +0 -0
  123. setiastro/images/spinner.gif +0 -0
  124. setiastro/images/stacking.png +0 -0
  125. setiastro/images/staradd.png +0 -0
  126. setiastro/images/staralign.png +0 -0
  127. setiastro/images/starnet.png +0 -0
  128. setiastro/images/starregistration.png +0 -0
  129. setiastro/images/starspike.png +0 -0
  130. setiastro/images/starstretch.png +0 -0
  131. setiastro/images/statstretch.png +0 -0
  132. setiastro/images/supernova.png +0 -0
  133. setiastro/images/uhs.png +0 -0
  134. setiastro/images/undoicon.png +0 -0
  135. setiastro/images/upscale.png +0 -0
  136. setiastro/images/viewbundle.png +0 -0
  137. setiastro/images/whitebalance.png +0 -0
  138. setiastro/images/wimi_icon_256x256.png +0 -0
  139. setiastro/images/wimilogo.png +0 -0
  140. setiastro/images/wims.png +0 -0
  141. setiastro/images/wrench_icon.png +0 -0
  142. setiastro/images/xisfliberator.png +0 -0
  143. setiastro/saspro/__init__.py +20 -0
  144. setiastro/saspro/__main__.py +809 -0
  145. setiastro/saspro/_generated/__init__.py +7 -0
  146. setiastro/saspro/_generated/build_info.py +2 -0
  147. setiastro/saspro/abe.py +1295 -0
  148. setiastro/saspro/abe_preset.py +196 -0
  149. setiastro/saspro/aberration_ai.py +694 -0
  150. setiastro/saspro/aberration_ai_preset.py +224 -0
  151. setiastro/saspro/accel_installer.py +218 -0
  152. setiastro/saspro/accel_workers.py +30 -0
  153. setiastro/saspro/add_stars.py +621 -0
  154. setiastro/saspro/astrobin_exporter.py +1007 -0
  155. setiastro/saspro/astrospike.py +153 -0
  156. setiastro/saspro/astrospike_python.py +1839 -0
  157. setiastro/saspro/autostretch.py +196 -0
  158. setiastro/saspro/backgroundneutral.py +560 -0
  159. setiastro/saspro/batch_convert.py +325 -0
  160. setiastro/saspro/batch_renamer.py +519 -0
  161. setiastro/saspro/blemish_blaster.py +488 -0
  162. setiastro/saspro/blink_comparator_pro.py +2926 -0
  163. setiastro/saspro/bundles.py +61 -0
  164. setiastro/saspro/bundles_dock.py +114 -0
  165. setiastro/saspro/cheat_sheet.py +178 -0
  166. setiastro/saspro/clahe.py +342 -0
  167. setiastro/saspro/comet_stacking.py +1377 -0
  168. setiastro/saspro/common_tr.py +107 -0
  169. setiastro/saspro/config.py +38 -0
  170. setiastro/saspro/config_bootstrap.py +40 -0
  171. setiastro/saspro/config_manager.py +316 -0
  172. setiastro/saspro/continuum_subtract.py +1617 -0
  173. setiastro/saspro/convo.py +1397 -0
  174. setiastro/saspro/convo_preset.py +414 -0
  175. setiastro/saspro/copyastro.py +187 -0
  176. setiastro/saspro/cosmicclarity.py +1564 -0
  177. setiastro/saspro/cosmicclarity_preset.py +407 -0
  178. setiastro/saspro/crop_dialog_pro.py +956 -0
  179. setiastro/saspro/crop_preset.py +189 -0
  180. setiastro/saspro/curve_editor_pro.py +2544 -0
  181. setiastro/saspro/curves_preset.py +375 -0
  182. setiastro/saspro/debayer.py +670 -0
  183. setiastro/saspro/debug_utils.py +29 -0
  184. setiastro/saspro/dnd_mime.py +35 -0
  185. setiastro/saspro/doc_manager.py +2641 -0
  186. setiastro/saspro/exoplanet_detector.py +2166 -0
  187. setiastro/saspro/file_utils.py +284 -0
  188. setiastro/saspro/fitsmodifier.py +745 -0
  189. setiastro/saspro/fix_bom.py +32 -0
  190. setiastro/saspro/free_torch_memory.py +48 -0
  191. setiastro/saspro/frequency_separation.py +1343 -0
  192. setiastro/saspro/function_bundle.py +1594 -0
  193. setiastro/saspro/generate_translations.py +2378 -0
  194. setiastro/saspro/ghs_dialog_pro.py +660 -0
  195. setiastro/saspro/ghs_preset.py +284 -0
  196. setiastro/saspro/graxpert.py +634 -0
  197. setiastro/saspro/graxpert_preset.py +287 -0
  198. setiastro/saspro/gui/__init__.py +0 -0
  199. setiastro/saspro/gui/main_window.py +8567 -0
  200. setiastro/saspro/gui/mixins/__init__.py +33 -0
  201. setiastro/saspro/gui/mixins/dock_mixin.py +263 -0
  202. setiastro/saspro/gui/mixins/file_mixin.py +443 -0
  203. setiastro/saspro/gui/mixins/geometry_mixin.py +403 -0
  204. setiastro/saspro/gui/mixins/header_mixin.py +441 -0
  205. setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
  206. setiastro/saspro/gui/mixins/menu_mixin.py +361 -0
  207. setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
  208. setiastro/saspro/gui/mixins/toolbar_mixin.py +1457 -0
  209. setiastro/saspro/gui/mixins/update_mixin.py +309 -0
  210. setiastro/saspro/gui/mixins/view_mixin.py +435 -0
  211. setiastro/saspro/halobgon.py +462 -0
  212. setiastro/saspro/header_viewer.py +448 -0
  213. setiastro/saspro/headless_utils.py +88 -0
  214. setiastro/saspro/histogram.py +753 -0
  215. setiastro/saspro/history_explorer.py +939 -0
  216. setiastro/saspro/i18n.py +156 -0
  217. setiastro/saspro/image_combine.py +414 -0
  218. setiastro/saspro/image_peeker_pro.py +1601 -0
  219. setiastro/saspro/imageops/__init__.py +37 -0
  220. setiastro/saspro/imageops/mdi_snap.py +292 -0
  221. setiastro/saspro/imageops/scnr.py +36 -0
  222. setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
  223. setiastro/saspro/imageops/stretch.py +244 -0
  224. setiastro/saspro/isophote.py +1179 -0
  225. setiastro/saspro/layers.py +208 -0
  226. setiastro/saspro/layers_dock.py +714 -0
  227. setiastro/saspro/lazy_imports.py +193 -0
  228. setiastro/saspro/legacy/__init__.py +2 -0
  229. setiastro/saspro/legacy/image_manager.py +2226 -0
  230. setiastro/saspro/legacy/numba_utils.py +3659 -0
  231. setiastro/saspro/legacy/xisf.py +1071 -0
  232. setiastro/saspro/linear_fit.py +534 -0
  233. setiastro/saspro/live_stacking.py +1830 -0
  234. setiastro/saspro/log_bus.py +5 -0
  235. setiastro/saspro/logging_config.py +460 -0
  236. setiastro/saspro/luminancerecombine.py +309 -0
  237. setiastro/saspro/main_helpers.py +201 -0
  238. setiastro/saspro/mask_creation.py +928 -0
  239. setiastro/saspro/masks_core.py +56 -0
  240. setiastro/saspro/mdi_widgets.py +353 -0
  241. setiastro/saspro/memory_utils.py +666 -0
  242. setiastro/saspro/metadata_patcher.py +75 -0
  243. setiastro/saspro/mfdeconv.py +3826 -0
  244. setiastro/saspro/mfdeconv_earlystop.py +71 -0
  245. setiastro/saspro/mfdeconvcudnn.py +3263 -0
  246. setiastro/saspro/mfdeconvsport.py +2382 -0
  247. setiastro/saspro/minorbodycatalog.py +567 -0
  248. setiastro/saspro/morphology.py +382 -0
  249. setiastro/saspro/multiscale_decomp.py +1290 -0
  250. setiastro/saspro/nbtorgb_stars.py +531 -0
  251. setiastro/saspro/numba_utils.py +3044 -0
  252. setiastro/saspro/numba_warmup.py +141 -0
  253. setiastro/saspro/ops/__init__.py +9 -0
  254. setiastro/saspro/ops/command_help_dialog.py +623 -0
  255. setiastro/saspro/ops/command_runner.py +217 -0
  256. setiastro/saspro/ops/commands.py +1594 -0
  257. setiastro/saspro/ops/script_editor.py +1102 -0
  258. setiastro/saspro/ops/scripts.py +1413 -0
  259. setiastro/saspro/ops/settings.py +679 -0
  260. setiastro/saspro/parallel_utils.py +554 -0
  261. setiastro/saspro/pedestal.py +121 -0
  262. setiastro/saspro/perfect_palette_picker.py +1070 -0
  263. setiastro/saspro/pipeline.py +110 -0
  264. setiastro/saspro/pixelmath.py +1600 -0
  265. setiastro/saspro/plate_solver.py +2444 -0
  266. setiastro/saspro/project_io.py +797 -0
  267. setiastro/saspro/psf_utils.py +136 -0
  268. setiastro/saspro/psf_viewer.py +549 -0
  269. setiastro/saspro/pyi_rthook_astroquery.py +95 -0
  270. setiastro/saspro/remove_green.py +314 -0
  271. setiastro/saspro/remove_stars.py +1625 -0
  272. setiastro/saspro/remove_stars_preset.py +404 -0
  273. setiastro/saspro/resources.py +477 -0
  274. setiastro/saspro/rgb_combination.py +207 -0
  275. setiastro/saspro/rgb_extract.py +19 -0
  276. setiastro/saspro/rgbalign.py +723 -0
  277. setiastro/saspro/runtime_imports.py +7 -0
  278. setiastro/saspro/runtime_torch.py +754 -0
  279. setiastro/saspro/save_options.py +72 -0
  280. setiastro/saspro/selective_color.py +1552 -0
  281. setiastro/saspro/sfcc.py +1430 -0
  282. setiastro/saspro/shortcuts.py +3043 -0
  283. setiastro/saspro/signature_insert.py +1099 -0
  284. setiastro/saspro/stacking_suite.py +18181 -0
  285. setiastro/saspro/star_alignment.py +7420 -0
  286. setiastro/saspro/star_alignment_preset.py +329 -0
  287. setiastro/saspro/star_metrics.py +49 -0
  288. setiastro/saspro/star_spikes.py +681 -0
  289. setiastro/saspro/star_stretch.py +470 -0
  290. setiastro/saspro/stat_stretch.py +506 -0
  291. setiastro/saspro/status_log_dock.py +78 -0
  292. setiastro/saspro/subwindow.py +3267 -0
  293. setiastro/saspro/supernovaasteroidhunter.py +1716 -0
  294. setiastro/saspro/swap_manager.py +99 -0
  295. setiastro/saspro/torch_backend.py +89 -0
  296. setiastro/saspro/torch_rejection.py +434 -0
  297. setiastro/saspro/translations/de_translations.py +3733 -0
  298. setiastro/saspro/translations/es_translations.py +3923 -0
  299. setiastro/saspro/translations/fr_translations.py +3842 -0
  300. setiastro/saspro/translations/integrate_translations.py +234 -0
  301. setiastro/saspro/translations/it_translations.py +3662 -0
  302. setiastro/saspro/translations/ja_translations.py +3585 -0
  303. setiastro/saspro/translations/pt_translations.py +3853 -0
  304. setiastro/saspro/translations/saspro_de.qm +0 -0
  305. setiastro/saspro/translations/saspro_de.ts +253 -0
  306. setiastro/saspro/translations/saspro_es.qm +0 -0
  307. setiastro/saspro/translations/saspro_es.ts +12520 -0
  308. setiastro/saspro/translations/saspro_fr.qm +0 -0
  309. setiastro/saspro/translations/saspro_fr.ts +12514 -0
  310. setiastro/saspro/translations/saspro_it.qm +0 -0
  311. setiastro/saspro/translations/saspro_it.ts +12520 -0
  312. setiastro/saspro/translations/saspro_ja.qm +0 -0
  313. setiastro/saspro/translations/saspro_ja.ts +257 -0
  314. setiastro/saspro/translations/saspro_pt.qm +0 -0
  315. setiastro/saspro/translations/saspro_pt.ts +257 -0
  316. setiastro/saspro/translations/saspro_zh.qm +0 -0
  317. setiastro/saspro/translations/saspro_zh.ts +12520 -0
  318. setiastro/saspro/translations/zh_translations.py +3659 -0
  319. setiastro/saspro/versioning.py +71 -0
  320. setiastro/saspro/view_bundle.py +1555 -0
  321. setiastro/saspro/wavescale_hdr.py +624 -0
  322. setiastro/saspro/wavescale_hdr_preset.py +101 -0
  323. setiastro/saspro/wavescalede.py +658 -0
  324. setiastro/saspro/wavescalede_preset.py +230 -0
  325. setiastro/saspro/wcs_update.py +374 -0
  326. setiastro/saspro/whitebalance.py +456 -0
  327. setiastro/saspro/widgets/__init__.py +48 -0
  328. setiastro/saspro/widgets/common_utilities.py +306 -0
  329. setiastro/saspro/widgets/graphics_views.py +122 -0
  330. setiastro/saspro/widgets/image_utils.py +518 -0
  331. setiastro/saspro/widgets/preview_dialogs.py +280 -0
  332. setiastro/saspro/widgets/spinboxes.py +275 -0
  333. setiastro/saspro/widgets/themed_buttons.py +13 -0
  334. setiastro/saspro/widgets/wavelet_utils.py +299 -0
  335. setiastro/saspro/window_shelf.py +185 -0
  336. setiastro/saspro/xisf.py +1123 -0
  337. setiastrosuitepro-1.6.1.dist-info/METADATA +267 -0
  338. setiastrosuitepro-1.6.1.dist-info/RECORD +342 -0
  339. setiastrosuitepro-1.6.1.dist-info/WHEEL +4 -0
  340. setiastrosuitepro-1.6.1.dist-info/entry_points.txt +6 -0
  341. setiastrosuitepro-1.6.1.dist-info/licenses/LICENSE +674 -0
  342. 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()