mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-25 14:25:02 +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>
578 lines
15 KiB
Python
578 lines
15 KiB
Python
"""Input components for FakeMUI."""
|
|
|
|
from PyQt6.QtWidgets import (
|
|
QPushButton, QToolButton, QLineEdit, QTextEdit, QComboBox,
|
|
QCheckBox, QRadioButton, QSlider, QWidget, QLabel,
|
|
QVBoxLayout, QHBoxLayout, QButtonGroup, QFrame, QSpinBox
|
|
)
|
|
from PyQt6.QtCore import Qt, pyqtSignal, QSize
|
|
from PyQt6.QtGui import QIcon, QFont
|
|
|
|
from .base import FakeMUIWidget, FakeMUIContainer, StyleMixin
|
|
|
|
|
|
class Button(QPushButton, StyleMixin):
|
|
"""Material-style button with variants."""
|
|
|
|
_base_class = 'btn'
|
|
|
|
def __init__(
|
|
self,
|
|
text: str = '',
|
|
parent=None,
|
|
primary: bool = False,
|
|
secondary: bool = False,
|
|
outline: bool = False,
|
|
ghost: bool = False,
|
|
sm: bool = False,
|
|
lg: bool = False,
|
|
icon: QIcon = None,
|
|
loading: bool = False,
|
|
disabled: bool = False,
|
|
):
|
|
super().__init__(text, parent)
|
|
|
|
modifiers = []
|
|
if primary:
|
|
modifiers.append('primary')
|
|
if secondary:
|
|
modifiers.append('secondary')
|
|
if outline:
|
|
modifiers.append('outline')
|
|
if ghost:
|
|
modifiers.append('ghost')
|
|
if sm:
|
|
modifiers.append('sm')
|
|
if lg:
|
|
modifiers.append('lg')
|
|
if loading:
|
|
modifiers.append('loading')
|
|
|
|
self.set_style_class(*modifiers)
|
|
|
|
if icon:
|
|
self.setIcon(icon)
|
|
|
|
self.setEnabled(not disabled and not loading)
|
|
self._loading = loading
|
|
|
|
self._apply_style()
|
|
|
|
def _apply_style(self):
|
|
"""Apply base button styling."""
|
|
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
self.setMinimumHeight(36)
|
|
|
|
def set_loading(self, loading: bool):
|
|
"""Set loading state."""
|
|
self._loading = loading
|
|
self.setEnabled(not loading)
|
|
if loading:
|
|
self.add_modifier('loading')
|
|
else:
|
|
self.remove_modifier('loading')
|
|
|
|
|
|
class IconButton(QToolButton, StyleMixin):
|
|
"""Circular icon button."""
|
|
|
|
_base_class = 'icon-btn'
|
|
|
|
def __init__(
|
|
self,
|
|
icon: QIcon = None,
|
|
parent=None,
|
|
size: str = 'medium', # small, medium, large
|
|
color: str = 'default', # default, primary, secondary, error
|
|
):
|
|
super().__init__(parent)
|
|
|
|
self.set_style_class(size, color)
|
|
|
|
if icon:
|
|
self.setIcon(icon)
|
|
|
|
self._apply_style(size)
|
|
|
|
def _apply_style(self, size: str):
|
|
"""Apply icon button styling."""
|
|
sizes = {'small': 32, 'medium': 40, 'large': 48}
|
|
s = sizes.get(size, 40)
|
|
self.setFixedSize(s, s)
|
|
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
|
|
|
|
class Fab(QPushButton, StyleMixin):
|
|
"""Floating action button."""
|
|
|
|
_base_class = 'fab'
|
|
|
|
def __init__(
|
|
self,
|
|
icon: QIcon = None,
|
|
text: str = '',
|
|
parent=None,
|
|
color: str = 'primary',
|
|
size: str = 'medium',
|
|
extended: bool = False,
|
|
):
|
|
super().__init__(parent)
|
|
|
|
if text:
|
|
self.setText(text)
|
|
if icon:
|
|
self.setIcon(icon)
|
|
|
|
modifiers = [color, size]
|
|
if extended:
|
|
modifiers.append('extended')
|
|
self.set_style_class(*modifiers)
|
|
|
|
self._apply_style(size, extended)
|
|
|
|
def _apply_style(self, size: str, extended: bool):
|
|
"""Apply FAB styling."""
|
|
sizes = {'small': 40, 'medium': 56, 'large': 72}
|
|
s = sizes.get(size, 56)
|
|
if extended:
|
|
self.setMinimumHeight(48)
|
|
self.setMinimumWidth(80)
|
|
else:
|
|
self.setFixedSize(s, s)
|
|
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
|
|
|
|
class Input(QLineEdit, StyleMixin):
|
|
"""Material-style text input."""
|
|
|
|
_base_class = 'input'
|
|
|
|
def __init__(
|
|
self,
|
|
parent=None,
|
|
placeholder: str = '',
|
|
value: str = '',
|
|
error: bool = False,
|
|
disabled: bool = False,
|
|
fullWidth: bool = False,
|
|
):
|
|
super().__init__(parent)
|
|
|
|
if placeholder:
|
|
self.setPlaceholderText(placeholder)
|
|
if value:
|
|
self.setText(value)
|
|
|
|
modifiers = []
|
|
if error:
|
|
modifiers.append('error')
|
|
if fullWidth:
|
|
modifiers.append('full-width')
|
|
self.set_style_class(*modifiers)
|
|
|
|
self.setEnabled(not disabled)
|
|
self._apply_style(fullWidth)
|
|
|
|
def _apply_style(self, fullWidth: bool):
|
|
"""Apply input styling."""
|
|
self.setMinimumHeight(40)
|
|
if fullWidth:
|
|
self.setMinimumWidth(0)
|
|
|
|
def set_error(self, error: bool):
|
|
"""Set error state."""
|
|
if error:
|
|
self.add_modifier('error')
|
|
else:
|
|
self.remove_modifier('error')
|
|
|
|
|
|
class TextArea(QTextEdit, StyleMixin):
|
|
"""Multi-line text input."""
|
|
|
|
_base_class = 'textarea'
|
|
|
|
def __init__(
|
|
self,
|
|
parent=None,
|
|
placeholder: str = '',
|
|
value: str = '',
|
|
rows: int = 4,
|
|
error: bool = False,
|
|
disabled: bool = False,
|
|
):
|
|
super().__init__(parent)
|
|
|
|
if placeholder:
|
|
self.setPlaceholderText(placeholder)
|
|
if value:
|
|
self.setPlainText(value)
|
|
|
|
modifiers = []
|
|
if error:
|
|
modifiers.append('error')
|
|
self.set_style_class(*modifiers)
|
|
|
|
self.setEnabled(not disabled)
|
|
self._apply_style(rows)
|
|
|
|
def _apply_style(self, rows: int):
|
|
"""Apply textarea styling."""
|
|
font_metrics = self.fontMetrics()
|
|
line_height = font_metrics.lineSpacing()
|
|
self.setMinimumHeight(line_height * rows + 16)
|
|
|
|
|
|
class Select(QComboBox, StyleMixin):
|
|
"""Dropdown select component."""
|
|
|
|
_base_class = 'select'
|
|
|
|
def __init__(
|
|
self,
|
|
parent=None,
|
|
options: list = None,
|
|
value: str = '',
|
|
placeholder: str = '',
|
|
error: bool = False,
|
|
disabled: bool = False,
|
|
):
|
|
super().__init__(parent)
|
|
|
|
if placeholder:
|
|
self.addItem(placeholder)
|
|
self.setCurrentIndex(0)
|
|
|
|
if options:
|
|
for opt in options:
|
|
if isinstance(opt, tuple):
|
|
self.addItem(opt[0], opt[1])
|
|
else:
|
|
self.addItem(str(opt))
|
|
|
|
if value:
|
|
index = self.findText(value)
|
|
if index >= 0:
|
|
self.setCurrentIndex(index)
|
|
|
|
modifiers = []
|
|
if error:
|
|
modifiers.append('error')
|
|
self.set_style_class(*modifiers)
|
|
|
|
self.setEnabled(not disabled)
|
|
self._apply_style()
|
|
|
|
def _apply_style(self):
|
|
"""Apply select styling."""
|
|
self.setMinimumHeight(40)
|
|
|
|
|
|
class CheckBox(QCheckBox, StyleMixin):
|
|
"""Material-style checkbox."""
|
|
|
|
_base_class = 'checkbox'
|
|
|
|
def __init__(
|
|
self,
|
|
text: str = '',
|
|
parent=None,
|
|
checked: bool = False,
|
|
disabled: bool = False,
|
|
color: str = 'primary',
|
|
):
|
|
super().__init__(text, parent)
|
|
|
|
self.setChecked(checked)
|
|
self.setEnabled(not disabled)
|
|
self.set_style_class(color)
|
|
|
|
|
|
class RadioButton(QRadioButton, StyleMixin):
|
|
"""Material-style radio button."""
|
|
|
|
_base_class = 'radio'
|
|
|
|
def __init__(
|
|
self,
|
|
text: str = '',
|
|
parent=None,
|
|
checked: bool = False,
|
|
disabled: bool = False,
|
|
color: str = 'primary',
|
|
):
|
|
super().__init__(text, parent)
|
|
|
|
self.setChecked(checked)
|
|
self.setEnabled(not disabled)
|
|
self.set_style_class(color)
|
|
|
|
|
|
class Switch(QCheckBox, StyleMixin):
|
|
"""Toggle switch component."""
|
|
|
|
_base_class = 'switch'
|
|
|
|
def __init__(
|
|
self,
|
|
text: str = '',
|
|
parent=None,
|
|
checked: bool = False,
|
|
disabled: bool = False,
|
|
color: str = 'primary',
|
|
):
|
|
super().__init__(text, parent)
|
|
|
|
self.setChecked(checked)
|
|
self.setEnabled(not disabled)
|
|
self.set_style_class(color)
|
|
|
|
|
|
class Slider(QSlider, StyleMixin):
|
|
"""Material-style slider."""
|
|
|
|
_base_class = 'slider'
|
|
|
|
def __init__(
|
|
self,
|
|
parent=None,
|
|
orientation: Qt.Orientation = Qt.Orientation.Horizontal,
|
|
min_val: int = 0,
|
|
max_val: int = 100,
|
|
value: int = 50,
|
|
disabled: bool = False,
|
|
color: str = 'primary',
|
|
):
|
|
super().__init__(orientation, parent)
|
|
|
|
self.setMinimum(min_val)
|
|
self.setMaximum(max_val)
|
|
self.setValue(value)
|
|
self.setEnabled(not disabled)
|
|
self.set_style_class(color)
|
|
|
|
|
|
class FormGroup(FakeMUIContainer):
|
|
"""Container for form controls."""
|
|
|
|
_base_class = 'form-group'
|
|
|
|
def __init__(self, parent=None, row: bool = False):
|
|
super().__init__(parent, 'horizontal' if row else 'vertical')
|
|
self.set_spacing(8)
|
|
|
|
|
|
class FormLabel(QLabel, StyleMixin):
|
|
"""Label for form fields."""
|
|
|
|
_base_class = 'form-label'
|
|
|
|
def __init__(
|
|
self,
|
|
text: str = '',
|
|
parent=None,
|
|
required: bool = False,
|
|
error: bool = False,
|
|
):
|
|
display_text = f"{text} *" if required else text
|
|
super().__init__(display_text, parent)
|
|
|
|
modifiers = []
|
|
if required:
|
|
modifiers.append('required')
|
|
if error:
|
|
modifiers.append('error')
|
|
self.set_style_class(*modifiers)
|
|
|
|
|
|
class FormHelperText(QLabel, StyleMixin):
|
|
"""Helper text below form fields."""
|
|
|
|
_base_class = 'form-helper-text'
|
|
|
|
def __init__(
|
|
self,
|
|
text: str = '',
|
|
parent=None,
|
|
error: bool = False,
|
|
):
|
|
super().__init__(text, parent)
|
|
|
|
modifiers = []
|
|
if error:
|
|
modifiers.append('error')
|
|
self.set_style_class(*modifiers)
|
|
|
|
self._apply_style()
|
|
|
|
def _apply_style(self):
|
|
"""Apply helper text styling."""
|
|
font = self.font()
|
|
font.setPointSize(font.pointSize() - 2)
|
|
self.setFont(font)
|
|
|
|
|
|
class TextField(FakeMUIContainer):
|
|
"""Complete text field with label and helper text."""
|
|
|
|
_base_class = 'text-field'
|
|
|
|
textChanged = pyqtSignal(str)
|
|
|
|
def __init__(
|
|
self,
|
|
parent=None,
|
|
label: str = '',
|
|
placeholder: str = '',
|
|
value: str = '',
|
|
helperText: str = '',
|
|
error: bool = False,
|
|
disabled: bool = False,
|
|
required: bool = False,
|
|
multiline: bool = False,
|
|
rows: int = 4,
|
|
):
|
|
super().__init__(parent, 'vertical')
|
|
|
|
self._error = error
|
|
|
|
# Label
|
|
if label:
|
|
self._label = FormLabel(label, required=required, error=error)
|
|
self.add_widget(self._label)
|
|
else:
|
|
self._label = None
|
|
|
|
# Input
|
|
if multiline:
|
|
self._input = TextArea(
|
|
placeholder=placeholder,
|
|
value=value,
|
|
rows=rows,
|
|
error=error,
|
|
disabled=disabled,
|
|
)
|
|
self._input.textChanged.connect(lambda: self.textChanged.emit(self._input.toPlainText()))
|
|
else:
|
|
self._input = Input(
|
|
placeholder=placeholder,
|
|
value=value,
|
|
error=error,
|
|
disabled=disabled,
|
|
fullWidth=True,
|
|
)
|
|
self._input.textChanged.connect(self.textChanged.emit)
|
|
self.add_widget(self._input)
|
|
|
|
# Helper text
|
|
if helperText:
|
|
self._helper = FormHelperText(helperText, error=error)
|
|
self.add_widget(self._helper)
|
|
else:
|
|
self._helper = None
|
|
|
|
if error:
|
|
self.add_modifier('error')
|
|
|
|
def text(self) -> str:
|
|
"""Get the current text value."""
|
|
if isinstance(self._input, TextArea):
|
|
return self._input.toPlainText()
|
|
return self._input.text()
|
|
|
|
def setText(self, text: str):
|
|
"""Set the text value."""
|
|
if isinstance(self._input, TextArea):
|
|
self._input.setPlainText(text)
|
|
else:
|
|
self._input.setText(text)
|
|
|
|
def setError(self, error: bool, message: str = ''):
|
|
"""Set error state."""
|
|
self._error = error
|
|
self._input.set_error(error)
|
|
if self._label:
|
|
if error:
|
|
self._label.add_modifier('error')
|
|
else:
|
|
self._label.remove_modifier('error')
|
|
if self._helper and message:
|
|
self._helper.setText(message)
|
|
if error:
|
|
self._helper.add_modifier('error')
|
|
else:
|
|
self._helper.remove_modifier('error')
|
|
|
|
|
|
class ToggleButton(QPushButton, StyleMixin):
|
|
"""Toggle button that can be selected."""
|
|
|
|
_base_class = 'toggle-btn'
|
|
|
|
def __init__(
|
|
self,
|
|
text: str = '',
|
|
parent=None,
|
|
selected: bool = False,
|
|
disabled: bool = False,
|
|
value: str = '',
|
|
):
|
|
super().__init__(text, parent)
|
|
|
|
self.setCheckable(True)
|
|
self.setChecked(selected)
|
|
self.setEnabled(not disabled)
|
|
self._value = value
|
|
|
|
modifiers = []
|
|
if selected:
|
|
modifiers.append('selected')
|
|
self.set_style_class(*modifiers)
|
|
|
|
self.clicked.connect(self._on_click)
|
|
|
|
def _on_click(self):
|
|
"""Handle click to update style."""
|
|
if self.isChecked():
|
|
self.add_modifier('selected')
|
|
else:
|
|
self.remove_modifier('selected')
|
|
|
|
@property
|
|
def value(self) -> str:
|
|
return self._value
|
|
|
|
|
|
class ToggleButtonGroup(FakeMUIContainer):
|
|
"""Group of toggle buttons with exclusive selection."""
|
|
|
|
_base_class = 'toggle-btn-group'
|
|
|
|
valueChanged = pyqtSignal(str)
|
|
|
|
def __init__(
|
|
self,
|
|
parent=None,
|
|
exclusive: bool = True,
|
|
orientation: str = 'horizontal',
|
|
):
|
|
super().__init__(parent, orientation)
|
|
|
|
self._button_group = QButtonGroup(self)
|
|
self._button_group.setExclusive(exclusive)
|
|
self._buttons = []
|
|
|
|
def add_button(self, button: ToggleButton):
|
|
"""Add a toggle button to the group."""
|
|
self._button_group.addButton(button)
|
|
self._buttons.append(button)
|
|
self.add_widget(button)
|
|
button.clicked.connect(lambda: self.valueChanged.emit(button.value))
|
|
|
|
def get_value(self) -> str:
|
|
"""Get the currently selected value."""
|
|
checked = self._button_group.checkedButton()
|
|
if checked and hasattr(checked, 'value'):
|
|
return checked.value
|
|
return ''
|