flimkit 0.12.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flimkit/FLIM/__init__.py +0 -0
- flimkit/FLIM/assemble.py +254 -0
- flimkit/FLIM/batch.py +681 -0
- flimkit/FLIM/bg_tools.py +51 -0
- flimkit/FLIM/fit_tools.py +244 -0
- flimkit/FLIM/fitters.py +1471 -0
- flimkit/FLIM/irf_tools.py +617 -0
- flimkit/FLIM/models.py +391 -0
- flimkit/GPU/__init__.py +85 -0
- flimkit/GPU/_base.py +391 -0
- flimkit/GPU/cuda.py +10 -0
- flimkit/GPU/mlx_backend.py +381 -0
- flimkit/GPU/mps.py +10 -0
- flimkit/GPU/rocm.py +10 -0
- flimkit/GPU/torch_backend.py +385 -0
- flimkit/UI/app_state.py +10 -0
- flimkit/UI/controller.py +139 -0
- flimkit/UI/expert_settings.py +248 -0
- flimkit/UI/fit_help.py +206 -0
- flimkit/UI/fov_preview.py +1085 -0
- flimkit/UI/gui.py +3919 -0
- flimkit/UI/icon.icns +0 -0
- flimkit/UI/icon.ico +0 -0
- flimkit/UI/icon.png +0 -0
- flimkit/UI/irf_widget.py +103 -0
- flimkit/UI/mode_controller.py +118 -0
- flimkit/UI/modes/__init__.py +0 -0
- flimkit/UI/modes/base.py +3 -0
- flimkit/UI/modes/batch_mode.py +312 -0
- flimkit/UI/modes/fov_mode.py +164 -0
- flimkit/UI/modes/irf_mode.py +80 -0
- flimkit/UI/modes/phasor_mode.py +131 -0
- flimkit/UI/modes/stitch_mode.py +254 -0
- flimkit/UI/phasor_panel.py +1087 -0
- flimkit/UI/progress_window.py +113 -0
- flimkit/UI/project_panel.py +262 -0
- flimkit/UI/results_panel.py +332 -0
- flimkit/UI/roi_tools.py +794 -0
- flimkit/UI/utils.py +217 -0
- flimkit/__init__.py +0 -0
- flimkit/_version.py +41 -0
- flimkit/cli.py +120 -0
- flimkit/configs.py +148 -0
- flimkit/dialogs.py +46 -0
- flimkit/formats/BH/__init__.py +0 -0
- flimkit/formats/BH/reader.py +296 -0
- flimkit/formats/BH/writer.py +86 -0
- flimkit/formats/ISS/__init__.py +0 -0
- flimkit/formats/ISS/fdflim.py +86 -0
- flimkit/formats/ISS/image.py +114 -0
- flimkit/formats/ISS/reader.py +223 -0
- flimkit/formats/PS/__init__.py +0 -0
- flimkit/formats/PS/reader.py +202 -0
- flimkit/formats/PTU/__init__.py +0 -0
- flimkit/formats/PTU/decode.py +27 -0
- flimkit/formats/PTU/phu.py +85 -0
- flimkit/formats/PTU/reader.py +235 -0
- flimkit/formats/PTU/series.py +258 -0
- flimkit/formats/PTU/stitch.py +1182 -0
- flimkit/formats/PTU/tools.py +94 -0
- flimkit/formats/__init__.py +2 -0
- flimkit/formats/flim_file.py +232 -0
- flimkit/formats/phasor.py +132 -0
- flimkit/formats/signal.py +170 -0
- flimkit/image/tools.py +124 -0
- flimkit/interactive.py +1857 -0
- flimkit/mpl_backend.py +22 -0
- flimkit/phasor/__init__.py +40 -0
- flimkit/phasor/filters.py +127 -0
- flimkit/phasor/fret.py +654 -0
- flimkit/phasor/interactive.py +556 -0
- flimkit/phasor/peaks.py +186 -0
- flimkit/phasor/signal.py +90 -0
- flimkit/phasor_launcher.py +314 -0
- flimkit/plugins/__init__.py +137 -0
- flimkit/plugins/bindings.py +116 -0
- flimkit/plugins/builtin/__init__.py +3 -0
- flimkit/plugins/builtin/core_tools.py +28 -0
- flimkit/plugins/loader.py +371 -0
- flimkit/plugins/registry.py +406 -0
- flimkit/project.py +197 -0
- flimkit/synth.py +145 -0
- flimkit/utils/__init__.py +0 -0
- flimkit/utils/batch_fit.py +301 -0
- flimkit/utils/config_manager.py +119 -0
- flimkit/utils/config_snapshot.py +30 -0
- flimkit/utils/crash_handler.py +183 -0
- flimkit/utils/display.py +197 -0
- flimkit/utils/enhanced_outputs.py +345 -0
- flimkit/utils/fancy.py +103 -0
- flimkit/utils/lifetime_image.py +243 -0
- flimkit/utils/misc.py +111 -0
- flimkit/utils/plotting.py +190 -0
- flimkit/utils/roi.py +370 -0
- flimkit/utils/session.py +51 -0
- flimkit/utils/update_check.py +198 -0
- flimkit/utils/xlsx_tools.py +97 -0
- flimkit/utils/xml_utils.py +219 -0
- flimkit-0.12.0.dist-info/METADATA +356 -0
- flimkit-0.12.0.dist-info/RECORD +104 -0
- flimkit-0.12.0.dist-info/WHEEL +5 -0
- flimkit-0.12.0.dist-info/entry_points.txt +2 -0
- flimkit-0.12.0.dist-info/licenses/LICENSE.md +11 -0
- flimkit-0.12.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
import inspect
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
import tkinter as tk
|
|
10
|
+
from tkinter import ttk, filedialog, messagebox, scrolledtext, simpledialog
|
|
11
|
+
import numpy as np
|
|
12
|
+
import matplotlib
|
|
13
|
+
import matplotlib.image as mpimg
|
|
14
|
+
from matplotlib.figure import Figure
|
|
15
|
+
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
|
16
|
+
from flimkit.utils import display
|
|
17
|
+
|
|
18
|
+
class ResultsPanel:
|
|
19
|
+
def __init__(self, parent, root=None):
|
|
20
|
+
self.parent = parent
|
|
21
|
+
self.root = root
|
|
22
|
+
self.frame = ttk.Frame(parent)
|
|
23
|
+
self.frame.columnconfigure(0, weight=1)
|
|
24
|
+
self.frame.rowconfigure(0, weight=1)
|
|
25
|
+
self._nb = ttk.Notebook(self.frame)
|
|
26
|
+
self._nb.grid(row=0, column=0, sticky='nsew')
|
|
27
|
+
self._build_progress()
|
|
28
|
+
self._build_summary()
|
|
29
|
+
self._build_images()
|
|
30
|
+
self._imgs = []
|
|
31
|
+
self._folder = None
|
|
32
|
+
self._img_i = 0
|
|
33
|
+
self._fit_result = None
|
|
34
|
+
self._output_dir = None
|
|
35
|
+
self._current_npz_path = None
|
|
36
|
+
self._scan_name = None
|
|
37
|
+
self._export_callback = None
|
|
38
|
+
self._load_callback = None
|
|
39
|
+
self._save_npz_callback = None
|
|
40
|
+
self._status = tk.StringVar(value='Ready.')
|
|
41
|
+
ttk.Label(self.frame, textvariable=self._status, foreground='grey').grid(
|
|
42
|
+
row=1, column=0, sticky='w', padx=4, pady=(2, 4))
|
|
43
|
+
|
|
44
|
+
def _build_progress(self):
|
|
45
|
+
f = ttk.Frame(self._nb, padding=4)
|
|
46
|
+
self._nb.add(f, text=' Progress ')
|
|
47
|
+
f.columnconfigure(0, weight=1)
|
|
48
|
+
f.rowconfigure(0, weight=1)
|
|
49
|
+
self.log = scrolledtext.ScrolledText(
|
|
50
|
+
f, state='disabled', wrap='word',
|
|
51
|
+
font=('Courier', 9), background='#1e1e1e', foreground='#d4d4d4')
|
|
52
|
+
self.log.grid(row=0, column=0, sticky='nsew')
|
|
53
|
+
btn_bar = ttk.Frame(f)
|
|
54
|
+
btn_bar.grid(row=1, column=0, sticky='ew', pady=(4, 0))
|
|
55
|
+
ttk.Button(btn_bar, text='Save log...', command=self._save_log).pack(side='left', padx=4)
|
|
56
|
+
ttk.Button(btn_bar, text='Clear log', command=self._clear_log).pack(side='right', padx=4)
|
|
57
|
+
|
|
58
|
+
def _clear_log(self):
|
|
59
|
+
self.log.configure(state='normal')
|
|
60
|
+
self.log.delete('1.0', tk.END)
|
|
61
|
+
self.log.configure(state='disabled')
|
|
62
|
+
|
|
63
|
+
def _save_log(self):
|
|
64
|
+
text = self.log.get('1.0', tk.END)
|
|
65
|
+
if not text.strip():
|
|
66
|
+
messagebox.showinfo('Nothing to save', 'The log is empty.')
|
|
67
|
+
return
|
|
68
|
+
path = filedialog.asksaveasfilename(
|
|
69
|
+
title='Save log as...',
|
|
70
|
+
initialfile=f"{self._scan_name}_log.txt" if self._scan_name else '',
|
|
71
|
+
defaultextension='.txt',
|
|
72
|
+
filetypes=[('Text files', '*.txt'), ('All', '*.*')])
|
|
73
|
+
if path:
|
|
74
|
+
Path(path).write_text(text, encoding='utf-8')
|
|
75
|
+
self._status.set(f"Log saved → {Path(path).name}")
|
|
76
|
+
|
|
77
|
+
def _on_export_clicked(self):
|
|
78
|
+
try:
|
|
79
|
+
print(f"[Export Button] Clicked - callback={self._export_callback is not None}, fit_result={self._fit_result is not None}, output_dir={self._output_dir}")
|
|
80
|
+
if self._export_callback and self._fit_result and self._output_dir:
|
|
81
|
+
print(f"[Export Button] Calling callback...")
|
|
82
|
+
self._export_callback(self._fit_result, self._output_dir)
|
|
83
|
+
else:
|
|
84
|
+
print(f"[Export Button] Missing: callback={self._export_callback} fit_result={self._fit_result is not None} output_dir={self._output_dir}")
|
|
85
|
+
except Exception as e:
|
|
86
|
+
print(f"[Export Button Error] {e}")
|
|
87
|
+
import traceback
|
|
88
|
+
traceback.print_exc()
|
|
89
|
+
|
|
90
|
+
def set_fit_result(self, fit_result: dict, output_dir: str, npz_path: str = None, scan_name: str = None):
|
|
91
|
+
self._fit_result = fit_result
|
|
92
|
+
self._output_dir = output_dir
|
|
93
|
+
if npz_path:
|
|
94
|
+
self._current_npz_path = npz_path
|
|
95
|
+
if scan_name:
|
|
96
|
+
self._scan_name = scan_name
|
|
97
|
+
has_images = any(isinstance(v, np.ndarray) for v in (fit_result or {}).values())
|
|
98
|
+
self._export_btn.configure(state='normal' if has_images else 'disabled')
|
|
99
|
+
|
|
100
|
+
def set_export_callback(self, callback):
|
|
101
|
+
self._export_callback = callback
|
|
102
|
+
|
|
103
|
+
def set_load_callback(self, callback):
|
|
104
|
+
self._load_callback = callback
|
|
105
|
+
|
|
106
|
+
def set_save_npz_callback(self, callback):
|
|
107
|
+
self._save_npz_callback = callback
|
|
108
|
+
|
|
109
|
+
def _on_save_npz_clicked(self):
|
|
110
|
+
try:
|
|
111
|
+
if self._save_npz_callback and self._output_dir:
|
|
112
|
+
self._save_npz_callback(self._output_dir)
|
|
113
|
+
except Exception as e:
|
|
114
|
+
print(f"[Save NPZ Error] {e}")
|
|
115
|
+
import traceback
|
|
116
|
+
traceback.print_exc()
|
|
117
|
+
|
|
118
|
+
def _export_summed_csv(self):
|
|
119
|
+
try:
|
|
120
|
+
import csv
|
|
121
|
+
from pathlib import Path
|
|
122
|
+
init_name = f"{self._scan_name}_summed_fit.csv" if self._scan_name else None
|
|
123
|
+
csv_file = filedialog.asksaveasfilename(
|
|
124
|
+
title='Export Summed Fit Data',
|
|
125
|
+
initialfile=init_name,
|
|
126
|
+
defaultextension='.csv',
|
|
127
|
+
filetypes=[('CSV files', '*.csv'), ('All files', '*.*')],
|
|
128
|
+
initialdir=self._output_dir)
|
|
129
|
+
if not csv_file:
|
|
130
|
+
return
|
|
131
|
+
rows = []
|
|
132
|
+
for item in self._tv.get_children():
|
|
133
|
+
values = self._tv.item(item)['values']
|
|
134
|
+
rows.append(values)
|
|
135
|
+
if not rows:
|
|
136
|
+
messagebox.showwarning('No Data', 'No summary data to export.')
|
|
137
|
+
return
|
|
138
|
+
with open(csv_file, 'w', newline='', encoding='utf-8') as f:
|
|
139
|
+
writer = csv.writer(f)
|
|
140
|
+
writer.writerow(['Parameter', 'Value', 'Unit'])
|
|
141
|
+
writer.writerows(rows)
|
|
142
|
+
messagebox.showinfo('Export Success', f"Summed fit data exported to:\n{Path(csv_file).name}")
|
|
143
|
+
self._status.set(f"Exported → {Path(csv_file).name}")
|
|
144
|
+
print(f"[Export] Summed fit CSV: {csv_file}")
|
|
145
|
+
except Exception as e:
|
|
146
|
+
messagebox.showerror('Export Error', f"Failed to export CSV:\n{e}")
|
|
147
|
+
import traceback
|
|
148
|
+
traceback.print_exc()
|
|
149
|
+
|
|
150
|
+
def _load_fitted_data(self):
|
|
151
|
+
npz_file = filedialog.askopenfilename(
|
|
152
|
+
title='Load Fitted Data',
|
|
153
|
+
filetypes=[('NumPy Archives', '*.npz'), ('All', '*.*')],
|
|
154
|
+
defaultextension='.npz')
|
|
155
|
+
if not npz_file:
|
|
156
|
+
return
|
|
157
|
+
if self._load_callback:
|
|
158
|
+
self._load_callback(npz_file)
|
|
159
|
+
|
|
160
|
+
def _build_summary(self):
|
|
161
|
+
f = ttk.Frame(self._nb, padding=4)
|
|
162
|
+
self._nb.add(f, text=' Fit Summary ')
|
|
163
|
+
f.columnconfigure(0, weight=1)
|
|
164
|
+
f.rowconfigure(0, weight=1)
|
|
165
|
+
cols = ('Parameter', 'Value', 'Unit')
|
|
166
|
+
tv = ttk.Treeview(f, columns=cols, show='headings')
|
|
167
|
+
tv.heading('Parameter', text='Parameter', anchor='w')
|
|
168
|
+
tv.heading('Value', text='Value', anchor='e')
|
|
169
|
+
tv.heading('Unit', text='Unit', anchor='w')
|
|
170
|
+
tv.column('Parameter', width=300, anchor='w', stretch=True)
|
|
171
|
+
tv.column('Value', width=110, anchor='e', stretch=False)
|
|
172
|
+
tv.column('Unit', width=70, anchor='w', stretch=False)
|
|
173
|
+
sb = ttk.Scrollbar(f, orient='vertical', command=tv.yview)
|
|
174
|
+
tv.configure(yscrollcommand=sb.set)
|
|
175
|
+
tv.grid(row=0, column=0, sticky='nsew')
|
|
176
|
+
sb.grid(row=0, column=1, sticky='ns')
|
|
177
|
+
tv.tag_configure('odd', background='#f5f7fa', foreground='#000000')
|
|
178
|
+
tv.tag_configure('even', background='#ffffff', foreground='#000000')
|
|
179
|
+
tv.tag_configure('warn', foreground='#c0550a', background='#fff8f0')
|
|
180
|
+
self._tv = tv
|
|
181
|
+
btn_bar = ttk.Frame(f)
|
|
182
|
+
btn_bar.grid(row=1, column=0, columnspan=2, sticky='ew', pady=(4, 0))
|
|
183
|
+
self._export_btn = ttk.Button(btn_bar, text='Export Images...',
|
|
184
|
+
command=self._on_export_clicked, state='disabled')
|
|
185
|
+
self._export_btn.pack(side='left', padx=4)
|
|
186
|
+
|
|
187
|
+
def _build_images(self):
|
|
188
|
+
f = ttk.Frame(self._nb, padding=4)
|
|
189
|
+
self._nb.add(f, text=' Images ')
|
|
190
|
+
f.columnconfigure(0, weight=1)
|
|
191
|
+
f.rowconfigure(0, weight=1)
|
|
192
|
+
fig = Figure(figsize=(5, 5), facecolor='#2b2b2b')
|
|
193
|
+
self._ax = fig.add_subplot(111)
|
|
194
|
+
self._ax.set_facecolor('#2b2b2b')
|
|
195
|
+
self._ax.axis('off')
|
|
196
|
+
self._canvas_mpl = FigureCanvasTkAgg(fig, master=f)
|
|
197
|
+
self._canvas_mpl.get_tk_widget().grid(row=0, column=0, sticky='nsew')
|
|
198
|
+
self._img_lbl = tk.StringVar(value='No images loaded')
|
|
199
|
+
ttk.Label(f, textvariable=self._img_lbl, foreground='grey').grid(
|
|
200
|
+
row=1, column=0, sticky='w', padx=4, pady=(2, 0))
|
|
201
|
+
self._img_slider_updating = False
|
|
202
|
+
self._img_slider = ttk.Scale(f, from_=0, to=0, orient='horizontal',
|
|
203
|
+
command=self._on_img_slider)
|
|
204
|
+
self._img_slider.grid(row=2, column=0, sticky='ew', padx=4, pady=(2, 0))
|
|
205
|
+
nav = ttk.Frame(f)
|
|
206
|
+
nav.grid(row=3, column=0, sticky='ew', pady=(4, 0))
|
|
207
|
+
ttk.Button(nav, text='◀ Prev', command=self._img_prev).pack(side='left', padx=4)
|
|
208
|
+
ttk.Button(nav, text='Next ▶', command=self._img_next).pack(side='left', padx=4)
|
|
209
|
+
ttk.Button(nav, text='Open folder', command=self._open_folder).pack(side='right', padx=4)
|
|
210
|
+
ttk.Button(nav, text='Save all...', command=self._save_all_imgs).pack(side='right', padx=4)
|
|
211
|
+
ttk.Button(nav, text='Save image...', command=self._save_img).pack(side='right', padx=4)
|
|
212
|
+
|
|
213
|
+
def populate_summary(self, rows: list):
|
|
214
|
+
for item in self._tv.get_children():
|
|
215
|
+
self._tv.delete(item)
|
|
216
|
+
self._tv.update()
|
|
217
|
+
for i, (param, val, unit) in enumerate(rows):
|
|
218
|
+
tag = 'warn' if param.startswith('⚠') else ('odd' if i % 2 else 'even')
|
|
219
|
+
self._tv.insert('', tk.END, values=(param, val, unit), tags=(tag,))
|
|
220
|
+
self._tv.update_idletasks()
|
|
221
|
+
if rows:
|
|
222
|
+
self._nb.select(1)
|
|
223
|
+
self._nb.update_idletasks()
|
|
224
|
+
|
|
225
|
+
def set_status(self, msg: str):
|
|
226
|
+
self._status.set(msg)
|
|
227
|
+
|
|
228
|
+
def grid(self, **kw):
|
|
229
|
+
self.frame.grid(**kw)
|
|
230
|
+
|
|
231
|
+
def load_images(self, folder: Optional[str]):
|
|
232
|
+
self._imgs = []
|
|
233
|
+
if folder and Path(folder).is_dir():
|
|
234
|
+
self._folder = folder
|
|
235
|
+
for pat in ('*.png', '*.tif', '*.tiff'):
|
|
236
|
+
self._imgs += sorted(Path(folder).glob(pat))
|
|
237
|
+
self._img_i = 0
|
|
238
|
+
self._img_slider.configure(to=max(len(self._imgs) - 1, 0))
|
|
239
|
+
self._sync_img_slider()
|
|
240
|
+
self._draw_img()
|
|
241
|
+
if self._imgs:
|
|
242
|
+
self._nb.select(2)
|
|
243
|
+
|
|
244
|
+
def _sync_img_slider(self):
|
|
245
|
+
self._img_slider_updating = True
|
|
246
|
+
try:
|
|
247
|
+
self._img_slider.set(self._img_i)
|
|
248
|
+
finally:
|
|
249
|
+
self._img_slider_updating = False
|
|
250
|
+
|
|
251
|
+
def _on_img_slider(self, value):
|
|
252
|
+
if self._img_slider_updating or not getattr(self, '_imgs', None):
|
|
253
|
+
return
|
|
254
|
+
i = max(0, min(int(round(float(value))), len(self._imgs) - 1))
|
|
255
|
+
if i != self._img_i:
|
|
256
|
+
self._img_i = i
|
|
257
|
+
self._draw_img()
|
|
258
|
+
|
|
259
|
+
def _draw_img(self):
|
|
260
|
+
self._ax.cla()
|
|
261
|
+
self._ax.set_facecolor('#2b2b2b')
|
|
262
|
+
self._ax.axis('off')
|
|
263
|
+
if not self._imgs:
|
|
264
|
+
self._img_lbl.set('No images found')
|
|
265
|
+
self._ax.text(0.5, 0.5, 'No images found',
|
|
266
|
+
ha='center', va='center', color='grey', fontsize=11,
|
|
267
|
+
transform=self._ax.transAxes)
|
|
268
|
+
else:
|
|
269
|
+
path = self._imgs[self._img_i]
|
|
270
|
+
self._img_lbl.set(f"{path.name} ({self._img_i + 1}/{len(self._imgs)})")
|
|
271
|
+
try:
|
|
272
|
+
img = mpimg.imread(str(path))
|
|
273
|
+
self._ax.imshow(img, aspect='equal')
|
|
274
|
+
except Exception as e:
|
|
275
|
+
self._ax.text(0.5, 0.5, f"Cannot load image:\n{e}",
|
|
276
|
+
ha='center', va='center', color='red',
|
|
277
|
+
fontsize=9, transform=self._ax.transAxes)
|
|
278
|
+
self._canvas_mpl.draw_idle()
|
|
279
|
+
|
|
280
|
+
def _img_prev(self):
|
|
281
|
+
if self._imgs:
|
|
282
|
+
self._img_i = (self._img_i - 1) % len(self._imgs)
|
|
283
|
+
self._sync_img_slider()
|
|
284
|
+
self._draw_img()
|
|
285
|
+
|
|
286
|
+
def _img_next(self):
|
|
287
|
+
if self._imgs:
|
|
288
|
+
self._img_i = (self._img_i + 1) % len(self._imgs)
|
|
289
|
+
self._sync_img_slider()
|
|
290
|
+
self._draw_img()
|
|
291
|
+
|
|
292
|
+
def _save_img(self):
|
|
293
|
+
if not self._imgs:
|
|
294
|
+
messagebox.showinfo('No image', 'No image is currently displayed.')
|
|
295
|
+
return
|
|
296
|
+
src = self._imgs[self._img_i]
|
|
297
|
+
path = filedialog.asksaveasfilename(
|
|
298
|
+
title='Save current image as...',
|
|
299
|
+
initialfile=src.name,
|
|
300
|
+
defaultextension=src.suffix,
|
|
301
|
+
filetypes=[
|
|
302
|
+
('PNG', '*.png'),
|
|
303
|
+
('TIFF', '*.tif *.tiff'),
|
|
304
|
+
('All', '*.*'),
|
|
305
|
+
])
|
|
306
|
+
if path:
|
|
307
|
+
import shutil
|
|
308
|
+
shutil.copy2(str(src), path)
|
|
309
|
+
self._status.set(f"Image saved → {Path(path).name}")
|
|
310
|
+
|
|
311
|
+
def _save_all_imgs(self):
|
|
312
|
+
if not self._imgs:
|
|
313
|
+
messagebox.showinfo('No images', 'No images are available to save.')
|
|
314
|
+
return
|
|
315
|
+
dest = filedialog.askdirectory(title='Save all images to...')
|
|
316
|
+
if not dest:
|
|
317
|
+
return
|
|
318
|
+
import shutil
|
|
319
|
+
dest_path = Path(dest)
|
|
320
|
+
for img in self._imgs:
|
|
321
|
+
shutil.copy2(str(img), str(dest_path / img.name))
|
|
322
|
+
self._status.set(f"{len(self._imgs)} image(s) saved → {dest_path.name}/")
|
|
323
|
+
|
|
324
|
+
def _open_folder(self):
|
|
325
|
+
import subprocess, platform
|
|
326
|
+
if not self._folder or not Path(self._folder).exists():
|
|
327
|
+
messagebox.showinfo('No folder', 'No output folder available yet.')
|
|
328
|
+
return
|
|
329
|
+
s = platform.system()
|
|
330
|
+
if s == 'Darwin': subprocess.Popen(['open', self._folder])
|
|
331
|
+
elif s == 'Windows': subprocess.Popen(['explorer', self._folder])
|
|
332
|
+
else: subprocess.Popen(['xdg-open', self._folder])
|