Files
metabuilder/python/fakemui/data_display.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

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)