mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
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>
527 lines
14 KiB
Python
527 lines
14 KiB
Python
"""Data display components for FakeMUI."""
|
|
|
|
from PyQt6.QtWidgets import (
|
|
QWidget, QLabel, QFrame, QVBoxLayout, QHBoxLayout,
|
|
QListWidget, QListWidgetItem, QTableWidget, QTableWidgetItem,
|
|
QHeaderView, QToolTip, QPushButton, QSizePolicy
|
|
)
|
|
from PyQt6.QtCore import Qt, pyqtSignal, QSize, QPoint
|
|
from PyQt6.QtGui import QPixmap, QColor, QPainter, QBrush, QPen, QFont, QIcon
|
|
|
|
from .base import FakeMUIWidget, FakeMUIContainer, StyleMixin
|
|
|
|
|
|
class Avatar(QLabel, StyleMixin):
|
|
"""Circular avatar component."""
|
|
|
|
_base_class = 'avatar'
|
|
|
|
def __init__(
|
|
self,
|
|
parent=None,
|
|
src: str = '',
|
|
alt: str = '',
|
|
size: int = 40,
|
|
variant: str = 'circular', # circular, rounded, square
|
|
color: str = 'default',
|
|
):
|
|
super().__init__(parent)
|
|
|
|
self._size = size
|
|
self._variant = variant
|
|
self._alt = alt
|
|
|
|
self.set_style_class(variant, color)
|
|
self.setFixedSize(size, size)
|
|
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
|
|
if src:
|
|
self.set_image(src)
|
|
elif alt:
|
|
self._show_initials(alt)
|
|
|
|
def set_image(self, src: str):
|
|
"""Set avatar image from file path."""
|
|
pixmap = QPixmap(src)
|
|
if not pixmap.isNull():
|
|
scaled = pixmap.scaled(
|
|
self._size, self._size,
|
|
Qt.AspectRatioMode.KeepAspectRatioByExpanding,
|
|
Qt.TransformationMode.SmoothTransformation
|
|
)
|
|
self.setPixmap(scaled)
|
|
|
|
def _show_initials(self, text: str):
|
|
"""Show initials when no image."""
|
|
initials = ''.join(word[0].upper() for word in text.split()[:2])
|
|
self.setText(initials)
|
|
|
|
|
|
class AvatarGroup(FakeMUIContainer):
|
|
"""Group of overlapping avatars."""
|
|
|
|
_base_class = 'avatar-group'
|
|
|
|
def __init__(self, parent=None, max_count: int = 5, spacing: int = -8):
|
|
super().__init__(parent, 'horizontal')
|
|
self._max_count = max_count
|
|
self._spacing = spacing
|
|
self._avatars = []
|
|
|
|
def add_avatar(self, avatar: Avatar):
|
|
"""Add an avatar to the group."""
|
|
if len(self._avatars) < self._max_count:
|
|
self._avatars.append(avatar)
|
|
self.add_widget(avatar)
|
|
|
|
|
|
class Badge(FakeMUIContainer):
|
|
"""Badge component to display status or count."""
|
|
|
|
_base_class = 'badge'
|
|
|
|
def __init__(
|
|
self,
|
|
parent=None,
|
|
content: str = '',
|
|
color: str = 'primary',
|
|
variant: str = 'standard', # standard, dot
|
|
invisible: bool = False,
|
|
max_count: int = 99,
|
|
):
|
|
super().__init__(parent, 'horizontal')
|
|
|
|
self._badge_label = QLabel()
|
|
self._badge_label.setObjectName('badge-content')
|
|
self._max_count = max_count
|
|
|
|
self.set_style_class(color, variant)
|
|
self.set_content(content)
|
|
self.set_invisible(invisible)
|
|
|
|
def set_content(self, content: str):
|
|
"""Set badge content."""
|
|
if content.isdigit():
|
|
num = int(content)
|
|
if num > self._max_count:
|
|
content = f"{self._max_count}+"
|
|
self._badge_label.setText(content)
|
|
|
|
def set_invisible(self, invisible: bool):
|
|
"""Set badge visibility."""
|
|
self._badge_label.setVisible(not invisible)
|
|
|
|
|
|
class Chip(QPushButton, StyleMixin):
|
|
"""Compact element for input, attributes, or actions."""
|
|
|
|
_base_class = 'chip'
|
|
|
|
onDelete = pyqtSignal()
|
|
|
|
def __init__(
|
|
self,
|
|
label: str = '',
|
|
parent=None,
|
|
icon: QIcon = None,
|
|
avatar: Avatar = None,
|
|
color: str = 'default',
|
|
variant: str = 'filled', # filled, outlined
|
|
size: str = 'medium',
|
|
clickable: bool = False,
|
|
deletable: bool = False,
|
|
):
|
|
super().__init__(label, parent)
|
|
|
|
modifiers = [color, variant, size]
|
|
if clickable:
|
|
modifiers.append('clickable')
|
|
if deletable:
|
|
modifiers.append('deletable')
|
|
self.set_style_class(*modifiers)
|
|
|
|
if icon:
|
|
self.setIcon(icon)
|
|
|
|
self._apply_style()
|
|
|
|
def _apply_style(self):
|
|
"""Apply chip styling."""
|
|
self.setMinimumHeight(32)
|
|
|
|
|
|
class Divider(QFrame, StyleMixin):
|
|
"""Visual divider line."""
|
|
|
|
_base_class = 'divider'
|
|
|
|
def __init__(
|
|
self,
|
|
parent=None,
|
|
orientation: str = 'horizontal',
|
|
variant: str = 'fullWidth', # fullWidth, inset, middle
|
|
text: str = '',
|
|
):
|
|
super().__init__(parent)
|
|
|
|
if orientation == 'vertical':
|
|
self.setFrameShape(QFrame.Shape.VLine)
|
|
else:
|
|
self.setFrameShape(QFrame.Shape.HLine)
|
|
|
|
self.setFrameShadow(QFrame.Shadow.Sunken)
|
|
self.set_style_class(orientation, variant)
|
|
|
|
|
|
class Icon(QLabel, StyleMixin):
|
|
"""Icon display component."""
|
|
|
|
_base_class = 'icon'
|
|
|
|
def __init__(
|
|
self,
|
|
name: str = '',
|
|
parent=None,
|
|
size: str = 'medium',
|
|
color: str = 'inherit',
|
|
):
|
|
super().__init__(parent)
|
|
|
|
self.set_style_class(size, color)
|
|
self._name = name
|
|
|
|
sizes = {'small': 18, 'medium': 24, 'large': 36, 'inherit': 24}
|
|
s = sizes.get(size, 24)
|
|
self.setFixedSize(s, s)
|
|
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
|
|
def set_icon(self, icon: QIcon):
|
|
"""Set icon from QIcon."""
|
|
pixmap = icon.pixmap(self.size())
|
|
self.setPixmap(pixmap)
|
|
|
|
|
|
class ListWidget(QFrame, StyleMixin):
|
|
"""Material-style list container."""
|
|
|
|
_base_class = 'list'
|
|
|
|
itemClicked = pyqtSignal(object)
|
|
|
|
def __init__(
|
|
self,
|
|
parent=None,
|
|
dense: bool = False,
|
|
spaced: bool = False,
|
|
):
|
|
super().__init__(parent)
|
|
|
|
self._layout = QVBoxLayout(self)
|
|
self._layout.setContentsMargins(0, 0, 0, 0)
|
|
self._layout.setSpacing(0)
|
|
self._items = []
|
|
|
|
modifiers = []
|
|
if dense:
|
|
modifiers.append('dense')
|
|
if spaced:
|
|
modifiers.append('spaced')
|
|
self.set_style_class(*modifiers)
|
|
|
|
def add_item(self, item: 'ListItem'):
|
|
"""Add a list item."""
|
|
self._items.append(item)
|
|
self._layout.addWidget(item)
|
|
item.clicked.connect(lambda: self.itemClicked.emit(item))
|
|
|
|
def clear(self):
|
|
"""Remove all items."""
|
|
for item in self._items:
|
|
item.deleteLater()
|
|
self._items.clear()
|
|
|
|
|
|
class ListItem(FakeMUIContainer):
|
|
"""List item component."""
|
|
|
|
_base_class = 'list-item'
|
|
|
|
clicked = pyqtSignal()
|
|
|
|
def __init__(
|
|
self,
|
|
parent=None,
|
|
clickable: bool = False,
|
|
selected: bool = False,
|
|
disabled: bool = False,
|
|
borderless: bool = False,
|
|
):
|
|
super().__init__(parent, 'horizontal')
|
|
|
|
modifiers = []
|
|
if clickable:
|
|
modifiers.append('clickable')
|
|
if selected:
|
|
modifiers.append('selected')
|
|
if disabled:
|
|
modifiers.append('disabled')
|
|
if borderless:
|
|
modifiers.append('borderless')
|
|
self.set_style_class(*modifiers)
|
|
|
|
self._clickable = clickable
|
|
if clickable:
|
|
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
|
|
def mousePressEvent(self, event):
|
|
"""Handle click."""
|
|
if self._clickable:
|
|
self.clicked.emit()
|
|
super().mousePressEvent(event)
|
|
|
|
|
|
class ListItemButton(QPushButton, StyleMixin):
|
|
"""Clickable list item."""
|
|
|
|
_base_class = 'list-item-button'
|
|
|
|
def __init__(
|
|
self,
|
|
parent=None,
|
|
selected: bool = False,
|
|
):
|
|
super().__init__(parent)
|
|
|
|
modifiers = []
|
|
if selected:
|
|
modifiers.append('selected')
|
|
self.set_style_class(*modifiers)
|
|
|
|
self._apply_style()
|
|
|
|
def _apply_style(self):
|
|
"""Apply list item button styling."""
|
|
self.setFlat(True)
|
|
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
|
|
|
|
class ListItemIcon(QLabel, StyleMixin):
|
|
"""Icon for list item."""
|
|
|
|
_base_class = 'list-item-icon'
|
|
|
|
def __init__(self, icon: QIcon = None, parent=None):
|
|
super().__init__(parent)
|
|
|
|
self.setObjectName('list-item-icon')
|
|
self.setFixedSize(40, 40)
|
|
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
|
|
if icon:
|
|
self.setPixmap(icon.pixmap(24, 24))
|
|
|
|
|
|
class ListItemText(FakeMUIContainer):
|
|
"""Text content for list item."""
|
|
|
|
_base_class = 'list-item-text'
|
|
|
|
def __init__(
|
|
self,
|
|
primary: str = '',
|
|
secondary: str = '',
|
|
parent=None,
|
|
):
|
|
super().__init__(parent, 'vertical')
|
|
|
|
self.setObjectName('list-item-text')
|
|
|
|
if primary:
|
|
self._primary = QLabel(primary)
|
|
self._primary.setObjectName('list-item-title')
|
|
self.add_widget(self._primary)
|
|
else:
|
|
self._primary = None
|
|
|
|
if secondary:
|
|
self._secondary = QLabel(secondary)
|
|
self._secondary.setObjectName('list-item-meta')
|
|
self.add_widget(self._secondary)
|
|
else:
|
|
self._secondary = None
|
|
|
|
def set_primary(self, text: str):
|
|
"""Set primary text."""
|
|
if self._primary:
|
|
self._primary.setText(text)
|
|
|
|
def set_secondary(self, text: str):
|
|
"""Set secondary text."""
|
|
if self._secondary:
|
|
self._secondary.setText(text)
|
|
|
|
|
|
class ListItemAvatar(QFrame, StyleMixin):
|
|
"""Avatar container for list item."""
|
|
|
|
_base_class = 'list-item-avatar'
|
|
|
|
def __init__(self, avatar: Avatar = None, parent=None):
|
|
super().__init__(parent)
|
|
|
|
layout = QHBoxLayout(self)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
if avatar:
|
|
layout.addWidget(avatar)
|
|
|
|
|
|
class ListSubheader(QLabel, StyleMixin):
|
|
"""Subheader for list sections."""
|
|
|
|
_base_class = 'list-subheader'
|
|
|
|
def __init__(self, text: str = '', parent=None):
|
|
super().__init__(text, parent)
|
|
self.setObjectName('list-subheader')
|
|
|
|
|
|
class TableWidget(QTableWidget, StyleMixin):
|
|
"""Material-style table."""
|
|
|
|
_base_class = 'table'
|
|
|
|
def __init__(
|
|
self,
|
|
parent=None,
|
|
columns: list = None,
|
|
dense: bool = False,
|
|
hover: bool = True,
|
|
):
|
|
super().__init__(parent)
|
|
|
|
if columns:
|
|
self.setColumnCount(len(columns))
|
|
self.setHorizontalHeaderLabels(columns)
|
|
|
|
modifiers = []
|
|
if dense:
|
|
modifiers.append('dense')
|
|
if hover:
|
|
modifiers.append('hover')
|
|
self.set_style_class(*modifiers)
|
|
|
|
self._apply_style()
|
|
|
|
def _apply_style(self):
|
|
"""Apply table styling."""
|
|
self.horizontalHeader().setStretchLastSection(True)
|
|
self.setAlternatingRowColors(True)
|
|
self.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
|
|
|
|
|
class TableHeader(QWidget):
|
|
"""Table header container (for compatibility)."""
|
|
pass
|
|
|
|
|
|
class TableBody(QWidget):
|
|
"""Table body container (for compatibility)."""
|
|
pass
|
|
|
|
|
|
class TableRow(QWidget):
|
|
"""Table row container (for compatibility)."""
|
|
pass
|
|
|
|
|
|
class TableCell(QTableWidgetItem):
|
|
"""Table cell item."""
|
|
|
|
def __init__(self, text: str = '', align: str = 'left'):
|
|
super().__init__(text)
|
|
|
|
alignments = {
|
|
'left': Qt.AlignmentFlag.AlignLeft,
|
|
'center': Qt.AlignmentFlag.AlignCenter,
|
|
'right': Qt.AlignmentFlag.AlignRight,
|
|
}
|
|
self.setTextAlignment(alignments.get(align, Qt.AlignmentFlag.AlignLeft) | Qt.AlignmentFlag.AlignVCenter)
|
|
|
|
|
|
class Tooltip(QWidget):
|
|
"""Tooltip component - uses Qt's built-in tooltip."""
|
|
|
|
@staticmethod
|
|
def show(widget: QWidget, text: str, position: str = 'bottom'):
|
|
"""Show tooltip on a widget."""
|
|
widget.setToolTip(text)
|
|
|
|
@staticmethod
|
|
def show_at(text: str, pos: QPoint):
|
|
"""Show tooltip at position."""
|
|
QToolTip.showText(pos, text)
|
|
|
|
|
|
class Typography(QLabel, StyleMixin):
|
|
"""Text display with variants."""
|
|
|
|
_base_class = 'typography'
|
|
|
|
VARIANTS = {
|
|
'h1': ('bold', 32),
|
|
'h2': ('bold', 28),
|
|
'h3': ('bold', 24),
|
|
'h4': ('bold', 20),
|
|
'h5': ('bold', 18),
|
|
'h6': ('bold', 16),
|
|
'subtitle1': ('medium', 16),
|
|
'subtitle2': ('medium', 14),
|
|
'body1': ('normal', 16),
|
|
'body2': ('normal', 14),
|
|
'caption': ('normal', 12),
|
|
'overline': ('normal', 10),
|
|
'button': ('bold', 14),
|
|
}
|
|
|
|
def __init__(
|
|
self,
|
|
text: str = '',
|
|
parent=None,
|
|
variant: str = 'body1',
|
|
color: str = 'textPrimary',
|
|
align: str = 'left',
|
|
noWrap: bool = False,
|
|
gutterBottom: bool = False,
|
|
):
|
|
super().__init__(text, parent)
|
|
|
|
modifiers = [variant, color]
|
|
if noWrap:
|
|
modifiers.append('nowrap')
|
|
if gutterBottom:
|
|
modifiers.append('gutter-bottom')
|
|
self.set_style_class(*modifiers)
|
|
|
|
# Set font based on variant
|
|
weight, size = self.VARIANTS.get(variant, ('normal', 14))
|
|
font = self.font()
|
|
font.setPointSize(size)
|
|
if weight == 'bold':
|
|
font.setBold(True)
|
|
elif weight == 'medium':
|
|
font.setWeight(QFont.Weight.Medium)
|
|
self.setFont(font)
|
|
|
|
# Set alignment
|
|
alignments = {
|
|
'left': Qt.AlignmentFlag.AlignLeft,
|
|
'center': Qt.AlignmentFlag.AlignCenter,
|
|
'right': Qt.AlignmentFlag.AlignRight,
|
|
'justify': Qt.AlignmentFlag.AlignJustify,
|
|
}
|
|
self.setAlignment(alignments.get(align, Qt.AlignmentFlag.AlignLeft))
|
|
|
|
if not noWrap:
|
|
self.setWordWrap(True)
|