Files
metabuilder/python/fakemui/feedback.py
johndoe6345789 c406b8df96 refactor: Reorganize FakeMUI by implementation type
Move FakeMUI content to appropriate root-level folders by implementation:

**React Components → components/fakemui/**
- 537 components (inputs, surfaces, layout, data-display, feedback,
  navigation, utils, atoms, lab, x, email, workflows)
- 416 SVG icons
- Full barrel exports in components/fakemui/index.ts

**QML Components → qml/**
- 104 Material Design 3 components (11 categories)
- 7 hybrid application views
- 8 desktop widgets
- qmldir module registration

**Python Bindings → python/fakemui/**
- 15 PyQt6 modules (120+ components)
- Full Python package structure with pyproject.toml

**SCSS/Styles → fakemui/** (renamed purpose)
- scss/ - Material Design 3 stylesheets
- styles/ - Component SCSS modules
- src/utils/ - Accessibility utilities
- index.ts now re-exports from components/fakemui/

This separation allows:
- React: import { Button } from '@metabuilder/components/fakemui'
- QML: import QmlComponents 1.0
- Python: from fakemui import Button, Card
- Backward compat: import { Button } from '@metabuilder/fakemui'

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 12:59:24 +00:00

300 lines
8.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Feedback components for FakeMUI."""
from PyQt6.QtWidgets import (
QWidget, QLabel, QFrame, QVBoxLayout, QHBoxLayout,
QProgressBar, QPushButton, QGraphicsOpacityEffect
)
from PyQt6.QtCore import (
Qt, pyqtSignal, QTimer, QPropertyAnimation,
QEasingCurve, QSize, pyqtProperty
)
from PyQt6.QtGui import QColor, QPainter, QPen, QBrush, QIcon
from .base import FakeMUIWidget, FakeMUIContainer, StyleMixin, THEME_COLORS
class Alert(FakeMUIContainer):
"""Alert component for displaying messages."""
_base_class = 'alert'
onClose = pyqtSignal()
def __init__(
self,
parent=None,
title: str = '',
message: str = '',
severity: str = 'info', # error, warning, info, success
icon: QIcon = None,
closable: bool = False,
action: QWidget = None,
):
super().__init__(parent, 'horizontal')
self.set_style_class(severity)
self.set_spacing(8)
self.set_padding(12)
# Icon
if icon:
icon_label = QLabel()
icon_label.setPixmap(icon.pixmap(24, 24))
icon_label.setObjectName('alert-icon')
self.add_widget(icon_label)
# Content
content = FakeMUIContainer(layout_type='vertical')
content.setObjectName('alert-content')
if title:
title_label = QLabel(title)
title_label.setObjectName('alert-title')
title_label.setStyleSheet('font-weight: bold;')
content.add_widget(title_label)
if message:
msg_label = QLabel(message)
msg_label.setObjectName('alert-message')
msg_label.setWordWrap(True)
content.add_widget(msg_label)
self.add_widget(content, stretch=1)
# Action
if action:
action.setObjectName('alert-action')
self.add_widget(action)
# Close button
if closable:
close_btn = QPushButton('×')
close_btn.setObjectName('alert-close')
close_btn.setFixedSize(24, 24)
close_btn.clicked.connect(self._close)
self.add_widget(close_btn)
def _close(self):
"""Close the alert."""
self.onClose.emit()
self.hide()
class Backdrop(QFrame, StyleMixin):
"""Full-screen backdrop overlay."""
_base_class = 'backdrop'
clicked = pyqtSignal()
def __init__(
self,
parent=None,
open: bool = False,
invisible: bool = False,
):
super().__init__(parent)
modifiers = []
if invisible:
modifiers.append('invisible')
self.set_style_class(*modifiers)
self.setVisible(open)
if not invisible:
self.setStyleSheet('background-color: rgba(0, 0, 0, 0.5);')
def mousePressEvent(self, event):
"""Emit click signal."""
self.clicked.emit()
super().mousePressEvent(event)
def set_open(self, open: bool):
"""Set visibility."""
self.setVisible(open)
class Spinner(QWidget):
"""Spinning loading indicator."""
def __init__(
self,
parent=None,
size: int = 40,
thickness: int = 4,
color: str = 'primary',
):
super().__init__(parent)
self.setFixedSize(size, size)
self._thickness = thickness
self._angle = 0
self._color = THEME_COLORS.get(color, THEME_COLORS['primary'])
self._timer = QTimer(self)
self._timer.timeout.connect(self._rotate)
self._timer.start(16) # ~60fps
def _rotate(self):
"""Rotate the spinner."""
self._angle = (self._angle + 6) % 360
self.update()
def paintEvent(self, event):
"""Draw the spinner."""
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
rect = self.rect().adjusted(
self._thickness, self._thickness,
-self._thickness, -self._thickness
)
pen = QPen(QColor(self._color))
pen.setWidth(self._thickness)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
painter.setPen(pen)
# Draw arc
painter.drawArc(rect, self._angle * 16, 270 * 16)
class CircularProgress(Spinner):
"""Alias for Spinner."""
pass
class LinearProgress(QProgressBar, StyleMixin):
"""Linear progress bar."""
_base_class = 'linear-progress'
def __init__(
self,
parent=None,
value: int = 0,
variant: str = 'determinate', # determinate, indeterminate, buffer
color: str = 'primary',
):
super().__init__(parent)
self.set_style_class(variant, color)
if variant == 'indeterminate':
self.setMinimum(0)
self.setMaximum(0)
else:
self.setMinimum(0)
self.setMaximum(100)
self.setValue(value)
self.setTextVisible(False)
self.setFixedHeight(4)
class Progress(LinearProgress):
"""Alias for LinearProgress."""
pass
class Skeleton(QFrame, StyleMixin):
"""Loading placeholder animation."""
_base_class = 'skeleton'
def __init__(
self,
parent=None,
variant: str = 'text', # text, rectangular, circular
width: int = None,
height: int = None,
animation: str = 'pulse', # pulse, wave, false
):
super().__init__(parent)
self.set_style_class(variant, animation)
if width:
self.setFixedWidth(width)
if height:
self.setFixedHeight(height)
elif variant == 'text':
self.setFixedHeight(20)
elif variant == 'circular':
size = width or 40
self.setFixedSize(size, size)
# Animation
if animation == 'pulse':
self._setup_pulse_animation()
def _setup_pulse_animation(self):
"""Setup pulse animation."""
self._opacity_effect = QGraphicsOpacityEffect(self)
self.setGraphicsEffect(self._opacity_effect)
self._animation = QPropertyAnimation(self._opacity_effect, b"opacity")
self._animation.setDuration(1500)
self._animation.setStartValue(0.3)
self._animation.setEndValue(1.0)
self._animation.setEasingCurve(QEasingCurve.Type.InOutSine)
self._animation.setLoopCount(-1) # Infinite
self._animation.start()
class Snackbar(FakeMUIContainer):
"""Brief message at bottom of screen."""
_base_class = 'snackbar'
onClose = pyqtSignal()
def __init__(
self,
parent=None,
message: str = '',
action: str = '',
autoHideDuration: int = 6000,
anchorOrigin: dict = None, # {'vertical': 'bottom', 'horizontal': 'center'}
):
super().__init__(parent, 'horizontal')
self.set_spacing(16)
self.set_padding(8)
# Message
msg_label = QLabel(message)
msg_label.setObjectName('snackbar-message')
self.add_widget(msg_label, stretch=1)
# Action button
if action:
action_btn = QPushButton(action)
action_btn.setObjectName('snackbar-action')
action_btn.setFlat(True)
action_btn.clicked.connect(self.close)
self.add_widget(action_btn)
# Auto hide timer
if autoHideDuration > 0:
self._timer = QTimer(self)
self._timer.setSingleShot(True)
self._timer.timeout.connect(self.close)
self._timer.start(autoHideDuration)
self.hide()
def show_message(self, message: str, duration: int = 6000):
"""Show the snackbar with a message."""
for child in self.findChildren(QLabel, 'snackbar-message'):
child.setText(message)
if hasattr(self, '_timer'):
self._timer.stop()
self._timer.start(duration)
self.show()
def close(self):
"""Close the snackbar."""
self.onClose.emit()
self.hide()