setiastrosuitepro 1.6.0__py3-none-any.whl → 1.6.4.post1__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.
Files changed (293) hide show
  1. setiastro/data/SASP_data.fits +0 -0
  2. setiastro/data/catalogs/List_of_Galaxies_with_Distances_Gly.csv +488 -0
  3. setiastro/data/catalogs/astrobin_filters.csv +890 -0
  4. setiastro/data/catalogs/astrobin_filters_page1_local.csv +51 -0
  5. setiastro/data/catalogs/cali2.csv +63 -0
  6. setiastro/data/catalogs/cali2color.csv +65 -0
  7. setiastro/data/catalogs/celestial_catalog - original.csv +16471 -0
  8. setiastro/data/catalogs/celestial_catalog.csv +24031 -0
  9. setiastro/data/catalogs/detected_stars.csv +24784 -0
  10. setiastro/data/catalogs/fits_header_data.csv +46 -0
  11. setiastro/data/catalogs/test.csv +8 -0
  12. setiastro/data/catalogs/updated_celestial_catalog.csv +16471 -0
  13. setiastro/images/Astro_Spikes.png +0 -0
  14. setiastro/images/Background_startup.jpg +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/rotatearbitrary.png +0 -0
  106. setiastro/images/rotateclockwise.png +0 -0
  107. setiastro/images/rotatecounterclockwise.png +0 -0
  108. setiastro/images/satellite.png +0 -0
  109. setiastro/images/script.png +0 -0
  110. setiastro/images/selectivecolor.png +0 -0
  111. setiastro/images/simbad.png +0 -0
  112. setiastro/images/slot0.png +0 -0
  113. setiastro/images/slot1.png +0 -0
  114. setiastro/images/slot2.png +0 -0
  115. setiastro/images/slot3.png +0 -0
  116. setiastro/images/slot4.png +0 -0
  117. setiastro/images/slot5.png +0 -0
  118. setiastro/images/slot6.png +0 -0
  119. setiastro/images/slot7.png +0 -0
  120. setiastro/images/slot8.png +0 -0
  121. setiastro/images/slot9.png +0 -0
  122. setiastro/images/spcc.png +0 -0
  123. setiastro/images/spin_precession_vs_lunar_distance.png +0 -0
  124. setiastro/images/spinner.gif +0 -0
  125. setiastro/images/stacking.png +0 -0
  126. setiastro/images/staradd.png +0 -0
  127. setiastro/images/staralign.png +0 -0
  128. setiastro/images/starnet.png +0 -0
  129. setiastro/images/starregistration.png +0 -0
  130. setiastro/images/starspike.png +0 -0
  131. setiastro/images/starstretch.png +0 -0
  132. setiastro/images/statstretch.png +0 -0
  133. setiastro/images/supernova.png +0 -0
  134. setiastro/images/uhs.png +0 -0
  135. setiastro/images/undoicon.png +0 -0
  136. setiastro/images/upscale.png +0 -0
  137. setiastro/images/viewbundle.png +0 -0
  138. setiastro/images/whitebalance.png +0 -0
  139. setiastro/images/wimi_icon_256x256.png +0 -0
  140. setiastro/images/wimilogo.png +0 -0
  141. setiastro/images/wims.png +0 -0
  142. setiastro/images/wrench_icon.png +0 -0
  143. setiastro/images/xisfliberator.png +0 -0
  144. setiastro/qml/ResourceMonitor.qml +126 -0
  145. setiastro/saspro/__main__.py +228 -67
  146. setiastro/saspro/_generated/build_info.py +2 -1
  147. setiastro/saspro/abe.py +76 -25
  148. setiastro/saspro/aberration_ai.py +14 -14
  149. setiastro/saspro/add_stars.py +15 -12
  150. setiastro/saspro/astrobin_exporter.py +61 -58
  151. setiastro/saspro/astrospike_python.py +3 -1
  152. setiastro/saspro/autostretch.py +4 -2
  153. setiastro/saspro/backgroundneutral.py +65 -14
  154. setiastro/saspro/batch_convert.py +8 -5
  155. setiastro/saspro/batch_renamer.py +39 -36
  156. setiastro/saspro/blemish_blaster.py +15 -12
  157. setiastro/saspro/blink_comparator_pro.py +605 -379
  158. setiastro/saspro/cheat_sheet.py +62 -17
  159. setiastro/saspro/clahe.py +34 -8
  160. setiastro/saspro/comet_stacking.py +103 -38
  161. setiastro/saspro/common_tr.py +107 -0
  162. setiastro/saspro/continuum_subtract.py +7 -7
  163. setiastro/saspro/convo.py +12 -9
  164. setiastro/saspro/copyastro.py +3 -0
  165. setiastro/saspro/cosmicclarity.py +77 -52
  166. setiastro/saspro/crop_dialog_pro.py +80 -45
  167. setiastro/saspro/curve_editor_pro.py +51 -33
  168. setiastro/saspro/debayer.py +6 -3
  169. setiastro/saspro/doc_manager.py +49 -19
  170. setiastro/saspro/exoplanet_detector.py +11 -11
  171. setiastro/saspro/fitsmodifier.py +48 -44
  172. setiastro/saspro/fix_bom.py +32 -0
  173. setiastro/saspro/frequency_separation.py +18 -12
  174. setiastro/saspro/function_bundle.py +18 -16
  175. setiastro/saspro/generate_translations.py +3092 -0
  176. setiastro/saspro/ghs_dialog_pro.py +19 -16
  177. setiastro/saspro/graxpert.py +3 -0
  178. setiastro/saspro/gui/main_window.py +471 -126
  179. setiastro/saspro/gui/mixins/dock_mixin.py +123 -11
  180. setiastro/saspro/gui/mixins/file_mixin.py +25 -20
  181. setiastro/saspro/gui/mixins/geometry_mixin.py +115 -15
  182. setiastro/saspro/gui/mixins/header_mixin.py +6 -6
  183. setiastro/saspro/gui/mixins/mask_mixin.py +8 -8
  184. setiastro/saspro/gui/mixins/menu_mixin.py +62 -33
  185. setiastro/saspro/gui/mixins/toolbar_mixin.py +382 -226
  186. setiastro/saspro/gui/mixins/update_mixin.py +26 -26
  187. setiastro/saspro/gui/statistics_dialog.py +47 -0
  188. setiastro/saspro/halobgon.py +29 -3
  189. setiastro/saspro/header_viewer.py +21 -18
  190. setiastro/saspro/histogram.py +29 -26
  191. setiastro/saspro/history_explorer.py +2 -0
  192. setiastro/saspro/i18n.py +168 -0
  193. setiastro/saspro/image_combine.py +3 -0
  194. setiastro/saspro/image_peeker_pro.py +52 -44
  195. setiastro/saspro/imageops/stretch.py +5 -13
  196. setiastro/saspro/isophote.py +3 -0
  197. setiastro/saspro/legacy/numba_utils.py +64 -47
  198. setiastro/saspro/linear_fit.py +3 -0
  199. setiastro/saspro/live_stacking.py +13 -2
  200. setiastro/saspro/mask_creation.py +180 -22
  201. setiastro/saspro/mfdeconv.py +5 -0
  202. setiastro/saspro/morphology.py +38 -13
  203. setiastro/saspro/multiscale_decomp.py +713 -256
  204. setiastro/saspro/nbtorgb_stars.py +12 -2
  205. setiastro/saspro/numba_utils.py +149 -48
  206. setiastro/saspro/ops/scripts.py +77 -17
  207. setiastro/saspro/ops/settings.py +177 -100
  208. setiastro/saspro/perfect_palette_picker.py +25 -7
  209. setiastro/saspro/pixelmath.py +114 -110
  210. setiastro/saspro/plate_solver.py +118 -108
  211. setiastro/saspro/remove_green.py +24 -7
  212. setiastro/saspro/remove_stars.py +136 -162
  213. setiastro/saspro/remove_stars_preset.py +55 -13
  214. setiastro/saspro/resources.py +46 -15
  215. setiastro/saspro/rgb_combination.py +19 -18
  216. setiastro/saspro/rgbalign.py +11 -11
  217. setiastro/saspro/save_options.py +5 -4
  218. setiastro/saspro/selective_color.py +84 -25
  219. setiastro/saspro/sfcc.py +119 -72
  220. setiastro/saspro/shortcuts.py +345 -36
  221. setiastro/saspro/signature_insert.py +4 -1
  222. setiastro/saspro/stacking_suite.py +2066 -1119
  223. setiastro/saspro/star_alignment.py +291 -331
  224. setiastro/saspro/star_spikes.py +137 -53
  225. setiastro/saspro/star_stretch.py +47 -10
  226. setiastro/saspro/stat_stretch.py +52 -16
  227. setiastro/saspro/status_log_dock.py +1 -1
  228. setiastro/saspro/subwindow.py +97 -36
  229. setiastro/saspro/supernovaasteroidhunter.py +68 -61
  230. setiastro/saspro/swap_manager.py +77 -42
  231. setiastro/saspro/translations/all_source_strings.json +4726 -0
  232. setiastro/saspro/translations/ar_translations.py +4096 -0
  233. setiastro/saspro/translations/de_translations.py +3728 -0
  234. setiastro/saspro/translations/es_translations.py +4169 -0
  235. setiastro/saspro/translations/fr_translations.py +4090 -0
  236. setiastro/saspro/translations/hi_translations.py +3803 -0
  237. setiastro/saspro/translations/integrate_translations.py +271 -0
  238. setiastro/saspro/translations/it_translations.py +4728 -0
  239. setiastro/saspro/translations/ja_translations.py +3834 -0
  240. setiastro/saspro/translations/pt_translations.py +3847 -0
  241. setiastro/saspro/translations/ru_translations.py +3082 -0
  242. setiastro/saspro/translations/saspro_ar.qm +0 -0
  243. setiastro/saspro/translations/saspro_ar.ts +16019 -0
  244. setiastro/saspro/translations/saspro_de.qm +0 -0
  245. setiastro/saspro/translations/saspro_de.ts +14548 -0
  246. setiastro/saspro/translations/saspro_es.qm +0 -0
  247. setiastro/saspro/translations/saspro_es.ts +16202 -0
  248. setiastro/saspro/translations/saspro_fr.qm +0 -0
  249. setiastro/saspro/translations/saspro_fr.ts +15870 -0
  250. setiastro/saspro/translations/saspro_hi.qm +0 -0
  251. setiastro/saspro/translations/saspro_hi.ts +14855 -0
  252. setiastro/saspro/translations/saspro_it.qm +0 -0
  253. setiastro/saspro/translations/saspro_it.ts +19046 -0
  254. setiastro/saspro/translations/saspro_ja.qm +0 -0
  255. setiastro/saspro/translations/saspro_ja.ts +14980 -0
  256. setiastro/saspro/translations/saspro_pt.qm +0 -0
  257. setiastro/saspro/translations/saspro_pt.ts +15024 -0
  258. setiastro/saspro/translations/saspro_ru.qm +0 -0
  259. setiastro/saspro/translations/saspro_ru.ts +11835 -0
  260. setiastro/saspro/translations/saspro_sw.qm +0 -0
  261. setiastro/saspro/translations/saspro_sw.ts +15237 -0
  262. setiastro/saspro/translations/saspro_uk.qm +0 -0
  263. setiastro/saspro/translations/saspro_uk.ts +15248 -0
  264. setiastro/saspro/translations/saspro_zh.qm +0 -0
  265. setiastro/saspro/translations/saspro_zh.ts +15289 -0
  266. setiastro/saspro/translations/sw_translations.py +3897 -0
  267. setiastro/saspro/translations/uk_translations.py +3929 -0
  268. setiastro/saspro/translations/zh_translations.py +3910 -0
  269. setiastro/saspro/versioning.py +77 -0
  270. setiastro/saspro/view_bundle.py +20 -17
  271. setiastro/saspro/wavescale_hdr.py +54 -33
  272. setiastro/saspro/wavescale_hdr_preset.py +6 -5
  273. setiastro/saspro/wavescalede.py +54 -31
  274. setiastro/saspro/wavescalede_preset.py +9 -7
  275. setiastro/saspro/whitebalance.py +58 -22
  276. setiastro/saspro/widgets/common_utilities.py +12 -11
  277. setiastro/saspro/widgets/minigame/game.js +991 -0
  278. setiastro/saspro/widgets/minigame/index.html +53 -0
  279. setiastro/saspro/widgets/minigame/style.css +241 -0
  280. setiastro/saspro/widgets/preview_dialogs.py +8 -8
  281. setiastro/saspro/widgets/resource_monitor.py +263 -0
  282. setiastro/saspro/widgets/spinboxes.py +18 -0
  283. setiastro/saspro/widgets/wavelet_utils.py +52 -20
  284. setiastro/saspro/wimi.py +7996 -0
  285. setiastro/saspro/wims.py +578 -0
  286. setiastro/saspro/window_shelf.py +2 -2
  287. {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/METADATA +15 -3
  288. setiastrosuitepro-1.6.4.post1.dist-info/RECORD +368 -0
  289. setiastrosuitepro-1.6.0.dist-info/RECORD +0 -174
  290. {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/WHEEL +0 -0
  291. {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/entry_points.txt +0 -0
  292. {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/licenses/LICENSE +0 -0
  293. {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/licenses/license.txt +0 -0
@@ -128,7 +128,8 @@ from PyQt6.QtGui import (QPixmap, QColor, QIcon, QKeySequence, QShortcut,
128
128
  )
129
129
 
130
130
  # ----- QtCore -----
131
- from PyQt6.QtCore import (Qt, pyqtSignal, QCoreApplication, QTimer, QSize, QSignalBlocker, QModelIndex, QThread, QUrl, QSettings, QEvent, QByteArray, QObject
131
+ from PyQt6.QtCore import (Qt, pyqtSignal, QCoreApplication, QTimer, QSize, QSignalBlocker, QModelIndex, QThread, QUrl, QSettings, QEvent, QByteArray, QObject,
132
+ QPropertyAnimation, QEasingCurve, QElapsedTimer
132
133
  )
133
134
 
134
135
  from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
@@ -186,7 +187,7 @@ from setiastro.saspro.resources import (
186
187
  platesolve_path, psf_path, supernova_path, starregistration_path,
187
188
  stacking_path, pedestal_icon_path, starspike_path, aperture_path,
188
189
  jwstpupil_path, signature_icon_path, livestacking_path, hrdiagram_path,
189
- convoicon_path, spcc_icon_path, sasp_data_path, exoicon_path, peeker_icon,
190
+ convoicon_path, spcc_icon_path, sasp_data_path, exoicon_path, peeker_icon,rotatearbitrary_path,
190
191
  dse_icon_path, astrobin_filters_csv_path, isophote_path, statstretch_path,
191
192
  starstretch_path, curves_path, disk_path, uhs_path, blink_path, ppp_path,
192
193
  nbtorgb_path, freqsep_path, contsub_path, halo_path, cosmic_path,
@@ -201,9 +202,32 @@ from setiastro.saspro.resources import (
201
202
  import faulthandler
202
203
 
203
204
  def _install_crash_logging():
204
- faulthandler.enable(all_threads=True)
205
+ # Ensure logging is configured before we try to use it
206
+ if not logging.getLogger().hasHandlers():
207
+ logging.basicConfig(
208
+ level=logging.WARNING,
209
+ format='%(asctime)s - %(levelname)s - %(message)s',
210
+ handlers=[logging.StreamHandler(sys.stderr)]
211
+ )
212
+
213
+ try:
214
+ faulthandler.enable(all_threads=True)
215
+ except (PermissionError, OSError) as e:
216
+ # On macOS, faulthandler.enable() might fail due to security restrictions
217
+ # We can still continue without it as it's primarily for debugging
218
+ try:
219
+ logging.warning(f"Could not enable faulthandler: {e}")
220
+ except Exception:
221
+ # Fallback if logging isn't configured yet
222
+ print(f"WARNING: Could not enable faulthandler: {e}")
223
+
205
224
  def _excepthook(t, v, tb):
206
- logging.critical("Uncaught exception", exc_info=(t, v, tb))
225
+ try:
226
+ logging.critical("Uncaught exception", exc_info=(t, v, tb))
227
+ except Exception:
228
+ # Fallback if logging fails
229
+ print(f"CRITICAL: Uncaught exception: {t.__name__}: {v}")
230
+ traceback.print_tb(tb)
207
231
  try:
208
232
  faulthandler.dump_traceback(file=sys.stderr)
209
233
  except Exception:
@@ -272,6 +296,16 @@ class AstroSuiteProMainWindow(
272
296
  def __init__(self, image_manager=None, parent=None,
273
297
  version: str = "dev", build_timestamp: str = "dev"):
274
298
  super().__init__(parent)
299
+ # Prevent white flash: start strictly transparent and force dark bg
300
+ self.setWindowOpacity(0.0)
301
+ self.setStyleSheet("QMainWindow { background-color: #0F0F19; }")
302
+
303
+ # --- Usage Stats ---
304
+ self._session_start_time = time.time()
305
+ self._stats_timer = QTimer(self)
306
+ self._stats_timer.timeout.connect(self._update_usage_stats)
307
+ self._stats_timer.start(60000) # Update every minute
308
+
275
309
  from setiastro.saspro.doc_manager import DocManager
276
310
  from setiastro.saspro.window_shelf import WindowShelf, MinimizeInterceptor
277
311
  from setiastro.saspro.imageops.mdi_snap import MdiSnapController
@@ -285,7 +319,13 @@ class AstroSuiteProMainWindow(
285
319
  self.setWindowIcon(self.app_icon)
286
320
  self._doc = None
287
321
  self._force_close_all = False
288
- self.settings = QSettings() # reuse everywhere
322
+ self._is_restarting = False # Flag to bypass exit confirmation on restart
323
+ self.settings = QSettings("SetiAstro", "SetiAstroSuitePro")
324
+
325
+ # Optimization: Cache the settings dialog for instant opening
326
+ self._settings_dlg_cache = None
327
+ # Pre-load settings dialog after 2.5 seconds (background)
328
+ QTimer.singleShot(1000, self._preload_settings)
289
329
  self._last_active_view = None
290
330
  self._current_active_sw = None
291
331
  self._last_active_sw = None
@@ -396,6 +436,7 @@ class AstroSuiteProMainWindow(
396
436
  self._init_console_dock()
397
437
  self._init_header_viewer_dock()
398
438
  self._init_layers_dock()
439
+ self._init_resource_monitor_overlay()
399
440
  self._shutting_down = False
400
441
  self._init_status_log_dock()
401
442
  self._init_log_dock()
@@ -426,26 +467,23 @@ class AstroSuiteProMainWindow(
426
467
  self.mdi.linkViewDropped.connect(self._on_linkview_drop)
427
468
 
428
469
  self.doc_manager.set_mdi_area(self.mdi)
429
-
470
+ # Coalesce undo/redo label refreshes
471
+ self._undo_redo_refresh_pending = False
472
+ self._undo_redo_refresh_timer = QTimer(self)
473
+ self._undo_redo_refresh_timer.setSingleShot(True)
474
+ self._undo_redo_refresh_timer.timeout.connect(self._do_undo_redo_label_refresh)
430
475
  # Keep the toolbar in sync whenever anything relevant changes
431
- self.doc_manager.documentAdded.connect(lambda *_: self.update_undo_redo_action_labels())
432
- self.doc_manager.documentRemoved.connect(lambda *_: self.update_undo_redo_action_labels())
433
- self.doc_manager.imageRegionUpdated.connect(lambda *_: self.update_undo_redo_action_labels())
434
- self.doc_manager.previewRepaintRequested.connect(lambda *_: self.update_undo_redo_action_labels())
435
-
436
- # Also refresh when the active subwindow changes
437
- try:
438
- self.mdi.subWindowActivated.connect(lambda *_: self.update_undo_redo_action_labels())
439
- except Exception:
440
- pass
441
-
442
- try:
443
- QApplication.instance().focusChanged.connect(
444
- lambda *_: QTimer.singleShot(0, self.update_undo_redo_action_labels)
445
- )
446
- except Exception:
447
- pass
448
-
476
+ self.doc_manager.documentAdded.connect(lambda *_: self._schedule_undo_redo_label_refresh())
477
+ self.doc_manager.documentRemoved.connect(lambda *_: self._schedule_undo_redo_label_refresh())
478
+ self.doc_manager.imageRegionUpdated.connect(lambda *_: self._schedule_undo_redo_label_refresh())
479
+ self.doc_manager.previewRepaintRequested.connect(lambda *_: self._schedule_undo_redo_label_refresh())
480
+ self.mdi.subWindowActivated.connect(lambda *_: self._schedule_undo_redo_label_refresh())
481
+
482
+ # optional: keep, but schedule (or remove entirely)
483
+ #try:
484
+ # QApplication.instance().focusChanged.connect(lambda *_: self._schedule_undo_redo_label_refresh())
485
+ #except Exception:
486
+ # pass
449
487
  self.shortcuts.load_shortcuts()
450
488
  self._ensure_persistent_names()
451
489
  self._restore_window_placement()
@@ -529,6 +567,45 @@ class AstroSuiteProMainWindow(
529
567
 
530
568
  # _init_log_dock, _hook_stdout_stderr, and _append_log_text are now in DockMixin
531
569
 
570
+ def _schedule_undo_redo_label_refresh(self):
571
+ # Coalesce many triggers into one UI update
572
+ if getattr(self, "_undo_redo_refresh_pending", False):
573
+ return
574
+ self._undo_redo_refresh_pending = True
575
+ # 0ms is fine *if* it’s a real attribute timer (not a local)
576
+ self._undo_redo_refresh_timer.start(0)
577
+
578
+ def _do_undo_redo_label_refresh(self):
579
+ self._undo_redo_refresh_pending = False
580
+ try:
581
+ self.update_undo_redo_action_labels()
582
+ except Exception:
583
+ pass
584
+
585
+
586
+ def _rebuild_menus_for_language(self):
587
+ """Rebuild menus after language change to apply new translations."""
588
+ try:
589
+ # Clear the menubar and rebuild it
590
+ mb = self.menuBar()
591
+ mb.clear()
592
+ self._init_menubar()
593
+ except Exception:
594
+ pass
595
+
596
+ def createPopupMenu(self):
597
+ """Override to add System Monitor to the toolbar/dock context menu."""
598
+ # Get the default popup menu from QMainWindow
599
+ menu = super().createPopupMenu()
600
+ if menu is None:
601
+ menu = QMenu(self)
602
+
603
+ # Add System Monitor toggle if available
604
+ if hasattr(self, "act_toggle_monitor") and self.act_toggle_monitor is not None:
605
+ menu.addSeparator()
606
+ menu.addAction(self.act_toggle_monitor)
607
+
608
+ return menu
532
609
 
533
610
  def _on_sw_activated(self, sw):
534
611
  if not sw:
@@ -544,7 +621,7 @@ class AstroSuiteProMainWindow(
544
621
  doc.changed.connect(self.update_undo_redo_action_labels)
545
622
  except Exception:
546
623
  pass
547
- self.update_undo_redo_action_labels()
624
+ self._schedule_undo_redo_label_refresh()
548
625
 
549
626
  def _promote_roi_preview_to_real_doc(self, st: dict, preview_doc) -> None:
550
627
  """
@@ -1142,11 +1219,11 @@ class AstroSuiteProMainWindow(
1142
1219
  global_pos = lw.viewport().mapToGlobal(pos)
1143
1220
 
1144
1221
  menu = QMenu(lw)
1145
- act_copy_selected = menu.addAction("Copy Selected")
1146
- act_copy_all = menu.addAction("Copy All")
1222
+ act_copy_selected = menu.addAction(self.tr("Copy Selected"))
1223
+ act_copy_all = menu.addAction(self.tr("Copy All"))
1147
1224
  menu.addSeparator()
1148
- act_select_all = menu.addAction("Select All Lines")
1149
- act_clear = menu.addAction("Clear Console")
1225
+ act_select_all = menu.addAction(self.tr("Select All Lines"))
1226
+ act_clear = menu.addAction(self.tr("Clear Console"))
1150
1227
 
1151
1228
  action = menu.exec(global_pos)
1152
1229
  if action is None:
@@ -1740,7 +1817,7 @@ class AstroSuiteProMainWindow(
1740
1817
 
1741
1818
  show_view_bundles(self)
1742
1819
  except Exception as e:
1743
- QMessageBox.warning(self, "View Bundles", f"Open failed:\n{e}")
1820
+ QMessageBox.warning(self, self.tr("View Bundles"), f"Open failed:\n{e}")
1744
1821
 
1745
1822
  def _open_function_bundles(self):
1746
1823
  from setiastro.saspro.function_bundle import show_function_bundles
@@ -1748,7 +1825,7 @@ class AstroSuiteProMainWindow(
1748
1825
 
1749
1826
  show_function_bundles(self)
1750
1827
  except Exception as e:
1751
- QMessageBox.warning(self, "Function Bundles", f"Open failed:\n{e}")
1828
+ QMessageBox.warning(self, self.tr("Function Bundles"), f"Open failed:\n{e}")
1752
1829
 
1753
1830
  def _open_scripts_folder(self):
1754
1831
  if hasattr(self, "scriptman"):
@@ -1784,6 +1861,7 @@ class AstroSuiteProMainWindow(
1784
1861
  actions = self._collect_all_qactions()
1785
1862
  except Exception:
1786
1863
  actions = self.findChildren(QAction)
1864
+
1787
1865
  for act in actions:
1788
1866
  for seq in _seqs_for_action(act):
1789
1867
  rows.append((_qs_to_str(seq), _describe_action(act), _where_for_action(act)))
@@ -1794,6 +1872,12 @@ class AstroSuiteProMainWindow(
1794
1872
  if seq and not seq.isEmpty():
1795
1873
  rows.append((_qs_to_str(seq), _describe_shortcut(sc), _where_for_shortcut(sc)))
1796
1874
 
1875
+ # 3) App-level shortcuts not represented by QAction/QShortcut
1876
+ try:
1877
+ add_extra_shortcuts(rows) # ✅ Ctrl+K, Ctrl+Alt+M, etc.
1878
+ except Exception:
1879
+ pass
1880
+
1797
1881
  # De-duplicate and sort by shortcut text
1798
1882
  rows = _uniq_keep_order(rows)
1799
1883
  rows.sort(key=lambda r: (r[0].lower(), r[1].lower()))
@@ -1803,43 +1887,43 @@ class AstroSuiteProMainWindow(
1803
1887
  # Manual list (extend anytime). Format: (Gesture, Context, Effect)
1804
1888
  rows = [
1805
1889
  # Command search
1806
- ("A", "Display Stretch", "Toggle Display Auto-Stretch"),
1807
- ("Ctrl+I", "Invert", "Invert the Image"),
1808
- ("Ctrl+Shift+P", "Command Search", "Focus the command search bar; Enter runs first match"),
1890
+ ("A", "Display Stretch", self.tr("Toggle Display Auto-Stretch")),
1891
+ ("Ctrl+I", "Invert", self.tr("Invert the Image")),
1892
+ ("Ctrl+Shift+P", "Command Search", self.tr("Focus the command search bar; Enter runs first match")),
1809
1893
 
1810
1894
  # View Icon
1811
- ("Drag view -> Off to Canvas", "View", "Duplicate Image"),
1812
- ("Drag view -> On to Other Image", "View", "Copy Zoom and Pan"),
1813
- ("Shift+Drag -> On to Other Image", "View", "Apply that image to the other as a mask"),
1814
- ("Ctrl+Drag -> On to Other Image", "View", "Copy Astrometric Solution"),
1895
+ ("Drag view -> Off to Canvas", "View", self.tr("Duplicate Image")),
1896
+ ("Drag view -> On to Other Image", "View", self.tr("Copy Zoom and Pan")),
1897
+ ("Shift+Drag -> On to Other Image", "View", self.tr("Apply that image to the other as a mask")),
1898
+ ("Ctrl+Drag -> On to Other Image", "View", self.tr("Copy Astrometric Solution")),
1815
1899
 
1816
1900
  # View zoom
1817
- ("Ctrl+1", "View", "Zoom to 100% (1:1)"),
1818
- ("Ctrl+0", "View", "Fit image to current window"),
1819
- ("Ctrl++", "View", "Zoom In"),
1820
- ("Ctrl+-", "View", "Zoom Out"),
1901
+ ("Ctrl+1", "View", self.tr("Zoom to 100% (1:1)")),
1902
+ ("Ctrl+0", "View", self.tr("Fit image to current window")),
1903
+ ("Ctrl++", "View", self.tr("Zoom In")),
1904
+ ("Ctrl+-", "View", self.tr("Zoom Out")),
1821
1905
 
1822
1906
  # Window switching
1823
- ("Ctrl+PgDown", "MDI", "Switch to previously active view"),
1824
- ("Ctrl+PgUp", "MDI", "Switch to next active view"),
1907
+ ("Ctrl+PgDown", "MDI", self.tr("Switch to previously active view")),
1908
+ ("Ctrl+PgUp", "MDI", self.tr("Switch to next active view")),
1825
1909
 
1826
1910
  # Shortcuts canvas + buttons
1827
- ("Alt+Drag (toolbar button)", "Toolbar", "Create a desktop shortcut for that action"),
1828
- ("Alt+Drag (shortcut button -> view)", "Shortcuts", "Headless apply the shortcut's command/preset to a view"),
1829
- ("Ctrl/Shift+Click", "Shortcuts", "Multi-select shortcut buttons"),
1830
- ("Drag (selection)", "Shortcuts", "Move selected shortcut buttons"),
1831
- ("Delete / Backspace", "Shortcuts", "Delete selected shortcut buttons"),
1832
- ("Ctrl+A", "Shortcuts", "Select all shortcut buttons"),
1833
- ("Double-click empty area", "MDI background", "Open files dialog"),
1911
+ ("Alt+Drag (toolbar button)", "Toolbar", self.tr("Create a desktop shortcut for that action")),
1912
+ ("Alt+Drag (shortcut button -> view)", "Shortcuts", self.tr("Headless apply the shortcut's command/preset to a view")),
1913
+ ("Ctrl/Shift+Click", "Shortcuts", self.tr("Multi-select shortcut buttons")),
1914
+ ("Drag (selection)", "Shortcuts", self.tr("Move selected shortcut buttons")),
1915
+ ("Delete / Backspace", "Shortcuts", self.tr("Delete selected shortcut buttons")),
1916
+ ("Ctrl+A", "Shortcuts", self.tr("Select all shortcut buttons")),
1917
+ ("Double-click empty area", "MDI background", self.tr("Open files dialog")),
1834
1918
 
1835
1919
  # Layers dock
1836
- ("Drag view -> Layers list", "Layers", "Add dragged view as a new layer (on top)"),
1837
- ("Shift+Drag mask -> Layers list", "Layers", "Attach dragged image as mask to the selected layer"),
1920
+ ("Drag view -> Layers list", "Layers", self.tr("Add dragged view as a new layer (on top)")),
1921
+ ("Shift+Drag mask -> Layers list", "Layers", self.tr("Attach dragged image as mask to the selected layer")),
1838
1922
 
1839
1923
  # Crop tool
1840
- ("Click-drag", "Crop Tool", "Draw a crop rectangle"),
1841
- ("Drag corner handles", "Crop Tool", "Resize crop rectangle"),
1842
- ("Shift+Drag on box", "Crop Tool", "Rotate crop rectangle"),
1924
+ ("Click-drag", "Crop Tool", self.tr("Draw a crop rectangle")),
1925
+ ("Drag corner handles", "Crop Tool", self.tr("Resize crop rectangle")),
1926
+ ("Shift+Drag on box", "Crop Tool", self.tr("Rotate crop rectangle")),
1843
1927
  ]
1844
1928
  return rows
1845
1929
 
@@ -1886,7 +1970,11 @@ class AstroSuiteProMainWindow(
1886
1970
  except Exception:
1887
1971
  pass
1888
1972
 
1889
- def _confirm_discard(self, title="New Project", msg="This will close all views and clear desktop shortcuts. Continue?"):
1973
+ def _confirm_discard(self, title=None, msg=None):
1974
+ if title is None:
1975
+ title = self.tr("New Project")
1976
+ if msg is None:
1977
+ msg = self.tr("This will close all views and clear desktop shortcuts. Continue?")
1890
1978
  btn = QMessageBox.question(self, title, msg,
1891
1979
  QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
1892
1980
  QMessageBox.StandardButton.No)
@@ -1894,8 +1982,8 @@ class AstroSuiteProMainWindow(
1894
1982
 
1895
1983
  def _clear_views_keep_shortcuts(self):
1896
1984
  if not self._confirm_discard(
1897
- title="Clear All Views",
1898
- msg="Close all views and documents? Desktop shortcuts will be preserved."
1985
+ title=self.tr("Clear All Views"),
1986
+ msg=self.tr("Close all views and documents? Desktop shortcuts will be preserved.")
1899
1987
  ):
1900
1988
  return
1901
1989
 
@@ -1952,7 +2040,7 @@ class AstroSuiteProMainWindow(
1952
2040
  if getattr(self, "doc_manager", None) and self.doc_manager._docs:
1953
2041
  if not self._confirm_discard(
1954
2042
  title=title,
1955
- msg=(
2043
+ msg=self.tr(
1956
2044
  "Loading a project will close current views and replace desktop shortcuts.\n"
1957
2045
  "Continue?"
1958
2046
  ),
@@ -2072,9 +2160,30 @@ class AstroSuiteProMainWindow(
2072
2160
  self._refresh_mask_action_states()
2073
2161
 
2074
2162
 
2163
+ # ---------------- Settings Caching ----------------
2164
+ def _preload_settings(self):
2165
+ """Build the SettingsDialog in the background so it opens instantly."""
2166
+ if getattr(self, "_settings_dlg_cache", None) is None:
2167
+ from setiastro.saspro.ops.settings import SettingsDialog
2168
+ try:
2169
+ self._settings_dlg_cache = SettingsDialog(self, self.settings)
2170
+ except Exception as e:
2171
+ # Log error but don't crash if preload fails
2172
+ print(f"Error preloading settings: {e}")
2173
+
2075
2174
  def _open_settings(self):
2076
2175
  from setiastro.saspro.ops.settings import SettingsDialog
2077
- dlg = SettingsDialog(self, self.settings)
2176
+
2177
+ # Create cache if it doesn't exist (e.g. opened before preload timer fired)
2178
+ if getattr(self, "_settings_dlg_cache", None) is None:
2179
+ self._settings_dlg_cache = SettingsDialog(self, self.settings)
2180
+
2181
+ dlg = self._settings_dlg_cache
2182
+
2183
+ # Refresh UI from current settings before showing
2184
+ if hasattr(dlg, "refresh_ui"):
2185
+ dlg.refresh_ui()
2186
+
2078
2187
  if dlg.exec():
2079
2188
  # (Optional) react to changes if needed
2080
2189
  pass
@@ -2592,8 +2701,8 @@ class AstroSuiteProMainWindow(
2592
2701
  except Exception:
2593
2702
  pass
2594
2703
 
2595
- # ðŸ'‡ this is the key: stay on top
2596
- dlg.setWindowFlag(Qt.WindowType.WindowStaysOnTopHint, True)
2704
+ # this is the key: stay on top
2705
+ # dlg.setWindowFlag(Qt.WindowType.WindowStaysOnTopHint, True)
2597
2706
 
2598
2707
  dlg.show()
2599
2708
  dlg.raise_()
@@ -4263,7 +4372,7 @@ class AstroSuiteProMainWindow(
4263
4372
  dlg.show()
4264
4373
 
4265
4374
  def _open_whats_in_my_sky(self):
4266
- from wims import WhatsInMySkyDialog
4375
+ from setiastro.saspro.wims import WhatsInMySkyDialog
4267
4376
  dlg = WhatsInMySkyDialog(
4268
4377
  parent=self,
4269
4378
  wims_path=wims_path, # window icon
@@ -4275,7 +4384,7 @@ class AstroSuiteProMainWindow(
4275
4384
 
4276
4385
  def _open_wimi(self):
4277
4386
  # Lazy import to avoid loading lightkurve at startup (~12s)
4278
- from wimi import WIMIDialog
4387
+ from setiastro.saspro.wimi import WIMIDialog
4279
4388
  dlg = WIMIDialog(
4280
4389
  parent=self,
4281
4390
  settings=getattr(self, "settings", None),
@@ -5446,6 +5555,10 @@ class AstroSuiteProMainWindow(
5446
5555
  "rotate_180": "geom_rotate_180",
5447
5556
  "geom_rotate_180": "geom_rotate_180",
5448
5557
 
5558
+ "rotate_any": "geom_rotate_any",
5559
+ "rotate_arbitrary": "geom_rotate_any",
5560
+ "geom_rotate_any": "geom_rotate_any",
5561
+
5449
5562
  "invert": "geom_invert",
5450
5563
  "geom_invert": "geom_invert",
5451
5564
 
@@ -6446,6 +6559,17 @@ class AstroSuiteProMainWindow(
6446
6559
  QMessageBox.warning(self, "Rotate 180Â deg", str(e))
6447
6560
  return
6448
6561
 
6562
+ if cid == "geom_rotate_any":
6563
+ try:
6564
+ angle = float(preset.get("angle_deg", preset.get("angle", 0.0)))
6565
+ called = _call_any(["_apply_geom_rot_any_to_doc"], doc, angle_deg=angle)
6566
+ if not called:
6567
+ raise RuntimeError("No rotate-any apply method found")
6568
+ self._log(f"Rotate ({angle:g}°) applied to '{target_sw.windowTitle()}'")
6569
+ except Exception as e:
6570
+ QMessageBox.warning(self, "Rotate...", str(e))
6571
+ return
6572
+
6449
6573
  if cid == "geom_rescale":
6450
6574
  try:
6451
6575
  factor = float(preset.get("factor", 1.0))
@@ -7251,7 +7375,7 @@ class AstroSuiteProMainWindow(
7251
7375
  self._search_dock = None
7252
7376
 
7253
7377
  # --- Right-side mini dock with the search box ---
7254
- self._search_dock = QDockWidget("Command Search", self)
7378
+ self._search_dock = QDockWidget(self.tr("Command Search"), self)
7255
7379
  self._search_dock.setObjectName("CommandSearchDock")
7256
7380
  # âœ... Allow moving/closing like other panels
7257
7381
  self._search_dock.setAllowedAreas(
@@ -7466,7 +7590,7 @@ class AstroSuiteProMainWindow(
7466
7590
  except Exception:
7467
7591
  pass
7468
7592
 
7469
- try: self.update_undo_redo_action_labels()
7593
+ try: self._schedule_undo_redo_label_refresh()
7470
7594
  except Exception as e:
7471
7595
  import logging
7472
7596
  logging.debug(f"Exception suppressed: {type(e).__name__}: {e}")
@@ -7726,6 +7850,90 @@ class AstroSuiteProMainWindow(
7726
7850
  flags |= Qt.WindowType.WindowMaximizeButtonHint
7727
7851
  sw.setWindowFlags(flags)
7728
7852
 
7853
+ # -------------------------------------------------------------------------
7854
+ # Explicitly size the window to valid dimensions so it doesn't default
7855
+ # to "maximized" or "full MDI area" if the previous window was large.
7856
+ # We target ~60% of the viewport height, clamped to sane bounds.
7857
+ # -------------------------------------------------------------------------
7858
+ vp = self.mdi.viewport()
7859
+ area = vp.rect() if vp else self.mdi.rect()
7860
+
7861
+ # Determine aspect ratio
7862
+ img_w = img_h = None
7863
+ try:
7864
+ img_w, img_h = self._infer_image_size(view)
7865
+ except Exception:
7866
+ pass
7867
+
7868
+ if not img_w or not img_h:
7869
+ aspect = 1.0
7870
+ else:
7871
+ aspect = float(img_w) / float(img_h)
7872
+
7873
+ # Clamp aspect
7874
+ aspect = max(0.3, min(aspect, 4.0))
7875
+
7876
+ target_h = int(area.height() * 0.6)
7877
+ target_w = int(target_h * aspect)
7878
+
7879
+ # Ensure it fits within the area (with some margin)
7880
+ max_w = int(area.width() * 0.9)
7881
+ max_h = int(area.height() * 0.9)
7882
+
7883
+ if target_w > max_w:
7884
+ target_w = max_w
7885
+ # Recalculate height to preserve aspect, if possible
7886
+ target_h = int(target_w / aspect)
7887
+
7888
+ if target_h > max_h:
7889
+ target_h = max_h
7890
+
7891
+ # Enforce minimums
7892
+ target_w = max(200, target_w)
7893
+ target_h = max(200, target_h)
7894
+
7895
+ sw.resize(target_w, target_h)
7896
+ sw.showNormal() # CRITICAL: clears any "maximized" flag from previous active window
7897
+
7898
+ # -------------------------------------------------------------------------
7899
+ # Smart Cascade: Position relative to the *currently active* window
7900
+ # (before we make the new one active).
7901
+ # -------------------------------------------------------------------------
7902
+ new_x, new_y = 0, 0
7903
+
7904
+ # Get dominant/active window *before* we activate the new one
7905
+ active = self.mdi.activeSubWindow()
7906
+ if active and active.isVisible() and not (active.windowState() & Qt.WindowState.WindowMinimized):
7907
+ # Cascade from the active window
7908
+ geo = active.geometry()
7909
+ new_x = geo.x() + 30
7910
+ new_y = geo.y() + 30
7911
+ else:
7912
+ # Fallback: try to find the "last added" visible window to cascade from
7913
+ # (useful if active is None but windows exist)
7914
+ try:
7915
+ subs = [s for s in self.mdi.subWindowList() if s.isVisible() and s is not sw]
7916
+ if subs:
7917
+ # simplistic "last created" might be at end of list
7918
+ last = subs[-1]
7919
+ geo = last.geometry()
7920
+ new_x = geo.x() + 30
7921
+ new_y = geo.y() + 30
7922
+ except Exception:
7923
+ pass
7924
+
7925
+ # Bounds check: don't let it drift completely off-screen
7926
+ # (allow valid title bar to be visible at least)
7927
+ if (new_x + target_w > area.width() + 50) or (new_y + 50 > area.height()):
7928
+ new_x = 0
7929
+ new_y = 0
7930
+
7931
+ # Clamp to 0 if negative for some reason
7932
+ new_x = max(0, new_x)
7933
+ new_y = max(0, new_y)
7934
+
7935
+ sw.move(new_x, new_y)
7936
+
7729
7937
  # ❌ removed the "fill MDI viewport" block - we *don't* want full-monitor first window
7730
7938
 
7731
7939
  # Show / activate
@@ -7870,7 +8078,7 @@ class AstroSuiteProMainWindow(
7870
8078
  # If no subwindows remain, clear all "active doc" UI bits, including header
7871
8079
  if not self.mdi.subWindowList():
7872
8080
  self.currentDocumentChanged.emit(None) # drives HeaderViewerDock.set_document(None)
7873
- self.update_undo_redo_action_labels()
8081
+ self._schedule_undo_redo_label_refresh()
7874
8082
  self._hdr_refresh_timer.start(0) # belt-and-suspenders for manual widgets
7875
8083
  # If your dock has its own set_document, call it explicitly too
7876
8084
  hv = getattr(self, "header_viewer", None)
@@ -8212,19 +8420,20 @@ class AstroSuiteProMainWindow(
8212
8420
 
8213
8421
  # Misc UI refreshes (guarded)
8214
8422
  try:
8215
- self.update_undo_redo_action_labels()
8216
- except Exception:
8217
- pass
8218
- try:
8219
- if hasattr(self, "_hdr_refresh_timer") and self._hdr_refresh_timer is not None:
8220
- self._hdr_refresh_timer.start(0)
8423
+ self._schedule_undo_redo_label_refresh()
8221
8424
  except Exception:
8222
8425
  pass
8426
+ #try:
8427
+ # if hasattr(self, "_hdr_refresh_timer") and self._hdr_refresh_timer is not None:
8428
+ # self._hdr_refresh_timer.start(0)
8429
+ #except Exception:
8430
+ # pass
8223
8431
  try:
8224
8432
  self._refresh_mask_action_states()
8225
8433
  except Exception:
8226
8434
  pass
8227
8435
 
8436
+
8228
8437
  def _sync_docman_active(self, doc):
8229
8438
  dm = self.doc_manager
8230
8439
  try:
@@ -8385,7 +8594,131 @@ class AstroSuiteProMainWindow(
8385
8594
  if self._suspend_dock_sync:
8386
8595
  QTimer.singleShot(0, lambda: self.changeEvent(QEvent(QEvent.Type.WindowStateChange)))
8387
8596
 
8597
+ def resizeEvent(self, event):
8598
+ super().resizeEvent(event)
8599
+ # Update floating resource monitor position if it exists (from DockMixin)
8600
+ if hasattr(self, "_update_monitor_position"):
8601
+ self._update_monitor_position()
8602
+
8603
+ def moveEvent(self, event):
8604
+ super().moveEvent(event)
8605
+ # Update floating resource monitor position if it exists (from DockMixin)
8606
+ if hasattr(self, "_update_monitor_position"):
8607
+ self._update_monitor_position()
8608
+
8609
+ def changeEvent(self, event):
8610
+ super().changeEvent(event)
8611
+ # 1. Existing logic for dock sync (re-instated from showEvent logic if needed, but usually changeEvent is enough)
8612
+ # (The snippet viewed previously showed showEvent firing a oneshot to call changeEvent)
8613
+
8614
+ # 2. Resource Monitor Sync
8615
+ if event.type() == QEvent.Type.WindowStateChange:
8616
+ if self.windowState() & Qt.WindowState.WindowMinimized:
8617
+ # App minimized -> hide overlay
8618
+ if hasattr(self, "resource_monitor") and self.resource_monitor:
8619
+ self.resource_monitor.hide()
8620
+ elif not (self.windowState() & Qt.WindowState.WindowMinimized):
8621
+ # Only auto-show if the initial fade-in is done
8622
+ if getattr(self, "_fade_in_complete", False):
8623
+ # App restored -> show overlay if enabled in settings
8624
+ if hasattr(self, "resource_monitor") and self.resource_monitor:
8625
+ if self.settings.value("ui/resource_monitor_visible", True, type=bool):
8626
+ self.resource_monitor.show()
8627
+ # Ensure position is correct upon restore
8628
+ if hasattr(self, "_update_monitor_position"):
8629
+ self._update_monitor_position()
8630
+
8631
+ def save_ui_state(self):
8632
+ """Save window geometry, state, and shortcuts to settings."""
8633
+ self._ensure_persistent_names()
8634
+ try:
8635
+ if self.isMaximized():
8636
+ self.settings.setValue("ui/main/maximized", True)
8637
+ # To get accurate geometry for non-maximized state, we'd need to showNormal()
8638
+ # but that causes flicker. For a restart, we'll just save what we have.
8639
+ else:
8640
+ self.settings.setValue("ui/main/maximized", False)
8641
+ self.settings.setValue("ui/main/geometry", self.saveGeometry())
8642
+
8643
+ self.settings.setValue("ui/main/state", self.saveState(version=1))
8644
+ except Exception:
8645
+ pass
8646
+
8647
+ # save shortcuts
8648
+ try:
8649
+ save_on_exit = self.settings.value("shortcuts/save_on_exit", True, type=bool)
8650
+ except Exception:
8651
+ save_on_exit = True
8652
+ if save_on_exit and hasattr(self, "shortcuts"):
8653
+ try:
8654
+ self.shortcuts.save_shortcuts()
8655
+ except Exception:
8656
+ pass
8657
+
8658
+ self.settings.sync()
8659
+
8660
+ def on_fade_in_complete(self):
8661
+ """Called when main window fade-in is finished."""
8662
+ self._fade_in_complete = True
8663
+ # Sync Monitor Visibility
8664
+ if hasattr(self, "resource_monitor") and self.resource_monitor:
8665
+ if not self.isMinimized() and self.settings.value("ui/resource_monitor_visible", True, type=bool):
8666
+ # Delay show to ensure visually pleasing sequence (monitor appears AFTER app)
8667
+ QTimer.singleShot(500, self.resource_monitor.show)
8668
+ # Ensure position
8669
+ QTimer.singleShot(600, self._update_monitor_position)
8670
+
8671
+ def keyPressEvent(self, event):
8672
+ """Handle key press events for secret shortcuts."""
8673
+ if (event.modifiers() == (Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.AltModifier) and
8674
+ event.key() == Qt.Key.Key_M):
8675
+
8676
+ # Secret minigame launcher
8677
+ # __file__ is in .../saspro/gui/main_window.py
8678
+ # We want to go up to .../saspro/
8679
+ base_pkg = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
8680
+ minigame_path = os.path.join(base_pkg, "widgets", "minigame", "index.html")
8681
+
8682
+ if os.path.exists(minigame_path):
8683
+ QDesktopServices.openUrl(QUrl.fromLocalFile(minigame_path))
8684
+ event.accept()
8685
+ return
8686
+
8687
+ super().keyPressEvent(event)
8688
+
8689
+ def _update_usage_stats(self):
8690
+ try:
8691
+ now = time.time()
8692
+ elapsed = now - self._session_start_time
8693
+ self._session_start_time = now # Reset session start to avoid double counting
8694
+
8695
+ total = self.settings.value("stats/total_time_seconds", 0.0, type=float)
8696
+ self.settings.setValue("stats/total_time_seconds", total + elapsed)
8697
+ except Exception:
8698
+ pass
8699
+
8700
+ def _on_tool_triggered(self):
8701
+ """Slot to track tool usage count."""
8702
+ try:
8703
+ count = self.settings.value("stats/opened_tools_count", 0, type=int)
8704
+ self.settings.setValue("stats/opened_tools_count", count + 1)
8705
+ except Exception:
8706
+ pass
8707
+
8388
8708
  def closeEvent(self, e):
8709
+ self._update_usage_stats()
8710
+
8711
+
8712
+ # Optimization: If restarting (e.g. language change), bypass confirmation and close immediately
8713
+ if getattr(self, "_is_restarting", False):
8714
+ e.accept()
8715
+ return
8716
+
8717
+ # Check if we have already faded out
8718
+ if getattr(self, "_fade_out_complete", False):
8719
+ # Proceed with shutdown
8720
+ self._do_shutdown_steps(e)
8721
+ return
8389
8722
 
8390
8723
  try:
8391
8724
  if hasattr(self, "_orig_stdout") and self._orig_stdout is not None:
@@ -8394,6 +8727,8 @@ class AstroSuiteProMainWindow(
8394
8727
  sys.stderr = self._orig_stderr
8395
8728
  except Exception:
8396
8729
  pass
8730
+
8731
+ # --- Confirmation Logic ---
8397
8732
  self._shutting_down = True
8398
8733
  # Gather open docs
8399
8734
  docs = []
@@ -8404,64 +8739,73 @@ class AstroSuiteProMainWindow(
8404
8739
  docs.append(d)
8405
8740
 
8406
8741
  edited = [d for d in docs if self._document_has_edits(d)]
8407
- msg = "Exit Seti Astro Suite Pro?"
8408
- detail = []
8409
- if docs:
8410
- detail.append(f"Open images: {len(docs)}")
8411
- if edited:
8412
- detail.append(f"Edited since open: {len(edited)}")
8413
- if detail:
8414
- msg += "\n\n" + "\n".join(detail)
8415
-
8416
- # --- stay-on-top message box ---
8417
- mbox = QMessageBox(self)
8418
- mbox.setIcon(QMessageBox.Icon.Question)
8419
- mbox.setWindowTitle("Confirm Exit")
8420
- mbox.setText(msg)
8421
- mbox.setStandardButtons(
8422
- QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
8423
- )
8424
- mbox.setDefaultButton(QMessageBox.StandardButton.No)
8425
- # ðŸ'‡ key line
8426
- mbox.setWindowFlag(Qt.WindowType.WindowStaysOnTopHint, True)
8427
- mbox.raise_()
8428
- mbox.activateWindow()
8429
- btn = mbox.exec()
8742
+ # If user has disabled exit confirmation (optional setting, but default is confirm)
8743
+ confirm = True
8744
+
8745
+ if confirm:
8746
+ msg = self.tr("Exit Seti Astro Suite Pro?")
8747
+ detail = []
8748
+ if docs:
8749
+ detail.append(self.tr("Open images:") + f" {len(docs)}")
8750
+ if edited:
8751
+ detail.append(self.tr("Edited since open:") + f" {len(edited)}")
8752
+ if detail:
8753
+ msg += "\n\n" + "\n".join(detail)
8754
+
8755
+ # --- stay-on-top message box ---
8756
+ mbox = QMessageBox(self)
8757
+ mbox.setIcon(QMessageBox.Icon.Question)
8758
+ mbox.setWindowTitle(self.tr("Confirm Exit"))
8759
+ mbox.setText(msg)
8760
+ mbox.setStandardButtons(
8761
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
8762
+ )
8763
+ mbox.setDefaultButton(QMessageBox.StandardButton.No)
8764
+ mbox.setWindowFlag(Qt.WindowType.WindowStaysOnTopHint, True)
8765
+ mbox.raise_()
8766
+ mbox.activateWindow()
8767
+ btn = mbox.exec()
8768
+
8769
+ if btn != QMessageBox.StandardButton.Yes:
8770
+ e.ignore()
8771
+ self._shutting_down = False
8772
+ return
8430
8773
 
8431
- if btn != QMessageBox.StandardButton.Yes:
8432
- e.ignore()
8433
- return
8774
+ # --- User confirmed (or no confirm needed) ---
8775
+ # Start Fade Out Animation
8776
+ e.ignore() # Defer close until animation completes
8777
+ self.setEnabled(False) # Prevent further interaction
8778
+
8779
+ # Hide monitor immediately when fade starts (user preference)
8780
+ if hasattr(self, "resource_monitor") and self.resource_monitor:
8781
+ try:
8782
+ if hasattr(self.resource_monitor, "backend"):
8783
+ self.resource_monitor.backend.stop()
8784
+ except Exception:
8785
+ pass
8786
+ self.resource_monitor.hide()
8787
+ self.resource_monitor.close()
8788
+
8789
+ self._anim_close = QPropertyAnimation(self, b"windowOpacity")
8790
+ self._anim_close.setDuration(800)
8791
+ self._anim_close.setStartValue(1.0)
8792
+ self._anim_close.setEndValue(0.0)
8793
+ self._anim_close.setEasingCurve(QEasingCurve.Type.OutQuad)
8794
+ self._anim_close.finished.connect(self._on_fade_out_finished)
8795
+ self._anim_close.start()
8796
+
8797
+ def _on_fade_out_finished(self):
8798
+ """Called when close animation completes."""
8799
+ self._fade_out_complete = True
8800
+ self.close()
8434
8801
 
8435
- # User confirmed: prevent per-subwindow prompts and proceed
8802
+ def _do_shutdown_steps(self, e):
8803
+ """Actual shutdown logic after verification and animation."""
8436
8804
  self._force_close_all = True
8437
8805
  self._shutting_down = True
8438
8806
 
8439
8807
  # Save UI layout/placement
8440
- self._ensure_persistent_names()
8441
- try:
8442
- if self.isMaximized():
8443
- self.settings.setValue("ui/main/maximized", True)
8444
- self.showNormal()
8445
- self.settings.setValue("ui/main/geometry", self.saveGeometry())
8446
- self.showMaximized()
8447
- else:
8448
- self.settings.setValue("ui/main/maximized", False)
8449
- self.settings.setValue("ui/main/geometry", self.saveGeometry())
8450
-
8451
- self.settings.setValue("ui/main/state", self.saveState(version=1))
8452
- except Exception:
8453
- pass
8454
-
8455
- # save shortcuts
8456
- try:
8457
- save_on_exit = self.settings.value("shortcuts/save_on_exit", True, type=bool)
8458
- except Exception:
8459
- save_on_exit = True
8460
- if save_on_exit and hasattr(self, "shortcuts"):
8461
- try:
8462
- self.shortcuts.save_shortcuts()
8463
- except Exception:
8464
- pass
8808
+ self.save_ui_state()
8465
8809
 
8466
8810
  # wait on bg threads
8467
8811
  try:
@@ -8480,6 +8824,7 @@ class AstroSuiteProMainWindow(
8480
8824
  # CheatSheet dialog and helper functions imported from setiastro.saspro.cheat_sheet
8481
8825
  from setiastro.saspro.cheat_sheet import (
8482
8826
  CheatSheetDialog as _CheatSheetDialog,
8827
+ add_extra_shortcuts,
8483
8828
  _qs_to_str,
8484
8829
  _clean_text,
8485
8830
  _uniq_keep_order,