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
@@ -60,7 +60,7 @@ class _AstrobinIdDelegate(QStyledItemDelegate):
60
60
 
61
61
  def createEditor(self, parent, option, index):
62
62
  editor = QLineEdit(parent)
63
- editor.setPlaceholderText("e.g. 4408")
63
+ editor.setPlaceholderText(self.tr("e.g. 4408"))
64
64
  editor.setValidator(QIntValidator(1, 999_999_999, editor))
65
65
  if self._completer is not None:
66
66
  editor.setCompleter(self._completer)
@@ -83,7 +83,7 @@ class FilterIdDialog(QDialog):
83
83
  current_map: Optional[Dict[str, str]] = None,
84
84
  offline_csv_default: Optional[str] = None):
85
85
  super().__init__(parent)
86
- self.setWindowTitle("AstroBin Filter IDs")
86
+ self.setWindowTitle(self.tr("AstroBin Filter IDs"))
87
87
  self.settings = settings
88
88
  self._offline_csv_default = offline_csv_default
89
89
  base_names = sorted({f for f in (filters_in_data or []) if f and f != "Unknown"}, key=str.lower)
@@ -96,14 +96,14 @@ class FilterIdDialog(QDialog):
96
96
 
97
97
  # Help row
98
98
  help_row = QHBoxLayout()
99
- help_label = QLabel("Edit filter names and their AstroBin numeric IDs.")
99
+ help_label = QLabel(self.tr("Edit filter names and their AstroBin numeric IDs."))
100
100
  help_btn = QToolButton(self)
101
101
  help_btn.setText("?")
102
- help_btn.setToolTip("Open AstroBin Equipment Explorer (Filters)")
102
+ help_btn.setToolTip(self.tr("Open AstroBin Equipment Explorer (Filters)"))
103
103
  help_btn.clicked.connect(lambda: webbrowser.open(ASTROBIN_FILTER_URL))
104
104
 
105
105
  self.load_db_btn = QPushButton(self)
106
- self.load_db_btn.setToolTip("Search or load the offline filters database.")
106
+ self.load_db_btn.setToolTip(self.tr("Search or load the offline filters database."))
107
107
  self.load_db_btn.clicked.connect(self._on_offline_action)
108
108
 
109
109
  help_row.addWidget(help_label)
@@ -115,7 +115,7 @@ class FilterIdDialog(QDialog):
115
115
  # Table
116
116
  self.table = QTableWidget(self)
117
117
  self.table.setColumnCount(2)
118
- self.table.setHorizontalHeaderLabels(["Filter name", "AstroBin ID"])
118
+ self.table.setHorizontalHeaderLabels([self.tr("Filter name"), self.tr("AstroBin ID")])
119
119
  self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
120
120
  self.table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
121
121
  self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
@@ -154,8 +154,8 @@ class FilterIdDialog(QDialog):
154
154
 
155
155
  # Row actions
156
156
  row_actions = QHBoxLayout()
157
- self.btn_add = QPushButton("Add row"); self.btn_add.clicked.connect(self._add_row)
158
- self.btn_del = QPushButton("Delete selected"); self.btn_del.clicked.connect(self._delete_selected_rows)
157
+ self.btn_add = QPushButton(self.tr("Add row")); self.btn_add.clicked.connect(self._add_row)
158
+ self.btn_del = QPushButton(self.tr("Delete selected")); self.btn_del.clicked.connect(self._delete_selected_rows)
159
159
  row_actions.addWidget(self.btn_add); row_actions.addWidget(self.btn_del); row_actions.addStretch(1)
160
160
  root.addLayout(row_actions)
161
161
 
@@ -238,7 +238,7 @@ class FilterIdDialog(QDialog):
238
238
  return None
239
239
 
240
240
  def _update_offline_button_text(self):
241
- self.load_db_btn.setText("Search offline DB…" if getattr(self, "_offline_rows", None) else "Load offline DB…")
241
+ self.load_db_btn.setText(self.tr("Search offline DB…") if getattr(self, "_offline_rows", None) else self.tr("Load offline DB…"))
242
242
 
243
243
  def _on_offline_action(self):
244
244
  if getattr(self, "_offline_rows", None):
@@ -250,7 +250,7 @@ class FilterIdDialog(QDialog):
250
250
  self._update_offline_button_text()
251
251
 
252
252
  def _browse_offline_db(self):
253
- path, _ = QFileDialog.getOpenFileName(self, "Select AstroBin Filters CSV", "", "CSV files (*.csv);;All files (*)")
253
+ path, _ = QFileDialog.getOpenFileName(self, self.tr("Select AstroBin Filters CSV"), "", self.tr("CSV files (*.csv);;All files (*)"))
254
254
  if not path:
255
255
  return
256
256
  self._load_offline_db(path)
@@ -302,16 +302,16 @@ class FilterIdDialog(QDialog):
302
302
 
303
303
  def _open_offline_search(self):
304
304
  if not getattr(self, "_offline_rows", None):
305
- QMessageBox.information(self, "No DB", "Offline filters database not loaded yet.")
305
+ QMessageBox.information(self, self.tr("No DB"), self.tr("Offline filters database not loaded yet."))
306
306
  return
307
307
 
308
- dlg = QDialog(self); dlg.setWindowTitle("Search AstroBin Filters (offline)")
308
+ dlg = QDialog(self); dlg.setWindowTitle(self.tr("Search AstroBin Filters (offline)"))
309
309
  v = QVBoxLayout(dlg)
310
- q = QLineEdit(dlg); q.setPlaceholderText("Search ID, brand, or name…")
310
+ q = QLineEdit(dlg); q.setPlaceholderText(self.tr("Search ID, brand, or name…"))
311
311
  v.addWidget(q)
312
312
 
313
313
  tbl = QTableWidget(dlg); tbl.setColumnCount(3)
314
- tbl.setHorizontalHeaderLabels(["ID", "Brand", "Name"])
314
+ tbl.setHorizontalHeaderLabels([self.tr("ID"), self.tr("Brand"), self.tr("Name")])
315
315
  hdr = tbl.horizontalHeader()
316
316
  hdr.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
317
317
  hdr.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
@@ -386,20 +386,20 @@ class AstrobinExportTab(QWidget):
386
386
 
387
387
  # LEFT
388
388
  left = QWidget(self); lyt = QVBoxLayout(left)
389
- self.info_lbl = QLabel("Load FITS via 'Select Folder…' or 'Add Files…' to begin.")
389
+ self.info_lbl = QLabel(self.tr("Load FITS via 'Select Folder…' or 'Add Files…' to begin."))
390
390
  lyt.addWidget(self.info_lbl)
391
391
 
392
392
  btn_row = QHBoxLayout()
393
- self.btn_open = QPushButton("Select Folder…"); self.btn_open.clicked.connect(self.open_directory)
394
- self.btn_add_files = QPushButton("Add Files…"); self.btn_add_files.clicked.connect(self.open_files)
395
- self.btn_clear = QPushButton("Clear"); self.btn_clear.clicked.connect(self.clear_images)
393
+ self.btn_open = QPushButton(self.tr("Select Folder…")); self.btn_open.clicked.connect(self.open_directory)
394
+ self.btn_add_files = QPushButton(self.tr("Add Files…")); self.btn_add_files.clicked.connect(self.open_files)
395
+ self.btn_clear = QPushButton(self.tr("Clear")); self.btn_clear.clicked.connect(self.clear_images)
396
396
  btn_row.addWidget(self.btn_open); btn_row.addWidget(self.btn_add_files); btn_row.addWidget(self.btn_clear)
397
397
  btn_row.addStretch(1)
398
398
  lyt.addLayout(btn_row)
399
399
 
400
400
  self.tree = QTreeWidget(self)
401
401
  self.tree.setColumnCount(1)
402
- self.tree.setHeaderLabels(["Files (Object → Filter → Exposure)"])
402
+ self.tree.setHeaderLabels([self.tr("Files (Object → Filter → Exposure)")])
403
403
  hdr = self.tree.header()
404
404
  hdr.setSectionResizeMode(0, QHeaderView.ResizeMode.Interactive)
405
405
  hdr.setStretchLastSection(False)
@@ -415,50 +415,50 @@ class AstrobinExportTab(QWidget):
415
415
  # RIGHT
416
416
  right = QWidget(self); rlyt = QVBoxLayout(right)
417
417
 
418
- form_box = QGroupBox("Global inputs (used if FITS headers are missing/zero)")
418
+ form_box = QGroupBox(self.tr("Global inputs (used if FITS headers are missing/zero)"))
419
419
  grid = QGridLayout(form_box)
420
420
 
421
421
  # row 0
422
- grid.addWidget(QLabel("f/number"), 0, 0)
423
- self.fnum_edit = QLineEdit(self); self.fnum_edit.setPlaceholderText("e.g. 4.0")
422
+ grid.addWidget(QLabel(self.tr("f/number")), 0, 0)
423
+ self.fnum_edit = QLineEdit(self); self.fnum_edit.setPlaceholderText(self.tr("e.g. 4.0"))
424
424
  self.fnum_edit.setValidator(QDoubleValidator(0.0, 999.0, 2, self)); self.fnum_edit.textChanged.connect(self._recompute)
425
425
  grid.addWidget(self.fnum_edit, 0, 1)
426
426
 
427
- grid.addWidget(QLabel("Darks (#)"), 0, 2)
427
+ grid.addWidget(QLabel(self.tr("Darks (#)")), 0, 2)
428
428
  self.darks_edit = QLineEdit(self); self._setup_int_line(self.darks_edit, 0, 999999)
429
429
  grid.addWidget(self.darks_edit, 0, 3)
430
430
 
431
- grid.addWidget(QLabel("Flats (#)"), 0, 4)
431
+ grid.addWidget(QLabel(self.tr("Flats (#)")), 0, 4)
432
432
  self.flats_edit = QLineEdit(self); self._setup_int_line(self.flats_edit, 0, 999999)
433
433
  grid.addWidget(self.flats_edit, 0, 5)
434
434
 
435
435
  # row 1
436
- grid.addWidget(QLabel("Flat-darks (#)"), 1, 0)
436
+ grid.addWidget(QLabel(self.tr("Flat-darks (#)")), 1, 0)
437
437
  self.flatdarks_edit = QLineEdit(self); self._setup_int_line(self.flatdarks_edit, 0, 999999)
438
438
  grid.addWidget(self.flatdarks_edit, 1, 1)
439
439
 
440
- grid.addWidget(QLabel("Bias (#)"), 1, 2)
440
+ grid.addWidget(QLabel(self.tr("Bias (#)")), 1, 2)
441
441
  self.bias_edit = QLineEdit(self); self._setup_int_line(self.bias_edit, 0, 999999)
442
442
  grid.addWidget(self.bias_edit, 1, 3)
443
443
 
444
- grid.addWidget(QLabel("Bortle"), 1, 4)
445
- self.bortle_edit = QLineEdit(self); self.bortle_edit.setPlaceholderText("0–9")
444
+ grid.addWidget(QLabel(self.tr("Bortle")), 1, 4)
445
+ self.bortle_edit = QLineEdit(self); self.bortle_edit.setPlaceholderText(self.tr("0–9"))
446
446
  self.bortle_edit.setValidator(QIntValidator(0, 9, self)); self.bortle_edit.textChanged.connect(self._recompute)
447
447
  grid.addWidget(self.bortle_edit, 1, 5)
448
448
 
449
449
  # row 2
450
- grid.addWidget(QLabel("Mean SQM"), 2, 0)
451
- self.mean_sqm_edit = QLineEdit(self); self.mean_sqm_edit.setPlaceholderText("e.g. 21.30")
450
+ grid.addWidget(QLabel(self.tr("Mean SQM")), 2, 0)
451
+ self.mean_sqm_edit = QLineEdit(self); self.mean_sqm_edit.setPlaceholderText(self.tr("e.g. 21.30"))
452
452
  self.mean_sqm_edit.setValidator(QDoubleValidator(0.0, 25.0, 2, self)); self.mean_sqm_edit.textChanged.connect(self._recompute)
453
453
  grid.addWidget(self.mean_sqm_edit, 2, 1)
454
454
 
455
- grid.addWidget(QLabel("Mean FWHM"), 2, 2)
456
- self.mean_fwhm_edit = QLineEdit(self); self.mean_fwhm_edit.setPlaceholderText("e.g. 2.10")
455
+ grid.addWidget(QLabel(self.tr("Mean FWHM")), 2, 2)
456
+ self.mean_fwhm_edit = QLineEdit(self); self.mean_fwhm_edit.setPlaceholderText(self.tr("e.g. 2.10"))
457
457
  self.mean_fwhm_edit.setValidator(QDoubleValidator(0.0, 50.0, 2, self)); self.mean_fwhm_edit.textChanged.connect(self._recompute)
458
458
  grid.addWidget(self.mean_fwhm_edit, 2, 3)
459
459
 
460
- self.noon_cb = QCheckBox("Group nights noon → noon (local time)")
461
- self.noon_cb.setToolTip("Prevents splitting a single observing night at midnight.")
460
+ self.noon_cb = QCheckBox(self.tr("Group nights noon → noon (local time)"))
461
+ self.noon_cb.setToolTip(self.tr("Prevents splitting a single observing night at midnight."))
462
462
  self.noon_cb.setChecked(self.settings.value("astrobin_exporter/noon_to_noon", True, type=bool))
463
463
  self.noon_cb.toggled.connect(self._recompute)
464
464
  grid.addWidget(self.noon_cb, 2, 4, 1, 2)
@@ -468,10 +468,10 @@ class AstrobinExportTab(QWidget):
468
468
  # Filter mapping row
469
469
  map_row = QHBoxLayout()
470
470
  self.filter_summary = QLabel(self._filters_summary_text()); map_row.addWidget(self.filter_summary)
471
- self.btn_edit_filters = QPushButton("Manage Filter IDs…"); self.btn_edit_filters.clicked.connect(self._edit_filters)
471
+ self.btn_edit_filters = QPushButton(self.tr("Manage Filter IDs…")); self.btn_edit_filters.clicked.connect(self._edit_filters)
472
472
  map_row.addWidget(self.btn_edit_filters)
473
473
  qmark = QToolButton(self); qmark.setText("?")
474
- qmark.setToolTip("Open AstroBin Equipment Explorer (Filters)")
474
+ qmark.setToolTip(self.tr("Open AstroBin Equipment Explorer (Filters)"))
475
475
  qmark.clicked.connect(lambda: webbrowser.open(ASTROBIN_FILTER_URL))
476
476
  map_row.addWidget(qmark); map_row.addStretch(1)
477
477
  map_wrap = QWidget(self); map_wrap.setLayout(map_row)
@@ -481,8 +481,8 @@ class AstrobinExportTab(QWidget):
481
481
 
482
482
  # Aggregated table
483
483
  self.table = QTableWidget(self)
484
- cols = ['date','filter','number','duration','gain','iso','binning','sensorCooling',
485
- 'fNumber','darks','flats','flatDarks','bias','bortle','meanSqm','meanFwhm','temperature']
484
+ cols = [self.tr('date'), self.tr('filter'), self.tr('number'), self.tr('duration'), self.tr('gain'), self.tr('iso'), self.tr('binning'), self.tr('sensorCooling'),
485
+ self.tr('fNumber'), self.tr('darks'), self.tr('flats'), self.tr('flatDarks'), self.tr('bias'), self.tr('bortle'), self.tr('meanSqm'), self.tr('meanFwhm'), self.tr('temperature')]
486
486
  self.table.setColumnCount(len(cols)); self.table.setHorizontalHeaderLabels(cols)
487
487
  hdr_tbl = self.table.horizontalHeader()
488
488
  hdr_tbl.setSectionResizeMode(QHeaderView.ResizeMode.Interactive)
@@ -499,8 +499,8 @@ class AstrobinExportTab(QWidget):
499
499
 
500
500
  # Actions
501
501
  act_row = QHBoxLayout()
502
- self.btn_refresh = QPushButton("Recompute"); self.btn_refresh.clicked.connect(self._recompute)
503
- self.btn_copy_csv = QPushButton("Copy CSV"); self.btn_copy_csv.clicked.connect(self._copy_csv_to_clipboard)
502
+ self.btn_refresh = QPushButton(self.tr("Recompute")); self.btn_refresh.clicked.connect(self._recompute)
503
+ self.btn_copy_csv = QPushButton(self.tr("Copy CSV")); self.btn_copy_csv.clicked.connect(self._copy_csv_to_clipboard)
504
504
  act_row.addWidget(self.btn_refresh); act_row.addWidget(self.btn_copy_csv); act_row.addStretch(1)
505
505
  rlyt.addLayout(act_row)
506
506
 
@@ -508,7 +508,7 @@ class AstrobinExportTab(QWidget):
508
508
  splitter.setSizes([360, 680])
509
509
  self.setLayout(root)
510
510
 
511
- self.info_lbl.setText("Load FITS/XISF via 'Select Folder…' or 'Add Files…' to begin.")
511
+ self.info_lbl.setText(self.tr("Load FITS/XISF via 'Select Folder…' or 'Add Files…' to begin."))
512
512
 
513
513
  def _setup_int_line(self, line: QLineEdit, lo: int, hi: int):
514
514
  line.setValidator(QIntValidator(lo, hi, self))
@@ -558,11 +558,11 @@ class AstrobinExportTab(QWidget):
558
558
  self.file_paths.clear(); self.records.clear(); self.rows.clear()
559
559
  self.tree.blockSignals(True); self.tree.clear(); self.tree.blockSignals(False)
560
560
  self.table.setRowCount(0); self.csv_view.clear()
561
- self.info_lbl.setText("Cleared. Load FITS via 'Select Folder…' or 'Add Files…' to begin.")
561
+ self.info_lbl.setText(self.tr("Cleared. Load FITS via 'Select Folder…' or 'Add Files…' to begin."))
562
562
 
563
563
  def open_directory(self):
564
564
  start = self._get_last_dir() or ""
565
- directory = QFileDialog.getExistingDirectory(self, "Select Folder Containing FITS/XISF Files", start)
565
+ directory = QFileDialog.getExistingDirectory(self, self.tr("Select Folder Containing FITS/XISF Files"), start)
566
566
  if not directory: return
567
567
  self._save_last_dir(directory)
568
568
 
@@ -573,7 +573,7 @@ class AstrobinExportTab(QWidget):
573
573
  paths.append(os.path.join(root, fn))
574
574
  paths.sort(key=self._natural_key)
575
575
  if not paths:
576
- QMessageBox.information(self, "No Images", "No .fit/.fits/.xisf files found.")
576
+ QMessageBox.information(self, self.tr("No Images"), self.tr("No .fit/.fits/.xisf files found."))
577
577
  return
578
578
  self.file_paths = paths
579
579
  self._read_headers(); self._build_tree(); self._recompute()
@@ -581,14 +581,14 @@ class AstrobinExportTab(QWidget):
581
581
  def open_files(self):
582
582
  start = self._get_last_dir() or ""
583
583
  paths, _ = QFileDialog.getOpenFileNames(
584
- self, "Select FITS/XISF Files", start, "FITS/XISF (*.fit *.fits *.xisf);;All Files (*)"
584
+ self, self.tr("Select FITS/XISF Files"), start, self.tr("FITS/XISF (*.fit *.fits *.xisf);;All Files (*)")
585
585
  )
586
586
  if not paths: return
587
587
  self._save_last_dir(os.path.dirname(paths[0]))
588
588
 
589
589
  new_paths = [p for p in paths if p not in self.file_paths]
590
590
  if not new_paths:
591
- QMessageBox.information(self, "No New Files", "All selected files are already in the list.")
591
+ QMessageBox.information(self, self.tr("No New Files"), self.tr("All selected files are already in the list."))
592
592
  return
593
593
 
594
594
  self.file_paths = sorted(set(self.file_paths + new_paths), key=self._natural_key)
@@ -709,9 +709,9 @@ class AstrobinExportTab(QWidget):
709
709
  print(f"[WARN] Failed to read {fp}: {e}")
710
710
  bad += 1
711
711
 
712
- msg = f"Loaded {ok} file(s)"
713
- if bad: msg += f" ({bad} failed)"
714
- if skipped_xisf: msg += f" — skipped {skipped_xisf} XISF (reader unavailable)"
712
+ msg = self.tr("Loaded {0} file(s)").format(ok)
713
+ if bad: msg += self.tr(" ({0} failed)").format(bad)
714
+ if skipped_xisf: msg += self.tr(" — skipped {0} XISF (reader unavailable)").format(skipped_xisf)
715
715
  self.info_lbl.setText(msg + ".")
716
716
 
717
717
  # ---------- helpers ----------
@@ -775,19 +775,19 @@ class AstrobinExportTab(QWidget):
775
775
  by_obj[obj][filt][exp] = lst
776
776
 
777
777
  for obj in sorted(by_obj, key=str.lower):
778
- obj_item = QTreeWidgetItem([f"Object: {obj}"])
778
+ obj_item = QTreeWidgetItem([self.tr("Object: {0}").format(obj)])
779
779
  obj_item.setFlags(obj_item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
780
780
  obj_item.setCheckState(0, Qt.CheckState.Checked)
781
781
  self.tree.addTopLevelItem(obj_item); obj_item.setExpanded(True)
782
782
 
783
783
  for filt in sorted(by_obj[obj], key=str.lower):
784
- filt_item = QTreeWidgetItem([f"Filter: {filt}"])
784
+ filt_item = QTreeWidgetItem([self.tr("Filter: {0}").format(filt)])
785
785
  filt_item.setFlags(filt_item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
786
786
  filt_item.setCheckState(0, Qt.CheckState.Checked)
787
787
  obj_item.addChild(filt_item); filt_item.setExpanded(True)
788
788
 
789
789
  for exp in sorted(by_obj[obj][filt].keys(), key=lambda e: str(e)):
790
- exp_item = QTreeWidgetItem([f"Exposure: {exp}"])
790
+ exp_item = QTreeWidgetItem([self.tr("Exposure: {0}").format(exp)])
791
791
  exp_item.setData(0, Qt.ItemDataRole.UserRole, float(exp))
792
792
  exp_item.setFlags(exp_item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
793
793
  exp_item.setCheckState(0, Qt.CheckState.Checked)
@@ -915,8 +915,8 @@ class AstrobinExportTab(QWidget):
915
915
  self._refresh_table(); self._refresh_csv_text()
916
916
 
917
917
  def _refresh_table(self):
918
- cols = ['date','filter','number','duration','gain','iso','binning','sensorCooling',
919
- 'fNumber','darks','flats','flatDarks','bias','bortle','meanSqm','meanFwhm','temperature']
918
+ cols = [self.tr('date'), self.tr('filter'), self.tr('number'), self.tr('duration'), self.tr('gain'), self.tr('iso'), self.tr('binning'), self.tr('sensorCooling'),
919
+ self.tr('fNumber'), self.tr('darks'), self.tr('flats'), self.tr('flatDarks'), self.tr('bias'), self.tr('bortle'), self.tr('meanSqm'), self.tr('meanFwhm'), self.tr('temperature')]
920
920
  self.table.setRowCount(len(self.rows))
921
921
  self.table.setColumnCount(len(cols))
922
922
  self.table.setHorizontalHeaderLabels(cols)
@@ -945,10 +945,10 @@ class AstrobinExportTab(QWidget):
945
945
  def _copy_csv_to_clipboard(self):
946
946
  txt = self._rows_to_csv_str()
947
947
  if not txt.strip():
948
- QMessageBox.information(self, "Nothing to copy", "There is no CSV content yet.")
948
+ QMessageBox.information(self, self.tr("Nothing to copy"), self.tr("There is no CSV content yet."))
949
949
  return
950
950
  QGuiApplication.clipboard().setText(txt)
951
- QMessageBox.information(self, "Copied", "CSV copied to clipboard.")
951
+ QMessageBox.information(self, self.tr("Copied"), self.tr("CSV copied to clipboard."))
952
952
 
953
953
  # ---------- filter map ----------
954
954
  def _load_filter_map(self) -> Dict[str, str]:
@@ -972,7 +972,7 @@ class AstrobinExportTab(QWidget):
972
972
  return mapping
973
973
 
974
974
  def _filters_summary_text(self) -> str:
975
- if not self._filter_map: return "No mappings set"
975
+ if not self._filter_map: return self.tr("No mappings set")
976
976
  pairs = sorted(self._filter_map.items(), key=lambda kv: kv[0].lower())
977
977
  return ", ".join(f"{k}→{v}" for k, v in pairs)
978
978
 
@@ -996,7 +996,10 @@ class AstrobinExportTab(QWidget):
996
996
  class AstrobinExporterDialog(QDialog):
997
997
  def __init__(self, parent=None, offline_filters_csv: Optional[str] = None):
998
998
  super().__init__(parent)
999
- self.setWindowTitle("AstroBin Exporter")
999
+ self.setWindowTitle(self.tr("AstroBin Exporter"))
1000
+ self.setWindowFlag(Qt.WindowType.Window, True)
1001
+ self.setWindowModality(Qt.WindowModality.NonModal)
1002
+ self.setModal(False)
1000
1003
  self.resize(980, 640)
1001
1004
  v = QVBoxLayout(self)
1002
1005
  self.tab = AstrobinExportTab(self, offline_filters_csv=offline_filters_csv)
@@ -1040,7 +1040,9 @@ class AstroSpikeWindow(QDialog):
1040
1040
  def __init__(self, image_data_255: np.ndarray, image_data_float: np.ndarray, ctx):
1041
1041
  super().__init__()
1042
1042
  self.setWindowTitle("AstroSpike - Star Diffraction Spikes")
1043
- self.setModal(True)
1043
+ self.setWindowFlag(Qt.WindowType.Window, True)
1044
+ self.setWindowModality(Qt.WindowModality.NonModal)
1045
+ self.setModal(False)
1044
1046
 
1045
1047
  self.ctx = ctx
1046
1048
  self.image_data = image_data_255 # uint8 for detection
@@ -180,8 +180,10 @@ def autostretch(
180
180
  lut = _compute_lut_from_sample(lum, target_median, sigma, maxv)
181
181
 
182
182
  out = np.empty_like(u, dtype=np.float32)
183
- for c in range(min(3, C)):
184
- out[..., c] = lut[u[..., c]]
183
+ # Vectorized LUT application: apply to all RGB channels at once
184
+ # lut is 1D array of float32; u[..., :3] selects RGB indices
185
+ out[..., :3] = lut[u[..., :3]]
186
+
185
187
  if C > 3: # pass-through non-RGB channels
186
188
  out[..., 3:] = u[..., 3:] / float(maxv)
187
189
  return out
@@ -42,12 +42,23 @@ def background_neutralize_rgb(img: np.ndarray, rect_xywh: tuple[int, int, int, i
42
42
 
43
43
  out = img.copy()
44
44
  eps = 1e-8
45
- for c in range(3):
46
- diff = float(medians[c] - avg_med)
47
- denom = 1.0 - diff
48
- if abs(denom) < eps:
49
- denom = eps if denom >= 0 else -eps
50
- out[..., c] = np.clip((out[..., c] - diff) / denom, 0.0, 1.0)
45
+
46
+ # Vectorized neutralization
47
+ # diff shape: (3,) -> (1, 1, 3)
48
+ diffs = (medians - avg_med).reshape(1, 1, 3)
49
+
50
+ # denom shape: (1, 1, 3)
51
+ denoms = 1.0 - diffs
52
+
53
+ # Avoid div-by-zero (vectorized)
54
+ # logic: if abs(denom) < eps, set to eps (sign matched)
55
+ # We can do this efficiently:
56
+ small_mask = np.abs(denoms) < eps
57
+ denoms[small_mask] = np.where(denoms[small_mask] >= 0, eps, -eps)
58
+
59
+ # Apply formula: (pixel - diff) / denom
60
+ out = (out - diffs) / denoms
61
+ out = np.clip(out, 0.0, 1.0)
51
62
 
52
63
  return out.astype(np.float32, copy=False)
53
64
 
@@ -237,14 +248,21 @@ def apply_background_neutral_to_doc(doc, preset: dict | None = None):
237
248
  class BackgroundNeutralizationDialog(QDialog):
238
249
  def __init__(self, parent, doc, icon: QIcon | None = None):
239
250
  super().__init__(parent)
251
+ self._main = parent
240
252
  self.doc = doc
253
+
254
+ # Connect to active document change signal
255
+ if hasattr(self._main, "currentDocumentChanged"):
256
+ self._main.currentDocumentChanged.connect(self._on_active_doc_changed)
257
+
241
258
  if icon:
242
259
  self.setWindowIcon(icon)
243
- self.setWindowTitle("Background Neutralization")
260
+ self.setWindowTitle(self.tr("Background Neutralization"))
244
261
  self.resize(900, 600)
245
262
 
246
263
  self.setWindowFlag(Qt.WindowType.Window, True)
247
- self.setWindowModality(Qt.WindowModality.ApplicationModal)
264
+ # Non-modal: allow user to switch between images while dialog is open
265
+ self.setWindowModality(Qt.WindowModality.NonModal)
248
266
  self.setModal(False)
249
267
  #self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
250
268
 
@@ -271,10 +289,10 @@ class BackgroundNeutralizationDialog(QDialog):
271
289
 
272
290
  # Buttons row
273
291
  btn_row = QHBoxLayout()
274
- self.btn_apply = QPushButton("Apply Neutralization")
275
- self.btn_cancel = QPushButton("Cancel")
276
- self.btn_toggle_stretch = QPushButton("Enable Auto-Stretch")
277
- self.btn_find_bg = QPushButton("Find Background")
292
+ self.btn_apply = QPushButton(self.tr("Apply Neutralization"))
293
+ self.btn_cancel = QPushButton(self.tr("Cancel"))
294
+ self.btn_toggle_stretch = QPushButton(self.tr("Enable Auto-Stretch"))
295
+ self.btn_find_bg = QPushButton(self.tr("Find Background"))
278
296
  btn_row.addWidget(self.btn_apply)
279
297
  btn_row.addWidget(self.btn_cancel)
280
298
  btn_row.addWidget(self.btn_toggle_stretch)
@@ -313,7 +331,14 @@ class BackgroundNeutralizationDialog(QDialog):
313
331
 
314
332
  self._load_image()
315
333
 
316
-
334
+ # ---- active document change ------------------------------------
335
+ def _on_active_doc_changed(self, doc):
336
+ """Called when user clicks a different image window."""
337
+ if doc is None or getattr(doc, "image", None) is None:
338
+ return
339
+ self.doc = doc
340
+ self.selection_item = None
341
+ self._load_image()
317
342
 
318
343
  # ---------- image display ----------
319
344
  def _doc_image_normalized(self) -> np.ndarray:
@@ -509,9 +534,35 @@ class BackgroundNeutralizationDialog(QDialog):
509
534
  metadata=meta,
510
535
  step_name="Background Neutralization",
511
536
  )
512
- self.accept()
537
+ # Dialog stays open so user can apply to other images
538
+ # Refresh to use the now-active document for next operation
539
+ self.accept() # or: self.close()
540
+
541
+ def closeEvent(self, e):
542
+ try:
543
+ if hasattr(self._main, "currentDocumentChanged"):
544
+ self._main.currentDocumentChanged.disconnect(self._on_active_doc_changed)
545
+ except Exception:
546
+ pass
547
+ super().closeEvent(e)
513
548
 
514
549
 
550
+ def _refresh_document_from_active(self):
551
+ """
552
+ Refresh the dialog's document reference to the currently active document.
553
+ This allows reusing the same dialog on different images.
554
+ """
555
+ try:
556
+ main = self.parent()
557
+ if main and hasattr(main, "_active_doc"):
558
+ new_doc = main._active_doc()
559
+ if new_doc is not None and new_doc is not self.doc:
560
+ self.doc = new_doc
561
+ # Refresh the preview image
562
+ self._load_preview()
563
+ except Exception:
564
+ pass
565
+
515
566
  def _zoom(self, factor: float):
516
567
  self._user_zoomed = True
517
568
  cur = self.graphics_view.transform().m11()
@@ -168,7 +168,10 @@ class _BatchWorker(QThread):
168
168
  class BatchConvertDialog(QDialog):
169
169
  def __init__(self, parent=None):
170
170
  super().__init__(parent)
171
- self.setWindowTitle("Batch Convert")
171
+ self.setWindowTitle(self.tr("Batch Convert"))
172
+ self.setWindowFlag(Qt.WindowType.Window, True)
173
+ self.setWindowModality(Qt.WindowModality.NonModal)
174
+ self.setModal(False)
172
175
  self.setMinimumWidth(560)
173
176
  self.worker: _BatchWorker | None = None
174
177
 
@@ -208,10 +211,10 @@ class BatchConvertDialog(QDialog):
208
211
  self.depth.addItem("Auto") # always first
209
212
  # choices will be populated based on fmt
210
213
 
211
- fmt_row.addWidget(QLabel("Output format:"))
214
+ fmt_row.addWidget(QLabel(self.tr("Output format:")))
212
215
  fmt_row.addWidget(self.fmt)
213
216
  fmt_row.addSpacing(16)
214
- fmt_row.addWidget(QLabel("Bit depth:"))
217
+ fmt_row.addWidget(QLabel(self.tr("Bit depth:")))
215
218
  fmt_row.addWidget(self.depth)
216
219
  fmt_row.addStretch(1)
217
220
  lay.addLayout(fmt_row)
@@ -222,8 +225,8 @@ class BatchConvertDialog(QDialog):
222
225
  self.bar.setRange(0, 100)
223
226
 
224
227
  # buttons
225
- self.start_btn = QPushButton("Start")
226
- self.cancel_btn = QPushButton("Cancel")
228
+ self.start_btn = QPushButton(self.tr("Start"))
229
+ self.cancel_btn = QPushButton(self.tr("Cancel"))
227
230
  self.cancel_btn.setEnabled(False)
228
231
  btns = QHBoxLayout()
229
232
  btns.addStretch(1)