Semapp 1.0.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.
Potentially problematic release.
This version of Semapp might be problematic. Click here for more details.
- semapp/Layout/__init__.py +26 -0
- semapp/Layout/create_button.py +496 -0
- semapp/Layout/main_window_att.py +54 -0
- semapp/Layout/settings.py +170 -0
- semapp/Layout/styles.py +152 -0
- semapp/Plot/__init__.py +8 -0
- semapp/Plot/frame_attributes.py +408 -0
- semapp/Plot/styles.py +40 -0
- semapp/Plot/utils.py +93 -0
- semapp/Processing/__init__.py +4 -0
- semapp/Processing/processing.py +599 -0
- semapp/__init__.py +0 -0
- semapp/main.py +86 -0
- semapp-1.0.0.dist-info/METADATA +21 -0
- semapp-1.0.0.dist-info/RECORD +19 -0
- semapp-1.0.0.dist-info/WHEEL +5 -0
- semapp-1.0.0.dist-info/entry_points.txt +2 -0
- semapp-1.0.0.dist-info/licenses/LICENSE +674 -0
- semapp-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A class to manage and display frames in the UI, providing functionality
|
|
3
|
+
for plotting and saving combined screenshots of images and plots.
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import numpy as np
|
|
7
|
+
import glob
|
|
8
|
+
import re
|
|
9
|
+
import pandas as pd
|
|
10
|
+
from PIL import Image
|
|
11
|
+
from PyQt5.QtWidgets import QFrame, QGroupBox, QWidget, QVBoxLayout, QPushButton, \
|
|
12
|
+
QGridLayout, QLabel, QFileDialog, QProgressDialog, QMessageBox
|
|
13
|
+
from PyQt5.QtGui import QImage, QPixmap
|
|
14
|
+
from PyQt5.QtCore import Qt
|
|
15
|
+
from matplotlib.figure import Figure
|
|
16
|
+
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
|
|
17
|
+
import matplotlib.pyplot as plt
|
|
18
|
+
from semapp.Plot.utils import create_savebutton
|
|
19
|
+
from semapp.Plot.styles import OPEN_BUTTON_STYLE, MESSAGE_BOX_STYLE, FRAME_STYLE
|
|
20
|
+
|
|
21
|
+
# Constants
|
|
22
|
+
FRAME_SIZE = 600
|
|
23
|
+
CANVAS_SIZE = 600
|
|
24
|
+
radius = 10
|
|
25
|
+
|
|
26
|
+
class PlotFrame(QWidget):
|
|
27
|
+
"""
|
|
28
|
+
A class to manage and display frames in the UI,
|
|
29
|
+
allowing plotting and image viewing.
|
|
30
|
+
Provides functionality to open and display TIFF
|
|
31
|
+
images and plot coordinate mappings.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, layout, button_frame):
|
|
35
|
+
"""
|
|
36
|
+
Initializes the PlotFrame class by setting up the
|
|
37
|
+
UI components and initializing variables.
|
|
38
|
+
|
|
39
|
+
:param layout: The layout to which the frames will be added.
|
|
40
|
+
:param button_frame: The button frame containing
|
|
41
|
+
additional control elements.
|
|
42
|
+
"""
|
|
43
|
+
super().__init__()
|
|
44
|
+
self.layout = layout
|
|
45
|
+
self.button_frame = button_frame
|
|
46
|
+
|
|
47
|
+
# Initialize state
|
|
48
|
+
self.coordinates = None
|
|
49
|
+
self.image_list = []
|
|
50
|
+
self.current_index = 0
|
|
51
|
+
self.canvas_connection_id = None
|
|
52
|
+
self.selected_wafer = None
|
|
53
|
+
self.radius = None
|
|
54
|
+
|
|
55
|
+
self._setup_frames()
|
|
56
|
+
self._setup_plot()
|
|
57
|
+
self._setup_controls()
|
|
58
|
+
|
|
59
|
+
def _setup_frames(self):
|
|
60
|
+
"""Initialize left and right display frames."""
|
|
61
|
+
# Left frame for images
|
|
62
|
+
self.frame_left = self._create_frame()
|
|
63
|
+
self.frame_left_layout = QVBoxLayout()
|
|
64
|
+
self.frame_left.setLayout(self.frame_left_layout)
|
|
65
|
+
|
|
66
|
+
# Right frame for plots
|
|
67
|
+
self.frame_right = self._create_frame()
|
|
68
|
+
self.frame_right_layout = QGridLayout()
|
|
69
|
+
self.frame_right.setLayout(self.frame_right_layout)
|
|
70
|
+
|
|
71
|
+
# Add frames to main layout
|
|
72
|
+
self.layout.addWidget(self.frame_left, 2, 0, 1, 3)
|
|
73
|
+
self.layout.addWidget(self.frame_right, 2, 3, 1, 3)
|
|
74
|
+
|
|
75
|
+
def _create_frame(self):
|
|
76
|
+
"""Create a styled frame with fixed size."""
|
|
77
|
+
frame = QFrame()
|
|
78
|
+
frame.setFrameShape(QFrame.StyledPanel)
|
|
79
|
+
frame.setStyleSheet(FRAME_STYLE)
|
|
80
|
+
frame.setFixedSize(FRAME_SIZE+100, FRAME_SIZE)
|
|
81
|
+
return frame
|
|
82
|
+
|
|
83
|
+
def _setup_plot(self):
|
|
84
|
+
"""Initialize matplotlib figure and canvas."""
|
|
85
|
+
self.figure = Figure(figsize=(5, 5))
|
|
86
|
+
self.ax = self.figure.add_subplot(111)
|
|
87
|
+
self.canvas = FigureCanvas(self.figure)
|
|
88
|
+
self.frame_right_layout.addWidget(self.canvas)
|
|
89
|
+
|
|
90
|
+
# Initialize image display
|
|
91
|
+
self.image_label = QLabel(self)
|
|
92
|
+
self.image_label.setAlignment(Qt.AlignCenter)
|
|
93
|
+
self.frame_left_layout.addWidget(self.image_label)
|
|
94
|
+
|
|
95
|
+
def _setup_controls(self):
|
|
96
|
+
"""Set up control buttons."""
|
|
97
|
+
create_savebutton(self.layout, self.frame_left, self.frame_right)
|
|
98
|
+
|
|
99
|
+
open_button = QPushButton('Open TIFF', self)
|
|
100
|
+
open_button.setStyleSheet(OPEN_BUTTON_STYLE)
|
|
101
|
+
open_button.clicked.connect(self.open_tiff)
|
|
102
|
+
self.layout.addWidget(open_button, 1, 5)
|
|
103
|
+
|
|
104
|
+
def extract_positions(self, filepath):
|
|
105
|
+
|
|
106
|
+
data = {
|
|
107
|
+
"SampleSize": None,
|
|
108
|
+
"DiePitch": {"X": None, "Y": None},
|
|
109
|
+
"DieOrigin": {"X": None, "Y": None},
|
|
110
|
+
"SampleCenterLocation": {"X": None, "Y": None},
|
|
111
|
+
"Defects": []
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
dans_defect_list = False
|
|
115
|
+
|
|
116
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
117
|
+
for ligne in f:
|
|
118
|
+
ligne = ligne.strip()
|
|
119
|
+
|
|
120
|
+
if ligne.startswith("SampleSize"):
|
|
121
|
+
match = re.search(r"SampleSize\s+1\s+(\d+)", ligne)
|
|
122
|
+
if match:
|
|
123
|
+
data["SampleSize"] = int(match.group(1))
|
|
124
|
+
|
|
125
|
+
elif ligne.startswith("DiePitch"):
|
|
126
|
+
match = re.search(r"DiePitch\s+([0-9.]+)\s+([0-9.]+);", ligne)
|
|
127
|
+
if match:
|
|
128
|
+
data["DiePitch"]["X"] = float(match.group(1))
|
|
129
|
+
data["DiePitch"]["Y"] = float(match.group(2))
|
|
130
|
+
|
|
131
|
+
elif ligne.startswith("DieOrigin"):
|
|
132
|
+
match = re.search(r"DieOrigin\s+([0-9.]+)\s+([0-9.]+);", ligne)
|
|
133
|
+
if match:
|
|
134
|
+
data["DieOrigin"]["X"] = float(match.group(1))
|
|
135
|
+
data["DieOrigin"]["Y"] = float(match.group(2))
|
|
136
|
+
|
|
137
|
+
elif ligne.startswith("SampleCenterLocation"):
|
|
138
|
+
match = re.search(r"SampleCenterLocation\s+([0-9.]+)\s+([0-9.]+);", ligne)
|
|
139
|
+
if match:
|
|
140
|
+
data["SampleCenterLocation"]["X"] = float(match.group(1))
|
|
141
|
+
data["SampleCenterLocation"]["Y"] = float(match.group(2))
|
|
142
|
+
|
|
143
|
+
elif ligne.startswith("DefectList"):
|
|
144
|
+
dans_defect_list = True
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
elif dans_defect_list:
|
|
148
|
+
if re.match(r"^\d+\s", ligne):
|
|
149
|
+
valeurs = ligne.split()
|
|
150
|
+
if len(valeurs) >= 18:
|
|
151
|
+
defect = {f"val{i+1}": float(val) for i, val in enumerate(valeurs[:18])}
|
|
152
|
+
data["Defects"].append(defect)
|
|
153
|
+
|
|
154
|
+
pitch_x = data["DiePitch"]["X"]
|
|
155
|
+
pitch_y = data["DiePitch"]["Y"]
|
|
156
|
+
Xcenter = data["SampleCenterLocation"]["X"]
|
|
157
|
+
Ycenter = data["SampleCenterLocation"]["Y"]
|
|
158
|
+
|
|
159
|
+
corrected_positions = []
|
|
160
|
+
|
|
161
|
+
for d in data["Defects"]:
|
|
162
|
+
val1 = d["val1"]
|
|
163
|
+
val2 = d["val2"]
|
|
164
|
+
val3 = d["val3"]
|
|
165
|
+
val4_scaled = d["val4"] * pitch_x - Xcenter
|
|
166
|
+
val5_scaled = d["val5"] * pitch_y - Ycenter
|
|
167
|
+
|
|
168
|
+
x_corr = round((val2 + val4_scaled) / 10000, 1)
|
|
169
|
+
y_corr = round((val3 + val5_scaled) / 10000, 1)
|
|
170
|
+
|
|
171
|
+
corrected_positions.append({
|
|
172
|
+
"defect_id": val1,
|
|
173
|
+
"X": x_corr,
|
|
174
|
+
"Y": y_corr
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
self.coordinates = pd.DataFrame(corrected_positions, columns=["X", "Y"])
|
|
179
|
+
|
|
180
|
+
return self.coordinates
|
|
181
|
+
|
|
182
|
+
def load_coordinates(self, csv_path):
|
|
183
|
+
"""
|
|
184
|
+
Loads the X/Y coordinates from a CSV file for plotting.
|
|
185
|
+
|
|
186
|
+
:param csv_path: Path to the CSV file containing the coordinates.
|
|
187
|
+
"""
|
|
188
|
+
if os.path.exists(csv_path):
|
|
189
|
+
self.coordinates = pd.read_csv(csv_path)
|
|
190
|
+
print(f"Coordinates loaded: {self.coordinates.head()}")
|
|
191
|
+
else:
|
|
192
|
+
print(f"CSV file not found: {csv_path}")
|
|
193
|
+
|
|
194
|
+
def open_tiff(self):
|
|
195
|
+
"""Handle TIFF file opening and display."""
|
|
196
|
+
self.selected_wafer = self.button_frame.get_selected_option()
|
|
197
|
+
|
|
198
|
+
if not all([self.selected_wafer]):
|
|
199
|
+
print("Recipe and wafer selection required")
|
|
200
|
+
self._reset_display()
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
folder_path = os.path.join(self.button_frame.folder_var_changed(),
|
|
205
|
+
str(self.selected_wafer))
|
|
206
|
+
|
|
207
|
+
# Recherche du fichier qui se termine par .001 dans le dossier
|
|
208
|
+
matching_files = glob.glob(os.path.join(folder_path, '*.001'))
|
|
209
|
+
|
|
210
|
+
# Si au moins un fichier correspond, on prend le premier
|
|
211
|
+
if matching_files:
|
|
212
|
+
recipe_path = matching_files[0]
|
|
213
|
+
else:
|
|
214
|
+
recipe_path = None # Ou tu peux lever une exception ou afficher un message d’erreur
|
|
215
|
+
# Charger les coordonnées depuis le fichier CSV (recipe)
|
|
216
|
+
self.coordinates = self.extract_positions(recipe_path)
|
|
217
|
+
|
|
218
|
+
tiff_path = os.path.join(folder_path, "data.tif")
|
|
219
|
+
|
|
220
|
+
if not os.path.isfile(tiff_path):
|
|
221
|
+
print(f"TIFF file not found in {folder_path}")
|
|
222
|
+
self._reset_display()
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
self._load_tiff(tiff_path)
|
|
226
|
+
self._update_plot() # Maintenant les coordonnées seront disponibles pour le plot
|
|
227
|
+
|
|
228
|
+
# Pop-up stylisé
|
|
229
|
+
msg = QMessageBox()
|
|
230
|
+
msg.setIcon(QMessageBox.Information)
|
|
231
|
+
msg.setText(f"Wafer {self.selected_wafer} opened successfully")
|
|
232
|
+
msg.setWindowTitle("Wafer Opened")
|
|
233
|
+
msg.setStyleSheet(MESSAGE_BOX_STYLE)
|
|
234
|
+
msg.exec_()
|
|
235
|
+
|
|
236
|
+
def _reset_display(self):
|
|
237
|
+
"""
|
|
238
|
+
Resets the display by clearing the figure and reinitializing the subplot.
|
|
239
|
+
Also clears the frame_left_layout to remove any existing widgets.
|
|
240
|
+
"""
|
|
241
|
+
# Clear all widgets from the left frame layout
|
|
242
|
+
while self.frame_left_layout.count():
|
|
243
|
+
item = self.frame_left_layout.takeAt(0)
|
|
244
|
+
widget = item.widget()
|
|
245
|
+
if widget is not None:
|
|
246
|
+
widget.deleteLater() # Properly delete the widget
|
|
247
|
+
|
|
248
|
+
# Recreate the image label in the left frame
|
|
249
|
+
self.image_label = QLabel(self)
|
|
250
|
+
self.image_label.setAlignment(Qt.AlignCenter)
|
|
251
|
+
self.frame_left_layout.addWidget(self.image_label)
|
|
252
|
+
|
|
253
|
+
# Clear the figure associated with the canvas
|
|
254
|
+
self.figure.clear()
|
|
255
|
+
self.ax = self.figure.add_subplot(111) # Create a new subplot
|
|
256
|
+
self.plot_mapping_tpl(self.ax) # Plot the default template
|
|
257
|
+
|
|
258
|
+
# Disconnect any existing signal connection
|
|
259
|
+
if self.canvas_connection_id is not None:
|
|
260
|
+
self.canvas.mpl_disconnect(self.canvas_connection_id)
|
|
261
|
+
self.canvas_connection_id = None
|
|
262
|
+
|
|
263
|
+
self.canvas.draw() # Redraw the updated canvas
|
|
264
|
+
|
|
265
|
+
def _update_plot(self):
|
|
266
|
+
"""
|
|
267
|
+
Updates the plot with the current wafer mapping.
|
|
268
|
+
Ensures the plot is clean before adding new data.
|
|
269
|
+
"""
|
|
270
|
+
if hasattr(self, 'ax') and self.ax:
|
|
271
|
+
self.ax.clear() # Clear the existing plot
|
|
272
|
+
else:
|
|
273
|
+
self.ax = self.figure.add_subplot(111) # Create new axes
|
|
274
|
+
|
|
275
|
+
self.plot_mapping_tpl(self.ax) # Plot wafer mapping
|
|
276
|
+
|
|
277
|
+
# Ensure only one connection to the button press event
|
|
278
|
+
if self.canvas_connection_id is not None:
|
|
279
|
+
self.canvas.mpl_disconnect(self.canvas_connection_id)
|
|
280
|
+
|
|
281
|
+
self.canvas_connection_id = self.canvas.mpl_connect(
|
|
282
|
+
'button_press_event', self.on_click)
|
|
283
|
+
self.canvas.draw()
|
|
284
|
+
|
|
285
|
+
def show_image(self):
|
|
286
|
+
"""
|
|
287
|
+
Displays the current image from the image list in the QLabel.
|
|
288
|
+
"""
|
|
289
|
+
if self.image_list:
|
|
290
|
+
pil_image = self.image_list[self.current_index]
|
|
291
|
+
pil_image = pil_image.convert("RGBA")
|
|
292
|
+
data = pil_image.tobytes("raw", "RGBA")
|
|
293
|
+
qimage = QImage(data, pil_image.width, pil_image.height,
|
|
294
|
+
QImage.Format_RGBA8888)
|
|
295
|
+
pixmap = QPixmap.fromImage(qimage)
|
|
296
|
+
self.image_label.setPixmap(pixmap)
|
|
297
|
+
|
|
298
|
+
def plot_mapping_tpl(self, ax):
|
|
299
|
+
"""Plots the mapping of the wafer with coordinate points."""
|
|
300
|
+
ax.set_xlabel('X (cm)', fontsize=20)
|
|
301
|
+
ax.set_ylabel('Y (cm)', fontsize=20)
|
|
302
|
+
|
|
303
|
+
if self.coordinates is not None:
|
|
304
|
+
x_coords = self.coordinates.iloc[:, 0]
|
|
305
|
+
y_coords = self.coordinates.iloc[:, 1]
|
|
306
|
+
|
|
307
|
+
# Calcul de la valeur maximale absolue parmi toutes les coordonnées
|
|
308
|
+
max_val = max(abs(x_coords).max(), abs(y_coords).max())
|
|
309
|
+
|
|
310
|
+
if max_val <= 5:
|
|
311
|
+
radius = 5
|
|
312
|
+
elif max_val <= 7.5:
|
|
313
|
+
radius = 7.5
|
|
314
|
+
elif max_val <= 10:
|
|
315
|
+
radius = 10
|
|
316
|
+
elif max_val <= 15:
|
|
317
|
+
radius = 15
|
|
318
|
+
else:
|
|
319
|
+
radius = max_val # fallback pour les cas supérieurs à 30
|
|
320
|
+
|
|
321
|
+
self.radius = radius
|
|
322
|
+
|
|
323
|
+
ax.scatter(x_coords, y_coords, color='blue', marker='o',
|
|
324
|
+
s=100, label='Positions')
|
|
325
|
+
|
|
326
|
+
# Mise à l'échelle du graphique en fonction du radius
|
|
327
|
+
ax.set_xlim(-radius - 1, radius + 1)
|
|
328
|
+
ax.set_ylim(-radius - 1, radius + 1)
|
|
329
|
+
|
|
330
|
+
circle = plt.Circle((0, 0), radius, color='black',
|
|
331
|
+
fill=False, linewidth=0.5)
|
|
332
|
+
ax.add_patch(circle)
|
|
333
|
+
ax.set_aspect('equal')
|
|
334
|
+
else:
|
|
335
|
+
print("No coordinates available to plot")
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
ax.figure.subplots_adjust(left=0.15, right=0.95, top=0.90, bottom=0.1)
|
|
339
|
+
self.canvas.draw()
|
|
340
|
+
|
|
341
|
+
def on_click(self, event):
|
|
342
|
+
"""
|
|
343
|
+
Handles mouse click events on the plot, identifying the closest point
|
|
344
|
+
and updating the plot with a red circle around the selected point.
|
|
345
|
+
|
|
346
|
+
:param event: The event generated by the mouse click.
|
|
347
|
+
"""
|
|
348
|
+
result = self.button_frame.get_selected_image()
|
|
349
|
+
if result is not None:
|
|
350
|
+
self.image_type, self_number_type = result
|
|
351
|
+
else:
|
|
352
|
+
print("No image selected.")
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
if event.inaxes:
|
|
356
|
+
x_pos = event.xdata
|
|
357
|
+
y_pos = event.ydata
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
if self.coordinates is not None and not self.coordinates.empty:
|
|
361
|
+
distances = np.sqrt((self.coordinates['X'] - x_pos) ** 2 +
|
|
362
|
+
(self.coordinates['Y'] - y_pos) ** 2)
|
|
363
|
+
closest_idx = distances.idxmin()
|
|
364
|
+
closest_pt = self.coordinates.iloc[closest_idx]
|
|
365
|
+
print(f"The closest point is: X = {closest_pt['X']}, "
|
|
366
|
+
f"Y = {closest_pt['Y']}")
|
|
367
|
+
|
|
368
|
+
# Replot with a red circle around the selected point
|
|
369
|
+
self.ax.clear() # Clear the existing plot
|
|
370
|
+
self.plot_mapping_tpl(self.ax)
|
|
371
|
+
self.ax.scatter([closest_pt['X']], [closest_pt['Y']],
|
|
372
|
+
color='red', marker='o', s=100,
|
|
373
|
+
label='Selected point')
|
|
374
|
+
coord_text = f"{closest_pt['X']:.1f} / {closest_pt['Y']:.1f}"
|
|
375
|
+
self.ax.text(-self.radius -0.5, self.radius-0.5, coord_text, fontsize=16, color='black')
|
|
376
|
+
self.canvas.draw()
|
|
377
|
+
|
|
378
|
+
# Update the image based on the selected point
|
|
379
|
+
result = self.image_type + (closest_idx * self_number_type)
|
|
380
|
+
self.current_index = result
|
|
381
|
+
self.show_image()
|
|
382
|
+
|
|
383
|
+
def _load_tiff(self, tiff_path):
|
|
384
|
+
"""Load and prepare TIFF images for display.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
tiff_path: Path to the TIFF file to load
|
|
388
|
+
"""
|
|
389
|
+
try:
|
|
390
|
+
img = Image.open(tiff_path)
|
|
391
|
+
self.image_list = []
|
|
392
|
+
|
|
393
|
+
# Load all TIFF pages and resize them
|
|
394
|
+
while True:
|
|
395
|
+
resized_img = img.copy().resize((CANVAS_SIZE, CANVAS_SIZE),
|
|
396
|
+
Image.Resampling.LANCZOS)
|
|
397
|
+
self.image_list.append(resized_img)
|
|
398
|
+
try:
|
|
399
|
+
img.seek(img.tell() + 1) # Move to next page
|
|
400
|
+
except EOFError:
|
|
401
|
+
break # No more pages
|
|
402
|
+
|
|
403
|
+
self.current_index = 0
|
|
404
|
+
self.show_image() # Display first image
|
|
405
|
+
|
|
406
|
+
except Exception as e:
|
|
407
|
+
print(f"Error loading TIFF file: {e}")
|
|
408
|
+
self._reset_display()
|
semapp/Plot/styles.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Styles definitions for the GUI components."""
|
|
2
|
+
|
|
3
|
+
# Button styles
|
|
4
|
+
OPEN_BUTTON_STYLE = """
|
|
5
|
+
QPushButton {
|
|
6
|
+
font-size: 16px;
|
|
7
|
+
background-color: #ffcc80;
|
|
8
|
+
border: 2px solid #8c8c8c;
|
|
9
|
+
border-radius: 10px;
|
|
10
|
+
padding: 5px;
|
|
11
|
+
height: 50px;
|
|
12
|
+
}
|
|
13
|
+
QPushButton:hover {
|
|
14
|
+
background-color: #ffb74d;
|
|
15
|
+
}
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
# Message box styles
|
|
19
|
+
MESSAGE_BOX_STYLE = """
|
|
20
|
+
QMessageBox {
|
|
21
|
+
background-color: white;
|
|
22
|
+
}
|
|
23
|
+
QMessageBox QLabel {
|
|
24
|
+
color: #333;
|
|
25
|
+
font-size: 14px;
|
|
26
|
+
}
|
|
27
|
+
QPushButton {
|
|
28
|
+
background-color: #ffcc80;
|
|
29
|
+
border: 2px solid #8c8c8c;
|
|
30
|
+
border-radius: 5px;
|
|
31
|
+
padding: 5px 15px;
|
|
32
|
+
font-size: 12px;
|
|
33
|
+
}
|
|
34
|
+
QPushButton:hover {
|
|
35
|
+
background-color: #ffb74d;
|
|
36
|
+
}
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
# Frame styles
|
|
40
|
+
FRAME_STYLE = "background-color: white;"
|
semapp/Plot/utils.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for frame management and screenshot functionality.
|
|
3
|
+
|
|
4
|
+
This module provides functions to create save buttons and manage frame layouts
|
|
5
|
+
in the application. It includes functionality for capturing and saving
|
|
6
|
+
combined screenshots of multiple frames.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from PyQt5.QtCore import Qt
|
|
10
|
+
from PyQt5.QtWidgets import QPushButton, QFileDialog
|
|
11
|
+
from PyQt5.QtGui import QPixmap, QPainter
|
|
12
|
+
|
|
13
|
+
# Style constants
|
|
14
|
+
SAVE_BUTTON_STYLE = """
|
|
15
|
+
QPushButton {
|
|
16
|
+
font-size: 16px;
|
|
17
|
+
background-color: #e1bee7;
|
|
18
|
+
border: 2px solid #8c8c8c;
|
|
19
|
+
border-radius: 10px;
|
|
20
|
+
padding: 5px;
|
|
21
|
+
height: 20px;
|
|
22
|
+
}
|
|
23
|
+
QPushButton:hover {
|
|
24
|
+
background-color: #ce93d8;
|
|
25
|
+
}
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
BUTTON_POSITION = {
|
|
29
|
+
'row': 4,
|
|
30
|
+
'column': 0,
|
|
31
|
+
'row_span': 1,
|
|
32
|
+
'column_span': 6
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def create_savebutton(layout, frame_left, frame_right):
|
|
37
|
+
"""
|
|
38
|
+
Create a save button to capture and save specific frames as a combined image.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
layout: The layout where the save button will be added
|
|
42
|
+
frame_left: The left frame to capture
|
|
43
|
+
frame_right: The right frame to capture
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
None
|
|
47
|
+
"""
|
|
48
|
+
def save_image():
|
|
49
|
+
"""
|
|
50
|
+
Capture and save specific frames as a combined image.
|
|
51
|
+
|
|
52
|
+
Creates a screenshot of both left and right frames, combines them
|
|
53
|
+
horizontally and saves the result as a PNG file.
|
|
54
|
+
"""
|
|
55
|
+
screen_left = frame_left.grab()
|
|
56
|
+
screen_right = frame_right.grab()
|
|
57
|
+
|
|
58
|
+
file_name, _ = QFileDialog.getSaveFileName(
|
|
59
|
+
parent=None,
|
|
60
|
+
caption="Save screenshot",
|
|
61
|
+
directory="",
|
|
62
|
+
filter="PNG Files (*.png);;All Files (*)"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if file_name:
|
|
66
|
+
combined_width = screen_left.width() + screen_right.width()
|
|
67
|
+
combined_height = max(screen_left.height(), screen_right.height())
|
|
68
|
+
combined_pixmap = QPixmap(combined_width, combined_height)
|
|
69
|
+
|
|
70
|
+
# Fill the combined QPixmap with a white background
|
|
71
|
+
combined_pixmap.fill(Qt.white)
|
|
72
|
+
|
|
73
|
+
painter = QPainter(combined_pixmap)
|
|
74
|
+
painter.drawPixmap(0, 0, screen_left)
|
|
75
|
+
painter.drawPixmap(screen_left.width(), 0, screen_right)
|
|
76
|
+
painter.end()
|
|
77
|
+
|
|
78
|
+
# Save the combined image
|
|
79
|
+
combined_pixmap.save(file_name, "PNG")
|
|
80
|
+
|
|
81
|
+
# Create and configure the save button
|
|
82
|
+
save_button = QPushButton("Screenshot")
|
|
83
|
+
save_button.setStyleSheet(SAVE_BUTTON_STYLE)
|
|
84
|
+
save_button.clicked.connect(save_image)
|
|
85
|
+
|
|
86
|
+
# Add the button to the layout using position constants
|
|
87
|
+
layout.addWidget(
|
|
88
|
+
save_button,
|
|
89
|
+
BUTTON_POSITION['row'],
|
|
90
|
+
BUTTON_POSITION['column'],
|
|
91
|
+
BUTTON_POSITION['row_span'],
|
|
92
|
+
BUTTON_POSITION['column_span']
|
|
93
|
+
)
|