233 Zeilen
8.0 KiB
Python
233 Zeilen
8.0 KiB
Python
"""
|
|
Dark Mode Toggle Widget for PyQt5
|
|
Based on Corporate Design Guidelines with smooth animations
|
|
"""
|
|
|
|
from PyQt5.QtWidgets import QWidget
|
|
from PyQt5.QtCore import Qt, pyqtSignal, QPropertyAnimation, QEasingCurve, QRectF, pyqtProperty, QByteArray
|
|
from PyQt5.QtGui import QPainter, QColor, QBrush, QPen
|
|
from PyQt5.QtSvg import QSvgRenderer
|
|
|
|
|
|
class DarkModeToggle(QWidget):
|
|
"""
|
|
Animated Toggle-Switch for Dark/Light Mode with embedded SVG Icons.
|
|
|
|
Features:
|
|
- Smooth 200ms animation with InOutCubic easing
|
|
- Embedded sun/moon icons (no external files needed)
|
|
- Corporate design colors (#CBD5E0 for light, #00D4FF for dark)
|
|
- Size: 60x30 pixels
|
|
- PyQt5 compatible
|
|
|
|
Signals:
|
|
toggled(bool): Emitted when toggle state changes (True = Dark Mode)
|
|
"""
|
|
|
|
# Signal emitted when toggle state changes
|
|
toggled = pyqtSignal(bool)
|
|
|
|
def __init__(self, parent=None, initial_dark_mode=False):
|
|
"""
|
|
Initialize the Dark Mode Toggle.
|
|
|
|
Args:
|
|
parent: Parent widget
|
|
initial_dark_mode: Initial state (False = Light Mode, True = Dark Mode)
|
|
"""
|
|
super().__init__(parent)
|
|
|
|
# Fixed size as per design spec
|
|
self.setFixedSize(60, 30)
|
|
self.setCursor(Qt.PointingHandCursor)
|
|
|
|
# State management
|
|
self._checked = initial_dark_mode
|
|
|
|
# Animation property for smooth sliding
|
|
self._handle_position = 27.0 if initial_dark_mode else 3.0
|
|
|
|
# Configure animation
|
|
self._animation = QPropertyAnimation(self, b"handle_position")
|
|
self._animation.setDuration(200) # 200ms as per spec
|
|
self._animation.setEasingCurve(QEasingCurve.InOutCubic)
|
|
|
|
# Initialize SVG renderers
|
|
self.sun_svg = QSvgRenderer()
|
|
self.moon_svg = QSvgRenderer()
|
|
|
|
# Load embedded SVG content
|
|
self._load_svg_icons()
|
|
|
|
# Tooltip for accessibility
|
|
self._update_tooltip()
|
|
|
|
def _load_svg_icons(self):
|
|
"""Load embedded SVG icons for sun and moon."""
|
|
|
|
# Sun icon for Light Mode (black strokes)
|
|
sun_svg_content = b'''<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M12 3V4M12 20V21M4 12H3M6.31412 6.31412L5.5 5.5M17.6859 6.31412L18.5 5.5M6.31412 17.69L5.5 18.5001M17.6859 17.69L18.5 18.5001M21 12H20M16 12C16 14.2091 14.2091 16 12 16C9.79086 16 8 14.2091 8 12C8 9.79086 9.79086 8 12 8C14.2091 8 16 9.79086 16 12Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
</svg>'''
|
|
|
|
# Moon icon for Dark Mode (dark blue strokes matching primary color)
|
|
moon_svg_content = b'''<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M3.32031 11.6835C3.32031 16.6541 7.34975 20.6835 12.3203 20.6835C16.1075 20.6835 19.3483 18.3443 20.6768 15.032C19.6402 15.4486 18.5059 15.6834 17.3203 15.6834C12.3497 15.6834 8.32031 11.654 8.32031 6.68342C8.32031 5.50338 8.55165 4.36259 8.96453 3.32996C5.65605 4.66028 3.32031 7.89912 3.32031 11.6835Z" stroke="#232D53" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
</svg>'''
|
|
|
|
# Load SVG data into renderers
|
|
self.sun_svg.load(QByteArray(sun_svg_content))
|
|
self.moon_svg.load(QByteArray(moon_svg_content))
|
|
|
|
# Property for animation
|
|
def get_handle_position(self):
|
|
"""Get current handle position for animation."""
|
|
return self._handle_position
|
|
|
|
def set_handle_position(self, pos):
|
|
"""Set handle position and trigger repaint."""
|
|
self._handle_position = pos
|
|
self.update() # Trigger paintEvent
|
|
|
|
# Define property for QPropertyAnimation
|
|
handle_position = pyqtProperty(float, get_handle_position, set_handle_position)
|
|
|
|
def paintEvent(self, event):
|
|
"""
|
|
Custom paint event to draw the toggle switch.
|
|
|
|
Draws:
|
|
1. Track (background pill shape)
|
|
2. Handle (white circular button)
|
|
3. Icon (sun or moon in the handle)
|
|
"""
|
|
painter = QPainter(self)
|
|
painter.setRenderHint(QPainter.Antialiasing)
|
|
|
|
# Draw track (pill-shaped background)
|
|
track_rect = QRectF(0, 0, self.width(), self.height())
|
|
track_radius = self.height() / 2
|
|
|
|
# Track color based on state
|
|
if self._checked:
|
|
# Dark mode active - cyan color from corporate design
|
|
track_color = QColor("#00D4FF")
|
|
else:
|
|
# Light mode active - gray color from design spec
|
|
track_color = QColor("#CBD5E0")
|
|
|
|
painter.setBrush(QBrush(track_color))
|
|
painter.setPen(QPen(Qt.NoPen))
|
|
painter.drawRoundedRect(track_rect, track_radius, track_radius)
|
|
|
|
# Draw handle (white circle that slides)
|
|
handle_diameter = self.height() - 6 # 24px (30 - 6)
|
|
handle_rect = QRectF(
|
|
self._handle_position, # Animated position
|
|
3, # 3px from top
|
|
handle_diameter,
|
|
handle_diameter
|
|
)
|
|
|
|
# White handle
|
|
painter.setBrush(QBrush(QColor("#FFFFFF")))
|
|
painter.setPen(QPen(Qt.NoPen))
|
|
painter.drawEllipse(handle_rect)
|
|
|
|
# Draw icon inside handle
|
|
icon_size = 16 # Icon size within the 24px handle
|
|
icon_rect = QRectF(
|
|
handle_rect.x() + (handle_diameter - icon_size) / 2,
|
|
handle_rect.y() + (handle_diameter - icon_size) / 2,
|
|
icon_size,
|
|
icon_size
|
|
)
|
|
|
|
# Render appropriate icon
|
|
if self._checked:
|
|
# Dark mode - show moon icon
|
|
self.moon_svg.render(painter, icon_rect)
|
|
else:
|
|
# Light mode - show sun icon
|
|
self.sun_svg.render(painter, icon_rect)
|
|
|
|
def mousePressEvent(self, event):
|
|
"""
|
|
Handle mouse click to toggle state.
|
|
|
|
Only responds to left mouse button clicks.
|
|
"""
|
|
if event.button() == Qt.LeftButton:
|
|
self.toggle()
|
|
|
|
def toggle(self):
|
|
"""Toggle between Light and Dark mode."""
|
|
self.setChecked(not self._checked)
|
|
|
|
def setChecked(self, checked):
|
|
"""
|
|
Set the toggle state programmatically.
|
|
|
|
Args:
|
|
checked: True for Dark Mode, False for Light Mode
|
|
"""
|
|
# Only proceed if state actually changes
|
|
if self._checked == checked:
|
|
return
|
|
|
|
self._checked = checked
|
|
|
|
# Stop any running animation
|
|
self._animation.stop()
|
|
|
|
# Set animation endpoints
|
|
if checked:
|
|
# Animate to dark mode position (right side)
|
|
# Handle should be at: width - handle_diameter - 3px margin
|
|
self._animation.setEndValue(self.width() - (self.height() - 6) - 3)
|
|
else:
|
|
# Animate to light mode position (left side)
|
|
self._animation.setEndValue(3.0)
|
|
|
|
# Start animation
|
|
self._animation.start()
|
|
|
|
# Update tooltip
|
|
self._update_tooltip()
|
|
|
|
# Emit signal for theme change
|
|
self.toggled.emit(checked)
|
|
|
|
def isChecked(self):
|
|
"""
|
|
Get current toggle state.
|
|
|
|
Returns:
|
|
bool: True if Dark Mode is active, False for Light Mode
|
|
"""
|
|
return self._checked
|
|
|
|
def _update_tooltip(self):
|
|
"""Update tooltip based on current state."""
|
|
if self._checked:
|
|
self.setToolTip("Zu Light Mode wechseln")
|
|
else:
|
|
self.setToolTip("Zu Dark Mode wechseln")
|
|
|
|
def isDarkMode(self):
|
|
"""
|
|
Convenience method to check if dark mode is active.
|
|
|
|
Returns:
|
|
bool: True if Dark Mode is active
|
|
"""
|
|
return self._checked
|
|
|
|
def isLightMode(self):
|
|
"""
|
|
Convenience method to check if light mode is active.
|
|
|
|
Returns:
|
|
bool: True if Light Mode is active
|
|
"""
|
|
return not self._checked |