toast_notification.py
fperson
python
10 months ago
17 kB
19
Indexable
# toast_notification.py
# code FPERSON
"""
ToastNotification - Eleganckie powiadomienia dla aplikacji PySide6
=== PODSTAWOWE UŻYCIE ===
from toast_notification import ToastNotification
# Prosty toast
ToastNotification("Operacja zakończona!", parent=self)
# Toast z typem
ToastNotification("Zapisano pomyślnie!", parent=self,
toast_type=ToastNotification.SUCCESS)
# Toast z pozycją
ToastNotification("Nowe powiadomienie", parent=self,
position=ToastNotification.TOP_RIGHT,
toast_type=ToastNotification.INFO)
# Toast z własnym czasem trwania (w milisekundach)
ToastNotification("Ważna wiadomość", parent=self,
duration=5000, # 5 sekund
toast_type=ToastNotification.WARNING)
=== TYPY TOASTÓW ===
ToastNotification.SUCCESS # ✓ Zielony - operacje zakończone sukcesem
ToastNotification.ERROR # ✗ Czerwony - błędy
ToastNotification.WARNING # ⚠ Pomarańczowy - ostrzeżenia
ToastNotification.INFO # ℹ Niebieski - informacje (domyślny)
=== POZYCJE ===
ToastNotification.TOP_LEFT # Górny lewy róg
ToastNotification.TOP_CENTER # Górny środek (domyślne)
ToastNotification.TOP_RIGHT # Górny prawy róg
ToastNotification.BOTTOM_LEFT # Dolny lewy róg
ToastNotification.BOTTOM_CENTER # Dolny środek
ToastNotification.BOTTOM_RIGHT # Dolny prawy róg
ToastNotification.CENTER # Środek ekranu
=== PEŁNY PRZYKŁAD W APLIKACJI ===
from PySide6.QtWidgets import QApplication, QMainWindow, QPushButton
from toast_notification import ToastNotification
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Toast Demo")
self.setGeometry(100, 100, 800, 600)
# Przycisk testowy
btn = QPushButton("Pokaż Toast", self)
btn.clicked.connect(self.show_toast)
self.setCentralWidget(btn)
def show_toast(self):
ToastNotification("Cześć! Jestem toast!", parent=self)
def save_file(self):
try:
# Twój kod zapisu...
ToastNotification("Plik zapisany!", parent=self,
toast_type=ToastNotification.SUCCESS,
position=ToastNotification.BOTTOM_RIGHT)
except Exception as e:
ToastNotification(f"Błąd: {e}", parent=self,
toast_type=ToastNotification.ERROR,
duration=5000)
if __name__ == "__main__":
app = QApplication([])
window = MainWindow()
window.show()
app.exec()
=== ZAAWANSOWANE UŻYCIE ===
# Wiele toastów naraz (automatycznie się stackują)
ToastNotification("Toast 1", parent=self)
ToastNotification("Toast 2", parent=self)
ToastNotification("Toast 3", parent=self)
# Długi toast z zawijaniem tekstu
ToastNotification(
"To jest bardzo długa wiadomość, która automatycznie "
"zawinie się do następnej linii, jeśli będzie za długa",
parent=self,
duration=6000
)
# Różne toasty w różnych miejscach jednocześnie
ToastNotification("Góra", parent=self, position=ToastNotification.TOP_CENTER)
ToastNotification("Dół", parent=self, position=ToastNotification.BOTTOM_CENTER)
ToastNotification("Prawo", parent=self, position=ToastNotification.TOP_RIGHT)
=== CECHY ===
✓ Automatyczne stackowanie wielu toastów
✓ Płynne animacje slide-in/slide-out
✓ Kliknięcie zamyka toast wcześniej
✓ Efekt hover (rozjaśnienie)
✓ Zawijanie długiego tekstu
✓ Zawsze na wierzchu aplikacji
✓ Przezroczyste tło z cieniem
"""
from PySide6.QtWidgets import QWidget, QLabel, QGraphicsOpacityEffect, QHBoxLayout, QGraphicsDropShadowEffect
from PySide6.QtCore import QTimer, QPropertyAnimation, QEasingCurve, Qt, QRect, QParallelAnimationGroup, QPoint
from PySide6.QtGui import QPainter, QColor, QFont
class ToastNotification(QWidget):
"""Konfigurowalny toast z różnymi pozycjami i czasami"""
# Typy toastów
SUCCESS = "success"
ERROR = "error"
INFO = "info"
WARNING = "warning"
# Pozycje
TOP_RIGHT = "top_right"
TOP_CENTER = "top_center"
TOP_LEFT = "top_left"
BOTTOM_RIGHT = "bottom_right"
BOTTOM_CENTER = "bottom_center"
BOTTOM_LEFT = "bottom_left"
CENTER = "center"
# Domyślne ustawienia
DEFAULT_DURATION = 3500 # 3.5 sekundy
DEFAULT_POSITION = TOP_CENTER
# Globalny rejestr aktywnych toastów dla stackowania
active_toasts = []
def __init__(self, message, parent=None, duration=None, toast_type=INFO, position=None):
super().__init__(parent)
self.toast_type = toast_type
self.duration = duration or self.DEFAULT_DURATION
self.position = position or self.DEFAULT_POSITION
self.setWindowFlags(Qt.FramelessWindowHint | Qt.Tool | Qt.WindowStaysOnTopHint)
self.setAttribute(Qt.WA_TranslucentBackground)
self.setup_ui(message)
self.setup_animations()
self.setup_shadow()
# Dodaj do rejestru aktywnych toastów
ToastNotification.active_toasts.append(self)
# Pozycjonowanie z uwzględnieniem innych toastów
if parent:
self.position_toast(parent)
# Kliknięcie żeby zamknąć
self.mousePressEvent = self.close_on_click
# Uruchom animację i timer
self.show_toast()
def setup_ui(self, message):
"""Konfiguruje interfejs toast"""
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
# Ikona + wiadomość
icon, colors = self.get_style_for_type()
full_message = f"{icon} {message}"
self.label = QLabel(full_message)
self.label.setAlignment(Qt.AlignCenter)
self.label.setWordWrap(True) # Zawijanie tekstu
self.label.setMaximumWidth(400) # Maksymalna szerokość
self.label.setMinimumWidth(250) # Minimalna szerokość
# Ustaw styl na podstawie typu
gradient_start, gradient_end, text_color = colors
self.label.setStyleSheet(f"""
QLabel {{
color: {text_color};
font-family: 'Segoe UI', 'SF Pro Display', sans-serif;
font-weight: 600;
font-size: 12px;
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop:0 {gradient_start}, stop:1 {gradient_end});
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.2);
padding: 15px 20px;
line-height: 1.3;
}}
""")
layout.addWidget(self.label)
# Dopasuj rozmiar toast do etykiety
self.adjustSize()
def get_style_for_type(self):
"""Zwraca ikonę i kolory dla typu toast"""
styles = {
self.SUCCESS: ("✓", ("rgba(39, 174, 96, 240)", "rgba(46, 204, 113, 240)", "white")),
self.ERROR: ("✗", ("rgba(231, 76, 60, 240)", "rgba(192, 57, 43, 240)", "white")),
self.WARNING: ("⚠", ("rgba(243, 156, 18, 240)", "rgba(230, 126, 34, 240)", "white")),
self.INFO: ("ℹ", ("rgba(52, 152, 219, 240)", "rgba(41, 128, 185, 240)", "white"))
}
return styles.get(self.toast_type, styles[self.INFO])
def position_toast(self, parent):
"""Pozycjonuje toast względem okna rodzica - POPRAWIONE"""
# Pobierz globalną pozycję okna rodzica
parent_global_pos = parent.mapToGlobal(QPoint(0, 0))
parent_size = parent.size()
# Stwórz rect z globalną pozycją
parent_rect = QRect(
parent_global_pos.x(),
parent_global_pos.y(),
parent_size.width(),
parent_size.height()
)
self.adjustSize()
toast_size = self.size()
# Oblicz offset dla stackowanych toastów
stack_offset = self.calculate_stack_offset()
# Bazowe pozycje - WZGLĘDEM OKNA APLIKACJI
positions = {
self.TOP_CENTER: (
parent_rect.x() + (parent_rect.width() - toast_size.width()) // 2,
parent_rect.y() + 20 + stack_offset
),
self.TOP_RIGHT: (
parent_rect.x() + parent_rect.width() - toast_size.width() - 20,
parent_rect.y() + 20 + stack_offset
),
self.TOP_LEFT: (
parent_rect.x() + 20,
parent_rect.y() + 20 + stack_offset
),
self.BOTTOM_CENTER: (
parent_rect.x() + (parent_rect.width() - toast_size.width()) // 2,
parent_rect.y() + parent_rect.height() - toast_size.height() - 20 - stack_offset
),
self.BOTTOM_RIGHT: (
parent_rect.x() + parent_rect.width() - toast_size.width() - 20,
parent_rect.y() + parent_rect.height() - toast_size.height() - 20 - stack_offset
),
self.BOTTOM_LEFT: (
parent_rect.x() + 20,
parent_rect.y() + parent_rect.height() - toast_size.height() - 20 - stack_offset
),
self.CENTER: (
parent_rect.x() + (parent_rect.width() - toast_size.width()) // 2,
parent_rect.y() + (parent_rect.height() - toast_size.height()) // 2 + stack_offset
)
}
target_x, target_y = positions.get(self.position, positions[self.TOP_CENTER])
# Pozycje dla animacji w zależności od kierunku
if self.position in [self.TOP_RIGHT, self.BOTTOM_RIGHT]:
# Slide z prawej
start_x = parent_rect.x() + parent_rect.width() + 50
start_y = target_y
elif self.position in [self.TOP_LEFT, self.BOTTOM_LEFT]:
# Slide z lewej
start_x = parent_rect.x() - toast_size.width() - 50
start_y = target_y
elif self.position in [self.TOP_CENTER]:
# Slide z góry
start_x = target_x
start_y = parent_rect.y() - toast_size.height() - 50
elif self.position in [self.BOTTOM_CENTER]:
# Slide z dołu
start_x = target_x
start_y = parent_rect.y() + parent_rect.height() + 50
else: # CENTER
# Fade in na miejscu
start_x = target_x
start_y = target_y
self.start_rect = QRect(start_x, start_y, toast_size.width(), toast_size.height())
self.target_rect = QRect(target_x, target_y, toast_size.width(), toast_size.height())
self.setGeometry(self.start_rect)
def calculate_stack_offset(self):
"""Oblicza offset dla stackowanych toastów tej samej pozycji"""
offset = 0
for toast in ToastNotification.active_toasts:
if toast != self and toast.position == self.position:
if self.position in [self.TOP_CENTER, self.TOP_LEFT, self.TOP_RIGHT]:
offset += toast.height() + 10 # Stack w dół
elif self.position in [self.BOTTOM_CENTER, self.BOTTOM_LEFT, self.BOTTOM_RIGHT]:
offset += toast.height() + 10 # Stack w górę
else: # CENTER
offset += 30 # Lekkie przesunięcie
return offset
def setup_shadow(self):
"""Dodaje cień do toast"""
shadow = QGraphicsDropShadowEffect()
shadow.setBlurRadius(20)
shadow.setXOffset(0)
shadow.setYOffset(5)
shadow.setColor(QColor(0, 0, 0, 60))
self.setGraphicsEffect(shadow)
def setup_animations(self):
"""Konfiguruje animacje"""
# Grupa animacji dla pojawienia się
self.show_group = QParallelAnimationGroup()
# Animacja przezroczystości
self.opacity_effect = QGraphicsOpacityEffect()
self.opacity_animation = QPropertyAnimation(self.opacity_effect, b"opacity")
self.opacity_animation.setDuration(500)
self.opacity_animation.setEasingCurve(QEasingCurve.OutCubic)
# Animacja pozycji
self.pos_animation = QPropertyAnimation(self, b"geometry")
self.pos_animation.setDuration(500)
self.pos_animation.setEasingCurve(
QEasingCurve.OutBack if self.position != self.CENTER else QEasingCurve.OutCubic)
self.show_group.addAnimation(self.opacity_animation)
if self.position != self.CENTER: # CENTER nie ma animacji pozycji
self.show_group.addAnimation(self.pos_animation)
# Grupa animacji dla ukrycia
self.hide_group = QParallelAnimationGroup()
self.hide_opacity = QPropertyAnimation(self.opacity_effect, b"opacity")
self.hide_opacity.setDuration(300)
self.hide_opacity.setEasingCurve(QEasingCurve.InCubic)
self.hide_pos = QPropertyAnimation(self, b"geometry")
self.hide_pos.setDuration(300)
self.hide_pos.setEasingCurve(QEasingCurve.InCubic)
self.hide_group.addAnimation(self.hide_opacity)
self.hide_group.addAnimation(self.hide_pos)
# Timer do ukrycia
self.timer = QTimer()
self.timer.timeout.connect(self.hide_toast)
self.timer.setSingleShot(True)
self.hide_group.finished.connect(self.cleanup)
def show_toast(self):
"""Pokazuje toast z animacją"""
self.show()
# Animacja opacity: 0 -> 1
self.opacity_animation.setStartValue(0.0)
self.opacity_animation.setEndValue(1.0)
# Animacja pozycji: slide in
if self.position != self.CENTER:
self.pos_animation.setStartValue(self.start_rect)
self.pos_animation.setEndValue(self.target_rect)
# Ustaw efekt przezroczystości
self.label.setGraphicsEffect(self.opacity_effect)
# Uruchom animacje
self.show_group.start()
self.timer.start(self.duration)
def hide_toast(self):
"""Ukrywa toast z animacją slide-out"""
current_rect = self.geometry()
# Pozycja ukrycia w zależności od pozycji
if self.position in [self.TOP_RIGHT, self.BOTTOM_RIGHT]:
hide_x = current_rect.x() + 350 # Slide out w prawo
hide_y = current_rect.y()
elif self.position in [self.TOP_LEFT, self.BOTTOM_LEFT]:
hide_x = current_rect.x() - 350 # Slide out w lewo
hide_y = current_rect.y()
elif self.position in [self.TOP_CENTER]:
hide_x = current_rect.x()
hide_y = current_rect.y() - 100 # Slide out w górę
elif self.position in [self.BOTTOM_CENTER]:
hide_x = current_rect.x()
hide_y = current_rect.y() + 100 # Slide out w dół
else: # CENTER i inne
hide_x = current_rect.x() + 350
hide_y = current_rect.y()
hide_rect = QRect(hide_x, hide_y, current_rect.width(), current_rect.height())
# Animacja opacity: 1 -> 0
self.hide_opacity.setStartValue(1.0)
self.hide_opacity.setEndValue(0.0)
# Animacja pozycji: slide out
self.hide_pos.setStartValue(current_rect)
self.hide_pos.setEndValue(hide_rect)
self.hide_group.start()
def cleanup(self):
"""Czyści toast z rejestru i zamyka widget"""
try:
ToastNotification.active_toasts.remove(self)
except ValueError:
pass # Toast już usunięty z listy
self.close()
self.deleteLater()
def close_on_click(self, event):
"""Zamyka toast po kliknięciu"""
self.timer.stop() # Zatrzymaj automatyczne ukrywanie
self.hide_toast()
def enterEvent(self, event):
"""Efekt hover - lekko rozjaśnij"""
if hasattr(self, 'label'):
current_style = self.label.styleSheet()
if "240)" in current_style:
self.label.setStyleSheet(current_style.replace("240)", "255)"))
def leaveEvent(self, event):
"""Przywróć normalny wygląd po hover"""
if hasattr(self, 'label'):
current_style = self.label.styleSheet()
if "255)" in current_style:
self.label.setStyleSheet(current_style.replace("255)", "240)"))Editor is loading...
Leave a Comment