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,443 @@
1
+ # pro/gui/mixins/file_mixin.py
2
+ """
3
+ File operations mixin for AstroSuiteProMainWindow.
4
+ """
5
+ from __future__ import annotations
6
+ import os
7
+ from typing import TYPE_CHECKING
8
+
9
+ from PyQt6.QtCore import Qt
10
+ from PyQt6.QtWidgets import QFileDialog, QMessageBox, QProgressDialog
11
+
12
+ if TYPE_CHECKING:
13
+ pass
14
+
15
+
16
+ class FileMixin:
17
+ """
18
+ Mixin for file operations.
19
+
20
+ Provides methods for opening, saving, importing, and exporting files.
21
+ """
22
+
23
+ def _load_recent_lists(self):
24
+ """Load recent files and projects from settings."""
25
+ if not hasattr(self, "settings"):
26
+ return
27
+
28
+ self._recent_image_paths = self.settings.value("recent/images", [], type=list) or []
29
+ self._recent_project_paths = self.settings.value("recent/projects", [], type=list) or []
30
+
31
+ def _save_recent_lists(self):
32
+ """Save recent files and projects to settings."""
33
+ if not hasattr(self, "settings"):
34
+ return
35
+
36
+ self.settings.setValue("recent/images", self._recent_image_paths[:self._recent_max])
37
+ self.settings.setValue("recent/projects", self._recent_project_paths[:self._recent_max])
38
+
39
+ def _open_recent_image(self, path: str):
40
+ """
41
+ Open a recent image file.
42
+
43
+ Args:
44
+ path: Path to the image file
45
+ """
46
+ if hasattr(self, "open_files"):
47
+ self.open_files([path])
48
+
49
+ def _open_recent_project(self, path: str):
50
+ """
51
+ Open a recent project file.
52
+
53
+ Args:
54
+ path: Path to the project file
55
+ """
56
+ if hasattr(self, "load_project"):
57
+ self.load_project(path)
58
+
59
+ def _add_to_recent_images(self, path: str):
60
+ """
61
+ Add a path to recent images list.
62
+
63
+ Args:
64
+ path: Path to add
65
+ """
66
+ if path in self._recent_image_paths:
67
+ self._recent_image_paths.remove(path)
68
+ self._recent_image_paths.insert(0, path)
69
+ self._recent_image_paths = self._recent_image_paths[:self._recent_max]
70
+ self._save_recent_lists()
71
+ if hasattr(self, "_rebuild_recent_menus"):
72
+ self._rebuild_recent_menus()
73
+
74
+ def _add_to_recent_projects(self, path: str):
75
+ """
76
+ Add a path to recent projects list.
77
+
78
+ Args:
79
+ path: Path to add
80
+ """
81
+ if path in self._recent_project_paths:
82
+ self._recent_project_paths.remove(path)
83
+ self._recent_project_paths.insert(0, path)
84
+ self._recent_project_paths = self._recent_project_paths[:self._recent_max]
85
+ self._save_recent_lists()
86
+ if hasattr(self, "_rebuild_recent_menus"):
87
+ self._rebuild_recent_menus()
88
+ # Extracted FILE methods
89
+
90
+ def open_files(self):
91
+ # One-stop "All Supported" plus focused groups the user can switch to
92
+ filters = (
93
+ "All Supported (*.png *.jpg *.jpeg *.tif *.tiff "
94
+ "*.fits *.fit *.fits.gz *.fit.gz *.fz *.xisf "
95
+ "*.cr2 *.cr3 *.nef *.arw *.dng *.raf *.orf *.rw2 *.pef);;"
96
+ "Astro (FITS/XISF) (*.xisf *.fits *.fit *.fits.gz *.fit.gz *.fz);;"
97
+ "RAW Images (*.cr2 *.cr3 *.nef *.arw *.dng *.raf *.orf *.rw2 *.pef);;"
98
+ "Common Images (*.png *.jpg *.jpeg *.tif *.tiff);;"
99
+ "All Files (*)"
100
+ )
101
+
102
+ # read last dir; validate it still exists
103
+ last_dir = self.settings.value("paths/last_open_dir", "", type=str) or ""
104
+ if last_dir and not os.path.isdir(last_dir):
105
+ last_dir = ""
106
+
107
+ paths, _ = QFileDialog.getOpenFileNames(self, self.tr("Open Images"), last_dir, filters)
108
+ if not paths:
109
+ return
110
+
111
+ # store the directory of the first picked file
112
+ try:
113
+ self.settings.setValue("paths/last_open_dir", os.path.dirname(paths[0]))
114
+ except Exception:
115
+ pass
116
+
117
+ # open each path (doc_manager should emit documentAdded; no manual spawn)
118
+ for p in paths:
119
+ try:
120
+ doc = self.docman.open_path(p) # this emits documentAdded
121
+ self._log(f"Opened: {p}")
122
+ self._add_recent_image(p) # âœ... track in MRU
123
+ except Exception as e:
124
+ QMessageBox.warning(self, self.tr("Open failed"), f"{p}\n\n{e}")
125
+
126
+ def save_active(self):
127
+ from setiastro.saspro.main_helpers import (
128
+ best_doc_name as _best_doc_name,
129
+ normalize_save_path_chosen_filter as _normalize_save_path_chosen_filter,
130
+ )
131
+ from setiastro.saspro.file_utils import _sanitize_filename
132
+
133
+ doc = self._active_doc()
134
+ if not doc:
135
+ return
136
+
137
+ filters = (
138
+ "FITS (*.fits *.fit);;"
139
+ "XISF (*.xisf);;"
140
+ "TIFF (*.tif *.tiff);;"
141
+ "PNG (*.png);;"
142
+ "JPEG (*.jpg *.jpeg)"
143
+ )
144
+
145
+ # --- Determine initial directory nicely -----------------------------
146
+ # 1) Try the document's original file path (strip any "::HDU" or "::XISF[...]" suffix)
147
+ orig_path = (doc.metadata or {}).get("file_path", "") or ""
148
+ if "::" in orig_path:
149
+ # e.g. "/foo/bar/file.fits::HDU 2" or "...::XISF[3]"
150
+ orig_path_fs = orig_path.split("::", 1)[0]
151
+ else:
152
+ orig_path_fs = orig_path
153
+
154
+ candidate_dir = ""
155
+ try:
156
+ if orig_path_fs:
157
+ pdir = os.path.dirname(orig_path_fs)
158
+ if pdir and os.path.isdir(pdir):
159
+ candidate_dir = pdir
160
+ except Exception:
161
+ candidate_dir = ""
162
+
163
+ # 2) Else, fall back to last save dir setting
164
+ if not candidate_dir:
165
+ candidate_dir = self.settings.value("paths/last_save_dir", "", type=str) or ""
166
+
167
+ # 3) Else, home directory
168
+ if not candidate_dir or not os.path.isdir(candidate_dir):
169
+ from pathlib import Path
170
+ candidate_dir = str(Path.home())
171
+
172
+ # --- Suggest a sane filename ---------------------------------------
173
+ suggested = _best_doc_name(doc)
174
+ suggested = os.path.splitext(suggested)[0] # remove any ext
175
+ suggested_safe = _sanitize_filename(suggested)
176
+ suggested_path = os.path.join(candidate_dir, suggested_safe)
177
+
178
+ # --- Open dialog ----------------------------------------
179
+ path, selected_filter = QFileDialog.getSaveFileName(self, self.tr("Save As"), suggested_path, filters)
180
+ if not path:
181
+ return
182
+
183
+ before = path
184
+ path, ext_norm = _normalize_save_path_chosen_filter(path, selected_filter)
185
+
186
+ # If we changed the path (e.g., sanitized), inform once
187
+ if before != path:
188
+ self._log(f"Adjusted filename for safety:\n {before}\n-> {path}")
189
+
190
+ # --- Bit depth selection ----------------------------------------
191
+ from setiastro.saspro.save_options import SaveOptionsDialog
192
+ current_bd = doc.metadata.get("bit_depth")
193
+ dlg = SaveOptionsDialog(self, ext_norm, current_bd)
194
+ if dlg.exec() != dlg.DialogCode.Accepted:
195
+ return
196
+ chosen_bd = dlg.selected_bit_depth()
197
+
198
+ # --- Save & remember folder ----------------------------------------
199
+ try:
200
+ self.docman.save_document(doc, path, bit_depth_override=chosen_bd)
201
+ self._log(f"Saved: {path} ({chosen_bd})")
202
+ self.settings.setValue("paths/last_save_dir", os.path.dirname(path))
203
+ except Exception as e:
204
+ QMessageBox.critical(self, self.tr("Save failed"), str(e))
205
+
206
+ def _load_recent_lists(self):
207
+ """Load MRU lists from QSettings."""
208
+ def _as_list(val):
209
+ if val is None:
210
+ return []
211
+ if isinstance(val, list):
212
+ return [str(v) for v in val if v]
213
+ if isinstance(val, str):
214
+ if not val:
215
+ return []
216
+ # allow ";;" separated fallback if ever needed
217
+ return [s for s in val.split(";;") if s]
218
+ return []
219
+
220
+ self._recent_image_paths = _as_list(
221
+ self.settings.value("recent/image_paths", [])
222
+ )
223
+ self._recent_project_paths = _as_list(
224
+ self.settings.value("recent/project_paths", [])
225
+ )
226
+
227
+ # Enforce max + uniqueness (most recent first)
228
+ def _dedupe_keep_order(seq):
229
+ seen = set()
230
+ out = []
231
+ for p in seq:
232
+ if p in seen:
233
+ continue
234
+ seen.add(p)
235
+ out.append(p)
236
+ return out[: self._recent_max]
237
+
238
+ self._recent_image_paths = _dedupe_keep_order(self._recent_image_paths)
239
+ self._recent_project_paths = _dedupe_keep_order(self._recent_project_paths)
240
+
241
+ def _save_recent_lists(self):
242
+ try:
243
+ self.settings.setValue("recent/image_paths", self._recent_image_paths)
244
+ self.settings.setValue("recent/project_paths", self._recent_project_paths)
245
+ except Exception:
246
+ pass
247
+
248
+ def _open_recent_image(self, path: str):
249
+ if not path:
250
+ return
251
+ if not os.path.exists(path):
252
+ if QMessageBox.question(
253
+ self,
254
+ self.tr("File not found"),
255
+ self.tr("The file does not exist:\n{path}\n\nRemove it from the recent images list?").replace("{path}", path),
256
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
257
+ ) == QMessageBox.StandardButton.Yes:
258
+ self._recent_image_paths = [p for p in self._recent_image_paths if p != path]
259
+ self._save_recent_lists()
260
+ self._rebuild_recent_menus()
261
+ return
262
+
263
+ try:
264
+ self.docman.open_path(path)
265
+ self._log(f"Opened (recent): {path}")
266
+ # bump to front
267
+ self._add_recent_image(path)
268
+ except Exception as e:
269
+ QMessageBox.warning(self, self.tr("Open failed"), f"{path}\n\n{e}")
270
+
271
+ def _open_recent_project(self, path: str):
272
+ if not path:
273
+ return
274
+ if not os.path.exists(path):
275
+ if QMessageBox.question(
276
+ self,
277
+ self.tr("Project not found"),
278
+ self.tr("The project file does not exist:\n{path}\n\nRemove it from the recent projects list?").replace("{path}", path),
279
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
280
+ ) == QMessageBox.StandardButton.Yes:
281
+ self._recent_project_paths = [p for p in self._recent_project_paths if p != path]
282
+ self._save_recent_lists()
283
+ self._rebuild_recent_menus()
284
+ return
285
+
286
+ if not self._prepare_for_project_load("Load Project"):
287
+ return
288
+
289
+ self._do_load_project_path(path)
290
+
291
+ def _add_recent_image(self, path: str):
292
+ p = os.path.abspath(path)
293
+ self._recent_image_paths = [p] + [
294
+ x for x in self._recent_image_paths if x != p
295
+ ]
296
+ self._recent_image_paths = self._recent_image_paths[: self._recent_max]
297
+ self._save_recent_lists()
298
+ self._rebuild_recent_menus()
299
+
300
+ def _add_recent_project(self, path: str):
301
+ p = os.path.abspath(path)
302
+ self._recent_project_paths = [p] + [
303
+ x for x in self._recent_project_paths if x != p
304
+ ]
305
+ self._recent_project_paths = self._recent_project_paths[: self._recent_max]
306
+ self._save_recent_lists()
307
+ self._rebuild_recent_menus()
308
+
309
+ def _save_project(self):
310
+ path, _ = QFileDialog.getSaveFileName(
311
+ self, self.tr("Save Project"), "", "SetiAstro Project (*.sas)"
312
+ )
313
+ if not path:
314
+ return
315
+ if not path.lower().endswith(".sas"):
316
+ path += ".sas"
317
+
318
+ docs = self._collect_open_documents()
319
+ if not docs:
320
+ QMessageBox.warning(self, self.tr("Save Project"), self.tr("No documents to save."))
321
+ return
322
+
323
+ try:
324
+ compress = self._ask_project_compress() # your existing yes/no dialog
325
+
326
+ # Busy dialog (indeterminate)
327
+ dlg = QProgressDialog(self.tr("Saving project..."), "", 0, 0, self)
328
+ dlg.setWindowTitle(self.tr("Saving"))
329
+ # PyQt6 (with PyQt5 fallback if you ever run it there)
330
+ try:
331
+ dlg.setWindowModality(Qt.WindowModality.ApplicationModal)
332
+ except AttributeError:
333
+ dlg.setWindowModality(Qt.ApplicationModal) # PyQt5
334
+
335
+ # Hide the cancel button (API differs across versions)
336
+ try:
337
+ dlg.setCancelButton(None)
338
+ except TypeError:
339
+ dlg.setCancelButtonText("")
340
+
341
+ dlg.setAutoClose(False)
342
+ dlg.setAutoReset(False)
343
+ dlg.show()
344
+
345
+ # Threaded save
346
+ from setiastro.saspro.widgets.common_utilities import ProjectSaveWorker as _ProjectSaveWorker
347
+
348
+ self._proj_save_worker = _ProjectSaveWorker(
349
+ path,
350
+ docs,
351
+ getattr(self, "shortcuts", None),
352
+ getattr(self, "mdi", None),
353
+ compress,
354
+ parent=self,
355
+ )
356
+
357
+ def _on_proj_save_ok():
358
+ dlg.close()
359
+ self._log("Project saved.")
360
+ self._add_recent_project(path)
361
+
362
+ self._proj_save_worker.ok.connect(_on_proj_save_ok)
363
+ self._proj_save_worker.error.connect(
364
+ lambda msg: (
365
+ dlg.close(),
366
+ QMessageBox.critical(self, self.tr("Save Project"), self.tr("Failed to save:\n{msg}").replace("{msg}", msg)),
367
+ )
368
+ )
369
+ self._proj_save_worker.finished.connect(
370
+ lambda: setattr(self, "_proj_save_worker", None)
371
+ )
372
+ self._proj_save_worker.start()
373
+
374
+ except Exception as e:
375
+ QMessageBox.critical(self, self.tr("Save Project"), self.tr("Failed to save:\n{e}").replace("{e}", str(e)))
376
+
377
+ def _load_project(self):
378
+ # warn / clear current desktop
379
+ if not self._prepare_for_project_load("Load Project"):
380
+ return
381
+
382
+ path, _ = QFileDialog.getOpenFileName(
383
+ self, self.tr("Load Project"), "", "SetiAstro Project (*.sas)"
384
+ )
385
+ if not path:
386
+ return
387
+
388
+ self._do_load_project_path(path)
389
+
390
+ def _new_project(self):
391
+ if not self._confirm_discard(title=self.tr("New Project"),
392
+ msg=self.tr("Start a new project? This closes all views and clears desktop shortcuts.")):
393
+ return
394
+
395
+ # Close views + docs + shelf
396
+ self._close_all_subwindows()
397
+ self._clear_all_documents()
398
+ self._clear_minimized_shelf()
399
+
400
+ # Clear desktop shortcuts (widgets + persisted positions)
401
+ try:
402
+ if getattr(self, "shortcuts", None):
403
+ self.shortcuts.clear()
404
+ else:
405
+ # Fallback: wipe persisted layout so nothing reloads later
406
+ from PyQt6.QtCore import QSettings
407
+ from setiastro.saspro.shortcuts import SET_KEY_V1, SET_KEY_V2
408
+ s = QSettings()
409
+ s.setValue(SET_KEY_V2, "[]")
410
+ s.remove(SET_KEY_V1)
411
+ s.sync()
412
+ except Exception:
413
+ pass
414
+
415
+ # (Optional) keep canvas ready for fresh adds
416
+ try:
417
+ if getattr(self, "shortcuts", None):
418
+ self.shortcuts.canvas.raise_()
419
+ self.shortcuts.canvas.show()
420
+ self.shortcuts.canvas.setFocus()
421
+ except Exception:
422
+ pass
423
+
424
+ self._log("New project workspace ready.")
425
+
426
+ def _collect_open_documents(self):
427
+ # Prefer DocManager if present
428
+ dm = getattr(self, "doc_manager", None) or getattr(self, "docman", None)
429
+ if dm is not None and getattr(dm, "_docs", None) is not None:
430
+ return list(dm._docs)
431
+
432
+ # Fallback: harvest from open subwindows
433
+ docs = []
434
+ for sw in self.mdi.subWindowList():
435
+ try:
436
+ view = sw.widget()
437
+ doc = getattr(view, "document", None)
438
+ if doc is not None:
439
+ docs.append(doc)
440
+ except Exception:
441
+ pass
442
+ return docs
443
+