Semapp 1.0.5__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.
- semapp/Layout/__init__.py +26 -0
- semapp/Layout/create_button.py +1248 -0
- semapp/Layout/main_window_att.py +54 -0
- semapp/Layout/settings.py +170 -0
- semapp/Layout/styles.py +152 -0
- semapp/Layout/toast.py +157 -0
- semapp/Plot/__init__.py +8 -0
- semapp/Plot/frame_attributes.py +690 -0
- semapp/Plot/overview_window.py +355 -0
- semapp/Plot/styles.py +55 -0
- semapp/Plot/utils.py +295 -0
- semapp/Processing/__init__.py +4 -0
- semapp/Processing/detection.py +513 -0
- semapp/Processing/klarf_reader.py +461 -0
- semapp/Processing/processing.py +686 -0
- semapp/Processing/rename_tif.py +498 -0
- semapp/Processing/split_tif.py +323 -0
- semapp/Processing/threshold.py +777 -0
- semapp/__init__.py +10 -0
- semapp/asset/icon.png +0 -0
- semapp/main.py +103 -0
- semapp-1.0.5.dist-info/METADATA +300 -0
- semapp-1.0.5.dist-info/RECORD +27 -0
- semapp-1.0.5.dist-info/WHEEL +5 -0
- semapp-1.0.5.dist-info/entry_points.txt +2 -0
- semapp-1.0.5.dist-info/licenses/LICENSE +674 -0
- semapp-1.0.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Manage the scrollbar and the functions for the window size
|
|
3
|
+
"""
|
|
4
|
+
from PyQt5.QtCore import Qt, QSize
|
|
5
|
+
from PyQt5.QtWidgets import QScrollArea, QVBoxLayout
|
|
6
|
+
from PyQt5.QtGui import QGuiApplication
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LayoutFrame:
|
|
10
|
+
"""Class for setting up layout and scroll area"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, main_window):
|
|
13
|
+
self.main_window = main_window
|
|
14
|
+
self.canvas_widget = None
|
|
15
|
+
self.canvas_layout = None
|
|
16
|
+
self.scroll_area = QScrollArea(main_window)
|
|
17
|
+
|
|
18
|
+
def setup_layout(self, canvas_widget, canvas_layout):
|
|
19
|
+
"""Set up layout with scroll area and other settings"""
|
|
20
|
+
self.canvas_widget = canvas_widget
|
|
21
|
+
self.canvas_layout = canvas_layout
|
|
22
|
+
|
|
23
|
+
# Set up the QScrollArea
|
|
24
|
+
self.scroll_area.setWidget(self.canvas_widget)
|
|
25
|
+
self.scroll_area.setWidgetResizable(True)
|
|
26
|
+
self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
|
27
|
+
self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
|
28
|
+
|
|
29
|
+
# Set layout for the main window
|
|
30
|
+
layout = QVBoxLayout(self.main_window)
|
|
31
|
+
layout.addWidget(self.scroll_area)
|
|
32
|
+
self.main_window.setLayout(layout)
|
|
33
|
+
|
|
34
|
+
def set_max_window_size(self):
|
|
35
|
+
"""Set the maximum window size based on the screen size"""
|
|
36
|
+
screen_geometry = QGuiApplication.primaryScreen().availableGeometry()
|
|
37
|
+
max_width = screen_geometry.width()
|
|
38
|
+
max_height = screen_geometry.height() - 50
|
|
39
|
+
self.main_window.setMaximumSize(max_width, max_height)
|
|
40
|
+
|
|
41
|
+
def position_window_top_left(self):
|
|
42
|
+
"""Position the window at the top-left corner of the screen"""
|
|
43
|
+
screen_geometry = QGuiApplication.primaryScreen().availableGeometry()
|
|
44
|
+
self.main_window.move(screen_geometry.topLeft())
|
|
45
|
+
|
|
46
|
+
def adjust_scroll_area_size(self):
|
|
47
|
+
"""Adjust the size of the window based on the content"""
|
|
48
|
+
self.canvas_widget.adjustSize()
|
|
49
|
+
optimal_size = self.canvas_widget.sizeHint()
|
|
50
|
+
|
|
51
|
+
screen_size = QGuiApplication.primaryScreen().availableSize()
|
|
52
|
+
new_size = QSize(min(optimal_size.width(), screen_size.width()) + 50,
|
|
53
|
+
min(optimal_size.height(), screen_size.height()) + 50)
|
|
54
|
+
self.main_window.resize(new_size)
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Module for settings window"""
|
|
2
|
+
# pylint: disable=no-name-in-module, trailing-whitespace, too-many-branches, too-many-statements
|
|
3
|
+
import os
|
|
4
|
+
import json
|
|
5
|
+
from PyQt5.QtWidgets import (
|
|
6
|
+
QDialog, QVBoxLayout, QTableWidget, QPushButton,
|
|
7
|
+
QTableWidgetItem
|
|
8
|
+
)
|
|
9
|
+
from PyQt5.QtCore import pyqtSignal
|
|
10
|
+
|
|
11
|
+
class SettingsWindow(QDialog):
|
|
12
|
+
"""Class for settings window"""
|
|
13
|
+
data_updated = pyqtSignal() # Signal emitted when data is updated
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
super().__init__()
|
|
17
|
+
self.setWindowTitle("Settings")
|
|
18
|
+
|
|
19
|
+
# Get the user's folder path (C:\Users\XXXXX)
|
|
20
|
+
self.user_folder = os.path.expanduser("~")
|
|
21
|
+
|
|
22
|
+
# Define the new folder "SEM" and the file to store settings
|
|
23
|
+
self.new_folder = os.path.join(self.user_folder, "SEM")
|
|
24
|
+
self.data_file = os.path.join(self.new_folder, "settings_data.json")
|
|
25
|
+
self.data = [] # Structure to store the table data
|
|
26
|
+
|
|
27
|
+
# Set up the UI layout
|
|
28
|
+
self.layout = QVBoxLayout()
|
|
29
|
+
self.setLayout(self.layout)
|
|
30
|
+
|
|
31
|
+
# Create and add the table and buttons to the layout
|
|
32
|
+
self.create_table()
|
|
33
|
+
self.create_buttons()
|
|
34
|
+
|
|
35
|
+
# Load data from the settings file
|
|
36
|
+
self.load_data()
|
|
37
|
+
|
|
38
|
+
def create_table(self):
|
|
39
|
+
"""Create the settings table."""
|
|
40
|
+
self.table = QTableWidget()
|
|
41
|
+
self.table.setColumnCount(2)
|
|
42
|
+
self.table.setHorizontalHeaderLabels(["Scale", "Image Type"])
|
|
43
|
+
|
|
44
|
+
# Temporarily block signals to prevent updates during initialization
|
|
45
|
+
self.table.blockSignals(True)
|
|
46
|
+
|
|
47
|
+
# Populate the table with existing data
|
|
48
|
+
for row_data in self.data:
|
|
49
|
+
if "Scale" in row_data and "Image Type" in row_data:
|
|
50
|
+
self.add_row(row_data["Scale"], row_data["Image Type"], update_data=False)
|
|
51
|
+
else:
|
|
52
|
+
print(f"Invalid row data: {row_data}")
|
|
53
|
+
|
|
54
|
+
# Re-enable signals after table population
|
|
55
|
+
self.table.blockSignals(False)
|
|
56
|
+
|
|
57
|
+
# Connect the itemChanged signal to update the data structure
|
|
58
|
+
self.table.itemChanged.connect(self.update_data)
|
|
59
|
+
self.layout.addWidget(self.table)
|
|
60
|
+
|
|
61
|
+
def create_buttons(self):
|
|
62
|
+
"""Create buttons to add and remove rows."""
|
|
63
|
+
add_button = QPushButton("Add Row")
|
|
64
|
+
remove_button = QPushButton("Remove Selected Row")
|
|
65
|
+
|
|
66
|
+
add_button.clicked.connect(self.add_row)
|
|
67
|
+
remove_button.clicked.connect(self.remove_selected_row)
|
|
68
|
+
|
|
69
|
+
self.layout.addWidget(add_button)
|
|
70
|
+
self.layout.addWidget(remove_button)
|
|
71
|
+
|
|
72
|
+
def add_row(self, scale="5x5", image_type="Type", update_data=True):
|
|
73
|
+
"""Add a new row to the table with default values."""
|
|
74
|
+
row_position = self.table.rowCount()
|
|
75
|
+
self.table.insertRow(row_position)
|
|
76
|
+
|
|
77
|
+
scale_item = QTableWidgetItem(scale)
|
|
78
|
+
image_type_item = QTableWidgetItem(image_type)
|
|
79
|
+
|
|
80
|
+
self.table.setItem(row_position, 0, scale_item)
|
|
81
|
+
self.table.setItem(row_position, 1, image_type_item)
|
|
82
|
+
|
|
83
|
+
if update_data:
|
|
84
|
+
self.data.append({"Scale": scale, "Image Type": image_type})
|
|
85
|
+
print("Row added:", self.data)
|
|
86
|
+
|
|
87
|
+
def remove_selected_row(self):
|
|
88
|
+
"""Remove the currently selected row from the table."""
|
|
89
|
+
current_row = self.table.currentRow()
|
|
90
|
+
if current_row != -1:
|
|
91
|
+
self.table.removeRow(current_row)
|
|
92
|
+
del self.data[current_row]
|
|
93
|
+
print("Row removed:", self.data)
|
|
94
|
+
|
|
95
|
+
def update_data(self, item):
|
|
96
|
+
"""Update the data structure when a cell value changes."""
|
|
97
|
+
try:
|
|
98
|
+
row = item.row()
|
|
99
|
+
column = item.column()
|
|
100
|
+
value = item.text().strip()
|
|
101
|
+
|
|
102
|
+
while len(self.data) <= row:
|
|
103
|
+
self.data.append({"Scale": "", "Image Type": ""})
|
|
104
|
+
|
|
105
|
+
if column == 0: # Update "Scale"
|
|
106
|
+
if not value.replace('.', '', 1).isdigit():
|
|
107
|
+
print(f"Invalid value for Scale: {value}")
|
|
108
|
+
return
|
|
109
|
+
self.data[row]["Scale"] = value
|
|
110
|
+
elif column == 1: # Update "Image Type"
|
|
111
|
+
self.data[row]["Image Type"] = value
|
|
112
|
+
else:
|
|
113
|
+
print(f"Unexpected column index: {column}")
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
self.save_data()
|
|
117
|
+
print(f"Data updated successfully: {self.data[row]}")
|
|
118
|
+
except Exception as e:
|
|
119
|
+
print(f"Error updating data: {e}")
|
|
120
|
+
|
|
121
|
+
def closeEvent(self, event):
|
|
122
|
+
"""Save data to a file when the dialog is closed."""
|
|
123
|
+
self.save_data()
|
|
124
|
+
self.data_updated.emit()
|
|
125
|
+
super().closeEvent(event)
|
|
126
|
+
|
|
127
|
+
def normalize_data(self):
|
|
128
|
+
"""Synchronize self.data with the actual table contents."""
|
|
129
|
+
self.data = self.get_table_data()
|
|
130
|
+
|
|
131
|
+
def save_data(self):
|
|
132
|
+
"""Save the table data to a JSON file."""
|
|
133
|
+
self.normalize_data()
|
|
134
|
+
with open(self.data_file, "w") as file:
|
|
135
|
+
json.dump(self.data, file, indent=4)
|
|
136
|
+
print("Data saved successfully.")
|
|
137
|
+
|
|
138
|
+
def load_data(self):
|
|
139
|
+
"""Load the table data from the JSON file."""
|
|
140
|
+
try:
|
|
141
|
+
with open(self.data_file, "r") as file:
|
|
142
|
+
self.data = json.load(file)
|
|
143
|
+
print("Data loaded successfully:", self.data)
|
|
144
|
+
except FileNotFoundError:
|
|
145
|
+
print("No previous data found. Starting fresh.")
|
|
146
|
+
self.data = []
|
|
147
|
+
except Exception as err:
|
|
148
|
+
print(f"Error loading data: {err}")
|
|
149
|
+
self.data = []
|
|
150
|
+
|
|
151
|
+
self.table.blockSignals(True)
|
|
152
|
+
self.table.setRowCount(0)
|
|
153
|
+
for row_data in self.data:
|
|
154
|
+
self.add_row(row_data.get("Scale", ""),
|
|
155
|
+
row_data.get("Image Type", ""), update_data=False)
|
|
156
|
+
self.table.blockSignals(False)
|
|
157
|
+
|
|
158
|
+
def get_table_data(self):
|
|
159
|
+
"""Get the current data from the table as a list of dictionaries."""
|
|
160
|
+
table_data = []
|
|
161
|
+
for row in range(self.table.rowCount()):
|
|
162
|
+
scale_item = self.table.item(row, 0)
|
|
163
|
+
image_type_item = self.table.item(row, 1)
|
|
164
|
+
|
|
165
|
+
if scale_item and image_type_item:
|
|
166
|
+
scale = scale_item.text()
|
|
167
|
+
image_type = image_type_item.text()
|
|
168
|
+
table_data.append({"Scale": scale, "Image Type": image_type})
|
|
169
|
+
|
|
170
|
+
return table_data
|
semapp/Layout/styles.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Module containing all styles for the GUI"""
|
|
2
|
+
|
|
3
|
+
# Radio button styles
|
|
4
|
+
RADIO_BUTTON_STYLE = """
|
|
5
|
+
QRadioButton {
|
|
6
|
+
spacing: 0px;
|
|
7
|
+
font-size: 14px;
|
|
8
|
+
}
|
|
9
|
+
QRadioButton::indicator {
|
|
10
|
+
width: 20px;
|
|
11
|
+
height: 20px;
|
|
12
|
+
}
|
|
13
|
+
QRadioButton::indicator:checked {
|
|
14
|
+
background-color: #f0ca41;
|
|
15
|
+
border: 2px solid black;
|
|
16
|
+
}
|
|
17
|
+
QRadioButton::indicator:unchecked {
|
|
18
|
+
background-color: white;
|
|
19
|
+
border: 2px solid #ccc;
|
|
20
|
+
}
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
# Settings button style
|
|
24
|
+
SETTINGS_BUTTON_STYLE = """
|
|
25
|
+
QPushButton {
|
|
26
|
+
font-size: 16px;
|
|
27
|
+
background-color: #b3e5fc;
|
|
28
|
+
border: 2px solid #8c8c8c;
|
|
29
|
+
border-radius: 10px;
|
|
30
|
+
padding: 5px;
|
|
31
|
+
height: 100px;
|
|
32
|
+
}
|
|
33
|
+
QPushButton:hover {
|
|
34
|
+
background-color: #64b5f6;
|
|
35
|
+
}
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
# Run button style
|
|
39
|
+
RUN_BUTTON_STYLE = """
|
|
40
|
+
QPushButton {
|
|
41
|
+
font-size: 16px;
|
|
42
|
+
background-color: #ffcc80;
|
|
43
|
+
border: 2px solid #8c8c8c;
|
|
44
|
+
border-radius: 10px;
|
|
45
|
+
padding: 5px;
|
|
46
|
+
height: 100px;
|
|
47
|
+
}
|
|
48
|
+
QPushButton:hover {
|
|
49
|
+
background-color: #ffb74d;
|
|
50
|
+
}
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
# Group box style
|
|
54
|
+
GROUP_BOX_STYLE = """
|
|
55
|
+
QGroupBox {
|
|
56
|
+
border: 1px solid black;
|
|
57
|
+
border-radius: 5px;
|
|
58
|
+
margin-top: 10px;
|
|
59
|
+
font-size: 20px;
|
|
60
|
+
font-weight: bold;
|
|
61
|
+
}
|
|
62
|
+
QGroupBox::title {
|
|
63
|
+
font-size: 14px;
|
|
64
|
+
font-weight: bold;
|
|
65
|
+
subcontrol-origin: margin;
|
|
66
|
+
subcontrol-position: top center;
|
|
67
|
+
}
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
# Wafer button styles
|
|
71
|
+
WAFER_BUTTON_DEFAULT_STYLE = """
|
|
72
|
+
QRadioButton {
|
|
73
|
+
spacing: 0px;
|
|
74
|
+
font-size: 16px;
|
|
75
|
+
}
|
|
76
|
+
QRadioButton::indicator {
|
|
77
|
+
width: 25px;
|
|
78
|
+
height: 25px;
|
|
79
|
+
}
|
|
80
|
+
QRadioButton::indicator:checked {
|
|
81
|
+
background-color: #ccffcc;
|
|
82
|
+
border: 2px solid black;
|
|
83
|
+
}
|
|
84
|
+
QRadioButton::indicator:unchecked {
|
|
85
|
+
background-color: white;
|
|
86
|
+
border: 2px solid #ccc;
|
|
87
|
+
}
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
WAFER_BUTTON_EXISTING_STYLE = """
|
|
91
|
+
QRadioButton {
|
|
92
|
+
spacing: 0px;
|
|
93
|
+
font-size: 16px;
|
|
94
|
+
}
|
|
95
|
+
QRadioButton::indicator {
|
|
96
|
+
width: 25px;
|
|
97
|
+
height: 25px;
|
|
98
|
+
border: 2px solid #ccc;
|
|
99
|
+
background-color: lightblue;
|
|
100
|
+
}
|
|
101
|
+
QRadioButton::indicator:checked {
|
|
102
|
+
background-color: #ccffcc;
|
|
103
|
+
border: 2px solid black;
|
|
104
|
+
}
|
|
105
|
+
QRadioButton::indicator:unchecked {
|
|
106
|
+
background-color: lightblue;
|
|
107
|
+
border: 2px solid #ccc;
|
|
108
|
+
}
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
WAFER_BUTTON_MISSING_STYLE = """
|
|
112
|
+
QRadioButton {
|
|
113
|
+
spacing: 0px;
|
|
114
|
+
font-size: 16px;
|
|
115
|
+
}
|
|
116
|
+
QRadioButton::indicator {
|
|
117
|
+
width: 25px;
|
|
118
|
+
height: 25px;
|
|
119
|
+
border: 2px solid #ccc;
|
|
120
|
+
background-color: lightcoral;
|
|
121
|
+
}
|
|
122
|
+
QRadioButton::indicator:checked {
|
|
123
|
+
background-color: #ccffcc;
|
|
124
|
+
border: 2px solid black;
|
|
125
|
+
}
|
|
126
|
+
QRadioButton::indicator:unchecked {
|
|
127
|
+
background-color: lightcoral;
|
|
128
|
+
border: 2px solid #ccc;
|
|
129
|
+
}
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
# Style for folder/file selection buttons
|
|
133
|
+
SELECT_BUTTON_STYLE = """
|
|
134
|
+
QPushButton {
|
|
135
|
+
font-size: 16px;
|
|
136
|
+
background-color: #b3e5fc;
|
|
137
|
+
border: 2px solid #8c8c8c;
|
|
138
|
+
border-radius: 10px;
|
|
139
|
+
padding: 10px;
|
|
140
|
+
}
|
|
141
|
+
QPushButton:hover {
|
|
142
|
+
background-color: #64b5f6;
|
|
143
|
+
}
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
# Style for path labels
|
|
147
|
+
PATH_LABEL_STYLE = """
|
|
148
|
+
QLabel {
|
|
149
|
+
font-size: 14px;
|
|
150
|
+
padding: 5px;
|
|
151
|
+
}
|
|
152
|
+
"""
|
semapp/Layout/toast.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Toast notification widget for non-intrusive user feedback.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from PyQt5.QtWidgets import QWidget, QLabel, QVBoxLayout
|
|
6
|
+
from PyQt5.QtCore import Qt, QTimer, QPropertyAnimation, QEasingCurve, QPoint
|
|
7
|
+
from PyQt5.QtGui import QFont
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ToastNotification(QWidget):
|
|
11
|
+
"""
|
|
12
|
+
A toast notification widget that appears temporarily to show messages.
|
|
13
|
+
Non-intrusive and auto-dismisses after a set duration.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, message, parent=None, duration=3000, notification_type="info"):
|
|
17
|
+
"""
|
|
18
|
+
Initialize the toast notification.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
message: Text message to display
|
|
22
|
+
parent: Parent widget
|
|
23
|
+
duration: Duration in milliseconds before auto-dismiss
|
|
24
|
+
notification_type: Type of notification ("info", "success", "warning", "error")
|
|
25
|
+
"""
|
|
26
|
+
super().__init__(parent)
|
|
27
|
+
|
|
28
|
+
self.duration = duration
|
|
29
|
+
self.notification_type = notification_type
|
|
30
|
+
|
|
31
|
+
# Set window flags for frameless, always on top, tooltip behavior
|
|
32
|
+
self.setWindowFlags(
|
|
33
|
+
Qt.ToolTip |
|
|
34
|
+
Qt.FramelessWindowHint |
|
|
35
|
+
Qt.WindowStaysOnTopHint
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Set up styles based on notification type
|
|
39
|
+
self._setup_style()
|
|
40
|
+
|
|
41
|
+
# Create layout and label
|
|
42
|
+
layout = QVBoxLayout()
|
|
43
|
+
layout.setContentsMargins(15, 10, 15, 10)
|
|
44
|
+
|
|
45
|
+
label = QLabel(message)
|
|
46
|
+
label.setWordWrap(True)
|
|
47
|
+
font = QFont()
|
|
48
|
+
font.setPointSize(10)
|
|
49
|
+
label.setFont(font)
|
|
50
|
+
label.setAlignment(Qt.AlignCenter)
|
|
51
|
+
|
|
52
|
+
layout.addWidget(label)
|
|
53
|
+
self.setLayout(layout)
|
|
54
|
+
|
|
55
|
+
# Adjust size to content
|
|
56
|
+
self.adjustSize()
|
|
57
|
+
|
|
58
|
+
def _setup_style(self):
|
|
59
|
+
"""Set up the style based on notification type."""
|
|
60
|
+
colors = {
|
|
61
|
+
"info": {
|
|
62
|
+
"bg": "#2196F3",
|
|
63
|
+
"border": "#1976D2"
|
|
64
|
+
},
|
|
65
|
+
"success": {
|
|
66
|
+
"bg": "#4CAF50",
|
|
67
|
+
"border": "#388E3C"
|
|
68
|
+
},
|
|
69
|
+
"warning": {
|
|
70
|
+
"bg": "#FF9800",
|
|
71
|
+
"border": "#F57C00"
|
|
72
|
+
},
|
|
73
|
+
"error": {
|
|
74
|
+
"bg": "#F44336",
|
|
75
|
+
"border": "#D32F2F"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
color = colors.get(self.notification_type, colors["info"])
|
|
80
|
+
|
|
81
|
+
self.setStyleSheet(f"""
|
|
82
|
+
QWidget {{
|
|
83
|
+
background-color: {color['bg']};
|
|
84
|
+
color: white;
|
|
85
|
+
border: 2px solid {color['border']};
|
|
86
|
+
border-radius: 8px;
|
|
87
|
+
padding: 5px;
|
|
88
|
+
}}
|
|
89
|
+
QLabel {{
|
|
90
|
+
background-color: transparent;
|
|
91
|
+
color: white;
|
|
92
|
+
border: none;
|
|
93
|
+
}}
|
|
94
|
+
""")
|
|
95
|
+
|
|
96
|
+
def show_toast(self):
|
|
97
|
+
"""
|
|
98
|
+
Show the toast notification with fade-in animation.
|
|
99
|
+
Auto-dismisses after the specified duration.
|
|
100
|
+
"""
|
|
101
|
+
if self.parent():
|
|
102
|
+
# Position relative to parent
|
|
103
|
+
parent_rect = self.parent().geometry()
|
|
104
|
+
x = parent_rect.right() - self.width() - 20
|
|
105
|
+
y = parent_rect.top() + 20
|
|
106
|
+
self.move(x, y)
|
|
107
|
+
else:
|
|
108
|
+
# Center on screen if no parent
|
|
109
|
+
from PyQt5.QtGui import QGuiApplication
|
|
110
|
+
screen = QGuiApplication.primaryScreen().geometry()
|
|
111
|
+
x = (screen.width() - self.width()) // 2
|
|
112
|
+
y = screen.height() - self.height() - 50
|
|
113
|
+
self.move(x, y)
|
|
114
|
+
|
|
115
|
+
# Set initial opacity for fade-in
|
|
116
|
+
self.setWindowOpacity(0.0)
|
|
117
|
+
self.show()
|
|
118
|
+
|
|
119
|
+
# Fade-in animation
|
|
120
|
+
fade_in = QPropertyAnimation(self, b"windowOpacity")
|
|
121
|
+
fade_in.setDuration(300)
|
|
122
|
+
fade_in.setStartValue(0.0)
|
|
123
|
+
fade_in.setEndValue(1.0)
|
|
124
|
+
fade_in.setEasingCurve(QEasingCurve.InOutQuad)
|
|
125
|
+
fade_in.start()
|
|
126
|
+
|
|
127
|
+
# Auto-dismiss after duration
|
|
128
|
+
QTimer.singleShot(self.duration, self._dismiss)
|
|
129
|
+
|
|
130
|
+
def _dismiss(self):
|
|
131
|
+
"""Dismiss the toast with fade-out animation."""
|
|
132
|
+
fade_out = QPropertyAnimation(self, b"windowOpacity")
|
|
133
|
+
fade_out.setDuration(300)
|
|
134
|
+
fade_out.setStartValue(1.0)
|
|
135
|
+
fade_out.setEndValue(0.0)
|
|
136
|
+
fade_out.setEasingCurve(QEasingCurve.InOutQuad)
|
|
137
|
+
fade_out.finished.connect(self.close)
|
|
138
|
+
fade_out.start()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def show_toast(parent, message, duration=3000, notification_type="info"):
|
|
142
|
+
"""
|
|
143
|
+
Convenience function to show a toast notification.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
parent: Parent widget
|
|
147
|
+
message: Message to display
|
|
148
|
+
duration: Duration in milliseconds
|
|
149
|
+
notification_type: Type of notification
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
ToastNotification: The created toast widget
|
|
153
|
+
"""
|
|
154
|
+
toast = ToastNotification(message, parent, duration, notification_type)
|
|
155
|
+
toast.show_toast()
|
|
156
|
+
return toast
|
|
157
|
+
|