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

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 ''