majoplot 0.1.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.
- majoplot/__init__.py +0 -0
- majoplot/__main__.py +25 -0
- majoplot/app/__init__.py +0 -0
- majoplot/app/cli.py +259 -0
- majoplot/app/gui.py +6 -0
- majoplot/config.json +11 -0
- majoplot/domain/base.py +433 -0
- majoplot/domain/importers/PPMS_Resistivity.py +128 -0
- majoplot/domain/importers/VSM.py +109 -0
- majoplot/domain/importers/XRD.py +62 -0
- majoplot/domain/muti_axes_spec.py +172 -0
- majoplot/domain/scenarios/PPMS_Resistivity/RT.py +119 -0
- majoplot/domain/scenarios/VSM/MT.py +131 -0
- majoplot/domain/scenarios/VSM/MT_insert.py +135 -0
- majoplot/domain/scenarios/VSM/MT_reliability_analysis.py +145 -0
- majoplot/domain/scenarios/XRD/Compare.py +104 -0
- majoplot/domain/utils.py +87 -0
- majoplot/gui/__init__.py +0 -0
- majoplot/gui/main.py +529 -0
- majoplot/infra/plotters/matplot.py +337 -0
- majoplot/infra/plotters/origin.py +1006 -0
- majoplot/infra/plotters/origin_utils/originlab_type_library.py +403 -0
- majoplot-0.1.0.dist-info/METADATA +81 -0
- majoplot-0.1.0.dist-info/RECORD +27 -0
- majoplot-0.1.0.dist-info/WHEEL +4 -0
- majoplot-0.1.0.dist-info/entry_points.txt +2 -0
- majoplot-0.1.0.dist-info/licenses/LICENSE +21 -0
majoplot/gui/main.py
ADDED
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import tkinter as tk
|
|
4
|
+
from tkinter import ttk, filedialog, messagebox
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from collections import OrderedDict
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ..app.cli import load_named_objects # reuse discovery helper
|
|
10
|
+
from ..domain.base import fail_signal, Figure
|
|
11
|
+
from ..domain.utils import pack_into_project, group_into_axes, group_into_figure
|
|
12
|
+
from ..infra.plotters.origin import OriginCOM, plot as oplot
|
|
13
|
+
from ..infra.plotters.matplot import plot as mplot
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _LRUCache:
|
|
17
|
+
"""A tiny LRU cache for rendered matplotlib Figure previews."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, maxsize: int = 3) -> None:
|
|
20
|
+
self.maxsize = maxsize
|
|
21
|
+
self._d: OrderedDict[int, Any] = OrderedDict()
|
|
22
|
+
|
|
23
|
+
def get(self, key: int) -> Any | None:
|
|
24
|
+
if key not in self._d:
|
|
25
|
+
return None
|
|
26
|
+
self._d.move_to_end(key)
|
|
27
|
+
return self._d[key]
|
|
28
|
+
|
|
29
|
+
def put(self, key: int, value: Any) -> None:
|
|
30
|
+
if key in self._d:
|
|
31
|
+
self._d.move_to_end(key)
|
|
32
|
+
self._d[key] = value
|
|
33
|
+
while len(self._d) > self.maxsize:
|
|
34
|
+
_, victim = self._d.popitem(last=False)
|
|
35
|
+
try:
|
|
36
|
+
import matplotlib.pyplot as plt
|
|
37
|
+
plt.close(victim)
|
|
38
|
+
except Exception:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def clear(self) -> None:
|
|
43
|
+
try:
|
|
44
|
+
import matplotlib.pyplot as plt
|
|
45
|
+
for v in self._d.values():
|
|
46
|
+
plt.close(v)
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
49
|
+
self._d.clear()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class MainWindow(ttk.Frame):
|
|
54
|
+
def __init__(self, master: tk.Tk) -> None:
|
|
55
|
+
super().__init__(master)
|
|
56
|
+
self.master = master
|
|
57
|
+
|
|
58
|
+
# State
|
|
59
|
+
self.importers: dict[str, Any] = {}
|
|
60
|
+
self.scenarios: dict[str, Any] = {}
|
|
61
|
+
self.figures: list[Figure] = []
|
|
62
|
+
self.raw_files_to_import: list[Path] = []
|
|
63
|
+
|
|
64
|
+
self._preview_cache = _LRUCache(maxsize=3)
|
|
65
|
+
self._current_preview_canvas = None # FigureCanvasTkAgg
|
|
66
|
+
|
|
67
|
+
# UI variables
|
|
68
|
+
self.importer_var = tk.StringVar(value="")
|
|
69
|
+
self.scenario_var = tk.StringVar(value="")
|
|
70
|
+
self.proj_dir_var = tk.StringVar(value=str(Path.cwd()))
|
|
71
|
+
self.save_mode_var = tk.StringVar(value="attach") # attach|overwrite
|
|
72
|
+
|
|
73
|
+
# Global archive inputs
|
|
74
|
+
self.global_proj_name_var = tk.StringVar(value="")
|
|
75
|
+
self.global_folder_path_var = tk.StringVar(value="")
|
|
76
|
+
|
|
77
|
+
# Per-figure archive editor
|
|
78
|
+
self.edit_proj_name_var = tk.StringVar(value="")
|
|
79
|
+
self.edit_folder_path_var = tk.StringVar(value="")
|
|
80
|
+
|
|
81
|
+
self._build_ui()
|
|
82
|
+
self._load_importers()
|
|
83
|
+
|
|
84
|
+
# ---------------- UI construction ----------------
|
|
85
|
+
def _build_ui(self) -> None:
|
|
86
|
+
self.master.title("Majoplot")
|
|
87
|
+
self.master.geometry("1400x720")
|
|
88
|
+
|
|
89
|
+
top = ttk.Frame(self)
|
|
90
|
+
top.pack(side=tk.TOP, fill=tk.X, padx=8, pady=8)
|
|
91
|
+
|
|
92
|
+
ttk.Label(top, text="Importer").pack(side=tk.LEFT)
|
|
93
|
+
self.importer_cb = ttk.Combobox(top, textvariable=self.importer_var, state="readonly", width=20)
|
|
94
|
+
self.importer_cb.pack(side=tk.LEFT, padx=(6, 12))
|
|
95
|
+
self.importer_cb.bind("<<ComboboxSelected>>", lambda _e: self._on_importer_selected())
|
|
96
|
+
|
|
97
|
+
ttk.Label(top, text="Scenario").pack(side=tk.LEFT)
|
|
98
|
+
self.scenario_cb = ttk.Combobox(top, textvariable=self.scenario_var, state="readonly", width=30)
|
|
99
|
+
self.scenario_cb.pack(side=tk.LEFT, padx=(6, 12))
|
|
100
|
+
|
|
101
|
+
ttk.Button(top, text="Select raw files...", command=self._pick_raw_files).pack(side=tk.LEFT, padx=(0, 8))
|
|
102
|
+
ttk.Button(top, text="Raw files → Figures", command=self._import_figures).pack(side=tk.LEFT, padx=(0, 24))
|
|
103
|
+
ttk.Button(top, text="Delete all figures", command=self._delete_all_figures).pack(side=tk.RIGHT)
|
|
104
|
+
|
|
105
|
+
# Main body: left raw files / middle preview / right save
|
|
106
|
+
body = ttk.PanedWindow(self, orient=tk.HORIZONTAL)
|
|
107
|
+
body.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=8, pady=(0, 8))
|
|
108
|
+
|
|
109
|
+
# Left: raw files
|
|
110
|
+
left = ttk.Frame(body)
|
|
111
|
+
body.add(left, weight=2)
|
|
112
|
+
ttk.Label(left, text="Raw files").pack(anchor="w")
|
|
113
|
+
self.raw_file_list = tk.Listbox(left, height=10)
|
|
114
|
+
self.raw_file_list.pack(fill=tk.BOTH, expand=True, pady=(4, 0))
|
|
115
|
+
|
|
116
|
+
# Middle: figure list + preview
|
|
117
|
+
mid = ttk.PanedWindow(body, orient=tk.HORIZONTAL)
|
|
118
|
+
body.add(mid, weight=12)
|
|
119
|
+
|
|
120
|
+
mid_left = ttk.Frame(mid)
|
|
121
|
+
mid.add(mid_left, weight=2)
|
|
122
|
+
ttk.Label(mid_left, text="Figures").pack(anchor="w")
|
|
123
|
+
self.figure_list = tk.Listbox(mid_left)
|
|
124
|
+
self.figure_list.pack(fill=tk.BOTH, expand=True, pady=(4, 6))
|
|
125
|
+
self.figure_list.bind("<<ListboxSelect>>", lambda _e: self._on_figure_selected())
|
|
126
|
+
|
|
127
|
+
# Per-figure actions
|
|
128
|
+
action = ttk.LabelFrame(mid_left, text="Selected Figure")
|
|
129
|
+
action.pack(fill=tk.X)
|
|
130
|
+
|
|
131
|
+
row1 = ttk.Frame(action)
|
|
132
|
+
row1.pack(fill=tk.X, padx=6, pady=4)
|
|
133
|
+
ttk.Button(row1, text="Delete", command=self._delete_selected_figure).pack(side=tk.LEFT)
|
|
134
|
+
ttk.Button(row1, text="Save this...", command=self._save_selected_figure).pack(side=tk.LEFT, padx=(6, 0))
|
|
135
|
+
|
|
136
|
+
row2 = ttk.Frame(action)
|
|
137
|
+
row2.pack(fill=tk.X, padx=6, pady=(4, 2))
|
|
138
|
+
ttk.Label(row2, text="proj_name").pack(side=tk.LEFT)
|
|
139
|
+
ttk.Entry(row2, textvariable=self.edit_proj_name_var, width=10).pack(side=tk.LEFT, padx=(6, 12))
|
|
140
|
+
ttk.Label(row2, text="folder_path").pack(side=tk.LEFT)
|
|
141
|
+
ttk.Entry(row2, textvariable=self.edit_folder_path_var, width=16).pack(side=tk.LEFT, padx=(6, 0))
|
|
142
|
+
|
|
143
|
+
row3 = ttk.Frame(action)
|
|
144
|
+
row3.pack(fill=tk.X, padx=6, pady=(2, 6))
|
|
145
|
+
ttk.Button(row3, text="Apply (append)", command=lambda: self._apply_archive_to_selected(overwrite=False)).pack(side=tk.LEFT)
|
|
146
|
+
ttk.Button(row3, text="Apply (overwrite)", command=lambda: self._apply_archive_to_selected(overwrite=True)).pack(side=tk.LEFT, padx=(6, 0))
|
|
147
|
+
|
|
148
|
+
mid_right = ttk.Frame(mid)
|
|
149
|
+
mid.add(mid_right, weight=8)
|
|
150
|
+
ttk.Label(mid_right, text="Matplotlib preview").pack(anchor="w")
|
|
151
|
+
self.preview_host = ttk.Frame(mid_right)
|
|
152
|
+
self.preview_host.pack(fill=tk.BOTH, expand=True, pady=(4, 0))
|
|
153
|
+
|
|
154
|
+
# Right: save all
|
|
155
|
+
right = ttk.Frame(body)
|
|
156
|
+
body.add(right, weight=2)
|
|
157
|
+
|
|
158
|
+
save_box = ttk.LabelFrame(right, text="Save all to OPJU")
|
|
159
|
+
save_box.pack(fill=tk.BOTH, expand=True)
|
|
160
|
+
|
|
161
|
+
g1 = ttk.Frame(save_box)
|
|
162
|
+
g1.pack(fill=tk.X, padx=8, pady=(8, 4))
|
|
163
|
+
ttk.Label(g1, text="proj_name").pack(side=tk.LEFT)
|
|
164
|
+
ttk.Entry(g1, textvariable=self.global_proj_name_var, width=18).pack(side=tk.LEFT, padx=(6, 0))
|
|
165
|
+
|
|
166
|
+
g2 = ttk.Frame(save_box)
|
|
167
|
+
g2.pack(fill=tk.X, padx=8, pady=4)
|
|
168
|
+
ttk.Label(g2, text="folder_path").pack(side=tk.LEFT)
|
|
169
|
+
ttk.Entry(g2, textvariable=self.global_folder_path_var, width=18).pack(side=tk.LEFT, padx=(6, 0))
|
|
170
|
+
|
|
171
|
+
g3 = ttk.Frame(save_box)
|
|
172
|
+
g3.pack(fill=tk.X, padx=8, pady=4)
|
|
173
|
+
ttk.Button(g3, text="Append to all", command=lambda: self._apply_archive_to_all(overwrite=False)).pack(side=tk.LEFT)
|
|
174
|
+
ttk.Button(g3, text="Overwrite all", command=lambda: self._apply_archive_to_all(overwrite=True)).pack(side=tk.LEFT, padx=(6, 0))
|
|
175
|
+
|
|
176
|
+
g4 = ttk.Frame(save_box)
|
|
177
|
+
g4.pack(fill=tk.X, padx=8, pady=(10, 4))
|
|
178
|
+
ttk.Label(g4, text="OPJU directory").pack(side=tk.LEFT)
|
|
179
|
+
proj_entry = ttk.Entry(g4, textvariable=self.proj_dir_var, state="readonly")
|
|
180
|
+
proj_entry.pack(side=tk.TOP, padx=(6, 6), fill=tk.X, expand=True)
|
|
181
|
+
proj_entry.bind("<Button-1>", lambda _e: self._pick_proj_dir())
|
|
182
|
+
|
|
183
|
+
ttk.Button(g4, text="Choose...", command=self._pick_proj_dir).pack(side=tk.LEFT)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
g5 = ttk.Frame(save_box)
|
|
187
|
+
g5.pack(fill=tk.X, padx=8, pady=4)
|
|
188
|
+
ttk.Label(g5, text="Mode").pack(side=tk.LEFT)
|
|
189
|
+
ttk.Radiobutton(g5, text="Attach", variable=self.save_mode_var, value="attach").pack(side=tk.LEFT, padx=(6, 0))
|
|
190
|
+
ttk.Radiobutton(g5, text="Overwrite", variable=self.save_mode_var, value="overwrite").pack(side=tk.LEFT, padx=(6, 0))
|
|
191
|
+
|
|
192
|
+
ttk.Button(save_box, text="Save ALL", command=self._save_all).pack(side=tk.BOTTOM, fill=tk.X, padx=8, pady=8)
|
|
193
|
+
|
|
194
|
+
self.pack(fill=tk.BOTH, expand=True)
|
|
195
|
+
|
|
196
|
+
# ---------------- Data discovery ----------------
|
|
197
|
+
def _load_importers(self) -> None:
|
|
198
|
+
self.importers = load_named_objects("majoplot.domain.importers")
|
|
199
|
+
names = sorted(self.importers.keys())
|
|
200
|
+
self.importer_cb["values"] = names
|
|
201
|
+
if names:
|
|
202
|
+
self.importer_var.set(names[0])
|
|
203
|
+
self._on_importer_selected()
|
|
204
|
+
|
|
205
|
+
def _on_importer_selected(self) -> None:
|
|
206
|
+
name = self.importer_var.get()
|
|
207
|
+
if not name or name not in self.importers:
|
|
208
|
+
return
|
|
209
|
+
self.scenarios = load_named_objects(f"majoplot.domain.scenarios.{name}")
|
|
210
|
+
snames = sorted(self.scenarios.keys())
|
|
211
|
+
self.scenario_cb["values"] = snames
|
|
212
|
+
if snames:
|
|
213
|
+
self.scenario_var.set(snames[0])
|
|
214
|
+
|
|
215
|
+
# ---------------- Import flow ----------------
|
|
216
|
+
def _pick_raw_files(self) -> None:
|
|
217
|
+
paths = filedialog.askopenfilenames(
|
|
218
|
+
title="Select raw data files",
|
|
219
|
+
initialdir=str(Path.cwd()),
|
|
220
|
+
filetypes=[("All files", "*.*")],
|
|
221
|
+
)
|
|
222
|
+
if not paths:
|
|
223
|
+
return
|
|
224
|
+
for p in paths:
|
|
225
|
+
pp = Path(p).resolve()
|
|
226
|
+
self.raw_files_to_import.append(pp)
|
|
227
|
+
self.raw_file_list.insert(tk.END, str(pp))
|
|
228
|
+
|
|
229
|
+
def _import_figures(self) -> None:
|
|
230
|
+
importer_name = self.importer_var.get()
|
|
231
|
+
scenario_name = self.scenario_var.get()
|
|
232
|
+
if not importer_name or importer_name not in self.importers:
|
|
233
|
+
messagebox.showerror("Error", "Please choose an importer.")
|
|
234
|
+
return
|
|
235
|
+
if not scenario_name or scenario_name not in self.scenarios:
|
|
236
|
+
messagebox.showerror("Error", "Please choose a scenario.")
|
|
237
|
+
return
|
|
238
|
+
if not self.raw_files_to_import:
|
|
239
|
+
messagebox.showwarning("No files", "Please select raw files first.")
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
importer = self.importers[importer_name]
|
|
243
|
+
scenario = self.scenarios[scenario_name]
|
|
244
|
+
|
|
245
|
+
# Only import files not yet imported into figures in this session
|
|
246
|
+
new_paths = self.raw_files_to_import
|
|
247
|
+
self.raw_files_to_import = []
|
|
248
|
+
self.raw_file_list.delete(0, tk.END)
|
|
249
|
+
|
|
250
|
+
raw_datas = []
|
|
251
|
+
for path in new_paths:
|
|
252
|
+
try:
|
|
253
|
+
with open(path, encoding="utf-8") as fp:
|
|
254
|
+
raw = importer.fetch_raw_data(fp, path.stem)
|
|
255
|
+
if raw is not fail_signal:
|
|
256
|
+
raw_datas.append(raw)
|
|
257
|
+
except Exception as e:
|
|
258
|
+
messagebox.showerror("Import error", f"Failed to read {path}:\n{e}")
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
try:
|
|
262
|
+
datas = scenario.preprocess(raw_datas)
|
|
263
|
+
except Exception as e:
|
|
264
|
+
messagebox.showerror("Preprocess error", f"Scenario preprocess failed:\n{e}")
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# Match CLI logic: Data -> Axes -> Figure
|
|
269
|
+
try:
|
|
270
|
+
axes_pool = group_into_axes(datas, scenario)
|
|
271
|
+
figures = group_into_figure(axes_pool, scenario)
|
|
272
|
+
except Exception as e:
|
|
273
|
+
messagebox.showerror(
|
|
274
|
+
"Grouping error",
|
|
275
|
+
"Failed to group Data into Figures.\n"
|
|
276
|
+
f"{e}",
|
|
277
|
+
)
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
added = 0
|
|
281
|
+
for fig in figures:
|
|
282
|
+
if not isinstance(fig, Figure):
|
|
283
|
+
continue
|
|
284
|
+
self.figures.append(fig)
|
|
285
|
+
self.figure_list.insert(tk.END, fig.spec.name)
|
|
286
|
+
added += 1
|
|
287
|
+
|
|
288
|
+
if added == 0:
|
|
289
|
+
messagebox.showinfo("No figures", "No figures were produced from the selected files.")
|
|
290
|
+
return
|
|
291
|
+
|
|
292
|
+
self._preview_cache.clear()
|
|
293
|
+
|
|
294
|
+
# ---------------- Figure selection & preview ----------------
|
|
295
|
+
def _selected_index(self) -> int | None:
|
|
296
|
+
sel = self.figure_list.curselection()
|
|
297
|
+
if not sel:
|
|
298
|
+
return None
|
|
299
|
+
return int(sel[0])
|
|
300
|
+
|
|
301
|
+
def _on_figure_selected(self) -> None:
|
|
302
|
+
idx = self._selected_index()
|
|
303
|
+
if idx is None or idx >= len(self.figures):
|
|
304
|
+
return
|
|
305
|
+
fig = self.figures[idx]
|
|
306
|
+
|
|
307
|
+
# Populate editor with a single pair if exists, else blank.
|
|
308
|
+
if fig.proj_folder:
|
|
309
|
+
# choose the first (stable ordering not guaranteed)
|
|
310
|
+
proj_name, folder = next(iter(fig.proj_folder.items()))
|
|
311
|
+
self.edit_proj_name_var.set(proj_name)
|
|
312
|
+
self.edit_folder_path_var.set(folder)
|
|
313
|
+
else:
|
|
314
|
+
self.edit_proj_name_var.set("")
|
|
315
|
+
self.edit_folder_path_var.set("")
|
|
316
|
+
|
|
317
|
+
self._show_preview(fig)
|
|
318
|
+
|
|
319
|
+
def _show_preview(self, myfig: Figure) -> None:
|
|
320
|
+
# Render with caching
|
|
321
|
+
key = id(myfig)
|
|
322
|
+
matfig = self._preview_cache.get(key)
|
|
323
|
+
if matfig is None:
|
|
324
|
+
try:
|
|
325
|
+
matfig = mplot(myfig)
|
|
326
|
+
except Exception as e:
|
|
327
|
+
messagebox.showerror("Preview error", f"Failed to render preview:\n{e}")
|
|
328
|
+
return
|
|
329
|
+
matfig.set_constrained_layout(True)
|
|
330
|
+
self._preview_cache.put(key, matfig)
|
|
331
|
+
|
|
332
|
+
# Close previous figure bound to old canvas to avoid leaking GUI event sources
|
|
333
|
+
try:
|
|
334
|
+
import matplotlib.pyplot as plt
|
|
335
|
+
if self._current_preview_canvas is not None:
|
|
336
|
+
old_fig = self._current_preview_canvas.figure
|
|
337
|
+
plt.close(old_fig)
|
|
338
|
+
except Exception:
|
|
339
|
+
pass
|
|
340
|
+
self._current_preview_canvas = None
|
|
341
|
+
|
|
342
|
+
# Replace preview canvas
|
|
343
|
+
for child in self.preview_host.winfo_children():
|
|
344
|
+
child.destroy()
|
|
345
|
+
|
|
346
|
+
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
|
|
347
|
+
|
|
348
|
+
canvas = FigureCanvasTkAgg(matfig, master=self.preview_host)
|
|
349
|
+
canvas.draw()
|
|
350
|
+
|
|
351
|
+
# Add toolbar (pan/zoom/home/save)
|
|
352
|
+
toolbar = NavigationToolbar2Tk(canvas, self.preview_host)
|
|
353
|
+
toolbar.update()
|
|
354
|
+
|
|
355
|
+
# Pack order matters: toolbar first, then canvas widget fills the rest
|
|
356
|
+
toolbar.pack(side=tk.TOP, fill=tk.X)
|
|
357
|
+
canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)
|
|
358
|
+
|
|
359
|
+
self._current_preview_canvas = canvas
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
# ---------------- Figure operations ----------------
|
|
363
|
+
def _delete_selected_figure(self) -> None:
|
|
364
|
+
idx = self._selected_index()
|
|
365
|
+
if idx is None or idx >= len(self.figures):
|
|
366
|
+
return
|
|
367
|
+
fig = self.figures.pop(idx)
|
|
368
|
+
self.figure_list.delete(idx)
|
|
369
|
+
self._preview_cache.clear()
|
|
370
|
+
# Clear preview if nothing selected
|
|
371
|
+
for child in self.preview_host.winfo_children():
|
|
372
|
+
child.destroy()
|
|
373
|
+
|
|
374
|
+
def _delete_all_figures(self) -> None:
|
|
375
|
+
# 0) If there is an active preview canvas, close the bound matplotlib Figure
|
|
376
|
+
# to avoid leaking GUI event sources.
|
|
377
|
+
try:
|
|
378
|
+
import matplotlib.pyplot as plt
|
|
379
|
+
if self._current_preview_canvas is not None:
|
|
380
|
+
try:
|
|
381
|
+
old_fig = self._current_preview_canvas.figure
|
|
382
|
+
plt.close(old_fig)
|
|
383
|
+
except Exception:
|
|
384
|
+
pass
|
|
385
|
+
except Exception:
|
|
386
|
+
pass
|
|
387
|
+
self._current_preview_canvas = None
|
|
388
|
+
|
|
389
|
+
# 1) Clear the in-memory model
|
|
390
|
+
self.figures.clear()
|
|
391
|
+
|
|
392
|
+
# 2) Clear the listbox UI and selection highlight
|
|
393
|
+
try:
|
|
394
|
+
self.figure_list.selection_clear(0, tk.END)
|
|
395
|
+
except Exception:
|
|
396
|
+
pass
|
|
397
|
+
self.figure_list.delete(0, tk.END)
|
|
398
|
+
|
|
399
|
+
# 3) Clear per-figure archive editor fields
|
|
400
|
+
self.edit_proj_name_var.set("")
|
|
401
|
+
self.edit_folder_path_var.set("")
|
|
402
|
+
|
|
403
|
+
# 4) Clear preview cache (LRUCache.clear() closes cached figures)
|
|
404
|
+
self._preview_cache.clear()
|
|
405
|
+
|
|
406
|
+
# 5) Remove any preview widgets from the host frame
|
|
407
|
+
for child in self.preview_host.winfo_children():
|
|
408
|
+
child.destroy()
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _apply_archive_to_selected(self, overwrite: bool) -> None:
|
|
412
|
+
idx = self._selected_index()
|
|
413
|
+
if idx is None or idx >= len(self.figures):
|
|
414
|
+
messagebox.showwarning("No selection", "Please select a figure.")
|
|
415
|
+
return
|
|
416
|
+
proj = self.edit_proj_name_var.get().strip()
|
|
417
|
+
folder = self.edit_folder_path_var.get().strip()
|
|
418
|
+
if not proj or not folder:
|
|
419
|
+
messagebox.showwarning("Invalid", "proj_name and folder_path are required.")
|
|
420
|
+
return
|
|
421
|
+
fig = self.figures[idx]
|
|
422
|
+
if overwrite:
|
|
423
|
+
fig.proj_folder = {proj: folder}
|
|
424
|
+
else:
|
|
425
|
+
fig.proj_folder[proj] = folder
|
|
426
|
+
messagebox.showinfo("Applied", "Archive info applied to selected figure.")
|
|
427
|
+
|
|
428
|
+
def _apply_archive_to_all(self, overwrite: bool) -> None:
|
|
429
|
+
proj = self.global_proj_name_var.get().strip()
|
|
430
|
+
folder = self.global_folder_path_var.get().strip()
|
|
431
|
+
if not proj or not folder:
|
|
432
|
+
messagebox.showwarning("Invalid", "proj_name and folder_path are required.")
|
|
433
|
+
return
|
|
434
|
+
for fig in self.figures:
|
|
435
|
+
if overwrite:
|
|
436
|
+
fig.proj_folder = {proj: folder}
|
|
437
|
+
else:
|
|
438
|
+
fig.proj_folder[proj] = folder
|
|
439
|
+
messagebox.showinfo("Applied", "Archive info applied to all figures.")
|
|
440
|
+
|
|
441
|
+
# ---------------- Save operations ----------------
|
|
442
|
+
def _pick_proj_dir(self) -> None:
|
|
443
|
+
d = filedialog.askdirectory(title="Select OPJU directory",initialdir=".", mustexist=True)
|
|
444
|
+
if d:
|
|
445
|
+
self.proj_dir_var.set(str(Path(d).resolve()))
|
|
446
|
+
|
|
447
|
+
def _save_selected_figure(self) -> None:
|
|
448
|
+
idx = self._selected_index()
|
|
449
|
+
if idx is None or idx >= len(self.figures):
|
|
450
|
+
messagebox.showwarning("No selection", "Please select a figure.")
|
|
451
|
+
return
|
|
452
|
+
if not self.figures[idx].proj_folder:
|
|
453
|
+
messagebox.showwarning("Missing archive", "Selected figure has no proj_name:folder_path mapping.")
|
|
454
|
+
return
|
|
455
|
+
target = filedialog.askdirectory(title="Select save directory", mustexist=True)
|
|
456
|
+
if not target:
|
|
457
|
+
return
|
|
458
|
+
proj_dir = Path(target).resolve()
|
|
459
|
+
overwrite = self.save_mode_var.get() == "overwrite"
|
|
460
|
+
|
|
461
|
+
projs = pack_into_project([self.figures[idx]])
|
|
462
|
+
|
|
463
|
+
try:
|
|
464
|
+
with OriginCOM(visible=False) as og:
|
|
465
|
+
for proj_name, proj in projs.items():
|
|
466
|
+
oplot(proj, proj_name, og, proj_dir=proj_dir, overwrite=overwrite)
|
|
467
|
+
except Exception as e:
|
|
468
|
+
messagebox.showerror("Save error", f"Failed to save to OPJU:\n{e}")
|
|
469
|
+
return
|
|
470
|
+
|
|
471
|
+
messagebox.showinfo("Saved", "Selected figure saved.")
|
|
472
|
+
|
|
473
|
+
def _save_all(self) -> None:
|
|
474
|
+
if not self.figures:
|
|
475
|
+
messagebox.showwarning("No figures", "Nothing to save.")
|
|
476
|
+
return
|
|
477
|
+
raw = self.proj_dir_var.get().strip()
|
|
478
|
+
if not raw:
|
|
479
|
+
messagebox.showwarning("Missing directory", "Please choose an OPJU directory first.")
|
|
480
|
+
return
|
|
481
|
+
|
|
482
|
+
proj_dir = Path(raw).expanduser().resolve()
|
|
483
|
+
if not proj_dir.exists():
|
|
484
|
+
messagebox.showwarning("Invalid directory", "Please choose a valid OPJU directory.")
|
|
485
|
+
return
|
|
486
|
+
overwrite = self.save_mode_var.get() == "overwrite"
|
|
487
|
+
|
|
488
|
+
projs = pack_into_project(self.figures)
|
|
489
|
+
|
|
490
|
+
try:
|
|
491
|
+
with OriginCOM(visible=False) as og:
|
|
492
|
+
for proj_name, proj in projs.items():
|
|
493
|
+
oplot(proj, proj_name, og, proj_dir=proj_dir, overwrite=overwrite)
|
|
494
|
+
except Exception as e:
|
|
495
|
+
messagebox.showerror("Save error", f"Failed to save to OPJU:\n{e}")
|
|
496
|
+
return
|
|
497
|
+
|
|
498
|
+
messagebox.showinfo("Saved", "All figures saved.")
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def main() -> None:
|
|
502
|
+
root = tk.Tk()
|
|
503
|
+
root.attributes("-topmost", False)
|
|
504
|
+
|
|
505
|
+
style = ttk.Style(root)
|
|
506
|
+
try:
|
|
507
|
+
style.theme_use("clam")
|
|
508
|
+
except Exception:
|
|
509
|
+
pass
|
|
510
|
+
|
|
511
|
+
app = MainWindow(root)
|
|
512
|
+
|
|
513
|
+
def _on_close():
|
|
514
|
+
# 1) Close all matplotlib figures (previews)
|
|
515
|
+
try:
|
|
516
|
+
import matplotlib.pyplot as plt
|
|
517
|
+
plt.close("all")
|
|
518
|
+
except Exception:
|
|
519
|
+
pass
|
|
520
|
+
|
|
521
|
+
# 2) Destroy tkinter root
|
|
522
|
+
try:
|
|
523
|
+
root.quit()
|
|
524
|
+
except Exception:
|
|
525
|
+
pass
|
|
526
|
+
root.destroy()
|
|
527
|
+
|
|
528
|
+
root.protocol("WM_DELETE_WINDOW", _on_close)
|
|
529
|
+
root.mainloop()
|