setiastrosuitepro 1.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- setiastro/__init__.py +2 -0
- setiastro/saspro/__init__.py +20 -0
- setiastro/saspro/__main__.py +784 -0
- setiastro/saspro/_generated/__init__.py +7 -0
- setiastro/saspro/_generated/build_info.py +2 -0
- setiastro/saspro/abe.py +1295 -0
- setiastro/saspro/abe_preset.py +196 -0
- setiastro/saspro/aberration_ai.py +694 -0
- setiastro/saspro/aberration_ai_preset.py +224 -0
- setiastro/saspro/accel_installer.py +218 -0
- setiastro/saspro/accel_workers.py +30 -0
- setiastro/saspro/add_stars.py +621 -0
- setiastro/saspro/astrobin_exporter.py +1007 -0
- setiastro/saspro/astrospike.py +153 -0
- setiastro/saspro/astrospike_python.py +1839 -0
- setiastro/saspro/autostretch.py +196 -0
- setiastro/saspro/backgroundneutral.py +560 -0
- setiastro/saspro/batch_convert.py +325 -0
- setiastro/saspro/batch_renamer.py +519 -0
- setiastro/saspro/blemish_blaster.py +488 -0
- setiastro/saspro/blink_comparator_pro.py +2923 -0
- setiastro/saspro/bundles.py +61 -0
- setiastro/saspro/bundles_dock.py +114 -0
- setiastro/saspro/cheat_sheet.py +168 -0
- setiastro/saspro/clahe.py +342 -0
- setiastro/saspro/comet_stacking.py +1377 -0
- setiastro/saspro/config.py +38 -0
- setiastro/saspro/config_bootstrap.py +40 -0
- setiastro/saspro/config_manager.py +316 -0
- setiastro/saspro/continuum_subtract.py +1617 -0
- setiastro/saspro/convo.py +1397 -0
- setiastro/saspro/convo_preset.py +414 -0
- setiastro/saspro/copyastro.py +187 -0
- setiastro/saspro/cosmicclarity.py +1564 -0
- setiastro/saspro/cosmicclarity_preset.py +407 -0
- setiastro/saspro/crop_dialog_pro.py +948 -0
- setiastro/saspro/crop_preset.py +189 -0
- setiastro/saspro/curve_editor_pro.py +2544 -0
- setiastro/saspro/curves_preset.py +375 -0
- setiastro/saspro/debayer.py +670 -0
- setiastro/saspro/debug_utils.py +29 -0
- setiastro/saspro/dnd_mime.py +35 -0
- setiastro/saspro/doc_manager.py +2634 -0
- setiastro/saspro/exoplanet_detector.py +2166 -0
- setiastro/saspro/file_utils.py +284 -0
- setiastro/saspro/fitsmodifier.py +744 -0
- setiastro/saspro/free_torch_memory.py +48 -0
- setiastro/saspro/frequency_separation.py +1343 -0
- setiastro/saspro/function_bundle.py +1594 -0
- setiastro/saspro/ghs_dialog_pro.py +660 -0
- setiastro/saspro/ghs_preset.py +284 -0
- setiastro/saspro/graxpert.py +634 -0
- setiastro/saspro/graxpert_preset.py +287 -0
- setiastro/saspro/gui/__init__.py +0 -0
- setiastro/saspro/gui/main_window.py +8494 -0
- setiastro/saspro/gui/mixins/__init__.py +33 -0
- setiastro/saspro/gui/mixins/dock_mixin.py +263 -0
- setiastro/saspro/gui/mixins/file_mixin.py +445 -0
- setiastro/saspro/gui/mixins/geometry_mixin.py +403 -0
- setiastro/saspro/gui/mixins/header_mixin.py +441 -0
- setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
- setiastro/saspro/gui/mixins/menu_mixin.py +361 -0
- setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +1324 -0
- setiastro/saspro/gui/mixins/update_mixin.py +309 -0
- setiastro/saspro/gui/mixins/view_mixin.py +435 -0
- setiastro/saspro/halobgon.py +462 -0
- setiastro/saspro/header_viewer.py +445 -0
- setiastro/saspro/headless_utils.py +88 -0
- setiastro/saspro/histogram.py +753 -0
- setiastro/saspro/history_explorer.py +939 -0
- setiastro/saspro/image_combine.py +414 -0
- setiastro/saspro/image_peeker_pro.py +1596 -0
- setiastro/saspro/imageops/__init__.py +37 -0
- setiastro/saspro/imageops/mdi_snap.py +292 -0
- setiastro/saspro/imageops/scnr.py +36 -0
- setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
- setiastro/saspro/imageops/stretch.py +244 -0
- setiastro/saspro/isophote.py +1179 -0
- setiastro/saspro/layers.py +208 -0
- setiastro/saspro/layers_dock.py +714 -0
- setiastro/saspro/lazy_imports.py +193 -0
- setiastro/saspro/legacy/__init__.py +2 -0
- setiastro/saspro/legacy/image_manager.py +2226 -0
- setiastro/saspro/legacy/numba_utils.py +3659 -0
- setiastro/saspro/legacy/xisf.py +1071 -0
- setiastro/saspro/linear_fit.py +534 -0
- setiastro/saspro/live_stacking.py +1830 -0
- setiastro/saspro/log_bus.py +5 -0
- setiastro/saspro/logging_config.py +460 -0
- setiastro/saspro/luminancerecombine.py +309 -0
- setiastro/saspro/main_helpers.py +201 -0
- setiastro/saspro/mask_creation.py +928 -0
- setiastro/saspro/masks_core.py +56 -0
- setiastro/saspro/mdi_widgets.py +353 -0
- setiastro/saspro/memory_utils.py +666 -0
- setiastro/saspro/metadata_patcher.py +75 -0
- setiastro/saspro/mfdeconv.py +3826 -0
- setiastro/saspro/mfdeconv_earlystop.py +71 -0
- setiastro/saspro/mfdeconvcudnn.py +3263 -0
- setiastro/saspro/mfdeconvsport.py +2382 -0
- setiastro/saspro/minorbodycatalog.py +567 -0
- setiastro/saspro/morphology.py +382 -0
- setiastro/saspro/multiscale_decomp.py +1290 -0
- setiastro/saspro/nbtorgb_stars.py +531 -0
- setiastro/saspro/numba_utils.py +3044 -0
- setiastro/saspro/numba_warmup.py +141 -0
- setiastro/saspro/ops/__init__.py +9 -0
- setiastro/saspro/ops/command_help_dialog.py +623 -0
- setiastro/saspro/ops/command_runner.py +217 -0
- setiastro/saspro/ops/commands.py +1594 -0
- setiastro/saspro/ops/script_editor.py +1102 -0
- setiastro/saspro/ops/scripts.py +1413 -0
- setiastro/saspro/ops/settings.py +560 -0
- setiastro/saspro/parallel_utils.py +554 -0
- setiastro/saspro/pedestal.py +121 -0
- setiastro/saspro/perfect_palette_picker.py +1053 -0
- setiastro/saspro/pipeline.py +110 -0
- setiastro/saspro/pixelmath.py +1600 -0
- setiastro/saspro/plate_solver.py +2435 -0
- setiastro/saspro/project_io.py +797 -0
- setiastro/saspro/psf_utils.py +136 -0
- setiastro/saspro/psf_viewer.py +549 -0
- setiastro/saspro/pyi_rthook_astroquery.py +95 -0
- setiastro/saspro/remove_green.py +314 -0
- setiastro/saspro/remove_stars.py +1625 -0
- setiastro/saspro/remove_stars_preset.py +404 -0
- setiastro/saspro/resources.py +472 -0
- setiastro/saspro/rgb_combination.py +207 -0
- setiastro/saspro/rgb_extract.py +19 -0
- setiastro/saspro/rgbalign.py +723 -0
- setiastro/saspro/runtime_imports.py +7 -0
- setiastro/saspro/runtime_torch.py +754 -0
- setiastro/saspro/save_options.py +72 -0
- setiastro/saspro/selective_color.py +1552 -0
- setiastro/saspro/sfcc.py +1425 -0
- setiastro/saspro/shortcuts.py +2807 -0
- setiastro/saspro/signature_insert.py +1099 -0
- setiastro/saspro/stacking_suite.py +17712 -0
- setiastro/saspro/star_alignment.py +7420 -0
- setiastro/saspro/star_alignment_preset.py +329 -0
- setiastro/saspro/star_metrics.py +49 -0
- setiastro/saspro/star_spikes.py +681 -0
- setiastro/saspro/star_stretch.py +470 -0
- setiastro/saspro/stat_stretch.py +502 -0
- setiastro/saspro/status_log_dock.py +78 -0
- setiastro/saspro/subwindow.py +3267 -0
- setiastro/saspro/supernovaasteroidhunter.py +1712 -0
- setiastro/saspro/swap_manager.py +99 -0
- setiastro/saspro/torch_backend.py +89 -0
- setiastro/saspro/torch_rejection.py +434 -0
- setiastro/saspro/view_bundle.py +1555 -0
- setiastro/saspro/wavescale_hdr.py +624 -0
- setiastro/saspro/wavescale_hdr_preset.py +100 -0
- setiastro/saspro/wavescalede.py +657 -0
- setiastro/saspro/wavescalede_preset.py +228 -0
- setiastro/saspro/wcs_update.py +374 -0
- setiastro/saspro/whitebalance.py +456 -0
- setiastro/saspro/widgets/__init__.py +48 -0
- setiastro/saspro/widgets/common_utilities.py +305 -0
- setiastro/saspro/widgets/graphics_views.py +122 -0
- setiastro/saspro/widgets/image_utils.py +518 -0
- setiastro/saspro/widgets/preview_dialogs.py +280 -0
- setiastro/saspro/widgets/spinboxes.py +275 -0
- setiastro/saspro/widgets/themed_buttons.py +13 -0
- setiastro/saspro/widgets/wavelet_utils.py +299 -0
- setiastro/saspro/window_shelf.py +185 -0
- setiastro/saspro/xisf.py +1123 -0
- setiastrosuitepro-1.6.0.dist-info/METADATA +266 -0
- setiastrosuitepro-1.6.0.dist-info/RECORD +174 -0
- setiastrosuitepro-1.6.0.dist-info/WHEEL +4 -0
- setiastrosuitepro-1.6.0.dist-info/entry_points.txt +6 -0
- setiastrosuitepro-1.6.0.dist-info/licenses/LICENSE +674 -0
- setiastrosuitepro-1.6.0.dist-info/licenses/license.txt +2580 -0
|
@@ -0,0 +1,445 @@
|
|
|
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, "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, "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, "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, "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
|
+
"File not found",
|
|
255
|
+
f"The file does not exist:\n{path}\n\n"
|
|
256
|
+
"Remove it from the recent images list?",
|
|
257
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
258
|
+
) == QMessageBox.StandardButton.Yes:
|
|
259
|
+
self._recent_image_paths = [p for p in self._recent_image_paths if p != path]
|
|
260
|
+
self._save_recent_lists()
|
|
261
|
+
self._rebuild_recent_menus()
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
try:
|
|
265
|
+
self.docman.open_path(path)
|
|
266
|
+
self._log(f"Opened (recent): {path}")
|
|
267
|
+
# bump to front
|
|
268
|
+
self._add_recent_image(path)
|
|
269
|
+
except Exception as e:
|
|
270
|
+
QMessageBox.warning(self, "Open failed", f"{path}\n\n{e}")
|
|
271
|
+
|
|
272
|
+
def _open_recent_project(self, path: str):
|
|
273
|
+
if not path:
|
|
274
|
+
return
|
|
275
|
+
if not os.path.exists(path):
|
|
276
|
+
if QMessageBox.question(
|
|
277
|
+
self,
|
|
278
|
+
"Project not found",
|
|
279
|
+
f"The project file does not exist:\n{path}\n\n"
|
|
280
|
+
"Remove it from the recent projects list?",
|
|
281
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
282
|
+
) == QMessageBox.StandardButton.Yes:
|
|
283
|
+
self._recent_project_paths = [p for p in self._recent_project_paths if p != path]
|
|
284
|
+
self._save_recent_lists()
|
|
285
|
+
self._rebuild_recent_menus()
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
if not self._prepare_for_project_load("Load Project"):
|
|
289
|
+
return
|
|
290
|
+
|
|
291
|
+
self._do_load_project_path(path)
|
|
292
|
+
|
|
293
|
+
def _add_recent_image(self, path: str):
|
|
294
|
+
p = os.path.abspath(path)
|
|
295
|
+
self._recent_image_paths = [p] + [
|
|
296
|
+
x for x in self._recent_image_paths if x != p
|
|
297
|
+
]
|
|
298
|
+
self._recent_image_paths = self._recent_image_paths[: self._recent_max]
|
|
299
|
+
self._save_recent_lists()
|
|
300
|
+
self._rebuild_recent_menus()
|
|
301
|
+
|
|
302
|
+
def _add_recent_project(self, path: str):
|
|
303
|
+
p = os.path.abspath(path)
|
|
304
|
+
self._recent_project_paths = [p] + [
|
|
305
|
+
x for x in self._recent_project_paths if x != p
|
|
306
|
+
]
|
|
307
|
+
self._recent_project_paths = self._recent_project_paths[: self._recent_max]
|
|
308
|
+
self._save_recent_lists()
|
|
309
|
+
self._rebuild_recent_menus()
|
|
310
|
+
|
|
311
|
+
def _save_project(self):
|
|
312
|
+
path, _ = QFileDialog.getSaveFileName(
|
|
313
|
+
self, "Save Project", "", "SetiAstro Project (*.sas)"
|
|
314
|
+
)
|
|
315
|
+
if not path:
|
|
316
|
+
return
|
|
317
|
+
if not path.lower().endswith(".sas"):
|
|
318
|
+
path += ".sas"
|
|
319
|
+
|
|
320
|
+
docs = self._collect_open_documents()
|
|
321
|
+
if not docs:
|
|
322
|
+
QMessageBox.warning(self, "Save Project", "No documents to save.")
|
|
323
|
+
return
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
compress = self._ask_project_compress() # your existing yes/no dialog
|
|
327
|
+
|
|
328
|
+
# Busy dialog (indeterminate)
|
|
329
|
+
dlg = QProgressDialog("Saving project...", "", 0, 0, self)
|
|
330
|
+
dlg.setWindowTitle("Saving")
|
|
331
|
+
# PyQt6 (with PyQt5 fallback if you ever run it there)
|
|
332
|
+
try:
|
|
333
|
+
dlg.setWindowModality(Qt.WindowModality.ApplicationModal)
|
|
334
|
+
except AttributeError:
|
|
335
|
+
dlg.setWindowModality(Qt.ApplicationModal) # PyQt5
|
|
336
|
+
|
|
337
|
+
# Hide the cancel button (API differs across versions)
|
|
338
|
+
try:
|
|
339
|
+
dlg.setCancelButton(None)
|
|
340
|
+
except TypeError:
|
|
341
|
+
dlg.setCancelButtonText("")
|
|
342
|
+
|
|
343
|
+
dlg.setAutoClose(False)
|
|
344
|
+
dlg.setAutoReset(False)
|
|
345
|
+
dlg.show()
|
|
346
|
+
|
|
347
|
+
# Threaded save
|
|
348
|
+
from setiastro.saspro.widgets.common_utilities import ProjectSaveWorker as _ProjectSaveWorker
|
|
349
|
+
|
|
350
|
+
self._proj_save_worker = _ProjectSaveWorker(
|
|
351
|
+
path,
|
|
352
|
+
docs,
|
|
353
|
+
getattr(self, "shortcuts", None),
|
|
354
|
+
getattr(self, "mdi", None),
|
|
355
|
+
compress,
|
|
356
|
+
parent=self,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
def _on_proj_save_ok():
|
|
360
|
+
dlg.close()
|
|
361
|
+
self._log("Project saved.")
|
|
362
|
+
self._add_recent_project(path)
|
|
363
|
+
|
|
364
|
+
self._proj_save_worker.ok.connect(_on_proj_save_ok)
|
|
365
|
+
self._proj_save_worker.error.connect(
|
|
366
|
+
lambda msg: (
|
|
367
|
+
dlg.close(),
|
|
368
|
+
QMessageBox.critical(self, "Save Project", f"Failed to save:\n{msg}"),
|
|
369
|
+
)
|
|
370
|
+
)
|
|
371
|
+
self._proj_save_worker.finished.connect(
|
|
372
|
+
lambda: setattr(self, "_proj_save_worker", None)
|
|
373
|
+
)
|
|
374
|
+
self._proj_save_worker.start()
|
|
375
|
+
|
|
376
|
+
except Exception as e:
|
|
377
|
+
QMessageBox.critical(self, "Save Project", f"Failed to save:\n{e}")
|
|
378
|
+
|
|
379
|
+
def _load_project(self):
|
|
380
|
+
# warn / clear current desktop
|
|
381
|
+
if not self._prepare_for_project_load("Load Project"):
|
|
382
|
+
return
|
|
383
|
+
|
|
384
|
+
path, _ = QFileDialog.getOpenFileName(
|
|
385
|
+
self, "Load Project", "", "SetiAstro Project (*.sas)"
|
|
386
|
+
)
|
|
387
|
+
if not path:
|
|
388
|
+
return
|
|
389
|
+
|
|
390
|
+
self._do_load_project_path(path)
|
|
391
|
+
|
|
392
|
+
def _new_project(self):
|
|
393
|
+
if not self._confirm_discard(title="New Project",
|
|
394
|
+
msg="Start a new project? This closes all views and clears desktop shortcuts."):
|
|
395
|
+
return
|
|
396
|
+
|
|
397
|
+
# Close views + docs + shelf
|
|
398
|
+
self._close_all_subwindows()
|
|
399
|
+
self._clear_all_documents()
|
|
400
|
+
self._clear_minimized_shelf()
|
|
401
|
+
|
|
402
|
+
# Clear desktop shortcuts (widgets + persisted positions)
|
|
403
|
+
try:
|
|
404
|
+
if getattr(self, "shortcuts", None):
|
|
405
|
+
self.shortcuts.clear()
|
|
406
|
+
else:
|
|
407
|
+
# Fallback: wipe persisted layout so nothing reloads later
|
|
408
|
+
from PyQt6.QtCore import QSettings
|
|
409
|
+
from setiastro.saspro.shortcuts import SET_KEY_V1, SET_KEY_V2
|
|
410
|
+
s = QSettings()
|
|
411
|
+
s.setValue(SET_KEY_V2, "[]")
|
|
412
|
+
s.remove(SET_KEY_V1)
|
|
413
|
+
s.sync()
|
|
414
|
+
except Exception:
|
|
415
|
+
pass
|
|
416
|
+
|
|
417
|
+
# (Optional) keep canvas ready for fresh adds
|
|
418
|
+
try:
|
|
419
|
+
if getattr(self, "shortcuts", None):
|
|
420
|
+
self.shortcuts.canvas.raise_()
|
|
421
|
+
self.shortcuts.canvas.show()
|
|
422
|
+
self.shortcuts.canvas.setFocus()
|
|
423
|
+
except Exception:
|
|
424
|
+
pass
|
|
425
|
+
|
|
426
|
+
self._log("New project workspace ready.")
|
|
427
|
+
|
|
428
|
+
def _collect_open_documents(self):
|
|
429
|
+
# Prefer DocManager if present
|
|
430
|
+
dm = getattr(self, "doc_manager", None) or getattr(self, "docman", None)
|
|
431
|
+
if dm is not None and getattr(dm, "_docs", None) is not None:
|
|
432
|
+
return list(dm._docs)
|
|
433
|
+
|
|
434
|
+
# Fallback: harvest from open subwindows
|
|
435
|
+
docs = []
|
|
436
|
+
for sw in self.mdi.subWindowList():
|
|
437
|
+
try:
|
|
438
|
+
view = sw.widget()
|
|
439
|
+
doc = getattr(view, "document", None)
|
|
440
|
+
if doc is not None:
|
|
441
|
+
docs.append(doc)
|
|
442
|
+
except Exception:
|
|
443
|
+
pass
|
|
444
|
+
return docs
|
|
445
|
+
|