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.
- setiastro/data/SASP_data.fits +0 -0
- setiastro/data/catalogs/List_of_Galaxies_with_Distances_Gly.csv +488 -0
- setiastro/data/catalogs/astrobin_filters.csv +890 -0
- setiastro/data/catalogs/astrobin_filters_page1_local.csv +51 -0
- setiastro/data/catalogs/cali2.csv +63 -0
- setiastro/data/catalogs/cali2color.csv +65 -0
- setiastro/data/catalogs/celestial_catalog - original.csv +16471 -0
- setiastro/data/catalogs/celestial_catalog.csv +24031 -0
- setiastro/data/catalogs/detected_stars.csv +24784 -0
- setiastro/data/catalogs/fits_header_data.csv +46 -0
- setiastro/data/catalogs/test.csv +8 -0
- setiastro/data/catalogs/updated_celestial_catalog.csv +16471 -0
- setiastro/images/Astro_Spikes.png +0 -0
- setiastro/images/Background_startup.jpg +0 -0
- setiastro/images/HRDiagram.png +0 -0
- setiastro/images/LExtract.png +0 -0
- setiastro/images/LInsert.png +0 -0
- setiastro/images/Oxygenation-atm-2.svg.png +0 -0
- setiastro/images/RGB080604.png +0 -0
- setiastro/images/abeicon.png +0 -0
- setiastro/images/aberration.png +0 -0
- setiastro/images/andromedatry.png +0 -0
- setiastro/images/andromedatry_satellited.png +0 -0
- setiastro/images/annotated.png +0 -0
- setiastro/images/aperture.png +0 -0
- setiastro/images/astrosuite.ico +0 -0
- setiastro/images/astrosuite.png +0 -0
- setiastro/images/astrosuitepro.icns +0 -0
- setiastro/images/astrosuitepro.ico +0 -0
- setiastro/images/astrosuitepro.png +0 -0
- setiastro/images/background.png +0 -0
- setiastro/images/background2.png +0 -0
- setiastro/images/benchmark.png +0 -0
- setiastro/images/big_moon_stabilizer_timeline.png +0 -0
- setiastro/images/big_moon_stabilizer_timeline_clean.png +0 -0
- setiastro/images/blaster.png +0 -0
- setiastro/images/blink.png +0 -0
- setiastro/images/clahe.png +0 -0
- setiastro/images/collage.png +0 -0
- setiastro/images/colorwheel.png +0 -0
- setiastro/images/contsub.png +0 -0
- setiastro/images/convo.png +0 -0
- setiastro/images/copyslot.png +0 -0
- setiastro/images/cosmic.png +0 -0
- setiastro/images/cosmicsat.png +0 -0
- setiastro/images/crop1.png +0 -0
- setiastro/images/cropicon.png +0 -0
- setiastro/images/curves.png +0 -0
- setiastro/images/cvs.png +0 -0
- setiastro/images/debayer.png +0 -0
- setiastro/images/denoise_cnn_custom.png +0 -0
- setiastro/images/denoise_cnn_graph.png +0 -0
- setiastro/images/disk.png +0 -0
- setiastro/images/dse.png +0 -0
- setiastro/images/exoicon.png +0 -0
- setiastro/images/eye.png +0 -0
- setiastro/images/fliphorizontal.png +0 -0
- setiastro/images/flipvertical.png +0 -0
- setiastro/images/font.png +0 -0
- setiastro/images/freqsep.png +0 -0
- setiastro/images/functionbundle.png +0 -0
- setiastro/images/graxpert.png +0 -0
- setiastro/images/green.png +0 -0
- setiastro/images/gridicon.png +0 -0
- setiastro/images/halo.png +0 -0
- setiastro/images/hdr.png +0 -0
- setiastro/images/histogram.png +0 -0
- setiastro/images/hubble.png +0 -0
- setiastro/images/imagecombine.png +0 -0
- setiastro/images/invert.png +0 -0
- setiastro/images/isophote.png +0 -0
- setiastro/images/isophote_demo_figure.png +0 -0
- setiastro/images/isophote_demo_image.png +0 -0
- setiastro/images/isophote_demo_model.png +0 -0
- setiastro/images/isophote_demo_residual.png +0 -0
- setiastro/images/jwstpupil.png +0 -0
- setiastro/images/linearfit.png +0 -0
- setiastro/images/livestacking.png +0 -0
- setiastro/images/mask.png +0 -0
- setiastro/images/maskapply.png +0 -0
- setiastro/images/maskcreate.png +0 -0
- setiastro/images/maskremove.png +0 -0
- setiastro/images/morpho.png +0 -0
- setiastro/images/mosaic.png +0 -0
- setiastro/images/multiscale_decomp.png +0 -0
- setiastro/images/nbtorgb.png +0 -0
- setiastro/images/neutral.png +0 -0
- setiastro/images/nuke.png +0 -0
- setiastro/images/openfile.png +0 -0
- setiastro/images/pedestal.png +0 -0
- setiastro/images/pen.png +0 -0
- setiastro/images/pixelmath.png +0 -0
- setiastro/images/platesolve.png +0 -0
- setiastro/images/ppp.png +0 -0
- setiastro/images/pro.png +0 -0
- setiastro/images/project.png +0 -0
- setiastro/images/psf.png +0 -0
- setiastro/images/redo.png +0 -0
- setiastro/images/redoicon.png +0 -0
- setiastro/images/rescale.png +0 -0
- setiastro/images/rgbalign.png +0 -0
- setiastro/images/rgbcombo.png +0 -0
- setiastro/images/rgbextract.png +0 -0
- setiastro/images/rotate180.png +0 -0
- setiastro/images/rotatearbitrary.png +0 -0
- setiastro/images/rotateclockwise.png +0 -0
- setiastro/images/rotatecounterclockwise.png +0 -0
- setiastro/images/satellite.png +0 -0
- setiastro/images/script.png +0 -0
- setiastro/images/selectivecolor.png +0 -0
- setiastro/images/simbad.png +0 -0
- setiastro/images/slot0.png +0 -0
- setiastro/images/slot1.png +0 -0
- setiastro/images/slot2.png +0 -0
- setiastro/images/slot3.png +0 -0
- setiastro/images/slot4.png +0 -0
- setiastro/images/slot5.png +0 -0
- setiastro/images/slot6.png +0 -0
- setiastro/images/slot7.png +0 -0
- setiastro/images/slot8.png +0 -0
- setiastro/images/slot9.png +0 -0
- setiastro/images/spcc.png +0 -0
- setiastro/images/spin_precession_vs_lunar_distance.png +0 -0
- setiastro/images/spinner.gif +0 -0
- setiastro/images/stacking.png +0 -0
- setiastro/images/staradd.png +0 -0
- setiastro/images/staralign.png +0 -0
- setiastro/images/starnet.png +0 -0
- setiastro/images/starregistration.png +0 -0
- setiastro/images/starspike.png +0 -0
- setiastro/images/starstretch.png +0 -0
- setiastro/images/statstretch.png +0 -0
- setiastro/images/supernova.png +0 -0
- setiastro/images/uhs.png +0 -0
- setiastro/images/undoicon.png +0 -0
- setiastro/images/upscale.png +0 -0
- setiastro/images/viewbundle.png +0 -0
- setiastro/images/whitebalance.png +0 -0
- setiastro/images/wimi_icon_256x256.png +0 -0
- setiastro/images/wimilogo.png +0 -0
- setiastro/images/wims.png +0 -0
- setiastro/images/wrench_icon.png +0 -0
- setiastro/images/xisfliberator.png +0 -0
- setiastro/qml/ResourceMonitor.qml +126 -0
- setiastro/saspro/__main__.py +228 -67
- setiastro/saspro/_generated/build_info.py +2 -1
- setiastro/saspro/abe.py +76 -25
- setiastro/saspro/aberration_ai.py +14 -14
- setiastro/saspro/add_stars.py +15 -12
- setiastro/saspro/astrobin_exporter.py +61 -58
- setiastro/saspro/astrospike_python.py +3 -1
- setiastro/saspro/autostretch.py +4 -2
- setiastro/saspro/backgroundneutral.py +65 -14
- setiastro/saspro/batch_convert.py +8 -5
- setiastro/saspro/batch_renamer.py +39 -36
- setiastro/saspro/blemish_blaster.py +15 -12
- setiastro/saspro/blink_comparator_pro.py +605 -379
- setiastro/saspro/cheat_sheet.py +62 -17
- setiastro/saspro/clahe.py +34 -8
- setiastro/saspro/comet_stacking.py +103 -38
- setiastro/saspro/common_tr.py +107 -0
- setiastro/saspro/continuum_subtract.py +7 -7
- setiastro/saspro/convo.py +12 -9
- setiastro/saspro/copyastro.py +3 -0
- setiastro/saspro/cosmicclarity.py +77 -52
- setiastro/saspro/crop_dialog_pro.py +80 -45
- setiastro/saspro/curve_editor_pro.py +51 -33
- setiastro/saspro/debayer.py +6 -3
- setiastro/saspro/doc_manager.py +49 -19
- setiastro/saspro/exoplanet_detector.py +11 -11
- setiastro/saspro/fitsmodifier.py +48 -44
- setiastro/saspro/fix_bom.py +32 -0
- setiastro/saspro/frequency_separation.py +18 -12
- setiastro/saspro/function_bundle.py +18 -16
- setiastro/saspro/generate_translations.py +3092 -0
- setiastro/saspro/ghs_dialog_pro.py +19 -16
- setiastro/saspro/graxpert.py +3 -0
- setiastro/saspro/gui/main_window.py +471 -126
- setiastro/saspro/gui/mixins/dock_mixin.py +123 -11
- setiastro/saspro/gui/mixins/file_mixin.py +25 -20
- setiastro/saspro/gui/mixins/geometry_mixin.py +115 -15
- setiastro/saspro/gui/mixins/header_mixin.py +6 -6
- setiastro/saspro/gui/mixins/mask_mixin.py +8 -8
- setiastro/saspro/gui/mixins/menu_mixin.py +62 -33
- setiastro/saspro/gui/mixins/toolbar_mixin.py +382 -226
- setiastro/saspro/gui/mixins/update_mixin.py +26 -26
- setiastro/saspro/gui/statistics_dialog.py +47 -0
- setiastro/saspro/halobgon.py +29 -3
- setiastro/saspro/header_viewer.py +21 -18
- setiastro/saspro/histogram.py +29 -26
- setiastro/saspro/history_explorer.py +2 -0
- setiastro/saspro/i18n.py +168 -0
- setiastro/saspro/image_combine.py +3 -0
- setiastro/saspro/image_peeker_pro.py +52 -44
- setiastro/saspro/imageops/stretch.py +5 -13
- setiastro/saspro/isophote.py +3 -0
- setiastro/saspro/legacy/numba_utils.py +64 -47
- setiastro/saspro/linear_fit.py +3 -0
- setiastro/saspro/live_stacking.py +13 -2
- setiastro/saspro/mask_creation.py +180 -22
- setiastro/saspro/mfdeconv.py +5 -0
- setiastro/saspro/morphology.py +38 -13
- setiastro/saspro/multiscale_decomp.py +713 -256
- setiastro/saspro/nbtorgb_stars.py +12 -2
- setiastro/saspro/numba_utils.py +149 -48
- setiastro/saspro/ops/scripts.py +77 -17
- setiastro/saspro/ops/settings.py +177 -100
- setiastro/saspro/perfect_palette_picker.py +25 -7
- setiastro/saspro/pixelmath.py +114 -110
- setiastro/saspro/plate_solver.py +118 -108
- setiastro/saspro/remove_green.py +24 -7
- setiastro/saspro/remove_stars.py +136 -162
- setiastro/saspro/remove_stars_preset.py +55 -13
- setiastro/saspro/resources.py +46 -15
- setiastro/saspro/rgb_combination.py +19 -18
- setiastro/saspro/rgbalign.py +11 -11
- setiastro/saspro/save_options.py +5 -4
- setiastro/saspro/selective_color.py +84 -25
- setiastro/saspro/sfcc.py +119 -72
- setiastro/saspro/shortcuts.py +345 -36
- setiastro/saspro/signature_insert.py +4 -1
- setiastro/saspro/stacking_suite.py +2066 -1119
- setiastro/saspro/star_alignment.py +291 -331
- setiastro/saspro/star_spikes.py +137 -53
- setiastro/saspro/star_stretch.py +47 -10
- setiastro/saspro/stat_stretch.py +52 -16
- setiastro/saspro/status_log_dock.py +1 -1
- setiastro/saspro/subwindow.py +97 -36
- setiastro/saspro/supernovaasteroidhunter.py +68 -61
- setiastro/saspro/swap_manager.py +77 -42
- setiastro/saspro/translations/all_source_strings.json +4726 -0
- setiastro/saspro/translations/ar_translations.py +4096 -0
- setiastro/saspro/translations/de_translations.py +3728 -0
- setiastro/saspro/translations/es_translations.py +4169 -0
- setiastro/saspro/translations/fr_translations.py +4090 -0
- setiastro/saspro/translations/hi_translations.py +3803 -0
- setiastro/saspro/translations/integrate_translations.py +271 -0
- setiastro/saspro/translations/it_translations.py +4728 -0
- setiastro/saspro/translations/ja_translations.py +3834 -0
- setiastro/saspro/translations/pt_translations.py +3847 -0
- setiastro/saspro/translations/ru_translations.py +3082 -0
- setiastro/saspro/translations/saspro_ar.qm +0 -0
- setiastro/saspro/translations/saspro_ar.ts +16019 -0
- setiastro/saspro/translations/saspro_de.qm +0 -0
- setiastro/saspro/translations/saspro_de.ts +14548 -0
- setiastro/saspro/translations/saspro_es.qm +0 -0
- setiastro/saspro/translations/saspro_es.ts +16202 -0
- setiastro/saspro/translations/saspro_fr.qm +0 -0
- setiastro/saspro/translations/saspro_fr.ts +15870 -0
- setiastro/saspro/translations/saspro_hi.qm +0 -0
- setiastro/saspro/translations/saspro_hi.ts +14855 -0
- setiastro/saspro/translations/saspro_it.qm +0 -0
- setiastro/saspro/translations/saspro_it.ts +19046 -0
- setiastro/saspro/translations/saspro_ja.qm +0 -0
- setiastro/saspro/translations/saspro_ja.ts +14980 -0
- setiastro/saspro/translations/saspro_pt.qm +0 -0
- setiastro/saspro/translations/saspro_pt.ts +15024 -0
- setiastro/saspro/translations/saspro_ru.qm +0 -0
- setiastro/saspro/translations/saspro_ru.ts +11835 -0
- setiastro/saspro/translations/saspro_sw.qm +0 -0
- setiastro/saspro/translations/saspro_sw.ts +15237 -0
- setiastro/saspro/translations/saspro_uk.qm +0 -0
- setiastro/saspro/translations/saspro_uk.ts +15248 -0
- setiastro/saspro/translations/saspro_zh.qm +0 -0
- setiastro/saspro/translations/saspro_zh.ts +15289 -0
- setiastro/saspro/translations/sw_translations.py +3897 -0
- setiastro/saspro/translations/uk_translations.py +3929 -0
- setiastro/saspro/translations/zh_translations.py +3910 -0
- setiastro/saspro/versioning.py +77 -0
- setiastro/saspro/view_bundle.py +20 -17
- setiastro/saspro/wavescale_hdr.py +54 -33
- setiastro/saspro/wavescale_hdr_preset.py +6 -5
- setiastro/saspro/wavescalede.py +54 -31
- setiastro/saspro/wavescalede_preset.py +9 -7
- setiastro/saspro/whitebalance.py +58 -22
- setiastro/saspro/widgets/common_utilities.py +12 -11
- setiastro/saspro/widgets/minigame/game.js +991 -0
- setiastro/saspro/widgets/minigame/index.html +53 -0
- setiastro/saspro/widgets/minigame/style.css +241 -0
- setiastro/saspro/widgets/preview_dialogs.py +8 -8
- setiastro/saspro/widgets/resource_monitor.py +263 -0
- setiastro/saspro/widgets/spinboxes.py +18 -0
- setiastro/saspro/widgets/wavelet_utils.py +52 -20
- setiastro/saspro/wimi.py +7996 -0
- setiastro/saspro/wims.py +578 -0
- setiastro/saspro/window_shelf.py +2 -2
- {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/METADATA +15 -3
- setiastrosuitepro-1.6.4.post1.dist-info/RECORD +368 -0
- setiastrosuitepro-1.6.0.dist-info/RECORD +0 -174
- {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/licenses/license.txt +0 -0
|
@@ -12,7 +12,7 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
|
12
12
|
from typing import Optional, List
|
|
13
13
|
from collections import defaultdict
|
|
14
14
|
# Qt
|
|
15
|
-
from PyQt6.QtCore import Qt, QTimer, QEvent, QPointF, QRectF, pyqtSignal, QSettings, QPoint
|
|
15
|
+
from PyQt6.QtCore import Qt, QTimer, QEvent, QPointF, QRectF, pyqtSignal, QSettings, QPoint, QCoreApplication
|
|
16
16
|
from PyQt6.QtGui import (QAction, QIcon, QImage, QPixmap, QBrush, QColor, QPalette,
|
|
17
17
|
QKeySequence, QWheelEvent, QShortcut, QDoubleValidator, QIntValidator)
|
|
18
18
|
from PyQt6.QtWidgets import (
|
|
@@ -65,7 +65,7 @@ class MetricsPanel(QWidget):
|
|
|
65
65
|
self._open_previews = []
|
|
66
66
|
|
|
67
67
|
self.plots, self.scats, self.lines = [], [], []
|
|
68
|
-
titles = ["FWHM (px)", "Eccentricity", "Background", "Star Count"]
|
|
68
|
+
titles = [self.tr("FWHM (px)"), self.tr("Eccentricity"), self.tr("Background"), self.tr("Star Count")]
|
|
69
69
|
for idx, title in enumerate(titles):
|
|
70
70
|
pw = pg.PlotWidget()
|
|
71
71
|
pw.setTitle(title)
|
|
@@ -167,14 +167,14 @@ class MetricsPanel(QWidget):
|
|
|
167
167
|
show = settings.value("metrics/showWarning", True, type=bool)
|
|
168
168
|
if show:
|
|
169
169
|
msg = QMessageBox(self)
|
|
170
|
-
msg.setWindowTitle("Heads-up")
|
|
171
|
-
msg.setText(
|
|
170
|
+
msg.setWindowTitle(self.tr("Heads-up"))
|
|
171
|
+
msg.setText(self.tr(
|
|
172
172
|
"This is going to use ALL your CPU cores and the UI may lock up until it finishes.\n\n"
|
|
173
173
|
"Continue?"
|
|
174
|
-
)
|
|
174
|
+
))
|
|
175
175
|
msg.setStandardButtons(QMessageBox.StandardButton.Yes |
|
|
176
176
|
QMessageBox.StandardButton.No)
|
|
177
|
-
cb = QCheckBox("Don't show again", msg)
|
|
177
|
+
cb = QCheckBox(self.tr("Don't show again"), msg)
|
|
178
178
|
msg.setCheckBox(cb)
|
|
179
179
|
if msg.exec() != QMessageBox.StandardButton.Yes:
|
|
180
180
|
return
|
|
@@ -189,7 +189,7 @@ class MetricsPanel(QWidget):
|
|
|
189
189
|
flags = [e.get('flagged', False) for e in loaded_images]
|
|
190
190
|
|
|
191
191
|
# progress dialog
|
|
192
|
-
prog = QProgressDialog("Computing frame metrics…", "Cancel", 0, n, self)
|
|
192
|
+
prog = QProgressDialog(self.tr("Computing frame metrics…"), self.tr("Cancel"), 0, n, self)
|
|
193
193
|
prog.setWindowModality(Qt.WindowModality.WindowModal)
|
|
194
194
|
prog.setMinimumDuration(0)
|
|
195
195
|
prog.setValue(0)
|
|
@@ -343,18 +343,19 @@ class MetricsWindow(QWidget):
|
|
|
343
343
|
def __init__(self, parent=None):
|
|
344
344
|
super().__init__(parent, Qt.WindowType.Window)
|
|
345
345
|
self._thresholds_per_group: dict[str, List[float|None]] = {}
|
|
346
|
-
self.setWindowTitle("Frame Metrics")
|
|
346
|
+
self.setWindowTitle(self.tr("Frame Metrics"))
|
|
347
347
|
self.resize(800, 600)
|
|
348
348
|
|
|
349
349
|
vbox = QVBoxLayout(self)
|
|
350
350
|
|
|
351
351
|
# ← **new** instructions label
|
|
352
|
-
instr = QLabel(
|
|
352
|
+
instr = QLabel(self.tr(
|
|
353
353
|
"Instructions:\n"
|
|
354
354
|
" • Use the filter dropdown to restrict by FILTER.\n"
|
|
355
355
|
" • Click a dot to flag/unflag a frame.\n"
|
|
356
356
|
" • Shift-click a dot to preview the image.\n"
|
|
357
|
-
" • Drag the red lines to set thresholds."
|
|
357
|
+
" • Drag the red lines to set thresholds."
|
|
358
|
+
),
|
|
358
359
|
self
|
|
359
360
|
)
|
|
360
361
|
instr.setWordWrap(True)
|
|
@@ -363,7 +364,7 @@ class MetricsWindow(QWidget):
|
|
|
363
364
|
|
|
364
365
|
# → filter selector
|
|
365
366
|
self.group_combo = QComboBox(self)
|
|
366
|
-
self.group_combo.addItem("All")
|
|
367
|
+
self.group_combo.addItem(self.tr("All"))
|
|
367
368
|
self.group_combo.currentTextChanged.connect(self._on_group_change)
|
|
368
369
|
vbox.addWidget(self.group_combo)
|
|
369
370
|
|
|
@@ -407,7 +408,7 @@ class MetricsWindow(QWidget):
|
|
|
407
408
|
continue
|
|
408
409
|
|
|
409
410
|
pct = (flagged_cnt / total * 100.0) if total else 0.0
|
|
410
|
-
self.status_label.setText(
|
|
411
|
+
self.status_label.setText(self.tr("Flagged Items {0}/{1} ({2:.1f}%)").format(flagged_cnt, total, pct))
|
|
411
412
|
|
|
412
413
|
|
|
413
414
|
def set_images(self, loaded_images, order=None):
|
|
@@ -417,7 +418,7 @@ class MetricsWindow(QWidget):
|
|
|
417
418
|
# ─── rebuild the combo-list of FILTER groups ─────────────
|
|
418
419
|
self.group_combo.blockSignals(True)
|
|
419
420
|
self.group_combo.clear()
|
|
420
|
-
self.group_combo.addItem("All")
|
|
421
|
+
self.group_combo.addItem(self.tr("All"))
|
|
421
422
|
seen = set()
|
|
422
423
|
for entry in loaded_images:
|
|
423
424
|
filt = entry.get('header', {}).get('FILTER', 'Unknown')
|
|
@@ -459,7 +460,7 @@ class MetricsWindow(QWidget):
|
|
|
459
460
|
cur = self.group_combo.currentText()
|
|
460
461
|
self.group_combo.blockSignals(True)
|
|
461
462
|
self.group_combo.clear()
|
|
462
|
-
self.group_combo.addItem("All")
|
|
463
|
+
self.group_combo.addItem(self.tr("All"))
|
|
463
464
|
seen = set()
|
|
464
465
|
for entry in self._all_images:
|
|
465
466
|
filt = (entry.get('header', {}) or {}).get('FILTER', 'Unknown')
|
|
@@ -478,32 +479,82 @@ class MetricsWindow(QWidget):
|
|
|
478
479
|
"""
|
|
479
480
|
Called when some frames were deleted/moved out of the list.
|
|
480
481
|
Does NOT recompute metrics. Just trims cached arrays and re-plots.
|
|
482
|
+
|
|
483
|
+
Robust against:
|
|
484
|
+
- removed indices referring to the old list (out of range)
|
|
485
|
+
- metrics_panel arrays being a different length than _all_images
|
|
486
|
+
- stale _order_all / _current_indices containing out-of-bounds indices
|
|
481
487
|
"""
|
|
482
488
|
if not removed:
|
|
483
489
|
return
|
|
484
|
-
removed = sorted(set(int(i) for i in removed))
|
|
485
490
|
|
|
486
|
-
#
|
|
487
|
-
|
|
491
|
+
# Unique + int
|
|
492
|
+
removed = sorted({int(i) for i in removed})
|
|
493
|
+
|
|
494
|
+
# ---- 1) Trim metrics panel caches SAFELY ----
|
|
495
|
+
# Prefer panel's current frame count, because it represents the arrays we must slice.
|
|
496
|
+
n_panel = getattr(self.metrics_panel, "n_frames", None)
|
|
497
|
+
if callable(n_panel):
|
|
498
|
+
n_panel = n_panel()
|
|
499
|
+
if not isinstance(n_panel, int) or n_panel <= 0:
|
|
500
|
+
# fallback: infer from metrics_data if present
|
|
501
|
+
md = getattr(self.metrics_panel, "metrics_data", None)
|
|
502
|
+
if md is not None and len(md) and md[0] is not None:
|
|
503
|
+
try:
|
|
504
|
+
n_panel = int(len(md[0]))
|
|
505
|
+
except Exception:
|
|
506
|
+
n_panel = 0
|
|
507
|
+
else:
|
|
508
|
+
n_panel = 0
|
|
509
|
+
|
|
510
|
+
if n_panel > 0:
|
|
511
|
+
removed_panel = [i for i in removed if 0 <= i < n_panel]
|
|
512
|
+
if removed_panel:
|
|
513
|
+
self.metrics_panel.remove_frames(removed_panel)
|
|
514
|
+
# else: panel has nothing (or isn't initialized) — just continue with ordering cleanup
|
|
488
515
|
|
|
489
|
-
# 2)
|
|
490
|
-
# (BlinkTab will already have mutated the underlying list for us)
|
|
516
|
+
# ---- 2) Update ordering arrays with the SAME removed set (but clamp later) ----
|
|
491
517
|
self._order_all = self._reindex_list_after_remove(self._order_all, removed)
|
|
492
|
-
self._current_indices
|
|
518
|
+
if self._current_indices is not None:
|
|
519
|
+
self._current_indices = self._reindex_list_after_remove(self._current_indices, removed)
|
|
493
520
|
|
|
494
|
-
# 3)
|
|
521
|
+
# ---- 3) Rebuild groups (filters may have disappeared) ----
|
|
495
522
|
self._rebuild_groups_from_images()
|
|
496
523
|
|
|
497
|
-
# 4)
|
|
524
|
+
# ---- 4) Plot with VALID indices only ----
|
|
525
|
+
n_imgs = len(self._all_images) if self._all_images is not None else 0
|
|
526
|
+
|
|
527
|
+
def _sanitize_indices(ixs):
|
|
528
|
+
if not ixs:
|
|
529
|
+
return []
|
|
530
|
+
out = []
|
|
531
|
+
seen = set()
|
|
532
|
+
for i in ixs:
|
|
533
|
+
try:
|
|
534
|
+
ii = int(i)
|
|
535
|
+
except Exception:
|
|
536
|
+
continue
|
|
537
|
+
if 0 <= ii < n_imgs and ii not in seen:
|
|
538
|
+
seen.add(ii)
|
|
539
|
+
out.append(ii)
|
|
540
|
+
return out
|
|
541
|
+
|
|
498
542
|
indices = self._current_indices if self._current_indices is not None else self._order_all
|
|
543
|
+
indices = _sanitize_indices(indices)
|
|
544
|
+
|
|
545
|
+
# If the current group became empty, fall back to "all"
|
|
546
|
+
if not indices and n_imgs:
|
|
547
|
+
indices = list(range(n_imgs))
|
|
548
|
+
self._current_indices = indices # optional: keeps UI consistent
|
|
549
|
+
|
|
499
550
|
self.metrics_panel.plot(self._all_images, indices=indices)
|
|
500
551
|
|
|
501
|
-
# 5)
|
|
552
|
+
# ---- 5) Recolor & status ----
|
|
502
553
|
self.metrics_panel.refresh_colors_and_status()
|
|
503
554
|
self._update_status()
|
|
504
555
|
|
|
505
556
|
def _on_group_change(self, name: str):
|
|
506
|
-
if name == "All":
|
|
557
|
+
if name == self.tr("All"):
|
|
507
558
|
self._current_indices = self._order_all
|
|
508
559
|
else:
|
|
509
560
|
# preserve Tree order inside the chosen FILTER
|
|
@@ -538,7 +589,7 @@ class MetricsWindow(QWidget):
|
|
|
538
589
|
else:
|
|
539
590
|
if order is not None:
|
|
540
591
|
self._order_all = list(order)
|
|
541
|
-
|
|
592
|
+
# re-plot the current group with the new ordering
|
|
542
593
|
self._on_group_change(self.group_combo.currentText())
|
|
543
594
|
|
|
544
595
|
class BlinkComparatorPro(QDialog):
|
|
@@ -547,7 +598,7 @@ class BlinkComparatorPro(QDialog):
|
|
|
547
598
|
def __init__(self, doc_manager=None, parent=None):
|
|
548
599
|
super().__init__(parent)
|
|
549
600
|
self.doc_manager = doc_manager
|
|
550
|
-
self.setWindowTitle("Blink Comparator")
|
|
601
|
+
self.setWindowTitle(self.tr("Blink Comparator"))
|
|
551
602
|
self.resize(1200, 700)
|
|
552
603
|
|
|
553
604
|
self.tab = BlinkTab(doc_manager=self.doc_manager, parent=self)
|
|
@@ -603,7 +654,7 @@ class BlinkTab(QWidget):
|
|
|
603
654
|
# --------------------
|
|
604
655
|
# Instruction Label
|
|
605
656
|
# --------------------
|
|
606
|
-
instruction_text = "Press 'F' to flag/unflag an image.\nRight-click on an image for more options."
|
|
657
|
+
instruction_text = self.tr("Press 'F' to flag/unflag an image.\nRight-click on an image for more options.")
|
|
607
658
|
self.instruction_label = QLabel(instruction_text, self)
|
|
608
659
|
self.instruction_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
609
660
|
self.instruction_label.setWordWrap(True)
|
|
@@ -622,33 +673,33 @@ class BlinkTab(QWidget):
|
|
|
622
673
|
button_layout = QHBoxLayout()
|
|
623
674
|
|
|
624
675
|
# "Select Images" Button
|
|
625
|
-
self.fileButton = QPushButton('Select Images', self)
|
|
676
|
+
self.fileButton = QPushButton(self.tr('Select Images'), self)
|
|
626
677
|
self.fileButton.clicked.connect(self.openFileDialog)
|
|
627
678
|
button_layout.addWidget(self.fileButton)
|
|
628
679
|
|
|
629
680
|
# "Select Directory" Button
|
|
630
|
-
self.dirButton = QPushButton('Select Directory', self)
|
|
681
|
+
self.dirButton = QPushButton(self.tr('Select Directory'), self)
|
|
631
682
|
self.dirButton.clicked.connect(self.openDirectoryDialog)
|
|
632
683
|
button_layout.addWidget(self.dirButton)
|
|
633
684
|
|
|
634
|
-
self.addButton = QPushButton("Add Additional", self)
|
|
685
|
+
self.addButton = QPushButton(self.tr("Add Additional"), self)
|
|
635
686
|
self.addButton.clicked.connect(self.addAdditionalImages)
|
|
636
687
|
button_layout.addWidget(self.addButton)
|
|
637
688
|
|
|
638
689
|
left_layout.addLayout(button_layout)
|
|
639
690
|
|
|
640
|
-
self.metrics_button = QPushButton("Show Metrics", self)
|
|
691
|
+
self.metrics_button = QPushButton(self.tr("Show Metrics"), self)
|
|
641
692
|
self.metrics_button.clicked.connect(self.show_metrics)
|
|
642
693
|
left_layout.addWidget(self.metrics_button)
|
|
643
694
|
|
|
644
695
|
push_row = QHBoxLayout()
|
|
645
|
-
self.send_lights_btn = QPushButton("→ Stacking: Lights", self)
|
|
646
|
-
self.send_lights_btn.setToolTip("Send selected (or all) blink files to the Stacking Suite → Light tab")
|
|
696
|
+
self.send_lights_btn = QPushButton(self.tr("→ Stacking: Lights"), self)
|
|
697
|
+
self.send_lights_btn.setToolTip(self.tr("Send selected (or all) blink files to the Stacking Suite → Light tab"))
|
|
647
698
|
self.send_lights_btn.clicked.connect(self._send_to_stacking_lights)
|
|
648
699
|
push_row.addWidget(self.send_lights_btn)
|
|
649
700
|
|
|
650
|
-
self.send_integ_btn = QPushButton("→ Stacking: Integration", self)
|
|
651
|
-
self.send_integ_btn.setToolTip("Send selected (or all) blink files to the Stacking Suite → Image Integration tab")
|
|
701
|
+
self.send_integ_btn = QPushButton(self.tr("→ Stacking: Integration"), self)
|
|
702
|
+
self.send_integ_btn.setToolTip(self.tr("Send selected (or all) blink files to the Stacking Suite → Image Integration tab"))
|
|
652
703
|
self.send_integ_btn.clicked.connect(self._send_to_stacking_integration)
|
|
653
704
|
push_row.addWidget(self.send_integ_btn)
|
|
654
705
|
|
|
@@ -687,7 +738,7 @@ class BlinkTab(QWidget):
|
|
|
687
738
|
# ----- Playback speed controls (0.1–10.0 fps) -----
|
|
688
739
|
speed_layout = QHBoxLayout()
|
|
689
740
|
|
|
690
|
-
speed_label = QLabel("Speed:", self)
|
|
741
|
+
speed_label = QLabel(self.tr("Speed:"), self)
|
|
691
742
|
speed_layout.addWidget(speed_label)
|
|
692
743
|
|
|
693
744
|
# Slider maps 1..100 -> 0.1..10.0 fps
|
|
@@ -695,7 +746,7 @@ class BlinkTab(QWidget):
|
|
|
695
746
|
self.speed_slider.setRange(1, 100)
|
|
696
747
|
self.speed_slider.setValue(int(round(self.play_fps * 10))) # play_fps is float
|
|
697
748
|
self.speed_slider.setTickPosition(QSlider.TickPosition.NoTicks)
|
|
698
|
-
self.speed_slider.setToolTip("Playback speed (0.1–10.0 fps)")
|
|
749
|
+
self.speed_slider.setToolTip(self.tr("Playback speed (0.1–10.0 fps)"))
|
|
699
750
|
speed_layout.addWidget(self.speed_slider, 1)
|
|
700
751
|
|
|
701
752
|
# Custom float spin (your class)
|
|
@@ -717,7 +768,7 @@ class BlinkTab(QWidget):
|
|
|
717
768
|
|
|
718
769
|
left_layout.addLayout(speed_layout)
|
|
719
770
|
|
|
720
|
-
self.export_button = QPushButton("Export Video…", self)
|
|
771
|
+
self.export_button = QPushButton(self.tr("Export Video…"), self)
|
|
721
772
|
self.export_button.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_DialogSaveButton))
|
|
722
773
|
self.export_button.clicked.connect(self.export_blink_video)
|
|
723
774
|
left_layout.addWidget(self.export_button)
|
|
@@ -725,7 +776,7 @@ class BlinkTab(QWidget):
|
|
|
725
776
|
# Tree view for file names
|
|
726
777
|
self.fileTree = QTreeWidget(self)
|
|
727
778
|
self.fileTree.setColumnCount(1)
|
|
728
|
-
self.fileTree.setHeaderLabels(["Image Files"])
|
|
779
|
+
self.fileTree.setHeaderLabels([self.tr("Image Files")])
|
|
729
780
|
self.fileTree.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) # Allow multiple selections
|
|
730
781
|
#self.fileTree.itemClicked.connect(self.on_item_clicked)
|
|
731
782
|
self.fileTree.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
|
@@ -740,12 +791,12 @@ class BlinkTab(QWidget):
|
|
|
740
791
|
left_layout.addWidget(self.fileTree)
|
|
741
792
|
|
|
742
793
|
# "Clear Flags" Button
|
|
743
|
-
self.clearFlagsButton = QPushButton('Clear Flags', self)
|
|
794
|
+
self.clearFlagsButton = QPushButton(self.tr('Clear Flags'), self)
|
|
744
795
|
self.clearFlagsButton.clicked.connect(self.clearFlags)
|
|
745
796
|
left_layout.addWidget(self.clearFlagsButton)
|
|
746
797
|
|
|
747
798
|
# "Clear Images" Button
|
|
748
|
-
self.clearButton = QPushButton('Clear Images', self)
|
|
799
|
+
self.clearButton = QPushButton(self.tr('Clear Images'), self)
|
|
749
800
|
self.clearButton.clicked.connect(self.clearImages)
|
|
750
801
|
left_layout.addWidget(self.clearButton)
|
|
751
802
|
|
|
@@ -755,7 +806,7 @@ class BlinkTab(QWidget):
|
|
|
755
806
|
left_layout.addWidget(self.progress_bar)
|
|
756
807
|
|
|
757
808
|
# Add loading message label
|
|
758
|
-
self.loading_label = QLabel("Loading images...", self)
|
|
809
|
+
self.loading_label = QLabel(self.tr("Loading images..."), self)
|
|
759
810
|
left_layout.addWidget(self.loading_label)
|
|
760
811
|
self.imagesChanged.emit(len(self.loaded_images))
|
|
761
812
|
|
|
@@ -772,9 +823,9 @@ class BlinkTab(QWidget):
|
|
|
772
823
|
# Zoom / preview toolbar (standardized)
|
|
773
824
|
zoom_controls_layout = QHBoxLayout()
|
|
774
825
|
|
|
775
|
-
self.zoom_in_btn = themed_toolbtn("zoom-in", "Zoom In")
|
|
776
|
-
self.zoom_out_btn = themed_toolbtn("zoom-out", "Zoom Out")
|
|
777
|
-
self.fit_btn = themed_toolbtn("zoom-fit-best", "Fit to Preview")
|
|
826
|
+
self.zoom_in_btn = themed_toolbtn("zoom-in", self.tr("Zoom In"))
|
|
827
|
+
self.zoom_out_btn = themed_toolbtn("zoom-out", self.tr("Zoom Out"))
|
|
828
|
+
self.fit_btn = themed_toolbtn("zoom-fit-best", self.tr("Fit to Preview"))
|
|
778
829
|
|
|
779
830
|
self.zoom_in_btn.clicked.connect(self.zoom_in)
|
|
780
831
|
self.zoom_out_btn.clicked.connect(self.zoom_out)
|
|
@@ -787,7 +838,7 @@ class BlinkTab(QWidget):
|
|
|
787
838
|
zoom_controls_layout.addStretch(1)
|
|
788
839
|
|
|
789
840
|
# Keep Aggressive Stretch as a text toggle (it’s not really a zoom action)
|
|
790
|
-
self.aggressive_button = QPushButton("Aggressive Stretch", self)
|
|
841
|
+
self.aggressive_button = QPushButton(self.tr("Aggressive Stretch"), self)
|
|
791
842
|
self.aggressive_button.setCheckable(True)
|
|
792
843
|
self.aggressive_button.clicked.connect(self.toggle_aggressive)
|
|
793
844
|
zoom_controls_layout.addWidget(self.aggressive_button)
|
|
@@ -928,14 +979,14 @@ class BlinkTab(QWidget):
|
|
|
928
979
|
def _send_to_stacking_lights(self):
|
|
929
980
|
paths = self._collect_paths_for_stacking()
|
|
930
981
|
if not paths:
|
|
931
|
-
QMessageBox.information(self, "No images", "There are no images to send.")
|
|
982
|
+
QMessageBox.information(self, self.tr("No images"), self.tr("There are no images to send."))
|
|
932
983
|
return
|
|
933
984
|
self.sendToStacking.emit(paths, "lights")
|
|
934
985
|
|
|
935
986
|
def _send_to_stacking_integration(self):
|
|
936
987
|
paths = self._collect_paths_for_stacking()
|
|
937
988
|
if not paths:
|
|
938
|
-
QMessageBox.information(self, "No images", "There are no images to send.")
|
|
989
|
+
QMessageBox.information(self, self.tr("No images"), self.tr("There are no images to send."))
|
|
939
990
|
return
|
|
940
991
|
self.sendToStacking.emit(paths, "integration")
|
|
941
992
|
|
|
@@ -945,7 +996,7 @@ class BlinkTab(QWidget):
|
|
|
945
996
|
# Ensure we have frames
|
|
946
997
|
leaves = self.get_all_leaf_items()
|
|
947
998
|
if not leaves:
|
|
948
|
-
QMessageBox.information(self, "No Images", "Load images before exporting.")
|
|
999
|
+
QMessageBox.information(self, self.tr("No Images"), self.tr("Load images before exporting."))
|
|
949
1000
|
return
|
|
950
1001
|
|
|
951
1002
|
# Ask options first (size, fps, selection scope)
|
|
@@ -960,7 +1011,7 @@ class BlinkTab(QWidget):
|
|
|
960
1011
|
if only_selected:
|
|
961
1012
|
sel_leaves = [it for it in self.fileTree.selectedItems() if it.childCount() == 0]
|
|
962
1013
|
if not sel_leaves:
|
|
963
|
-
QMessageBox.information(self, "No Selection", "No individual frames selected.")
|
|
1014
|
+
QMessageBox.information(self, self.tr("No Selection"), self.tr("No individual frames selected."))
|
|
964
1015
|
return
|
|
965
1016
|
names = {it.text(0).lstrip("⚠️ ").strip() for it in sel_leaves}
|
|
966
1017
|
order = [i for i in self._tree_order_indices()
|
|
@@ -969,13 +1020,13 @@ class BlinkTab(QWidget):
|
|
|
969
1020
|
order = self._tree_order_indices()
|
|
970
1021
|
|
|
971
1022
|
if not order:
|
|
972
|
-
QMessageBox.information(self, "No Frames", "Nothing to export.")
|
|
1023
|
+
QMessageBox.information(self, self.tr("No Frames"), self.tr("Nothing to export."))
|
|
973
1024
|
return
|
|
974
1025
|
|
|
975
1026
|
if len(order) < 2:
|
|
976
1027
|
ret = QMessageBox.question(
|
|
977
|
-
self, "Only one frame",
|
|
978
|
-
"You're about to export a video with a single frame. Continue?",
|
|
1028
|
+
self, self.tr("Only one frame"),
|
|
1029
|
+
self.tr("You're about to export a video with a single frame. Continue?"),
|
|
979
1030
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
980
1031
|
QMessageBox.StandardButton.No,
|
|
981
1032
|
)
|
|
@@ -984,22 +1035,22 @@ class BlinkTab(QWidget):
|
|
|
984
1035
|
|
|
985
1036
|
# Ask where to save
|
|
986
1037
|
out_path, _ = QFileDialog.getSaveFileName(
|
|
987
|
-
self, "Export Blink Video", "blink.mp4", "Video (*.mp4 *.avi)"
|
|
1038
|
+
self, self.tr("Export Blink Video"), "blink.mp4", self.tr("Video (*.mp4 *.avi)")
|
|
988
1039
|
)
|
|
989
1040
|
if not out_path:
|
|
990
1041
|
return
|
|
991
1042
|
# Let _open_video_writer_portable decide the real extension; we pass requested
|
|
992
1043
|
writer, out_path, backend = self._open_video_writer_portable(out_path, (target_w, target_h), fps)
|
|
993
1044
|
if writer is None:
|
|
994
|
-
QMessageBox.critical(self, "Export",
|
|
995
|
-
"No compatible video codec found.\n\n"
|
|
996
|
-
"Tip: install FFmpeg or `pip install imageio[ffmpeg]` for a portable fallback."
|
|
1045
|
+
QMessageBox.critical(self, self.tr("Export"),
|
|
1046
|
+
self.tr("No compatible video codec found.\n\n"
|
|
1047
|
+
"Tip: install FFmpeg or `pip install imageio[ffmpeg]` for a portable fallback.")
|
|
997
1048
|
)
|
|
998
1049
|
return
|
|
999
1050
|
|
|
1000
1051
|
# Progress UI
|
|
1001
|
-
prog = QProgressDialog("Rendering video…", "Cancel", 0, len(order), self)
|
|
1002
|
-
prog.setWindowTitle("Export Blink Video")
|
|
1052
|
+
prog = QProgressDialog(self.tr("Rendering video…"), self.tr("Cancel"), 0, len(order), self)
|
|
1053
|
+
prog.setWindowTitle(self.tr("Export Blink Video"))
|
|
1003
1054
|
prog.setAutoClose(True)
|
|
1004
1055
|
prog.setMinimumDuration(300)
|
|
1005
1056
|
|
|
@@ -1047,25 +1098,25 @@ class BlinkTab(QWidget):
|
|
|
1047
1098
|
os.remove(out_path)
|
|
1048
1099
|
except Exception:
|
|
1049
1100
|
pass
|
|
1050
|
-
QMessageBox.information(self, "Export", "Export canceled.")
|
|
1101
|
+
QMessageBox.information(self, self.tr("Export"), self.tr("Export canceled."))
|
|
1051
1102
|
return
|
|
1052
1103
|
|
|
1053
1104
|
if frames_written == 0:
|
|
1054
|
-
QMessageBox.critical(self, "Export", "No frames were written (codec/back-end issue?).")
|
|
1105
|
+
QMessageBox.critical(self, self.tr("Export"), self.tr("No frames were written (codec/back-end issue?)."))
|
|
1055
1106
|
return
|
|
1056
1107
|
|
|
1057
|
-
QMessageBox.information(self, "Export",
|
|
1108
|
+
QMessageBox.information(self, self.tr("Export"), self.tr("Saved: {0}\nFrames: {1} @ {2} fps").format(out_path, frames_written, fps))
|
|
1058
1109
|
|
|
1059
1110
|
|
|
1060
1111
|
|
|
1061
1112
|
def _ask_video_options(self, default_fps: float):
|
|
1062
1113
|
"""Options dialog for size, fps, and whether to limit to current selection."""
|
|
1063
1114
|
dlg = QDialog(self)
|
|
1064
|
-
dlg.setWindowTitle("Video Options")
|
|
1115
|
+
dlg.setWindowTitle(self.tr("Video Options"))
|
|
1065
1116
|
layout = QGridLayout(dlg)
|
|
1066
1117
|
|
|
1067
1118
|
# Size
|
|
1068
|
-
layout.addWidget(QLabel("Size:"), 0, 0)
|
|
1119
|
+
layout.addWidget(QLabel(self.tr("Size:")), 0, 0)
|
|
1069
1120
|
size_combo = QComboBox(dlg)
|
|
1070
1121
|
size_combo.addItem("HD 1280×720", (1280, 720))
|
|
1071
1122
|
size_combo.addItem("Full HD 1920×1080", (1920, 1080))
|
|
@@ -1074,7 +1125,7 @@ class BlinkTab(QWidget):
|
|
|
1074
1125
|
layout.addWidget(size_combo, 0, 1)
|
|
1075
1126
|
|
|
1076
1127
|
# FPS
|
|
1077
|
-
layout.addWidget(QLabel("FPS:"), 1, 0)
|
|
1128
|
+
layout.addWidget(QLabel(self.tr("FPS:")), 1, 0)
|
|
1078
1129
|
fps_edit = QDoubleSpinBox(dlg)
|
|
1079
1130
|
fps_edit.setRange(0.1, 60.0)
|
|
1080
1131
|
fps_edit.setDecimals(2)
|
|
@@ -1083,13 +1134,13 @@ class BlinkTab(QWidget):
|
|
|
1083
1134
|
layout.addWidget(fps_edit, 1, 1)
|
|
1084
1135
|
|
|
1085
1136
|
# Only selected?
|
|
1086
|
-
only_selected = QCheckBox("Export only selected frames", dlg)
|
|
1137
|
+
only_selected = QCheckBox(self.tr("Export only selected frames"), dlg)
|
|
1087
1138
|
only_selected.setChecked(False) # default: export everything in tree order
|
|
1088
1139
|
layout.addWidget(only_selected, 2, 0, 1, 2)
|
|
1089
1140
|
|
|
1090
1141
|
# Buttons
|
|
1091
1142
|
btns = QHBoxLayout()
|
|
1092
|
-
ok = QPushButton("OK", dlg); cancel = QPushButton("Cancel", dlg)
|
|
1143
|
+
ok = QPushButton(self.tr("OK"), dlg); cancel = QPushButton(self.tr("Cancel"), dlg)
|
|
1093
1144
|
ok.clicked.connect(dlg.accept); cancel.clicked.connect(dlg.reject)
|
|
1094
1145
|
btns.addWidget(ok); btns.addWidget(cancel)
|
|
1095
1146
|
layout.addLayout(btns, 3, 0, 1, 2)
|
|
@@ -1110,12 +1161,13 @@ class BlinkTab(QWidget):
|
|
|
1110
1161
|
|
|
1111
1162
|
if not use_aggr:
|
|
1112
1163
|
if stored.dtype == np.uint8:
|
|
1113
|
-
|
|
1164
|
+
return stored
|
|
1114
1165
|
elif stored.dtype == np.uint16:
|
|
1115
|
-
|
|
1166
|
+
return (stored >> 8).astype(np.uint8)
|
|
1116
1167
|
else:
|
|
1117
|
-
|
|
1118
|
-
|
|
1168
|
+
# ✅ display-only normalization for float / weird ranges
|
|
1169
|
+
f01 = self._ensure_float01(stored)
|
|
1170
|
+
return (f01 * 255.0).astype(np.uint8)
|
|
1119
1171
|
|
|
1120
1172
|
base01 = self._as_float01(stored)
|
|
1121
1173
|
|
|
@@ -1206,14 +1258,32 @@ class BlinkTab(QWidget):
|
|
|
1206
1258
|
|
|
1207
1259
|
def _update_loaded_count_label(self, n: int):
|
|
1208
1260
|
# pluralize nicely
|
|
1209
|
-
self.loading_label.setText(
|
|
1261
|
+
self.loading_label.setText(self.tr("Loaded {0} image{1}.").format(n, 's' if n != 1 else ''))
|
|
1210
1262
|
|
|
1211
1263
|
def _apply_playback_interval(self, *_):
|
|
1212
|
-
# read from custom spin if present
|
|
1213
|
-
fps = float(
|
|
1264
|
+
# read from custom spin if present (support both .value() and .value attribute)
|
|
1265
|
+
fps = float(getattr(self, "play_fps", 1.0))
|
|
1266
|
+
|
|
1267
|
+
if hasattr(self, "speed_spin") and self.speed_spin is not None:
|
|
1268
|
+
try:
|
|
1269
|
+
v = getattr(self.speed_spin, "value", None)
|
|
1270
|
+
if callable(v):
|
|
1271
|
+
fps = float(v()) # QDoubleSpinBox-style
|
|
1272
|
+
elif v is not None:
|
|
1273
|
+
fps = float(v) # CustomDoubleSpinBox stores numeric attribute
|
|
1274
|
+
else:
|
|
1275
|
+
# last-resort: try Qt API name
|
|
1276
|
+
fps = float(self.speed_spin.value())
|
|
1277
|
+
except Exception:
|
|
1278
|
+
# fall back to existing play_fps
|
|
1279
|
+
pass
|
|
1280
|
+
|
|
1214
1281
|
fps = max(0.1, min(10.0, fps))
|
|
1215
1282
|
self.play_fps = fps
|
|
1216
|
-
|
|
1283
|
+
|
|
1284
|
+
if hasattr(self, "playback_timer") and self.playback_timer is not None:
|
|
1285
|
+
self.playback_timer.setInterval(int(round(1000.0 / fps))) # 0.1 fps -> 10000 ms
|
|
1286
|
+
|
|
1217
1287
|
|
|
1218
1288
|
def _on_current_item_changed_safe(self, current, previous):
|
|
1219
1289
|
if not current:
|
|
@@ -1238,12 +1308,41 @@ class BlinkTab(QWidget):
|
|
|
1238
1308
|
if item is not None:
|
|
1239
1309
|
self.fileTree.scrollToItem(item, QAbstractItemView.ScrollHint.EnsureVisible)
|
|
1240
1310
|
|
|
1241
|
-
def
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1311
|
+
def _leaf_path(self, item: QTreeWidgetItem) -> str | None:
|
|
1312
|
+
"""Return full path for a leaf item, preferring UserRole; fallback to basename match."""
|
|
1313
|
+
if not item or item.childCount() > 0:
|
|
1314
|
+
return None
|
|
1315
|
+
|
|
1316
|
+
p = item.data(0, Qt.ItemDataRole.UserRole)
|
|
1317
|
+
if p and isinstance(p, str):
|
|
1318
|
+
return p
|
|
1319
|
+
|
|
1320
|
+
# fallback: basename match (legacy items)
|
|
1321
|
+
name = item.text(0).lstrip("⚠️ ").strip()
|
|
1322
|
+
if not name:
|
|
1323
|
+
return None
|
|
1324
|
+
return next((x for x in self.image_paths if os.path.basename(x) == name), None)
|
|
1325
|
+
|
|
1326
|
+
|
|
1327
|
+
def _leaf_index(self, item: QTreeWidgetItem) -> int | None:
|
|
1328
|
+
"""Return index into image_paths/loaded_images for a leaf item."""
|
|
1329
|
+
p = self._leaf_path(item)
|
|
1330
|
+
if not p:
|
|
1331
|
+
return None
|
|
1332
|
+
try:
|
|
1333
|
+
return self.image_paths.index(p)
|
|
1334
|
+
except ValueError:
|
|
1335
|
+
return None
|
|
1336
|
+
|
|
1337
|
+
|
|
1338
|
+
def _set_leaf_display(self, item: QTreeWidgetItem, *, base_name: str, flagged: bool, full_path: str):
|
|
1339
|
+
"""Update a leaf item's text + UserRole consistently."""
|
|
1340
|
+
disp = base_name
|
|
1341
|
+
if flagged:
|
|
1342
|
+
disp = f"⚠️ {disp}"
|
|
1343
|
+
item.setText(0, disp)
|
|
1344
|
+
item.setData(0, Qt.ItemDataRole.UserRole, full_path)
|
|
1345
|
+
|
|
1247
1346
|
|
|
1248
1347
|
def clearFlags(self):
|
|
1249
1348
|
"""Clear all flagged states, update tree icons & metrics."""
|
|
@@ -1280,14 +1379,14 @@ class BlinkTab(QWidget):
|
|
|
1280
1379
|
"""Let the user pick more images to append to the blink list."""
|
|
1281
1380
|
file_paths, _ = QFileDialog.getOpenFileNames(
|
|
1282
1381
|
self,
|
|
1283
|
-
"Add Additional Images",
|
|
1382
|
+
self.tr("Add Additional Images"),
|
|
1284
1383
|
"",
|
|
1285
|
-
"Images (*.png *.tif *.tiff *.fits *.fit *.xisf *.cr2 *.nef *.arw *.dng *.raf *.orf *.rw2 *.pef);;All Files (*)"
|
|
1384
|
+
self.tr("Images (*.png *.tif *.tiff *.fits *.fit *.xisf *.cr2 *.nef *.arw *.dng *.raf *.orf *.rw2 *.pef);;All Files (*)")
|
|
1286
1385
|
)
|
|
1287
1386
|
# filter out duplicates
|
|
1288
1387
|
new_paths = [p for p in file_paths if p not in self.image_paths]
|
|
1289
1388
|
if not new_paths:
|
|
1290
|
-
QMessageBox.information(self, "No New Images", "No new images selected or already loaded.")
|
|
1389
|
+
QMessageBox.information(self, self.tr("No New Images"), self.tr("No new images selected or already loaded."))
|
|
1291
1390
|
return
|
|
1292
1391
|
self._appendImages(new_paths)
|
|
1293
1392
|
|
|
@@ -1335,7 +1434,7 @@ class BlinkTab(QWidget):
|
|
|
1335
1434
|
self.add_item_to_tree(path)
|
|
1336
1435
|
|
|
1337
1436
|
# update status
|
|
1338
|
-
self.loading_label.setText(
|
|
1437
|
+
self.loading_label.setText(self.tr("Loaded {0} images.").format(len(self.loaded_images)))
|
|
1339
1438
|
if self.metrics_window and self.metrics_window.isVisible():
|
|
1340
1439
|
self.metrics_window.update_metrics(self.loaded_images, order=self._tree_order_indices())
|
|
1341
1440
|
|
|
@@ -1351,7 +1450,7 @@ class BlinkTab(QWidget):
|
|
|
1351
1450
|
order = self._tree_order_indices()
|
|
1352
1451
|
self.metrics_window.set_images(self.loaded_images, order=order)
|
|
1353
1452
|
panel = self.metrics_window.metrics_panel
|
|
1354
|
-
self.thresholds_by_group["All"] = [line.value() for line in panel.lines]
|
|
1453
|
+
self.thresholds_by_group[self.tr("All")] = [line.value() for line in panel.lines]
|
|
1355
1454
|
self.metrics_window.show()
|
|
1356
1455
|
self.metrics_window.raise_()
|
|
1357
1456
|
|
|
@@ -1405,7 +1504,7 @@ class BlinkTab(QWidget):
|
|
|
1405
1504
|
thr_list[metric_idx] = threshold
|
|
1406
1505
|
|
|
1407
1506
|
# build the list of indices to re-evaluate
|
|
1408
|
-
if group == "All":
|
|
1507
|
+
if group == self.tr("All"):
|
|
1409
1508
|
indices = range(len(self.loaded_images))
|
|
1410
1509
|
else:
|
|
1411
1510
|
indices = [
|
|
@@ -1450,6 +1549,7 @@ class BlinkTab(QWidget):
|
|
|
1450
1549
|
"""Rebuild the left tree from self.loaded_images without reloading or recomputing."""
|
|
1451
1550
|
self.fileTree.clear()
|
|
1452
1551
|
from collections import defaultdict
|
|
1552
|
+
|
|
1453
1553
|
grouped = defaultdict(list)
|
|
1454
1554
|
for entry in self.loaded_images:
|
|
1455
1555
|
hdr = entry.get('header', {}) or {}
|
|
@@ -1467,25 +1567,29 @@ class BlinkTab(QWidget):
|
|
|
1467
1567
|
by_object[obj][fil][exp] = paths
|
|
1468
1568
|
|
|
1469
1569
|
for obj in sorted(by_object, key=lambda o: o.lower()):
|
|
1470
|
-
obj_item = QTreeWidgetItem([
|
|
1570
|
+
obj_item = QTreeWidgetItem([self.tr("Object: {0}").format(obj)])
|
|
1471
1571
|
self.fileTree.addTopLevelItem(obj_item)
|
|
1472
1572
|
obj_item.setExpanded(True)
|
|
1573
|
+
|
|
1473
1574
|
for fil in sorted(by_object[obj], key=lambda f: f.lower()):
|
|
1474
|
-
|
|
1475
|
-
obj_item.addChild(
|
|
1476
|
-
|
|
1575
|
+
filt_item = QTreeWidgetItem([self.tr("Filter: {0}").format(fil)])
|
|
1576
|
+
obj_item.addChild(filt_item)
|
|
1577
|
+
filt_item.setExpanded(True)
|
|
1578
|
+
|
|
1477
1579
|
for exp in sorted(by_object[obj][fil], key=lambda e: str(e).lower()):
|
|
1478
|
-
exp_item = QTreeWidgetItem([
|
|
1479
|
-
|
|
1580
|
+
exp_item = QTreeWidgetItem([self.tr("Exposure: {0}").format(exp)])
|
|
1581
|
+
filt_item.addChild(exp_item)
|
|
1480
1582
|
exp_item.setExpanded(True)
|
|
1583
|
+
|
|
1481
1584
|
for p in by_object[obj][fil][exp]:
|
|
1482
1585
|
leaf = QTreeWidgetItem([os.path.basename(p)])
|
|
1483
1586
|
leaf.setData(0, Qt.ItemDataRole.UserRole, p)
|
|
1484
1587
|
exp_item.addChild(leaf)
|
|
1485
1588
|
|
|
1486
|
-
# 🔹
|
|
1589
|
+
# 🔹 Re-apply flagged styling
|
|
1487
1590
|
RED = Qt.GlobalColor.red
|
|
1488
1591
|
normal = self.fileTree.palette().color(QPalette.ColorRole.WindowText)
|
|
1592
|
+
|
|
1489
1593
|
for idx, entry in enumerate(self.loaded_images):
|
|
1490
1594
|
item = self.get_tree_item_for_index(idx)
|
|
1491
1595
|
if not item:
|
|
@@ -1499,32 +1603,25 @@ class BlinkTab(QWidget):
|
|
|
1499
1603
|
item.setForeground(0, QBrush(normal))
|
|
1500
1604
|
|
|
1501
1605
|
|
|
1606
|
+
|
|
1502
1607
|
def _after_list_changed(self, removed_indices: List[int] | None = None):
|
|
1503
|
-
"""Call after you mutate image_paths/loaded_images. Keeps UI + metrics in sync w/o recompute."""
|
|
1504
|
-
# 1) rebuild the tree (groups collapse if empty)
|
|
1505
1608
|
self._rebuild_tree_from_loaded()
|
|
1506
1609
|
self.imagesChanged.emit(len(self.loaded_images))
|
|
1507
1610
|
|
|
1508
|
-
# 2) refresh metrics (if open) WITHOUT recomputing SEP
|
|
1509
1611
|
if self.metrics_window and self.metrics_window.isVisible():
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
self.metrics_window.remove_indices(list(removed_indices))
|
|
1514
|
-
else:
|
|
1515
|
-
# just order changed or paths changed -> replot current group
|
|
1516
|
-
self.metrics_window.update_metrics(
|
|
1517
|
-
self.loaded_images,
|
|
1518
|
-
order=self._tree_order_indices()
|
|
1519
|
-
)
|
|
1612
|
+
# ✅ safest: rebind images + rebuild plot order from tree
|
|
1613
|
+
self.metrics_window.set_images(self.loaded_images, order=self._tree_order_indices())
|
|
1614
|
+
self._sync_metrics_flags()
|
|
1520
1615
|
|
|
1521
1616
|
def get_tree_item_for_index(self, idx):
|
|
1522
|
-
|
|
1617
|
+
target_path = self.image_paths[idx]
|
|
1523
1618
|
for item in self.get_all_leaf_items():
|
|
1524
|
-
|
|
1619
|
+
p = item.data(0, Qt.ItemDataRole.UserRole)
|
|
1620
|
+
if p == target_path:
|
|
1525
1621
|
return item
|
|
1526
1622
|
return None
|
|
1527
1623
|
|
|
1624
|
+
|
|
1528
1625
|
def compute_metric(self, metric_idx, entry):
|
|
1529
1626
|
"""Recompute a single metric for one image. Use cached orig_background for metric 2."""
|
|
1530
1627
|
# metric 2 is the pre-stretch background we already computed
|
|
@@ -1573,7 +1670,7 @@ class BlinkTab(QWidget):
|
|
|
1573
1670
|
|
|
1574
1671
|
def openDirectoryDialog(self):
|
|
1575
1672
|
"""Allow users to select a directory and load all images within it recursively."""
|
|
1576
|
-
directory = QFileDialog.getExistingDirectory(self, "Select Directory", "")
|
|
1673
|
+
directory = QFileDialog.getExistingDirectory(self, self.tr("Select Directory"), "")
|
|
1577
1674
|
if directory:
|
|
1578
1675
|
# Supported image extensions
|
|
1579
1676
|
supported_extensions = (
|
|
@@ -1594,15 +1691,15 @@ class BlinkTab(QWidget):
|
|
|
1594
1691
|
if new_file_paths:
|
|
1595
1692
|
self.loadImages(new_file_paths)
|
|
1596
1693
|
else:
|
|
1597
|
-
QMessageBox.information(self, "No Images Found", "No supported image files were found in the selected directory.")
|
|
1694
|
+
QMessageBox.information(self, self.tr("No Images Found"), self.tr("No supported image files were found in the selected directory."))
|
|
1598
1695
|
|
|
1599
1696
|
|
|
1600
1697
|
def clearImages(self):
|
|
1601
1698
|
"""Clear all loaded images and reset the tree view."""
|
|
1602
1699
|
confirmation = QMessageBox.question(
|
|
1603
1700
|
self,
|
|
1604
|
-
"Clear All Images",
|
|
1605
|
-
"Are you sure you want to clear all loaded images?",
|
|
1701
|
+
self.tr("Clear All Images"),
|
|
1702
|
+
self.tr("Are you sure you want to clear all loaded images?"),
|
|
1606
1703
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
1607
1704
|
QMessageBox.StandardButton.No
|
|
1608
1705
|
)
|
|
@@ -1613,10 +1710,10 @@ class BlinkTab(QWidget):
|
|
|
1613
1710
|
self.image_labels.clear()
|
|
1614
1711
|
self.fileTree.clear()
|
|
1615
1712
|
self.preview_label.clear()
|
|
1616
|
-
self.preview_label.setText('No image selected.')
|
|
1713
|
+
self.preview_label.setText(self.tr('No image selected.'))
|
|
1617
1714
|
self.current_pixmap = None
|
|
1618
1715
|
self.progress_bar.setValue(0)
|
|
1619
|
-
self.loading_label.setText("Loading images...")
|
|
1716
|
+
self.loading_label.setText(self.tr("Loading images..."))
|
|
1620
1717
|
self.imagesChanged.emit(len(self.loaded_images))
|
|
1621
1718
|
|
|
1622
1719
|
# (legacy) if you still have this, you can delete it:
|
|
@@ -1649,31 +1746,27 @@ class BlinkTab(QWidget):
|
|
|
1649
1746
|
# 1) load
|
|
1650
1747
|
image, header, bit_depth, is_mono = load_image(file_path)
|
|
1651
1748
|
if image is None or image.size == 0:
|
|
1652
|
-
|
|
1749
|
+
msg = QCoreApplication.translate("BlinkTab", "Empty image")
|
|
1750
|
+
raise ValueError(msg)
|
|
1653
1751
|
|
|
1654
1752
|
# 2) optional debayer
|
|
1655
1753
|
if is_mono:
|
|
1656
|
-
# adjust this call to match your debayer signature
|
|
1657
1754
|
image = BlinkTab.debayer_image(image, file_path, header)
|
|
1658
1755
|
|
|
1659
|
-
# ✅ NEW: force 0..1 range BEFORE SEP + stretch
|
|
1660
1756
|
image = BlinkTab._ensure_float01(image)
|
|
1661
1757
|
|
|
1662
|
-
# 3) SEP background on mono float32
|
|
1663
1758
|
data = np.asarray(image, dtype=np.float32, order='C')
|
|
1664
1759
|
if data.ndim == 3:
|
|
1665
1760
|
data = data.mean(axis=2)
|
|
1666
1761
|
bkg = sep.Background(data)
|
|
1667
1762
|
global_back = bkg.globalback
|
|
1668
1763
|
|
|
1669
|
-
# 4) stretch
|
|
1670
1764
|
target_med = 0.25
|
|
1671
1765
|
if image.ndim == 2:
|
|
1672
1766
|
stretched = stretch_mono_image(image, target_med)
|
|
1673
1767
|
else:
|
|
1674
1768
|
stretched = stretch_color_image(image, target_med, linked=False)
|
|
1675
1769
|
|
|
1676
|
-
# 5) cast to target_dtype
|
|
1677
1770
|
clipped = np.clip(stretched, 0.0, 1.0)
|
|
1678
1771
|
if target_dtype is np.uint8:
|
|
1679
1772
|
stored = (clipped * 255).astype(np.uint8)
|
|
@@ -1823,7 +1916,7 @@ class BlinkTab(QWidget):
|
|
|
1823
1916
|
leaf.setData(0, Qt.ItemDataRole.UserRole, p)
|
|
1824
1917
|
exp_item.addChild(leaf)
|
|
1825
1918
|
|
|
1826
|
-
self.loading_label.setText(
|
|
1919
|
+
self.loading_label.setText(self.tr("Loaded {0} images.").format(len(self.loaded_images)))
|
|
1827
1920
|
self.progress_bar.setValue(100)
|
|
1828
1921
|
self.imagesChanged.emit(len(self.loaded_images))
|
|
1829
1922
|
if self.metrics_window and self.metrics_window.isVisible():
|
|
@@ -1848,33 +1941,35 @@ class BlinkTab(QWidget):
|
|
|
1848
1941
|
|
|
1849
1942
|
|
|
1850
1943
|
def _toggle_flag_on_item(self, item: QTreeWidgetItem, *, sync_metrics: bool = True):
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
if file_path is None:
|
|
1944
|
+
idx = self._leaf_index(item)
|
|
1945
|
+
if idx is None:
|
|
1854
1946
|
return
|
|
1855
1947
|
|
|
1856
|
-
idx = self.image_paths.index(file_path)
|
|
1857
1948
|
entry = self.loaded_images[idx]
|
|
1858
|
-
entry['flagged'] = not entry
|
|
1949
|
+
entry['flagged'] = not bool(entry.get('flagged', False))
|
|
1859
1950
|
|
|
1860
1951
|
RED = Qt.GlobalColor.red
|
|
1861
|
-
|
|
1862
|
-
|
|
1952
|
+
normal_color = self.fileTree.palette().color(QPalette.ColorRole.WindowText)
|
|
1953
|
+
|
|
1954
|
+
base = os.path.basename(self.image_paths[idx])
|
|
1863
1955
|
|
|
1864
1956
|
if entry['flagged']:
|
|
1865
|
-
item.setText(0, f"⚠️ {
|
|
1957
|
+
item.setText(0, f"⚠️ {base}")
|
|
1866
1958
|
item.setForeground(0, QBrush(RED))
|
|
1867
1959
|
else:
|
|
1868
|
-
item.setText(0,
|
|
1960
|
+
item.setText(0, base)
|
|
1869
1961
|
item.setForeground(0, QBrush(normal_color))
|
|
1870
1962
|
|
|
1963
|
+
# Keep UserRole correct (in case this was a legacy leaf)
|
|
1964
|
+
item.setData(0, Qt.ItemDataRole.UserRole, self.image_paths[idx])
|
|
1965
|
+
|
|
1871
1966
|
if sync_metrics:
|
|
1872
1967
|
self._sync_metrics_flags()
|
|
1873
1968
|
|
|
1874
1969
|
def flag_current_image(self):
|
|
1875
1970
|
item = self.fileTree.currentItem()
|
|
1876
1971
|
if not item:
|
|
1877
|
-
QMessageBox.warning(self, "No Selection", "No image is currently selected to flag.")
|
|
1972
|
+
QMessageBox.warning(self, self.tr("No Selection"), self.tr("No image is currently selected to flag."))
|
|
1878
1973
|
return
|
|
1879
1974
|
self._toggle_flag_on_item(item) # ← this now updates the metrics panel too
|
|
1880
1975
|
self.next_item()
|
|
@@ -1950,7 +2045,7 @@ class BlinkTab(QWidget):
|
|
|
1950
2045
|
|
|
1951
2046
|
leaves = self.get_all_leaf_items()
|
|
1952
2047
|
if not leaves:
|
|
1953
|
-
QMessageBox.information(self, "No Images", "Load some images first.")
|
|
2048
|
+
QMessageBox.information(self, self.tr("No Images"), self.tr("Load some images first."))
|
|
1954
2049
|
return
|
|
1955
2050
|
|
|
1956
2051
|
# Ensure a current leaf item is selected
|
|
@@ -1972,9 +2067,9 @@ class BlinkTab(QWidget):
|
|
|
1972
2067
|
"""Allow users to select multiple images and add them to the existing list."""
|
|
1973
2068
|
file_paths, _ = QFileDialog.getOpenFileNames(
|
|
1974
2069
|
self,
|
|
1975
|
-
"Open Images",
|
|
2070
|
+
self.tr("Open Images"),
|
|
1976
2071
|
"",
|
|
1977
|
-
"Images (*.png *.tif *.tiff *.fits *.fit *.xisf *.cr2 *.cr3 *.nef *.arw *.dng *.raf *.orf *.rw2 *.pef);;All Files (*)"
|
|
2072
|
+
self.tr("Images (*.png *.tif *.tiff *.fits *.fit *.xisf *.cr2 *.cr3 *.nef *.arw *.dng *.raf *.orf *.rw2 *.pef);;All Files (*)")
|
|
1978
2073
|
)
|
|
1979
2074
|
|
|
1980
2075
|
# Filter out already loaded images to prevent duplicates
|
|
@@ -1983,7 +2078,7 @@ class BlinkTab(QWidget):
|
|
|
1983
2078
|
if new_file_paths:
|
|
1984
2079
|
self.loadImages(new_file_paths)
|
|
1985
2080
|
else:
|
|
1986
|
-
QMessageBox.information(self, "No New Images", "No new images were selected or all selected images are already loaded.")
|
|
2081
|
+
QMessageBox.information(self, self.tr("No New Images"), self.tr("No new images were selected or all selected images are already loaded."))
|
|
1987
2082
|
|
|
1988
2083
|
|
|
1989
2084
|
def debayer_fits(self, image_data, bayer_pattern):
|
|
@@ -2033,7 +2128,7 @@ class BlinkTab(QWidget):
|
|
|
2033
2128
|
return np.stack([r, g, b], axis=-1)
|
|
2034
2129
|
|
|
2035
2130
|
else:
|
|
2036
|
-
raise ValueError(
|
|
2131
|
+
raise ValueError(self.tr("Unsupported Bayer pattern: {0}").format(bayer_pattern))
|
|
2037
2132
|
|
|
2038
2133
|
def remove_item_from_tree(self, file_path):
|
|
2039
2134
|
"""Remove a specific item from the tree view based on file path."""
|
|
@@ -2150,59 +2245,36 @@ class BlinkTab(QWidget):
|
|
|
2150
2245
|
g = (g1 + g2) / 2
|
|
2151
2246
|
return np.stack([r, g, b], axis=-1)
|
|
2152
2247
|
else:
|
|
2153
|
-
raise ValueError(
|
|
2248
|
+
raise ValueError(self.tr("Unsupported Bayer pattern: {0}").format(bayer_pattern))
|
|
2154
2249
|
|
|
2155
2250
|
|
|
2156
2251
|
|
|
2157
2252
|
def on_item_clicked(self, item, column):
|
|
2158
2253
|
self.fileTree.setFocus()
|
|
2254
|
+
if not item or item.childCount() > 0:
|
|
2255
|
+
return
|
|
2159
2256
|
|
|
2160
|
-
|
|
2161
|
-
file_path = next((p for p in self.image_paths if os.path.basename(p) == name), None)
|
|
2257
|
+
file_path = self._leaf_path(item)
|
|
2162
2258
|
if not file_path:
|
|
2163
2259
|
return
|
|
2164
2260
|
|
|
2165
2261
|
self._capture_view_center_norm()
|
|
2166
2262
|
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
# --- Fast path: just display what we cached in RAM ---
|
|
2172
|
-
if not self.aggressive_stretch_enabled:
|
|
2173
|
-
# Convert to 8-bit only if needed (no additional stretch)
|
|
2174
|
-
if stored.dtype == np.uint8:
|
|
2175
|
-
disp8 = stored
|
|
2176
|
-
elif stored.dtype == np.uint16:
|
|
2177
|
-
disp8 = (stored >> 8).astype(np.uint8) # ~ /257, quick & vectorized
|
|
2178
|
-
else: # float32 in [0..1]
|
|
2179
|
-
disp8 = (np.clip(stored, 0.0, 1.0) * 255.0).astype(np.uint8)
|
|
2180
|
-
|
|
2181
|
-
else:
|
|
2182
|
-
# Aggressive mode: compute only here (from float01)
|
|
2183
|
-
base01 = self._as_float01(stored)
|
|
2184
|
-
# Siril-style autostretch
|
|
2185
|
-
if base01.ndim == 2:
|
|
2186
|
-
st = siril_style_autostretch(base01, sigma=self.current_sigma)
|
|
2187
|
-
disp01 = self._as_float01(st) # <-- IMPORTANT: handles 0..255 or 0..1 correctly
|
|
2188
|
-
else:
|
|
2189
|
-
base01 = self._as_float01(stored)
|
|
2190
|
-
|
|
2191
|
-
if base01.ndim == 2:
|
|
2192
|
-
disp01 = self._aggressive_display_boost(base01, strength=self.current_sigma)
|
|
2193
|
-
else:
|
|
2194
|
-
lum = base01.mean(axis=2).astype(np.float32)
|
|
2195
|
-
lum_boost = self._aggressive_display_boost(lum, strength=self.current_sigma)
|
|
2196
|
-
gain = lum_boost / (lum + 1e-6)
|
|
2197
|
-
disp01 = np.clip(base01 * gain[..., None], 0.0, 1.0)
|
|
2263
|
+
try:
|
|
2264
|
+
idx = self.image_paths.index(file_path)
|
|
2265
|
+
except ValueError:
|
|
2266
|
+
return
|
|
2198
2267
|
|
|
2199
|
-
|
|
2268
|
+
entry = self.loaded_images[idx]
|
|
2200
2269
|
|
|
2270
|
+
# ✅ single source of truth (handles aggressive + mono + color)
|
|
2271
|
+
disp8 = self._make_display_frame(entry)
|
|
2201
2272
|
|
|
2202
2273
|
qimage = self.convert_to_qimage(disp8)
|
|
2203
2274
|
self.current_pixmap = QPixmap.fromImage(qimage)
|
|
2204
2275
|
self.apply_zoom()
|
|
2205
2276
|
|
|
2277
|
+
|
|
2206
2278
|
def _capture_view_center_norm(self):
|
|
2207
2279
|
"""Remember the current viewport center as a fraction of the content size."""
|
|
2208
2280
|
sa = self.scroll_area
|
|
@@ -2305,7 +2377,7 @@ class BlinkTab(QWidget):
|
|
|
2305
2377
|
self.apply_zoom()
|
|
2306
2378
|
else:
|
|
2307
2379
|
print("No image loaded. Cannot fit to preview.")
|
|
2308
|
-
QMessageBox.warning(self, "Warning", "No image loaded. Cannot fit to preview.")
|
|
2380
|
+
QMessageBox.warning(self, self.tr("Warning"), self.tr("No image loaded. Cannot fit to preview."))
|
|
2309
2381
|
|
|
2310
2382
|
def _is_leaf(self, item: Optional[QTreeWidgetItem]) -> bool:
|
|
2311
2383
|
return bool(item and item.childCount() == 0)
|
|
@@ -2318,108 +2390,173 @@ class BlinkTab(QWidget):
|
|
|
2318
2390
|
|
|
2319
2391
|
menu = QMenu(self)
|
|
2320
2392
|
|
|
2321
|
-
push_action = QAction("Open in Document Window", self)
|
|
2393
|
+
push_action = QAction(self.tr("Open in Document Window"), self)
|
|
2322
2394
|
push_action.triggered.connect(lambda: self.push_to_docs(item))
|
|
2323
2395
|
menu.addAction(push_action)
|
|
2324
2396
|
|
|
2325
|
-
rename_action = QAction("Rename", self)
|
|
2397
|
+
rename_action = QAction(self.tr("Rename"), self)
|
|
2326
2398
|
rename_action.triggered.connect(lambda: self.rename_item(item))
|
|
2327
2399
|
menu.addAction(rename_action)
|
|
2328
2400
|
|
|
2329
2401
|
# 🔹 NEW: batch rename selected
|
|
2330
|
-
batch_rename_action = QAction("Batch Rename Selected…", self)
|
|
2402
|
+
batch_rename_action = QAction(self.tr("Batch Rename Selected…"), self)
|
|
2331
2403
|
batch_rename_action.triggered.connect(self.batch_rename_items)
|
|
2332
2404
|
menu.addAction(batch_rename_action)
|
|
2333
2405
|
|
|
2334
|
-
move_action = QAction("Move Selected Items", self)
|
|
2406
|
+
move_action = QAction(self.tr("Move Selected Items"), self)
|
|
2335
2407
|
move_action.triggered.connect(self.move_items)
|
|
2336
2408
|
menu.addAction(move_action)
|
|
2337
2409
|
|
|
2338
|
-
delete_action = QAction("Delete Selected Items", self)
|
|
2410
|
+
delete_action = QAction(self.tr("Delete Selected Items"), self)
|
|
2339
2411
|
delete_action.triggered.connect(self.delete_items)
|
|
2340
2412
|
menu.addAction(delete_action)
|
|
2341
2413
|
|
|
2342
2414
|
menu.addSeparator()
|
|
2343
2415
|
|
|
2344
|
-
batch_delete_action = QAction("Delete All Flagged Images", self)
|
|
2416
|
+
batch_delete_action = QAction(self.tr("Delete All Flagged Images"), self)
|
|
2345
2417
|
batch_delete_action.triggered.connect(self.batch_delete_flagged_images)
|
|
2346
2418
|
menu.addAction(batch_delete_action)
|
|
2347
2419
|
|
|
2348
|
-
batch_move_action = QAction("Move All Flagged Images", self)
|
|
2420
|
+
batch_move_action = QAction(self.tr("Move All Flagged Images"), self)
|
|
2349
2421
|
batch_move_action.triggered.connect(self.batch_move_flagged_images)
|
|
2350
2422
|
menu.addAction(batch_move_action)
|
|
2351
2423
|
|
|
2352
2424
|
# 🔹 NEW: rename all flagged images
|
|
2353
|
-
rename_flagged_action = QAction("Rename Flagged Images…", self)
|
|
2425
|
+
rename_flagged_action = QAction(self.tr("Rename Flagged Images…"), self)
|
|
2354
2426
|
rename_flagged_action.triggered.connect(self.rename_flagged_images)
|
|
2355
2427
|
menu.addAction(rename_flagged_action)
|
|
2356
2428
|
|
|
2357
2429
|
menu.addSeparator()
|
|
2358
2430
|
|
|
2359
|
-
send_lights_act = QAction("Send to Stacking → Lights", self)
|
|
2431
|
+
send_lights_act = QAction(self.tr("Send to Stacking → Lights"), self)
|
|
2360
2432
|
send_lights_act.triggered.connect(self._send_to_stacking_lights)
|
|
2361
2433
|
menu.addAction(send_lights_act)
|
|
2362
2434
|
|
|
2363
|
-
send_integ_act = QAction("Send to Stacking → Integration", self)
|
|
2435
|
+
send_integ_act = QAction(self.tr("Send to Stacking → Integration"), self)
|
|
2364
2436
|
send_integ_act.triggered.connect(self._send_to_stacking_integration)
|
|
2365
2437
|
menu.addAction(send_integ_act)
|
|
2366
2438
|
|
|
2367
2439
|
menu.exec(self.fileTree.mapToGlobal(pos))
|
|
2368
2440
|
|
|
2369
2441
|
|
|
2370
|
-
def push_to_docs(self, item):
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2442
|
+
def push_to_docs(self, item: QTreeWidgetItem):
|
|
2443
|
+
"""
|
|
2444
|
+
Push the currently selected blink leaf image into DocManager as a new document,
|
|
2445
|
+
preserving all original metadata (original_header, meta, bit_depth, is_mono, etc.)
|
|
2446
|
+
and swapping ONLY the numpy image array.
|
|
2447
|
+
"""
|
|
2448
|
+
if not item or item.childCount() > 0:
|
|
2449
|
+
return
|
|
2450
|
+
|
|
2451
|
+
# --- Resolve full path safely (UserRole-first) ---
|
|
2452
|
+
file_path = item.data(0, Qt.ItemDataRole.UserRole)
|
|
2453
|
+
if not file_path or not isinstance(file_path, str):
|
|
2454
|
+
# legacy fallback: try to map by displayed name
|
|
2455
|
+
file_name = item.text(0).lstrip("⚠️ ").strip()
|
|
2456
|
+
file_path = next((p for p in self.image_paths if os.path.basename(p) == file_name), None)
|
|
2457
|
+
|
|
2374
2458
|
if not file_path:
|
|
2375
2459
|
return
|
|
2376
|
-
|
|
2460
|
+
|
|
2461
|
+
try:
|
|
2462
|
+
idx = self.image_paths.index(file_path)
|
|
2463
|
+
except ValueError:
|
|
2464
|
+
return
|
|
2465
|
+
|
|
2377
2466
|
entry = self.loaded_images[idx]
|
|
2378
2467
|
|
|
2379
|
-
# Find main window + doc manager
|
|
2468
|
+
# --- Find main window + doc manager ---
|
|
2380
2469
|
mw = self._main_window()
|
|
2381
2470
|
dm = self.doc_manager or (getattr(mw, "docman", None) if mw else None)
|
|
2382
2471
|
if not mw or not dm:
|
|
2383
|
-
QMessageBox.warning(self, "Document Manager", "Main window or DocManager not available.")
|
|
2472
|
+
QMessageBox.warning(self, self.tr("Document Manager"), self.tr("Main window or DocManager not available."))
|
|
2384
2473
|
return
|
|
2385
2474
|
|
|
2386
|
-
#
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2475
|
+
# --- Build the swapped payload (image replaced, metadata preserved) ---
|
|
2476
|
+
# Whatever you're storing as entry['image_data'] (uint16/float/etc), normalize to float01 for display pipeline.
|
|
2477
|
+
# If your DocManager expects native dtype instead, swap _as_float01 for your native image.
|
|
2478
|
+
np_image_f01 = self._as_float01(entry["image_data"]).astype(np.float32, copy=False)
|
|
2479
|
+
|
|
2480
|
+
# Preserve your full load_image return structure as much as possible:
|
|
2481
|
+
# load_image returns: image, original_header, bit_depth, is_mono, meta
|
|
2482
|
+
original_header = entry.get("original_header", entry.get("header", None))
|
|
2483
|
+
bit_depth = entry.get("bit_depth", None)
|
|
2484
|
+
is_mono = entry.get("is_mono", None)
|
|
2485
|
+
meta = entry.get("meta", {})
|
|
2486
|
+
|
|
2487
|
+
# Keep meta dict style your app uses; add source tag without clobbering
|
|
2488
|
+
if isinstance(meta, dict):
|
|
2489
|
+
meta = dict(meta)
|
|
2490
|
+
meta.setdefault("source", "BlinkComparatorPro")
|
|
2491
|
+
meta.setdefault("file_path", file_path)
|
|
2492
|
+
|
|
2493
|
+
# This is the "all the other stuff" you wanted preserved
|
|
2494
|
+
payload = {
|
|
2495
|
+
"file_path": file_path,
|
|
2496
|
+
"original_header": original_header,
|
|
2497
|
+
"bit_depth": bit_depth,
|
|
2498
|
+
"is_mono": is_mono,
|
|
2499
|
+
"meta": meta,
|
|
2500
|
+
"source": "BlinkComparatorPro",
|
|
2394
2501
|
}
|
|
2502
|
+
|
|
2395
2503
|
title = os.path.basename(file_path)
|
|
2396
2504
|
|
|
2397
|
-
# Create
|
|
2505
|
+
# --- Create document using whatever DocManager API exists ---
|
|
2398
2506
|
doc = None
|
|
2399
2507
|
try:
|
|
2400
|
-
if
|
|
2401
|
-
|
|
2508
|
+
# Preferred: if you have a method that mirrors open_file/load_image shape
|
|
2509
|
+
if hasattr(dm, "open_from_load_image"):
|
|
2510
|
+
# (image, original_header, bit_depth, is_mono, meta)
|
|
2511
|
+
doc = dm.open_from_load_image(np_image_f01, original_header, bit_depth, is_mono, meta, title=title)
|
|
2512
|
+
|
|
2513
|
+
elif hasattr(dm, "open_array"):
|
|
2514
|
+
# Some of your code expects metadata in doc.metadata; pass payload whole
|
|
2515
|
+
doc = dm.open_array(np_image_f01, metadata=payload, title=title)
|
|
2516
|
+
|
|
2402
2517
|
elif hasattr(dm, "open_numpy"):
|
|
2403
|
-
doc = dm.open_numpy(np_image_f01, metadata=
|
|
2518
|
+
doc = dm.open_numpy(np_image_f01, metadata=payload, title=title)
|
|
2519
|
+
|
|
2404
2520
|
elif hasattr(dm, "create_document"):
|
|
2405
|
-
|
|
2521
|
+
# Try both signatures
|
|
2522
|
+
try:
|
|
2523
|
+
doc = dm.create_document(image=np_image_f01, metadata=payload, name=title)
|
|
2524
|
+
except TypeError:
|
|
2525
|
+
doc = dm.create_document(np_image_f01, payload, title)
|
|
2526
|
+
|
|
2406
2527
|
else:
|
|
2407
|
-
raise AttributeError("DocManager lacks
|
|
2528
|
+
raise AttributeError("DocManager lacks a known creation method")
|
|
2529
|
+
|
|
2408
2530
|
except Exception as e:
|
|
2409
|
-
QMessageBox.critical(self, "Doc Manager",
|
|
2531
|
+
QMessageBox.critical(self, self.tr("Doc Manager"), self.tr("Failed to create document:\n{0}").format(e))
|
|
2410
2532
|
return
|
|
2411
2533
|
|
|
2412
2534
|
if doc is None:
|
|
2413
|
-
QMessageBox.critical(self, "Doc Manager", "DocManager returned no document.")
|
|
2535
|
+
QMessageBox.critical(self, self.tr("Doc Manager"), self.tr("DocManager returned no document."))
|
|
2414
2536
|
return
|
|
2415
2537
|
|
|
2416
|
-
#
|
|
2538
|
+
# --- Hand off to DocManager flow (DocManager should trigger MDI + window creation) ---
|
|
2417
2539
|
try:
|
|
2418
|
-
|
|
2540
|
+
# If your architecture already auto-spawns windows on documentAdded,
|
|
2541
|
+
# you should NOT call mw._spawn_subwindow_for(doc) here.
|
|
2542
|
+
if hasattr(dm, "add_document"):
|
|
2543
|
+
dm.add_document(doc)
|
|
2544
|
+
elif hasattr(dm, "register_document"):
|
|
2545
|
+
dm.register_document(doc)
|
|
2546
|
+
else:
|
|
2547
|
+
# If open_array/open_numpy already registers the doc internally, do nothing.
|
|
2548
|
+
pass
|
|
2549
|
+
|
|
2550
|
+
# If you *must* spawn manually (older path), keep as fallback
|
|
2551
|
+
if hasattr(mw, "_spawn_subwindow_for"):
|
|
2552
|
+
mw._spawn_subwindow_for(doc)
|
|
2553
|
+
|
|
2419
2554
|
if hasattr(mw, "_log"):
|
|
2420
2555
|
mw._log(f"Blink → opened '{title}' as new document")
|
|
2556
|
+
|
|
2421
2557
|
except Exception as e:
|
|
2422
|
-
QMessageBox.critical(self, "UI",
|
|
2558
|
+
QMessageBox.critical(self, self.tr("UI"), self.tr("Failed to open subwindow:\n{0}").format(e))
|
|
2559
|
+
|
|
2423
2560
|
|
|
2424
2561
|
|
|
2425
2562
|
# optional shim to keep any old calls working
|
|
@@ -2428,27 +2565,55 @@ class BlinkTab(QWidget):
|
|
|
2428
2565
|
|
|
2429
2566
|
|
|
2430
2567
|
|
|
2431
|
-
def rename_item(self, item):
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
new_name, ok = QInputDialog.getText(self, "Rename Image", "Enter new name:", text=current_name)
|
|
2568
|
+
def rename_item(self, item: QTreeWidgetItem):
|
|
2569
|
+
if not item or item.childCount() > 0:
|
|
2570
|
+
return
|
|
2435
2571
|
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2572
|
+
idx = self._leaf_index(item)
|
|
2573
|
+
if idx is None:
|
|
2574
|
+
return
|
|
2575
|
+
|
|
2576
|
+
old_path = self.image_paths[idx]
|
|
2577
|
+
old_base = os.path.basename(old_path)
|
|
2578
|
+
|
|
2579
|
+
new_name, ok = QInputDialog.getText(
|
|
2580
|
+
self,
|
|
2581
|
+
self.tr("Rename Image"),
|
|
2582
|
+
self.tr("Enter new name:"),
|
|
2583
|
+
text=old_base
|
|
2584
|
+
)
|
|
2585
|
+
if not ok:
|
|
2586
|
+
return
|
|
2587
|
+
|
|
2588
|
+
new_name = (new_name or "").strip()
|
|
2589
|
+
if not new_name:
|
|
2590
|
+
return
|
|
2591
|
+
|
|
2592
|
+
new_path = os.path.join(os.path.dirname(old_path), new_name)
|
|
2593
|
+
|
|
2594
|
+
# Avoid overwrite
|
|
2595
|
+
if os.path.exists(new_path):
|
|
2596
|
+
QMessageBox.critical(self, self.tr("Error"), self.tr("A file with that name already exists."))
|
|
2597
|
+
return
|
|
2598
|
+
|
|
2599
|
+
try:
|
|
2600
|
+
os.rename(old_path, new_path)
|
|
2601
|
+
except Exception as e:
|
|
2602
|
+
QMessageBox.critical(self, self.tr("Error"), self.tr("Failed to rename the file: {0}").format(e))
|
|
2603
|
+
return
|
|
2604
|
+
|
|
2605
|
+
# Update internal structures
|
|
2606
|
+
self.image_paths[idx] = new_path
|
|
2607
|
+
self.loaded_images[idx]['file_path'] = new_path
|
|
2608
|
+
|
|
2609
|
+
# Update the leaf item
|
|
2610
|
+
flagged = bool(self.loaded_images[idx].get("flagged", False))
|
|
2611
|
+
self._set_leaf_display(item, base_name=new_name, flagged=flagged, full_path=new_path)
|
|
2612
|
+
|
|
2613
|
+
# Rebuild so natural sort stays correct and groups update
|
|
2614
|
+
self._after_list_changed()
|
|
2615
|
+
self._sync_metrics_flags()
|
|
2441
2616
|
|
|
2442
|
-
try:
|
|
2443
|
-
# Rename the file
|
|
2444
|
-
os.rename(file_path, new_file_path)
|
|
2445
|
-
print(f"File renamed from {current_name} to {new_name}")
|
|
2446
|
-
|
|
2447
|
-
# Update the image paths and tree view
|
|
2448
|
-
self.image_paths[self.image_paths.index(file_path)] = new_file_path
|
|
2449
|
-
item.setText(0, new_name)
|
|
2450
|
-
except Exception as e:
|
|
2451
|
-
QMessageBox.critical(self, "Error", f"Failed to rename the file: {e}")
|
|
2452
2617
|
|
|
2453
2618
|
def rename_flagged_images(self):
|
|
2454
2619
|
"""Prefix all *flagged* images on disk and in the tree."""
|
|
@@ -2459,17 +2624,17 @@ class BlinkTab(QWidget):
|
|
|
2459
2624
|
if not flagged_indices:
|
|
2460
2625
|
QMessageBox.information(
|
|
2461
2626
|
self,
|
|
2462
|
-
"Rename Flagged Images",
|
|
2463
|
-
"There are no flagged images to rename."
|
|
2627
|
+
self.tr("Rename Flagged Images"),
|
|
2628
|
+
self.tr("There are no flagged images to rename.")
|
|
2464
2629
|
)
|
|
2465
2630
|
return
|
|
2466
2631
|
|
|
2467
2632
|
# Small dialog like in your mockup: just a prefix field
|
|
2468
2633
|
dlg = QDialog(self)
|
|
2469
|
-
dlg.setWindowTitle("Rename flagged images")
|
|
2634
|
+
dlg.setWindowTitle(self.tr("Rename flagged images"))
|
|
2470
2635
|
layout = QVBoxLayout(dlg)
|
|
2471
2636
|
|
|
2472
|
-
layout.addWidget(QLabel("Prefix to add to flagged image filenames:", dlg))
|
|
2637
|
+
layout.addWidget(QLabel(self.tr("Prefix to add to flagged image filenames:"), dlg))
|
|
2473
2638
|
|
|
2474
2639
|
prefix_edit = QLineEdit(dlg)
|
|
2475
2640
|
prefix_edit.setText("Bad_") # sensible default
|
|
@@ -2494,9 +2659,9 @@ class BlinkTab(QWidget):
|
|
|
2494
2659
|
# Allow empty but warn – otherwise user may be confused
|
|
2495
2660
|
ret = QMessageBox.question(
|
|
2496
2661
|
self,
|
|
2497
|
-
"No Prefix",
|
|
2498
|
-
"No prefix entered. This will not change any filenames.\n\n"
|
|
2499
|
-
"Continue anyway?",
|
|
2662
|
+
self.tr("No Prefix"),
|
|
2663
|
+
self.tr("No prefix entered. This will not change any filenames.\n\n"
|
|
2664
|
+
"Continue anyway?"),
|
|
2500
2665
|
QMessageBox.StandardButton.Yes | QDialogButtonBox.StandardButton.No,
|
|
2501
2666
|
QMessageBox.StandardButton.No,
|
|
2502
2667
|
)
|
|
@@ -2549,102 +2714,125 @@ class BlinkTab(QWidget):
|
|
|
2549
2714
|
# Also sync the metrics panel flags/colors
|
|
2550
2715
|
self._sync_metrics_flags()
|
|
2551
2716
|
|
|
2552
|
-
msg =
|
|
2717
|
+
msg = self.tr("Renamed {0} flagged image{1}.").format(successes, 's' if successes != 1 else '')
|
|
2553
2718
|
if failures:
|
|
2554
|
-
msg +=
|
|
2719
|
+
msg += self.tr("\n\n{0} file(s) could not be renamed:").format(len(failures))
|
|
2555
2720
|
for old, err in failures[:10]: # don’t spam too hard
|
|
2556
2721
|
msg += f"\n• {os.path.basename(old)} – {err}"
|
|
2557
2722
|
|
|
2558
|
-
QMessageBox.information(self, "Rename Flagged Images", msg)
|
|
2723
|
+
QMessageBox.information(self, self.tr("Rename Flagged Images"), msg)
|
|
2559
2724
|
|
|
2560
2725
|
|
|
2561
2726
|
def batch_rename_items(self):
|
|
2562
|
-
"""Batch rename selected items by adding a prefix or suffix."""
|
|
2563
|
-
selected_items = self.fileTree.selectedItems()
|
|
2564
|
-
|
|
2727
|
+
"""Batch rename selected leaf items by adding a prefix and/or suffix."""
|
|
2728
|
+
selected_items = [it for it in self.fileTree.selectedItems() if it and it.childCount() == 0]
|
|
2565
2729
|
if not selected_items:
|
|
2566
|
-
QMessageBox.warning(self, "Warning", "No items selected for renaming.")
|
|
2730
|
+
QMessageBox.warning(self, self.tr("Warning"), self.tr("No individual image items selected for renaming."))
|
|
2567
2731
|
return
|
|
2568
2732
|
|
|
2569
|
-
# Create a custom dialog for entering the prefix and suffix
|
|
2570
2733
|
dialog = QDialog(self)
|
|
2571
|
-
dialog.setWindowTitle("Batch Rename")
|
|
2734
|
+
dialog.setWindowTitle(self.tr("Batch Rename"))
|
|
2572
2735
|
dialog_layout = QVBoxLayout(dialog)
|
|
2573
2736
|
|
|
2574
|
-
|
|
2575
|
-
dialog_layout.addWidget(instruction_label)
|
|
2737
|
+
dialog_layout.addWidget(QLabel(self.tr("Enter a prefix or suffix to rename selected files:"), dialog))
|
|
2576
2738
|
|
|
2577
|
-
# Create fields for prefix and suffix
|
|
2578
2739
|
form_layout = QHBoxLayout()
|
|
2579
|
-
|
|
2580
2740
|
prefix_field = QLineEdit(dialog)
|
|
2581
|
-
prefix_field.setPlaceholderText("Prefix")
|
|
2741
|
+
prefix_field.setPlaceholderText(self.tr("Prefix"))
|
|
2582
2742
|
form_layout.addWidget(prefix_field)
|
|
2583
2743
|
|
|
2584
|
-
|
|
2585
|
-
|
|
2744
|
+
mid_label = QLabel(self.tr("filename"), dialog)
|
|
2745
|
+
mid_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
2746
|
+
form_layout.addWidget(mid_label)
|
|
2586
2747
|
|
|
2587
2748
|
suffix_field = QLineEdit(dialog)
|
|
2588
|
-
suffix_field.setPlaceholderText("Suffix")
|
|
2749
|
+
suffix_field.setPlaceholderText(self.tr("Suffix"))
|
|
2589
2750
|
form_layout.addWidget(suffix_field)
|
|
2590
|
-
|
|
2591
2751
|
dialog_layout.addLayout(form_layout)
|
|
2592
2752
|
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2753
|
+
btns = QHBoxLayout()
|
|
2754
|
+
ok_button = QPushButton(self.tr("OK"), dialog)
|
|
2755
|
+
cancel_button = QPushButton(self.tr("Cancel"), dialog)
|
|
2596
2756
|
ok_button.clicked.connect(dialog.accept)
|
|
2597
|
-
button_layout.addWidget(ok_button)
|
|
2598
|
-
|
|
2599
|
-
cancel_button = QPushButton("Cancel", dialog)
|
|
2600
2757
|
cancel_button.clicked.connect(dialog.reject)
|
|
2601
|
-
|
|
2758
|
+
btns.addWidget(ok_button)
|
|
2759
|
+
btns.addWidget(cancel_button)
|
|
2760
|
+
dialog_layout.addLayout(btns)
|
|
2602
2761
|
|
|
2603
|
-
|
|
2762
|
+
if dialog.exec() != QDialog.DialogCode.Accepted:
|
|
2763
|
+
return
|
|
2604
2764
|
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
prefix = prefix_field.text().strip()
|
|
2608
|
-
suffix = suffix_field.text().strip()
|
|
2765
|
+
prefix = (prefix_field.text() or "").strip()
|
|
2766
|
+
suffix = (suffix_field.text() or "").strip()
|
|
2609
2767
|
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
file_path = next((path for path in self.image_paths if os.path.basename(path) == current_name), None)
|
|
2768
|
+
if not prefix and not suffix:
|
|
2769
|
+
QMessageBox.information(self, self.tr("Batch Rename"), self.tr("No prefix or suffix entered. Nothing to do."))
|
|
2770
|
+
return
|
|
2614
2771
|
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
directory = os.path.dirname(file_path)
|
|
2618
|
-
new_name = f"{prefix}{current_name}{suffix}"
|
|
2619
|
-
new_file_path = os.path.join(directory, new_name)
|
|
2772
|
+
renamed = 0
|
|
2773
|
+
failures = []
|
|
2620
2774
|
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2775
|
+
# Work on indices so we can update lists safely
|
|
2776
|
+
indices = []
|
|
2777
|
+
for it in selected_items:
|
|
2778
|
+
idx = self._leaf_index(it)
|
|
2779
|
+
if idx is not None:
|
|
2780
|
+
indices.append((idx, it))
|
|
2625
2781
|
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2782
|
+
for idx, it in indices:
|
|
2783
|
+
old_path = self.image_paths[idx]
|
|
2784
|
+
directory, base = os.path.split(old_path)
|
|
2629
2785
|
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2786
|
+
new_base = f"{prefix}{base}{suffix}"
|
|
2787
|
+
new_path = os.path.join(directory, new_base)
|
|
2788
|
+
|
|
2789
|
+
if new_path == old_path:
|
|
2790
|
+
continue
|
|
2791
|
+
|
|
2792
|
+
if os.path.exists(new_path):
|
|
2793
|
+
failures.append((old_path, self.tr("target already exists")))
|
|
2794
|
+
continue
|
|
2795
|
+
|
|
2796
|
+
try:
|
|
2797
|
+
os.rename(old_path, new_path)
|
|
2798
|
+
except Exception as e:
|
|
2799
|
+
failures.append((old_path, str(e)))
|
|
2800
|
+
continue
|
|
2801
|
+
|
|
2802
|
+
# Update internal lists
|
|
2803
|
+
self.image_paths[idx] = new_path
|
|
2804
|
+
self.loaded_images[idx]["file_path"] = new_path
|
|
2805
|
+
|
|
2806
|
+
# Update leaf item
|
|
2807
|
+
flagged = bool(self.loaded_images[idx].get("flagged", False))
|
|
2808
|
+
self._set_leaf_display(it, base_name=new_base, flagged=flagged, full_path=new_path)
|
|
2809
|
+
|
|
2810
|
+
renamed += 1
|
|
2811
|
+
|
|
2812
|
+
# Rebuild so group headers + natural order stay correct
|
|
2813
|
+
self._after_list_changed()
|
|
2814
|
+
self._sync_metrics_flags()
|
|
2815
|
+
|
|
2816
|
+
msg = self.tr("Batch renamed {0} file{1}.").format(renamed, "s" if renamed != 1 else "")
|
|
2817
|
+
if failures:
|
|
2818
|
+
msg += self.tr("\n\n{0} file(s) failed:").format(len(failures))
|
|
2819
|
+
for old, err in failures[:10]:
|
|
2820
|
+
msg += f"\n• {os.path.basename(old)} – {err}"
|
|
2821
|
+
QMessageBox.information(self, self.tr("Batch Rename"), msg)
|
|
2633
2822
|
|
|
2634
|
-
print(f"Batch renamed {len(selected_items)} items.")
|
|
2635
2823
|
|
|
2636
2824
|
def batch_delete_flagged_images(self):
|
|
2637
2825
|
"""Delete all flagged images."""
|
|
2638
2826
|
flagged_images = [img for img in self.loaded_images if img['flagged']]
|
|
2639
2827
|
|
|
2640
2828
|
if not flagged_images:
|
|
2641
|
-
QMessageBox.information(self, "No Flagged Images", "There are no flagged images to delete.")
|
|
2829
|
+
QMessageBox.information(self, self.tr("No Flagged Images"), self.tr("There are no flagged images to delete."))
|
|
2642
2830
|
return
|
|
2643
2831
|
|
|
2644
2832
|
confirmation = QMessageBox.question(
|
|
2645
2833
|
self,
|
|
2646
|
-
"Confirm Batch Deletion",
|
|
2647
|
-
|
|
2834
|
+
self.tr("Confirm Batch Deletion"),
|
|
2835
|
+
self.tr("Are you sure you want to permanently delete {0} flagged images? This action is irreversible.").format(len(flagged_images)),
|
|
2648
2836
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
2649
2837
|
QMessageBox.StandardButton.No
|
|
2650
2838
|
)
|
|
@@ -2672,147 +2860,187 @@ class BlinkTab(QWidget):
|
|
|
2672
2860
|
self.loaded_images.remove(img)
|
|
2673
2861
|
self.remove_item_from_tree(file_path)
|
|
2674
2862
|
|
|
2675
|
-
QMessageBox.information(self, "Batch Deletion",
|
|
2863
|
+
QMessageBox.information(self, self.tr("Batch Deletion"), self.tr("Deleted {0} flagged images.").format(len(removed_indices)))
|
|
2676
2864
|
|
|
2677
2865
|
# 🔁 refresh tree + metrics (no recompute)
|
|
2678
2866
|
self._after_list_changed(removed_indices)
|
|
2679
2867
|
|
|
2680
2868
|
def batch_move_flagged_images(self):
|
|
2681
|
-
"""Move all flagged images to a selected directory."""
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
QMessageBox.information(self, "No Flagged Images", "There are no flagged images to move.")
|
|
2869
|
+
"""Move all flagged images to a selected directory AND remove them from the blink list."""
|
|
2870
|
+
flagged_indices = [i for i, e in enumerate(self.loaded_images) if e.get("flagged", False)]
|
|
2871
|
+
if not flagged_indices:
|
|
2872
|
+
QMessageBox.information(self, self.tr("No Flagged Images"), self.tr("There are no flagged images to move."))
|
|
2686
2873
|
return
|
|
2687
2874
|
|
|
2688
|
-
|
|
2689
|
-
destination_dir = QFileDialog.getExistingDirectory(self, "Select Destination Folder", "")
|
|
2875
|
+
destination_dir = QFileDialog.getExistingDirectory(self, self.tr("Select Destination Folder"), "")
|
|
2690
2876
|
if not destination_dir:
|
|
2691
|
-
return
|
|
2877
|
+
return
|
|
2692
2878
|
|
|
2693
|
-
|
|
2694
|
-
src_path = img['file_path']
|
|
2695
|
-
file_name = os.path.basename(src_path)
|
|
2696
|
-
dest_path = os.path.join(destination_dir, file_name)
|
|
2879
|
+
failures = []
|
|
2697
2880
|
|
|
2881
|
+
# Move first (use current paths from indices)
|
|
2882
|
+
for i in flagged_indices:
|
|
2883
|
+
src_path = self.image_paths[i]
|
|
2884
|
+
dest_path = os.path.join(destination_dir, os.path.basename(src_path))
|
|
2698
2885
|
try:
|
|
2699
2886
|
os.rename(src_path, dest_path)
|
|
2700
|
-
print(f"Moved flagged image from {src_path} to {dest_path}")
|
|
2701
2887
|
except Exception as e:
|
|
2702
|
-
|
|
2703
|
-
QMessageBox.critical(self, "Error", f"Failed to move {src_path}: {e}")
|
|
2704
|
-
continue
|
|
2888
|
+
failures.append((src_path, str(e)))
|
|
2705
2889
|
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
img['flagged'] = False # Reset flag if desired
|
|
2890
|
+
# Remove from lists ONLY if move succeeded
|
|
2891
|
+
# Build a set of indices to remove: those that did NOT fail
|
|
2892
|
+
failed_src = {p for p, _ in failures}
|
|
2893
|
+
removed_indices = [i for i in flagged_indices if self.image_paths[i] not in failed_src]
|
|
2711
2894
|
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
self.
|
|
2895
|
+
removed_indices = sorted(set(removed_indices), reverse=True)
|
|
2896
|
+
for idx in removed_indices:
|
|
2897
|
+
if 0 <= idx < len(self.image_paths):
|
|
2898
|
+
del self.image_paths[idx]
|
|
2899
|
+
if 0 <= idx < len(self.loaded_images):
|
|
2900
|
+
del self.loaded_images[idx]
|
|
2901
|
+
|
|
2902
|
+
if removed_indices:
|
|
2903
|
+
self._after_list_changed(removed_indices)
|
|
2904
|
+
|
|
2905
|
+
if failures:
|
|
2906
|
+
msg = self.tr("Moved {0} flagged file(s). {1} failed:").format(len(removed_indices), len(failures))
|
|
2907
|
+
for p, err in failures[:10]:
|
|
2908
|
+
msg += f"\n• {os.path.basename(p)} – {err}"
|
|
2909
|
+
QMessageBox.warning(self, self.tr("Batch Move"), msg)
|
|
2910
|
+
else:
|
|
2911
|
+
QMessageBox.information(self, self.tr("Batch Move"), self.tr("Moved and removed {0} flagged image(s).").format(len(removed_indices)))
|
|
2715
2912
|
|
|
2716
|
-
QMessageBox.information(self, "Batch Move", f"Moved {len(flagged_images)} flagged images.")
|
|
2717
|
-
self._after_list_changed(removed_indices=None)
|
|
2718
2913
|
|
|
2719
2914
|
def move_items(self):
|
|
2720
|
-
"""Move selected images
|
|
2721
|
-
selected_items = self.fileTree.selectedItems()
|
|
2915
|
+
"""Move selected leaf images to a selected directory AND remove them from the blink list."""
|
|
2916
|
+
selected_items = [it for it in self.fileTree.selectedItems() if it and it.childCount() == 0]
|
|
2722
2917
|
if not selected_items:
|
|
2723
|
-
QMessageBox.warning(self, "Warning", "No items selected for moving.")
|
|
2918
|
+
QMessageBox.warning(self, self.tr("Warning"), self.tr("No individual image items selected for moving."))
|
|
2724
2919
|
return
|
|
2725
2920
|
|
|
2726
|
-
|
|
2727
|
-
new_dir = QFileDialog.getExistingDirectory(self,
|
|
2728
|
-
"Select Destination Folder",
|
|
2729
|
-
"")
|
|
2921
|
+
new_dir = QFileDialog.getExistingDirectory(self, self.tr("Select Destination Folder"), "")
|
|
2730
2922
|
if not new_dir:
|
|
2731
2923
|
return
|
|
2732
2924
|
|
|
2733
|
-
# Keep track of which on‐disk paths we actually moved
|
|
2734
|
-
moved_old_paths = []
|
|
2735
2925
|
removed_indices = []
|
|
2926
|
+
failures = []
|
|
2736
2927
|
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
if not
|
|
2928
|
+
# Collect (idx, old_path, item) first to avoid index drift
|
|
2929
|
+
triplets = []
|
|
2930
|
+
for it in selected_items:
|
|
2931
|
+
p = self._leaf_path(it)
|
|
2932
|
+
if not p:
|
|
2742
2933
|
continue
|
|
2743
|
-
|
|
2934
|
+
try:
|
|
2935
|
+
idx = self.image_paths.index(p)
|
|
2936
|
+
except ValueError:
|
|
2937
|
+
continue
|
|
2938
|
+
triplets.append((idx, p, it))
|
|
2744
2939
|
|
|
2745
|
-
|
|
2940
|
+
for idx, old_path, it in triplets:
|
|
2941
|
+
base = os.path.basename(old_path)
|
|
2942
|
+
new_path = os.path.join(new_dir, base)
|
|
2746
2943
|
try:
|
|
2747
2944
|
os.rename(old_path, new_path)
|
|
2748
2945
|
except Exception as e:
|
|
2749
|
-
|
|
2946
|
+
failures.append((old_path, str(e)))
|
|
2750
2947
|
continue
|
|
2751
2948
|
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
# 1) Remove the leaf from the tree
|
|
2755
|
-
parent = item.parent() or self.fileTree.invisibleRootItem()
|
|
2756
|
-
parent.removeChild(item)
|
|
2949
|
+
removed_indices.append(idx)
|
|
2757
2950
|
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
del self.loaded_images[idx]
|
|
2951
|
+
# remove leaf from tree immediately (optional; _after_list_changed will rebuild anyway)
|
|
2952
|
+
#parent = it.parent() or self.fileTree.invisibleRootItem()
|
|
2953
|
+
#parent.removeChild(it)
|
|
2762
2954
|
|
|
2763
|
-
|
|
2764
|
-
|
|
2955
|
+
# Purge arrays descending
|
|
2956
|
+
removed_indices = sorted(set(removed_indices), reverse=True)
|
|
2957
|
+
for idx in removed_indices:
|
|
2958
|
+
if 0 <= idx < len(self.image_paths):
|
|
2959
|
+
del self.image_paths[idx]
|
|
2960
|
+
if 0 <= idx < len(self.loaded_images):
|
|
2961
|
+
del self.loaded_images[idx]
|
|
2765
2962
|
|
|
2963
|
+
if removed_indices:
|
|
2964
|
+
self._after_list_changed(removed_indices)
|
|
2766
2965
|
|
|
2966
|
+
if failures:
|
|
2967
|
+
msg = self.tr("Moved {0} file(s). {1} failed:").format(len(removed_indices), len(failures))
|
|
2968
|
+
for old, err in failures[:10]:
|
|
2969
|
+
msg += f"\n• {os.path.basename(old)} – {err}"
|
|
2970
|
+
QMessageBox.warning(self, self.tr("Move Selected Items"), msg)
|
|
2971
|
+
else:
|
|
2972
|
+
QMessageBox.information(self, self.tr("Move Selected Items"), self.tr("Moved and removed {0} item(s).").format(len(removed_indices)))
|
|
2767
2973
|
|
|
2768
2974
|
def delete_items(self):
|
|
2769
|
-
"""Delete
|
|
2770
|
-
selected_items = self.fileTree.selectedItems()
|
|
2771
|
-
|
|
2975
|
+
"""Delete selected leaf images from disk and remove them from the blink list."""
|
|
2976
|
+
selected_items = [it for it in self.fileTree.selectedItems() if it and it.childCount() == 0]
|
|
2772
2977
|
if not selected_items:
|
|
2773
|
-
QMessageBox.warning(self, "Warning", "No items selected for deletion.")
|
|
2978
|
+
QMessageBox.warning(self, self.tr("Warning"), self.tr("No individual image items selected for deletion."))
|
|
2774
2979
|
return
|
|
2775
2980
|
|
|
2776
|
-
# Confirmation dialog
|
|
2777
2981
|
reply = QMessageBox.question(
|
|
2778
2982
|
self,
|
|
2779
|
-
|
|
2780
|
-
|
|
2983
|
+
self.tr("Confirm Deletion"),
|
|
2984
|
+
self.tr("Are you sure you want to permanently delete {0} selected images? This action is irreversible.").format(len(selected_items)),
|
|
2781
2985
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
2782
2986
|
QMessageBox.StandardButton.No
|
|
2783
2987
|
)
|
|
2988
|
+
if reply != QMessageBox.StandardButton.Yes:
|
|
2989
|
+
return
|
|
2784
2990
|
|
|
2785
2991
|
removed_indices = []
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2992
|
+
failures = []
|
|
2993
|
+
|
|
2994
|
+
# Snapshot first
|
|
2995
|
+
triplets = []
|
|
2996
|
+
for it in selected_items:
|
|
2997
|
+
p = self._leaf_path(it)
|
|
2998
|
+
if not p:
|
|
2999
|
+
continue
|
|
3000
|
+
try:
|
|
3001
|
+
idx = self.image_paths.index(p)
|
|
3002
|
+
except ValueError:
|
|
3003
|
+
continue
|
|
3004
|
+
triplets.append((idx, p, it))
|
|
3005
|
+
|
|
3006
|
+
for idx, path, it in triplets:
|
|
3007
|
+
try:
|
|
3008
|
+
os.remove(path)
|
|
3009
|
+
except Exception as e:
|
|
3010
|
+
failures.append((path, str(e)))
|
|
3011
|
+
continue
|
|
3012
|
+
|
|
3013
|
+
removed_indices.append(idx)
|
|
3014
|
+
|
|
3015
|
+
# remove from tree immediately (optional)
|
|
3016
|
+
parent = it.parent() or self.fileTree.invisibleRootItem()
|
|
3017
|
+
parent.removeChild(it)
|
|
3018
|
+
|
|
3019
|
+
# Purge arrays descending
|
|
3020
|
+
removed_indices = sorted(set(removed_indices), reverse=True)
|
|
3021
|
+
for idx in removed_indices:
|
|
3022
|
+
if 0 <= idx < len(self.image_paths):
|
|
2805
3023
|
del self.image_paths[idx]
|
|
3024
|
+
if 0 <= idx < len(self.loaded_images):
|
|
2806
3025
|
del self.loaded_images[idx]
|
|
2807
3026
|
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
3027
|
+
# Clear preview safely
|
|
3028
|
+
self.preview_label.clear()
|
|
3029
|
+
self.preview_label.setText(self.tr("No image selected."))
|
|
3030
|
+
self.current_pixmap = None
|
|
2812
3031
|
|
|
2813
|
-
|
|
3032
|
+
if removed_indices:
|
|
2814
3033
|
self._after_list_changed(removed_indices)
|
|
2815
3034
|
|
|
3035
|
+
if failures:
|
|
3036
|
+
msg = self.tr("Deleted {0} file(s). {1} failed:").format(len(removed_indices), len(failures))
|
|
3037
|
+
for p, err in failures[:10]:
|
|
3038
|
+
msg += f"\n• {os.path.basename(p)} – {err}"
|
|
3039
|
+
QMessageBox.warning(self, self.tr("Delete Selected Items"), msg)
|
|
3040
|
+
else:
|
|
3041
|
+
QMessageBox.information(self, self.tr("Delete Selected Items"), self.tr("Deleted {0} item(s).").format(len(removed_indices)))
|
|
3042
|
+
|
|
3043
|
+
|
|
2816
3044
|
def eventFilter(self, source, event):
|
|
2817
3045
|
"""Handle mouse events for dragging."""
|
|
2818
3046
|
if source == self.scroll_area.viewport():
|
|
@@ -2875,16 +3103,14 @@ class BlinkTab(QWidget):
|
|
|
2875
3103
|
self.on_item_clicked(cur, 0)
|
|
2876
3104
|
|
|
2877
3105
|
def convert_to_qimage(self, img_array):
|
|
2878
|
-
"""Convert numpy image array to QImage."""
|
|
2879
|
-
# 1) Bring everything into a uint8 (0–255) array
|
|
2880
3106
|
if img_array.dtype == np.uint8:
|
|
2881
3107
|
arr8 = img_array
|
|
2882
3108
|
elif img_array.dtype == np.uint16:
|
|
2883
|
-
# downscale 16-bit → 8-bit
|
|
2884
3109
|
arr8 = (img_array.astype(np.float32) / 65535.0 * 255.0).clip(0,255).astype(np.uint8)
|
|
2885
3110
|
else:
|
|
2886
|
-
#
|
|
2887
|
-
|
|
3111
|
+
# ✅ display-only normalize floats outside 0..1
|
|
3112
|
+
f01 = self._ensure_float01(img_array)
|
|
3113
|
+
arr8 = (f01 * 255.0).astype(np.uint8)
|
|
2888
3114
|
|
|
2889
3115
|
h, w = arr8.shape[:2]
|
|
2890
3116
|
buffer = arr8.tobytes()
|