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.

Potentially problematic release.


This version of setiastrosuitepro might be problematic. Click here for more details.

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
@@ -71,7 +71,7 @@ class UpdateMixin:
71
71
  def check_for_updates_now(self):
72
72
  """Check for updates interactively (show result to user)."""
73
73
  if self.statusBar():
74
- self.statusBar().showMessage("Checking for updates...")
74
+ self.statusBar().showMessage(self.tr("Checking for updates..."))
75
75
  self._kick_update_check(interactive=True)
76
76
 
77
77
  def check_for_updates_startup(self):
@@ -109,8 +109,8 @@ class UpdateMixin:
109
109
  if self.statusBar():
110
110
  self.statusBar().showMessage("Update check failed.", 5000)
111
111
  if interactive:
112
- QMessageBox.warning(self, "Update Check Failed",
113
- f"Unable to check for updates.\n\n{err}")
112
+ QMessageBox.warning(self, self.tr("Update Check Failed"),
113
+ self.tr("Unable to check for updates.\n\n{err}").replace("{err}", err))
114
114
  else:
115
115
  print(f"[updates] check failed: {err}")
116
116
  return
@@ -122,8 +122,8 @@ class UpdateMixin:
122
122
  if self.statusBar():
123
123
  self.statusBar().showMessage("Update check failed (bad JSON).", 5000)
124
124
  if interactive:
125
- QMessageBox.warning(self, "Update Check Failed",
126
- f"Update JSON is invalid.\n\n{je}")
125
+ QMessageBox.warning(self, self.tr("Update Check Failed"),
126
+ self.tr("Update JSON is invalid.\n\n{je}").replace("{je}", str(je)))
127
127
  else:
128
128
  print(f"[updates] bad JSON: {je}")
129
129
  return
@@ -136,8 +136,8 @@ class UpdateMixin:
136
136
  if self.statusBar():
137
137
  self.statusBar().showMessage("Update check failed (no 'version').", 5000)
138
138
  if interactive:
139
- QMessageBox.warning(self, "Update Check Failed",
140
- "Update JSON missing the 'version' field.")
139
+ QMessageBox.warning(self, self.tr("Update Check Failed"),
140
+ self.tr("Update JSON missing the 'version' field."))
141
141
  else:
142
142
  print("[updates] JSON missing 'version'")
143
143
  return
@@ -148,13 +148,13 @@ class UpdateMixin:
148
148
 
149
149
  if available:
150
150
  if self.statusBar():
151
- self.statusBar().showMessage(f"Update available: {latest_str}", 5000)
151
+ self.statusBar().showMessage(self.tr("Update available: {0}").format(latest_str), 5000)
152
152
  msg_box = QMessageBox(self)
153
153
  msg_box.setIcon(QMessageBox.Icon.Information)
154
- msg_box.setWindowTitle("Update Available")
155
- msg_box.setText(f"A new version ({latest_str}) is available!")
154
+ msg_box.setWindowTitle(self.tr("Update Available"))
155
+ msg_box.setText(self.tr("A new version ({0}) is available!").format(latest_str))
156
156
  if notes:
157
- msg_box.setInformativeText(f"Release Notes:\n{notes}")
157
+ msg_box.setInformativeText(self.tr("Release Notes:\n{0}").format(notes))
158
158
  msg_box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
159
159
  msg_box.setDefaultButton(QMessageBox.StandardButton.Yes)
160
160
 
@@ -170,7 +170,7 @@ class UpdateMixin:
170
170
  "Linux" if plat.startswith("linux") else "", ""
171
171
  )
172
172
  if not link:
173
- QMessageBox.warning(self, "Download", "No download link available for this platform.")
173
+ QMessageBox.warning(self, self.tr("Download"), self.tr("No download link available for this platform."))
174
174
  return
175
175
 
176
176
  if plat.startswith("win"):
@@ -183,8 +183,8 @@ class UpdateMixin:
183
183
  if self.statusBar():
184
184
  self.statusBar().showMessage("You're up to date.", 3000)
185
185
  if interactive:
186
- QMessageBox.information(self, "Up to Date",
187
- "You're already running the latest version.")
186
+ QMessageBox.information(self, self.tr("Up to Date"),
187
+ self.tr("You're already running the latest version."))
188
188
  finally:
189
189
  reply.deleteLater()
190
190
 
@@ -229,7 +229,7 @@ class UpdateMixin:
229
229
 
230
230
  reply.downloadProgress.connect(
231
231
  lambda rec, tot: self.statusBar().showMessage(
232
- f"Downloading update... {rec / 1024:.1f} KB / {tot / 1024:.1f} KB" if tot > 0 else "Downloading update..."
232
+ self.tr("Downloading update... {0:.1f} KB / {1:.1f} KB").format(rec / 1024, tot / 1024) if tot > 0 else self.tr("Downloading update...")
233
233
  )
234
234
  )
235
235
 
@@ -244,8 +244,8 @@ class UpdateMixin:
244
244
  target_path = Path(reply.property("target_path"))
245
245
 
246
246
  if reply.error() != QNetworkReply.NetworkError.NoError:
247
- QMessageBox.warning(self, "Update Failed",
248
- f"Could not download update:\n{reply.errorString()}")
247
+ QMessageBox.warning(self, self.tr("Update Failed"),
248
+ self.tr("Could not download update:\n{0}").format(reply.errorString()))
249
249
  return
250
250
 
251
251
  # Write the .zip
@@ -254,8 +254,8 @@ class UpdateMixin:
254
254
  with open(target_path, "wb") as f:
255
255
  f.write(data)
256
256
  except Exception as e:
257
- QMessageBox.warning(self, "Update Failed",
258
- f"Could not save update to disk:\n{e}")
257
+ QMessageBox.warning(self, self.tr("Update Failed"),
258
+ self.tr("Could not save update to disk:\n{0}").format(e))
259
259
  return
260
260
 
261
261
  self.statusBar().showMessage(f"Update downloaded to {target_path}", 5000)
@@ -267,8 +267,8 @@ class UpdateMixin:
267
267
  with zipfile.ZipFile(target_path, "r") as zf:
268
268
  zf.extractall(extract_dir)
269
269
  except Exception as e:
270
- QMessageBox.warning(self, "Update Failed",
271
- f"Could not extract update zip:\n{e}")
270
+ QMessageBox.warning(self, self.tr("Update Failed"),
271
+ self.tr("Could not extract update zip:\n{0}").format(e))
272
272
  return
273
273
 
274
274
  # Look recursively for an .exe
@@ -277,7 +277,7 @@ class UpdateMixin:
277
277
  QMessageBox.warning(
278
278
  self,
279
279
  "Update Failed",
280
- f"Downloaded ZIP did not contain an .exe installer.\nFolder: {extract_dir}"
280
+ self.tr("Downloaded ZIP did not contain an .exe installer.\nFolder: {0}").format(extract_dir)
281
281
  )
282
282
  return
283
283
 
@@ -289,8 +289,8 @@ class UpdateMixin:
289
289
  # Ask to run
290
290
  ok = QMessageBox.question(
291
291
  self,
292
- "Run Installer",
293
- "The update has been downloaded.\n\nRun the installer now? (SAS will close.)",
292
+ self.tr("Run Installer"),
293
+ self.tr("The update has been downloaded.\n\nRun the installer now? (SAS will close.)"),
294
294
  QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
295
295
  QMessageBox.StandardButton.Yes,
296
296
  )
@@ -301,8 +301,8 @@ class UpdateMixin:
301
301
  try:
302
302
  subprocess.Popen([str(installer_path)], shell=False)
303
303
  except Exception as e:
304
- QMessageBox.warning(self, "Update Failed",
305
- f"Could not start installer:\n{e}")
304
+ QMessageBox.warning(self, self.tr("Update Failed"),
305
+ self.tr("Could not start installer:\n{0}").format(e))
306
306
  return
307
307
 
308
308
  # Close app so the installer can overwrite files
@@ -0,0 +1,47 @@
1
+ from PyQt6.QtWidgets import QDialog, QVBoxLayout, QLabel, QFormLayout, QPushButton
2
+ from PyQt6.QtCore import Qt, QSettings
3
+ from PyQt6.QtGui import QIcon
4
+
5
+ class StatisticsDialog(QDialog):
6
+ def __init__(self, parent=None):
7
+ super().__init__(parent)
8
+ self.setWindowTitle(self.tr("App Statistics"))
9
+ self.setWindowFlags(self.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint)
10
+ self.resize(300, 200)
11
+
12
+ # Settings to read stats
13
+ self.settings = QSettings("SetiAstro", "SetiAstroSuitePro")
14
+
15
+ layout = QVBoxLayout(self)
16
+
17
+ form_layout = QFormLayout()
18
+
19
+ # Time Spent
20
+ total_seconds = self.settings.value("stats/total_time_seconds", 0, type=float)
21
+ days = int(total_seconds // 86400)
22
+ hours = int((total_seconds % 86400) // 3600)
23
+ minutes = int((total_seconds % 3600) // 60)
24
+
25
+ time_str = f"{days} {self.tr('Days')}, {hours} {self.tr('Hours')}, {minutes} {self.tr('Minutes')}"
26
+ if days == 0:
27
+ time_str = f"{hours} {self.tr('Hours')}, {minutes} {self.tr('Minutes')}"
28
+
29
+ self.lbl_time = QLabel(time_str)
30
+ form_layout.addRow(self.tr("Time Spent:"), self.lbl_time)
31
+
32
+ # Images Opened
33
+ images_count = self.settings.value("stats/opened_images_count", 0, type=int)
34
+ self.lbl_images = QLabel(str(images_count))
35
+ form_layout.addRow(self.tr("Images Opened:"), self.lbl_images)
36
+
37
+ # Tools Opened
38
+ tools_count = self.settings.value("stats/opened_tools_count", 0, type=int)
39
+ self.lbl_tools = QLabel(str(tools_count))
40
+ form_layout.addRow(self.tr("Tools Opened:"), self.lbl_tools)
41
+
42
+ layout.addLayout(form_layout)
43
+
44
+ # Close button
45
+ btn_close = QPushButton(self.tr("Close"))
46
+ btn_close.clicked.connect(self.accept)
47
+ layout.addWidget(btn_close, alignment=Qt.AlignmentFlag.AlignRight)
@@ -249,6 +249,9 @@ class HaloBGonDialogPro(QDialog):
249
249
  def __init__(self, parent, doc, icon: Optional[QIcon] = None):
250
250
  super().__init__(parent)
251
251
  self.setWindowTitle("Halo-B-Gon")
252
+ self.setWindowFlag(Qt.WindowType.Window, True)
253
+ self.setWindowModality(Qt.WindowModality.NonModal)
254
+ self.setModal(False)
252
255
  if icon:
253
256
  try: self.setWindowIcon(icon)
254
257
  except Exception as e:
@@ -427,7 +430,8 @@ class HaloBGonDialogPro(QDialog):
427
430
  except Exception:
428
431
  pass
429
432
 
430
- self.accept()
433
+ # Dialog stays open - refresh document for next operation
434
+ self._refresh_document_from_active()
431
435
  return
432
436
  else:
433
437
  # Fallback: try legacy spawner if present; else warn and overwrite.
@@ -437,7 +441,8 @@ class HaloBGonDialogPro(QDialog):
437
441
  if callable(spawner):
438
442
  title = self.doc.display_name() if hasattr(self.doc, "display_name") else "Image"
439
443
  spawner(out, f"{title} [Halo-B-Gon]")
440
- self.accept()
444
+ # Dialog stays open - refresh document for next operation
445
+ self._refresh_document_from_active()
441
446
  return
442
447
  else:
443
448
  QMessageBox.warning(
@@ -448,11 +453,32 @@ class HaloBGonDialogPro(QDialog):
448
453
 
449
454
  # Overwrite current (original behavior)
450
455
  self._apply_overwrite(out)
451
- self.accept()
456
+ # Dialog stays open - refresh document for next operation
457
+ self._refresh_document_from_active()
452
458
 
453
459
  except Exception as e:
454
460
  QMessageBox.critical(self, "Halo-B-Gon", f"Failed to apply:\n{e}")
455
461
 
462
+ def _refresh_document_from_active(self):
463
+ """
464
+ Refresh the dialog's document reference to the currently active document.
465
+ This allows reusing the same dialog on different images.
466
+ """
467
+ try:
468
+ main = self.parent()
469
+ if main and hasattr(main, "_active_doc"):
470
+ new_doc = main._active_doc()
471
+ if new_doc is not None and new_doc is not self.doc:
472
+ self.doc = new_doc
473
+ # Refresh preview for new document
474
+ self.orig = np.clip(np.asarray(new_doc.image, dtype=np.float32), 0.0, 1.0)
475
+ disp = self.orig
476
+ if disp.ndim == 2: disp = disp[..., None].repeat(3, axis=2)
477
+ elif disp.ndim == 3 and disp.shape[2] == 1: disp = disp.repeat(3, axis=2)
478
+ self._disp_base = disp
479
+ self._update_preview()
480
+ except Exception:
481
+ pass
456
482
 
457
483
 
458
484
  def _reset(self):
@@ -30,15 +30,15 @@ class HeaderViewerDock(QDockWidget):
30
30
  Supports FITS headers and XISF file & image metadata.
31
31
  """
32
32
  def __init__(self, parent=None):
33
- super().__init__("Header Viewer", parent)
33
+ super().__init__(self.tr("Header Viewer"), parent)
34
34
  self._doc: Optional[ImageDocument] = None
35
35
  self._doc_conn = False
36
36
 
37
37
  self._tree = QTreeWidget()
38
- self._tree.setHeaderLabels(["Key", "Value"])
38
+ self._tree.setHeaderLabels([self.tr("Key"), self.tr("Value")])
39
39
  self._tree.setColumnWidth(0, 220)
40
40
 
41
- self._save_btn = QPushButton("Save Metadata…")
41
+ self._save_btn = QPushButton(self.tr("Save Metadata…"))
42
42
  self._save_btn.clicked.connect(self._save_metadata)
43
43
  self._dm = None # <-- NEW: DocManager to query "active"
44
44
  self._follow_hover = False # <-- optional toggle if you ever want hover-follow
@@ -174,6 +174,9 @@ class HeaderViewerDock(QDockWidget):
174
174
 
175
175
  # --- helpers ---------------------------------------------------------
176
176
  def _populate_header_dict(self, d: dict, title="Header (dict)"):
177
+ # We translate the default title if it's the default, but often title is passed in.
178
+ # If title is passed in English from other methods, we should translate it at the call site or here if possible.
179
+ # Since title is variable, we'll leave it as is, but ensure call sites pass translated strings.
177
180
  root = QTreeWidgetItem([title])
178
181
  self._tree.addTopLevelItem(root)
179
182
  for k, v in d.items():
@@ -196,11 +199,11 @@ class HeaderViewerDock(QDockWidget):
196
199
  pass
197
200
  self._populate_fits_header(hdr)
198
201
  elif fmt == "dict":
199
- self._populate_header_dict(snap.get("items") or {}, "Header (snapshot)")
202
+ self._populate_header_dict(snap.get("items") or {}, self.tr("Header (snapshot)"))
200
203
  else:
201
204
  # generic repr fallback
202
205
  txt = (snap or {}).get("text", "")
203
- node = QTreeWidgetItem(["Header (snapshot)"])
206
+ node = QTreeWidgetItem([self.tr("Header (snapshot)")])
204
207
  self._tree.addTopLevelItem(node)
205
208
  node.addChild(QTreeWidgetItem(["repr", str(txt)]))
206
209
 
@@ -219,7 +222,7 @@ class HeaderViewerDock(QDockWidget):
219
222
 
220
223
  # 2) dict-style header (e.g., XISF-style properties captured as dict)
221
224
  if isinstance(hdr, dict):
222
- self._populate_header_dict(hdr, "Header (dict from document)")
225
+ self._populate_header_dict(hdr, self.tr("Header (dict from document)"))
223
226
  return True
224
227
 
225
228
  # 3) JSON-safe snapshot captured by DocManager
@@ -231,7 +234,7 @@ class HeaderViewerDock(QDockWidget):
231
234
  # 4) XISF properties stored in metadata (common keys)
232
235
  for k in ("xisf_header", "xisf_properties"):
233
236
  if isinstance(meta.get(k), dict):
234
- self._populate_header_dict(meta[k], "XISF Properties (document)")
237
+ self._populate_header_dict(meta[k], self.tr("XISF Properties (document)"))
235
238
  return True
236
239
 
237
240
  return False
@@ -264,7 +267,7 @@ class HeaderViewerDock(QDockWidget):
264
267
  xisf = XISF(path)
265
268
  props = getattr(xisf, "properties", None)
266
269
  if isinstance(props, dict):
267
- self._populate_header_dict(props, "XISF Properties")
270
+ self._populate_header_dict(props, self.tr("XISF Properties"))
268
271
  return True
269
272
  except Exception:
270
273
  pass
@@ -277,14 +280,14 @@ class HeaderViewerDock(QDockWidget):
277
280
  self._tree.clear()
278
281
  base_doc = self._unwrap_base_doc(self._doc)
279
282
  if not base_doc:
280
- self.setWindowTitle("Header Viewer")
283
+ self.setWindowTitle(self.tr("Header Viewer"))
281
284
  return
282
285
  self._doc = base_doc
283
286
 
284
287
  meta = self._doc.metadata or {}
285
288
  path = (meta.get("file_path") or "") if isinstance(meta.get("file_path"), str) else ""
286
- base = os.path.basename(path) if path else (meta.get("display_name") or "Untitled")
287
- self.setWindowTitle(f"Header: {base}")
289
+ base = os.path.basename(path) if path else (meta.get("display_name") or self.tr("Untitled"))
290
+ self.setWindowTitle(self.tr("Header: {0}").format(base))
288
291
 
289
292
  try:
290
293
  # 1) Prefer header data already stored with the document
@@ -304,7 +307,7 @@ class HeaderViewerDock(QDockWidget):
304
307
  pass
305
308
 
306
309
  # 4) Always show remaining lightweight metadata (skip heavy blobs we already rendered)
307
- info_root = QTreeWidgetItem(["Metadata"])
310
+ info_root = QTreeWidgetItem([self.tr("Metadata")])
308
311
  self._tree.addTopLevelItem(info_root)
309
312
  for k, v in meta.items():
310
313
  if k in ("original_header", "fits_header", "header", "wcs", "__header_snapshot__", "xisf_header", "xisf_properties"):
@@ -320,7 +323,7 @@ class HeaderViewerDock(QDockWidget):
320
323
 
321
324
  # ---- population helpers ----
322
325
  def _populate_fits_header(self, header: Any):
323
- root = QTreeWidgetItem(["FITS Header"])
326
+ root = QTreeWidgetItem([self.tr("FITS Header")])
324
327
  self._tree.addTopLevelItem(root)
325
328
 
326
329
  # FITS Header: sanitize and iterate cards defensively
@@ -354,7 +357,7 @@ class HeaderViewerDock(QDockWidget):
354
357
 
355
358
  def _populate_wcs(self, wcs_obj):
356
359
  """Show a real astropy.wcs.WCS as header-like key/values."""
357
- root = QTreeWidgetItem(["WCS"])
360
+ root = QTreeWidgetItem([self.tr("WCS")])
358
361
  self._tree.addTopLevelItem(root)
359
362
  try:
360
363
  # Use relax=True so SIP/etc. are included if present.
@@ -381,14 +384,14 @@ class HeaderViewerDock(QDockWidget):
381
384
  img_meta: Dict[str, Any] = img_meta_list[0] if img_meta_list else {}
382
385
 
383
386
  # File-level metadata
384
- froot = QTreeWidgetItem(["XISF File Metadata"])
387
+ froot = QTreeWidgetItem([self.tr("XISF File Metadata")])
385
388
  self._tree.addTopLevelItem(froot)
386
389
  for k, v in file_meta.items():
387
390
  vstr = v.get("value", "") if isinstance(v, dict) else v
388
391
  froot.addChild(QTreeWidgetItem([str(k), str(vstr)]))
389
392
 
390
393
  # Image-level metadata
391
- iroot = QTreeWidgetItem(["XISF Image Metadata"])
394
+ iroot = QTreeWidgetItem([self.tr("XISF Image Metadata")])
392
395
  self._tree.addTopLevelItem(iroot)
393
396
 
394
397
  # FITS-like keywords (nested)
@@ -418,7 +421,7 @@ class HeaderViewerDock(QDockWidget):
418
421
  def _save_metadata(self):
419
422
  if not self._doc:
420
423
  return
421
- path, _ = QFileDialog.getSaveFileName(self, "Save Metadata", "", "CSV (*.csv)")
424
+ path, _ = QFileDialog.getSaveFileName(self, self.tr("Save Metadata"), "", self.tr("CSV (*.csv)"))
422
425
  if not path:
423
426
  return
424
427
 
@@ -442,4 +445,4 @@ class HeaderViewerDock(QDockWidget):
442
445
  w.writerow(["Key", "Value"])
443
446
  w.writerows(rows)
444
447
  except Exception as e:
445
- QMessageBox.critical(self, "Save Metadata", f"Failed to save:\n{e}")
448
+ QMessageBox.critical(self, self.tr("Save Metadata"), self.tr("Failed to save:\n{0}").format(e))
@@ -29,7 +29,10 @@ class HistogramDialog(QDialog):
29
29
  pivotPicked = pyqtSignal(float) # normalized [0..1] x position for GHS pivot
30
30
  def __init__(self, parent, document):
31
31
  super().__init__(parent)
32
- self.setWindowTitle("Histogram")
32
+ self.setWindowTitle(self.tr("Histogram"))
33
+ self.setWindowFlag(Qt.WindowType.Window, True)
34
+ self.setWindowModality(Qt.WindowModality.NonModal)
35
+ self.setModal(False)
33
36
  self.doc = document
34
37
  self.image = _to_float_preserve(document.image)
35
38
 
@@ -110,10 +113,10 @@ class HistogramDialog(QDialog):
110
113
  self.hist_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
111
114
  self.scroll_area.setWidget(self.hist_label)
112
115
  self.hist_label.installEventFilter(self)
113
- self.hist_label.setToolTip(
116
+ self.hist_label.setToolTip(self.tr(
114
117
  "Ctrl+Click on the histogram to send that intensity as the "
115
118
  "pivot to Hyperbolic Stretch (if open)."
116
- )
119
+ ))
117
120
  self.scroll_area.viewport().installEventFilter(self)
118
121
 
119
122
  splitter.addWidget(self.scroll_area)
@@ -123,8 +126,8 @@ class HistogramDialog(QDialog):
123
126
  self.stats_table.setRowCount(7)
124
127
  self.stats_table.setColumnCount(1)
125
128
  self.stats_table.setVerticalHeaderLabels([
126
- "Min", "Max", "Median", "StdDev",
127
- "MAD", "Low Clipped", "High Clipped"
129
+ self.tr("Min"), self.tr("Max"), self.tr("Median"), self.tr("StdDev"),
130
+ self.tr("MAD"), self.tr("Low Clipped"), self.tr("High Clipped")
128
131
  ])
129
132
 
130
133
  # Let it grow/shrink with the splitter
@@ -159,31 +162,31 @@ class HistogramDialog(QDialog):
159
162
  self.zoom_slider.setTickPosition(QSlider.TickPosition.TicksBelow)
160
163
  self.zoom_slider.valueChanged.connect(self._on_zoom_changed)
161
164
 
162
- ctl.addWidget(QLabel("Zoom:"))
165
+ ctl.addWidget(QLabel(self.tr("Zoom:")))
163
166
  ctl.addWidget(self.zoom_slider)
164
167
 
165
- self.btn_logx = QPushButton("Toggle Log X-Axis", self)
168
+ self.btn_logx = QPushButton(self.tr("Toggle Log X-Axis"), self)
166
169
  self.btn_logx.setCheckable(True)
167
170
  self.btn_logx.toggled.connect(self._toggle_log_x)
168
171
  ctl.addWidget(self.btn_logx)
169
172
 
170
- self.btn_logy = QPushButton("Toggle Log Y-Axis", self)
173
+ self.btn_logy = QPushButton(self.tr("Toggle Log Y-Axis"), self)
171
174
  self.btn_logy.setCheckable(True)
172
175
  self.btn_logy.toggled.connect(self._toggle_log_y)
173
176
  ctl.addWidget(self.btn_logy)
174
177
 
175
178
  self.btn_sensor_max = QToolButton(self)
176
179
  self.btn_sensor_max.setText("?")
177
- self.btn_sensor_max.setToolTip(
180
+ self.btn_sensor_max.setToolTip(self.tr(
178
181
  "Set your camera's true saturation level for clipping warnings.\n"
179
182
  "Tip: take an overexposed frame and see its max ADU."
180
- )
183
+ ))
181
184
  self.btn_sensor_max.clicked.connect(self._prompt_sensor_max)
182
185
  ctl.addWidget(self.btn_sensor_max)
183
186
 
184
187
  main_layout.addLayout(ctl)
185
188
 
186
- btn_close = QPushButton("Close", self)
189
+ btn_close = QPushButton(self.tr("Close"), self)
187
190
  btn_close.clicked.connect(self.accept)
188
191
  main_layout.addWidget(btn_close)
189
192
 
@@ -408,7 +411,7 @@ class HistogramDialog(QDialog):
408
411
  p.setPen(QPen(QColor(220, 0, 0), 2, Qt.PenStyle.DashLine))
409
412
  p.drawLine(x, top_margin, x, axis_y)
410
413
  p.drawText(min(x + 4, width - 80), top_margin + 12,
411
- f"True Max {self.sensor_max01:.4f}")
414
+ self.tr("True Max {0:.4f}").format(self.sensor_max01))
412
415
  # store mapping info for Ctrl+click → normalized x
413
416
  try:
414
417
  self._click_mapping = {
@@ -567,13 +570,13 @@ class HistogramDialog(QDialog):
567
570
  eps = 1e-6 # tolerance for "exactly 0/1" after float ops
568
571
 
569
572
  row_defs = [
570
- ("Min", lambda c: float(np.min(c)), "{:.4f}"),
571
- ("Max", lambda c: float(np.max(c)), "{:.4f}"),
572
- ("Median", lambda c: float(np.median(c)), "{:.4f}"),
573
- ("StdDev", lambda c: float(np.std(c)), "{:.4f}"),
574
- ("MAD", lambda c: float(np.median(np.abs(c - np.median(c)))), "{:.4f}"),
575
- ("Low Clipped", lambda c: _clip_fmt(c, low=True, eps=eps), "{}"),
576
- ("High Clipped", lambda c: _clip_fmt(c, low=False, eps=eps), "{}"),
573
+ (self.tr("Min"), lambda c: float(np.min(c)), "{:.4f}"),
574
+ (self.tr("Max"), lambda c: float(np.max(c)), "{:.4f}"),
575
+ (self.tr("Median"), lambda c: float(np.median(c)), "{:.4f}"),
576
+ (self.tr("StdDev"), lambda c: float(np.std(c)), "{:.4f}"),
577
+ (self.tr("MAD"), lambda c: float(np.median(np.abs(c - np.median(c)))), "{:.4f}"),
578
+ (self.tr("Low Clipped"), lambda c: _clip_fmt(c, low=True, eps=eps), "{}"),
579
+ (self.tr("High Clipped"), lambda c: _clip_fmt(c, low=False, eps=eps), "{}"),
577
580
  ]
578
581
 
579
582
  def _clip_fmt(c, low: bool, eps: float):
@@ -600,7 +603,7 @@ class HistogramDialog(QDialog):
600
603
  it.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
601
604
 
602
605
  # --- visual pop for non-trivial clipping ---
603
- if lab in ("Low Clipped", "High Clipped"):
606
+ if lab in (self.tr("Low Clipped"), self.tr("High Clipped")):
604
607
  # text looks like: "123 (0.456%)"
605
608
  try:
606
609
  pct_str = text.split("(")[1].split("%")[0]
@@ -674,11 +677,11 @@ class HistogramDialog(QDialog):
674
677
 
675
678
  val, ok = QInputDialog.getInt(
676
679
  self,
677
- "Sensor True Max (ADU)",
678
- f"Enter your sensor's true saturation value in native ADU.\n"
679
- f"(Typical max for this file type is {self.native_theoretical_max})\n\n"
680
+ self.tr("Sensor True Max (ADU)"),
681
+ self.tr("Enter your sensor's true saturation value in native ADU.\n"
682
+ "(Typical max for this file type is {0})\n\n"
680
683
  "You can measure this by taking a deliberately overexposed frame\n"
681
- "and reading its maximum pixel value.",
684
+ "and reading its maximum pixel value.").format(self.native_theoretical_max),
682
685
  int(current),
683
686
  1,
684
687
  int(self.native_theoretical_max)
@@ -690,8 +693,8 @@ class HistogramDialog(QDialog):
690
693
  # float images / unknown depth: allow normalized max
691
694
  val, ok = QInputDialog.getDouble(
692
695
  self,
693
- "Histogram Effective Max",
694
- "Enter effective maximum for clipping (normalized units).",
696
+ self.tr("Histogram Effective Max"),
697
+ self.tr("Enter effective maximum for clipping (normalized units)."),
695
698
  float(self.sensor_max01),
696
699
  1e-6,
697
700
  1.0,
@@ -436,6 +436,8 @@ class HistoryExplorerDialog(QDialog):
436
436
  def __init__(self, document, parent=None):
437
437
  super().__init__(parent)
438
438
  self.setWindowTitle("History Explorer")
439
+ self.setWindowFlag(Qt.WindowType.Window, True)
440
+ self.setWindowModality(Qt.WindowModality.NonModal)
439
441
  self.setModal(False)
440
442
  self.doc = document
441
443