mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-25 06:14:59 +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>
931 lines
30 KiB
Python
931 lines
30 KiB
Python
"""
|
||
FakeMUI X Module for PyQt6
|
||
|
||
Provides advanced components like DataGrid and DatePicker.
|
||
"""
|
||
|
||
from typing import Any, Callable, Dict, List, Optional, Union
|
||
from datetime import date, datetime, time
|
||
from PyQt6.QtWidgets import (
|
||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QFrame,
|
||
QTableWidget, QTableWidgetItem, QHeaderView, QCheckBox,
|
||
QComboBox, QPushButton, QLineEdit, QCalendarWidget,
|
||
QTimeEdit, QDateEdit, QSpinBox, QScrollArea, QMenu,
|
||
QAbstractItemView, QSizePolicy, QGridLayout
|
||
)
|
||
from PyQt6.QtCore import Qt, pyqtSignal, QDate, QTime
|
||
from PyQt6.QtGui import QFont, QColor, QAction
|
||
|
||
from .base import FakeMUIWidget, StyleMixin
|
||
|
||
|
||
# =============================================================================
|
||
# DataGrid
|
||
# =============================================================================
|
||
|
||
class DataGridColumn:
|
||
"""Configuration for a DataGrid column."""
|
||
|
||
def __init__(
|
||
self,
|
||
field: str,
|
||
header_name: Optional[str] = None,
|
||
width: Optional[int] = None,
|
||
flex: Optional[float] = None,
|
||
sortable: bool = True,
|
||
filterable: bool = True,
|
||
editable: bool = False,
|
||
render_cell: Optional[Callable] = None,
|
||
value_formatter: Optional[Callable] = None,
|
||
):
|
||
self.field = field
|
||
self.header_name = header_name or field
|
||
self.width = width
|
||
self.flex = flex
|
||
self.sortable = sortable
|
||
self.filterable = filterable
|
||
self.editable = editable
|
||
self.render_cell = render_cell
|
||
self.value_formatter = value_formatter
|
||
|
||
|
||
class DataGrid(QFrame, StyleMixin):
|
||
"""
|
||
A powerful data table component.
|
||
|
||
Signals:
|
||
selection_changed: Emitted when selection changes.
|
||
sort_changed: Emitted when sort changes.
|
||
row_clicked: Emitted when a row is clicked.
|
||
cell_clicked: Emitted when a cell is clicked.
|
||
page_changed: Emitted when page changes.
|
||
"""
|
||
|
||
selection_changed = pyqtSignal(list) # List of selected row IDs
|
||
sort_changed = pyqtSignal(str, str) # field, direction ('asc'/'desc')
|
||
row_clicked = pyqtSignal(dict) # row data
|
||
cell_clicked = pyqtSignal(str, object) # field, value
|
||
page_changed = pyqtSignal(int) # page number
|
||
|
||
def __init__(
|
||
self,
|
||
parent: Optional[QWidget] = None,
|
||
rows: Optional[List[Dict]] = None,
|
||
columns: Optional[List[DataGridColumn]] = None,
|
||
page_size: int = 10,
|
||
rows_per_page_options: Optional[List[int]] = None,
|
||
pagination: bool = True,
|
||
checkbox_selection: bool = False,
|
||
disable_selection_on_click: bool = False,
|
||
auto_height: bool = False,
|
||
density: str = "standard",
|
||
loading: bool = False,
|
||
get_row_id: Optional[Callable] = None,
|
||
):
|
||
super().__init__(parent)
|
||
StyleMixin.__init__(self)
|
||
|
||
self._rows = rows or []
|
||
self._columns = columns or []
|
||
self._page_size = page_size
|
||
self._rows_per_page_options = rows_per_page_options or [5, 10, 25, 50]
|
||
self._pagination = pagination
|
||
self._checkbox_selection = checkbox_selection
|
||
self._disable_selection_on_click = disable_selection_on_click
|
||
self._auto_height = auto_height
|
||
self._density = density
|
||
self._loading = loading
|
||
self._get_row_id = get_row_id or (lambda row: row.get("id", id(row)))
|
||
|
||
self._current_page = 0
|
||
self._sort_field: Optional[str] = None
|
||
self._sort_direction: str = "asc"
|
||
self._selected_ids: List[Any] = []
|
||
|
||
self._setup_ui()
|
||
self._apply_styles()
|
||
self._refresh_data()
|
||
|
||
def _setup_ui(self):
|
||
"""Set up the UI components."""
|
||
layout = QVBoxLayout(self)
|
||
layout.setContentsMargins(0, 0, 0, 0)
|
||
layout.setSpacing(0)
|
||
|
||
# Table
|
||
self._table = QTableWidget()
|
||
self._table.setAlternatingRowColors(True)
|
||
self._table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||
self._table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
||
self._table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
|
||
self._table.verticalHeader().setVisible(False)
|
||
self._table.setShowGrid(True)
|
||
self._table.horizontalHeader().sectionClicked.connect(self._on_header_clicked)
|
||
self._table.cellClicked.connect(self._on_cell_clicked)
|
||
|
||
layout.addWidget(self._table, 1)
|
||
|
||
# Footer with pagination
|
||
if self._pagination:
|
||
self._footer = QFrame()
|
||
footer_layout = QHBoxLayout(self._footer)
|
||
footer_layout.setContentsMargins(8, 4, 8, 4)
|
||
|
||
# Rows per page selector
|
||
footer_layout.addWidget(QLabel("Rows per page:"))
|
||
self._page_size_combo = QComboBox()
|
||
for opt in self._rows_per_page_options:
|
||
self._page_size_combo.addItem(str(opt), opt)
|
||
self._page_size_combo.setCurrentText(str(self._page_size))
|
||
self._page_size_combo.currentIndexChanged.connect(self._on_page_size_changed)
|
||
footer_layout.addWidget(self._page_size_combo)
|
||
|
||
footer_layout.addStretch()
|
||
|
||
# Page info
|
||
self._page_info = QLabel()
|
||
footer_layout.addWidget(self._page_info)
|
||
|
||
# Navigation buttons
|
||
self._prev_btn = QPushButton("‹")
|
||
self._prev_btn.setFixedWidth(32)
|
||
self._prev_btn.clicked.connect(self._prev_page)
|
||
footer_layout.addWidget(self._prev_btn)
|
||
|
||
self._next_btn = QPushButton("›")
|
||
self._next_btn.setFixedWidth(32)
|
||
self._next_btn.clicked.connect(self._next_page)
|
||
footer_layout.addWidget(self._next_btn)
|
||
|
||
layout.addWidget(self._footer)
|
||
|
||
# Loading overlay
|
||
self._loading_label = QLabel("Loading...")
|
||
self._loading_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
self._loading_label.setStyleSheet("""
|
||
QLabel {
|
||
background-color: rgba(255, 255, 255, 0.8);
|
||
font-size: 14px;
|
||
color: #666;
|
||
}
|
||
""")
|
||
self._loading_label.setVisible(False)
|
||
|
||
def _apply_styles(self):
|
||
"""Apply styles based on density."""
|
||
row_heights = {
|
||
"compact": 36,
|
||
"standard": 52,
|
||
"comfortable": 64,
|
||
}
|
||
row_height = row_heights.get(self._density, 52)
|
||
|
||
self.add_class("fakemui-data-grid")
|
||
self.add_class(f"fakemui-data-grid-density-{self._density}")
|
||
|
||
self.setStyleSheet(f"""
|
||
QFrame {{
|
||
background-color: white;
|
||
border: 1px solid #e0e0e0;
|
||
border-radius: 4px;
|
||
}}
|
||
QTableWidget {{
|
||
background-color: white;
|
||
border: none;
|
||
gridline-color: #e0e0e0;
|
||
}}
|
||
QTableWidget::item {{
|
||
padding: 8px;
|
||
}}
|
||
QHeaderView::section {{
|
||
background-color: #fafafa;
|
||
padding: 8px;
|
||
border: none;
|
||
border-bottom: 1px solid #e0e0e0;
|
||
font-weight: 500;
|
||
}}
|
||
QHeaderView::section:hover {{
|
||
background-color: #f5f5f5;
|
||
}}
|
||
QTableWidget::item:selected {{
|
||
background-color: rgba(25, 118, 210, 0.08);
|
||
color: black;
|
||
}}
|
||
QTableWidget::item:alternate {{
|
||
background-color: #fafafa;
|
||
}}
|
||
""")
|
||
|
||
self._table.verticalHeader().setDefaultSectionSize(row_height)
|
||
|
||
def _refresh_data(self):
|
||
"""Refresh the table data."""
|
||
# Sort data
|
||
sorted_rows = self._rows[:]
|
||
if self._sort_field:
|
||
reverse = self._sort_direction == "desc"
|
||
sorted_rows.sort(
|
||
key=lambda r: r.get(self._sort_field, "") or "",
|
||
reverse=reverse
|
||
)
|
||
|
||
# Paginate
|
||
if self._pagination:
|
||
start = self._current_page * self._page_size
|
||
end = start + self._page_size
|
||
display_rows = sorted_rows[start:end]
|
||
else:
|
||
display_rows = sorted_rows
|
||
|
||
# Set up columns
|
||
col_count = len(self._columns)
|
||
if self._checkbox_selection:
|
||
col_count += 1
|
||
|
||
self._table.setColumnCount(col_count)
|
||
|
||
headers = []
|
||
if self._checkbox_selection:
|
||
headers.append("☐")
|
||
for col in self._columns:
|
||
header = col.header_name
|
||
if col.field == self._sort_field:
|
||
header += " ↑" if self._sort_direction == "asc" else " ↓"
|
||
headers.append(header)
|
||
self._table.setHorizontalHeaderLabels(headers)
|
||
|
||
# Set up rows
|
||
self._table.setRowCount(len(display_rows))
|
||
|
||
for row_idx, row_data in enumerate(display_rows):
|
||
col_offset = 0
|
||
row_id = self._get_row_id(row_data)
|
||
|
||
# Checkbox column
|
||
if self._checkbox_selection:
|
||
checkbox = QCheckBox()
|
||
checkbox.setChecked(row_id in self._selected_ids)
|
||
checkbox.stateChanged.connect(
|
||
lambda state, rid=row_id: self._on_checkbox_changed(rid, state)
|
||
)
|
||
self._table.setCellWidget(row_idx, 0, checkbox)
|
||
col_offset = 1
|
||
|
||
# Data columns
|
||
for col_idx, col in enumerate(self._columns):
|
||
value = row_data.get(col.field, "")
|
||
|
||
# Apply formatters
|
||
if col.render_cell:
|
||
display_value = col.render_cell({
|
||
"row": row_data,
|
||
"value": value,
|
||
"field": col.field
|
||
})
|
||
elif col.value_formatter:
|
||
display_value = col.value_formatter({"value": value})
|
||
else:
|
||
display_value = str(value) if value is not None else ""
|
||
|
||
item = QTableWidgetItem(str(display_value))
|
||
item.setData(Qt.ItemDataRole.UserRole, row_data)
|
||
self._table.setItem(row_idx, col_idx + col_offset, item)
|
||
|
||
# Update pagination info
|
||
if self._pagination:
|
||
total = len(self._rows)
|
||
start = self._current_page * self._page_size + 1
|
||
end = min((self._current_page + 1) * self._page_size, total)
|
||
self._page_info.setText(f"{start}–{end} of {total}")
|
||
|
||
self._prev_btn.setEnabled(self._current_page > 0)
|
||
self._next_btn.setEnabled(end < total)
|
||
|
||
def _on_header_clicked(self, col_idx: int):
|
||
"""Handle header click for sorting."""
|
||
if self._checkbox_selection:
|
||
if col_idx == 0:
|
||
return # Checkbox column
|
||
col_idx -= 1
|
||
|
||
if col_idx < len(self._columns):
|
||
col = self._columns[col_idx]
|
||
if col.sortable:
|
||
if self._sort_field == col.field:
|
||
self._sort_direction = "desc" if self._sort_direction == "asc" else "asc"
|
||
else:
|
||
self._sort_field = col.field
|
||
self._sort_direction = "asc"
|
||
|
||
self.sort_changed.emit(self._sort_field, self._sort_direction)
|
||
self._refresh_data()
|
||
|
||
def _on_cell_clicked(self, row: int, col: int):
|
||
"""Handle cell click."""
|
||
item = self._table.item(row, col)
|
||
if item:
|
||
row_data = item.data(Qt.ItemDataRole.UserRole)
|
||
if row_data:
|
||
self.row_clicked.emit(row_data)
|
||
|
||
if self._checkbox_selection:
|
||
col -= 1
|
||
if col >= 0 and col < len(self._columns):
|
||
field = self._columns[col].field
|
||
value = row_data.get(field)
|
||
self.cell_clicked.emit(field, value)
|
||
|
||
def _on_checkbox_changed(self, row_id: Any, state: int):
|
||
"""Handle checkbox state change."""
|
||
if state == Qt.CheckState.Checked.value:
|
||
if row_id not in self._selected_ids:
|
||
self._selected_ids.append(row_id)
|
||
else:
|
||
if row_id in self._selected_ids:
|
||
self._selected_ids.remove(row_id)
|
||
|
||
self.selection_changed.emit(self._selected_ids)
|
||
|
||
def _on_page_size_changed(self, index: int):
|
||
"""Handle page size change."""
|
||
self._page_size = self._page_size_combo.currentData()
|
||
self._current_page = 0
|
||
self._refresh_data()
|
||
|
||
def _prev_page(self):
|
||
"""Go to previous page."""
|
||
if self._current_page > 0:
|
||
self._current_page -= 1
|
||
self.page_changed.emit(self._current_page)
|
||
self._refresh_data()
|
||
|
||
def _next_page(self):
|
||
"""Go to next page."""
|
||
max_page = (len(self._rows) - 1) // self._page_size
|
||
if self._current_page < max_page:
|
||
self._current_page += 1
|
||
self.page_changed.emit(self._current_page)
|
||
self._refresh_data()
|
||
|
||
def set_rows(self, rows: List[Dict]):
|
||
"""Set the data rows."""
|
||
self._rows = rows
|
||
self._current_page = 0
|
||
self._refresh_data()
|
||
|
||
def set_columns(self, columns: List[DataGridColumn]):
|
||
"""Set the columns."""
|
||
self._columns = columns
|
||
self._refresh_data()
|
||
|
||
def get_selected_rows(self) -> List[Dict]:
|
||
"""Get selected rows."""
|
||
return [
|
||
row for row in self._rows
|
||
if self._get_row_id(row) in self._selected_ids
|
||
]
|
||
|
||
@property
|
||
def loading(self) -> bool:
|
||
return self._loading
|
||
|
||
@loading.setter
|
||
def loading(self, value: bool):
|
||
self._loading = value
|
||
self._loading_label.setVisible(value)
|
||
|
||
|
||
# Aliases for MUI X compatibility
|
||
DataGridPro = DataGrid
|
||
DataGridPremium = DataGrid
|
||
|
||
|
||
# =============================================================================
|
||
# DatePicker
|
||
# =============================================================================
|
||
|
||
class DatePicker(QFrame, StyleMixin):
|
||
"""
|
||
Date selection component.
|
||
|
||
Signals:
|
||
date_changed: Emitted when date changes.
|
||
"""
|
||
|
||
date_changed = pyqtSignal(object) # date or None
|
||
|
||
def __init__(
|
||
self,
|
||
parent: Optional[QWidget] = None,
|
||
value: Optional[date] = None,
|
||
label: Optional[str] = None,
|
||
format_str: str = "MM/dd/yyyy",
|
||
min_date: Optional[date] = None,
|
||
max_date: Optional[date] = None,
|
||
disabled: bool = False,
|
||
read_only: bool = False,
|
||
error: bool = False,
|
||
helper_text: Optional[str] = None,
|
||
placeholder: str = "Select date",
|
||
clearable: bool = False,
|
||
):
|
||
super().__init__(parent)
|
||
StyleMixin.__init__(self)
|
||
|
||
self._value = value
|
||
self._label_text = label
|
||
self._format_str = format_str
|
||
self._min_date = min_date
|
||
self._max_date = max_date
|
||
self._disabled = disabled
|
||
self._read_only = read_only
|
||
self._error = error
|
||
self._helper_text = helper_text
|
||
self._placeholder = placeholder
|
||
self._clearable = clearable
|
||
|
||
self._setup_ui()
|
||
self._apply_styles()
|
||
|
||
def _setup_ui(self):
|
||
"""Set up the UI components."""
|
||
layout = QVBoxLayout(self)
|
||
layout.setContentsMargins(0, 0, 0, 0)
|
||
layout.setSpacing(4)
|
||
|
||
# Label
|
||
if self._label_text:
|
||
self._label = QLabel(self._label_text)
|
||
self._label.setStyleSheet("font-size: 12px; color: rgba(0,0,0,0.6);")
|
||
layout.addWidget(self._label)
|
||
|
||
# Input container
|
||
input_container = QHBoxLayout()
|
||
input_container.setSpacing(0)
|
||
|
||
# Date input
|
||
self._date_edit = QDateEdit()
|
||
self._date_edit.setCalendarPopup(True)
|
||
self._date_edit.setDisplayFormat(self._qt_format())
|
||
|
||
if self._value:
|
||
self._date_edit.setDate(QDate(self._value.year, self._value.month, self._value.day))
|
||
|
||
if self._min_date:
|
||
self._date_edit.setMinimumDate(QDate(
|
||
self._min_date.year, self._min_date.month, self._min_date.day
|
||
))
|
||
|
||
if self._max_date:
|
||
self._date_edit.setMaximumDate(QDate(
|
||
self._max_date.year, self._max_date.month, self._max_date.day
|
||
))
|
||
|
||
self._date_edit.setEnabled(not self._disabled)
|
||
self._date_edit.setReadOnly(self._read_only)
|
||
self._date_edit.dateChanged.connect(self._on_date_changed)
|
||
|
||
input_container.addWidget(self._date_edit, 1)
|
||
|
||
# Clear button
|
||
if self._clearable:
|
||
self._clear_btn = QPushButton("×")
|
||
self._clear_btn.setFixedSize(24, 24)
|
||
self._clear_btn.clicked.connect(self._clear_date)
|
||
self._clear_btn.setStyleSheet("""
|
||
QPushButton {
|
||
border: none;
|
||
background: transparent;
|
||
font-size: 16px;
|
||
color: #666;
|
||
}
|
||
QPushButton:hover {
|
||
color: #333;
|
||
}
|
||
""")
|
||
input_container.addWidget(self._clear_btn)
|
||
|
||
layout.addLayout(input_container)
|
||
|
||
# Helper text
|
||
if self._helper_text:
|
||
self._helper = QLabel(self._helper_text)
|
||
color = "#d32f2f" if self._error else "rgba(0,0,0,0.6)"
|
||
self._helper.setStyleSheet(f"font-size: 12px; color: {color};")
|
||
layout.addWidget(self._helper)
|
||
|
||
def _qt_format(self) -> str:
|
||
"""Convert format string to Qt format."""
|
||
return self._format_str.replace("MM", "MM").replace("dd", "dd").replace("yyyy", "yyyy")
|
||
|
||
def _apply_styles(self):
|
||
"""Apply styles."""
|
||
self.add_class("fakemui-datepicker")
|
||
if self._error:
|
||
self.add_class("fakemui-datepicker-error")
|
||
if self._disabled:
|
||
self.add_class("fakemui-datepicker-disabled")
|
||
|
||
border_color = "#d32f2f" if self._error else "#c4c4c4"
|
||
|
||
self._date_edit.setStyleSheet(f"""
|
||
QDateEdit {{
|
||
border: 1px solid {border_color};
|
||
border-radius: 4px;
|
||
padding: 8px 12px;
|
||
background: white;
|
||
}}
|
||
QDateEdit:focus {{
|
||
border-color: #1976d2;
|
||
}}
|
||
QDateEdit:disabled {{
|
||
background: #f5f5f5;
|
||
color: #9e9e9e;
|
||
}}
|
||
""")
|
||
|
||
def _on_date_changed(self, qdate: QDate):
|
||
"""Handle date change."""
|
||
self._value = date(qdate.year(), qdate.month(), qdate.day())
|
||
self.date_changed.emit(self._value)
|
||
|
||
def _clear_date(self):
|
||
"""Clear the date."""
|
||
self._value = None
|
||
self.date_changed.emit(None)
|
||
|
||
@property
|
||
def value(self) -> Optional[date]:
|
||
return self._value
|
||
|
||
@value.setter
|
||
def value(self, val: Optional[date]):
|
||
self._value = val
|
||
if val:
|
||
self._date_edit.setDate(QDate(val.year, val.month, val.day))
|
||
self.date_changed.emit(val)
|
||
|
||
|
||
# =============================================================================
|
||
# TimePicker
|
||
# =============================================================================
|
||
|
||
class TimePicker(QFrame, StyleMixin):
|
||
"""
|
||
Time selection component.
|
||
|
||
Signals:
|
||
time_changed: Emitted when time changes.
|
||
"""
|
||
|
||
time_changed = pyqtSignal(object) # time or None
|
||
|
||
def __init__(
|
||
self,
|
||
parent: Optional[QWidget] = None,
|
||
value: Optional[time] = None,
|
||
label: Optional[str] = None,
|
||
ampm: bool = False,
|
||
disabled: bool = False,
|
||
read_only: bool = False,
|
||
error: bool = False,
|
||
helper_text: Optional[str] = None,
|
||
placeholder: str = "Select time",
|
||
):
|
||
super().__init__(parent)
|
||
StyleMixin.__init__(self)
|
||
|
||
self._value = value
|
||
self._label_text = label
|
||
self._ampm = ampm
|
||
self._disabled = disabled
|
||
self._read_only = read_only
|
||
self._error = error
|
||
self._helper_text = helper_text
|
||
self._placeholder = placeholder
|
||
|
||
self._setup_ui()
|
||
self._apply_styles()
|
||
|
||
def _setup_ui(self):
|
||
"""Set up the UI components."""
|
||
layout = QVBoxLayout(self)
|
||
layout.setContentsMargins(0, 0, 0, 0)
|
||
layout.setSpacing(4)
|
||
|
||
# Label
|
||
if self._label_text:
|
||
self._label = QLabel(self._label_text)
|
||
self._label.setStyleSheet("font-size: 12px; color: rgba(0,0,0,0.6);")
|
||
layout.addWidget(self._label)
|
||
|
||
# Time input
|
||
self._time_edit = QTimeEdit()
|
||
if self._ampm:
|
||
self._time_edit.setDisplayFormat("hh:mm AP")
|
||
else:
|
||
self._time_edit.setDisplayFormat("HH:mm")
|
||
|
||
if self._value:
|
||
self._time_edit.setTime(QTime(self._value.hour, self._value.minute))
|
||
|
||
self._time_edit.setEnabled(not self._disabled)
|
||
self._time_edit.setReadOnly(self._read_only)
|
||
self._time_edit.timeChanged.connect(self._on_time_changed)
|
||
|
||
layout.addWidget(self._time_edit)
|
||
|
||
# Helper text
|
||
if self._helper_text:
|
||
self._helper = QLabel(self._helper_text)
|
||
color = "#d32f2f" if self._error else "rgba(0,0,0,0.6)"
|
||
self._helper.setStyleSheet(f"font-size: 12px; color: {color};")
|
||
layout.addWidget(self._helper)
|
||
|
||
def _apply_styles(self):
|
||
"""Apply styles."""
|
||
self.add_class("fakemui-timepicker")
|
||
|
||
border_color = "#d32f2f" if self._error else "#c4c4c4"
|
||
|
||
self._time_edit.setStyleSheet(f"""
|
||
QTimeEdit {{
|
||
border: 1px solid {border_color};
|
||
border-radius: 4px;
|
||
padding: 8px 12px;
|
||
background: white;
|
||
}}
|
||
QTimeEdit:focus {{
|
||
border-color: #1976d2;
|
||
}}
|
||
QTimeEdit:disabled {{
|
||
background: #f5f5f5;
|
||
color: #9e9e9e;
|
||
}}
|
||
""")
|
||
|
||
def _on_time_changed(self, qtime: QTime):
|
||
"""Handle time change."""
|
||
self._value = time(qtime.hour(), qtime.minute())
|
||
self.time_changed.emit(self._value)
|
||
|
||
@property
|
||
def value(self) -> Optional[time]:
|
||
return self._value
|
||
|
||
@value.setter
|
||
def value(self, val: Optional[time]):
|
||
self._value = val
|
||
if val:
|
||
self._time_edit.setTime(QTime(val.hour, val.minute))
|
||
self.time_changed.emit(val)
|
||
|
||
|
||
# =============================================================================
|
||
# DateTimePicker
|
||
# =============================================================================
|
||
|
||
class DateTimePicker(QFrame, StyleMixin):
|
||
"""
|
||
Combined date and time picker.
|
||
|
||
Signals:
|
||
datetime_changed: Emitted when datetime changes.
|
||
"""
|
||
|
||
datetime_changed = pyqtSignal(object) # datetime or None
|
||
|
||
def __init__(
|
||
self,
|
||
parent: Optional[QWidget] = None,
|
||
value: Optional[datetime] = None,
|
||
label: Optional[str] = None,
|
||
disabled: bool = False,
|
||
):
|
||
super().__init__(parent)
|
||
StyleMixin.__init__(self)
|
||
|
||
self._value = value
|
||
self._label_text = label
|
||
self._disabled = disabled
|
||
|
||
self._setup_ui()
|
||
|
||
def _setup_ui(self):
|
||
"""Set up the UI components."""
|
||
layout = QVBoxLayout(self)
|
||
layout.setContentsMargins(0, 0, 0, 0)
|
||
layout.setSpacing(4)
|
||
|
||
# Label
|
||
if self._label_text:
|
||
self._label = QLabel(self._label_text)
|
||
self._label.setStyleSheet("font-size: 12px; color: rgba(0,0,0,0.6);")
|
||
layout.addWidget(self._label)
|
||
|
||
# Pickers container
|
||
pickers = QHBoxLayout()
|
||
pickers.setSpacing(8)
|
||
|
||
# Date picker
|
||
self._date_picker = DatePicker(
|
||
value=self._value.date() if self._value else None,
|
||
disabled=self._disabled
|
||
)
|
||
self._date_picker.date_changed.connect(self._on_date_changed)
|
||
pickers.addWidget(self._date_picker)
|
||
|
||
# Time picker
|
||
self._time_picker = TimePicker(
|
||
value=self._value.time() if self._value else None,
|
||
disabled=self._disabled
|
||
)
|
||
self._time_picker.time_changed.connect(self._on_time_changed)
|
||
pickers.addWidget(self._time_picker)
|
||
|
||
layout.addLayout(pickers)
|
||
|
||
self.add_class("fakemui-datetimepicker")
|
||
|
||
def _on_date_changed(self, new_date: Optional[date]):
|
||
"""Handle date change."""
|
||
if new_date:
|
||
current_time = self._value.time() if self._value else time(0, 0)
|
||
self._value = datetime.combine(new_date, current_time)
|
||
else:
|
||
self._value = None
|
||
self.datetime_changed.emit(self._value)
|
||
|
||
def _on_time_changed(self, new_time: Optional[time]):
|
||
"""Handle time change."""
|
||
if new_time:
|
||
current_date = self._value.date() if self._value else date.today()
|
||
self._value = datetime.combine(current_date, new_time)
|
||
self.datetime_changed.emit(self._value)
|
||
|
||
@property
|
||
def value(self) -> Optional[datetime]:
|
||
return self._value
|
||
|
||
@value.setter
|
||
def value(self, val: Optional[datetime]):
|
||
self._value = val
|
||
if val:
|
||
self._date_picker.value = val.date()
|
||
self._time_picker.value = val.time()
|
||
self.datetime_changed.emit(val)
|
||
|
||
|
||
# =============================================================================
|
||
# StaticDatePicker (Calendar View)
|
||
# =============================================================================
|
||
|
||
class StaticDatePicker(QFrame, StyleMixin):
|
||
"""
|
||
A static (always visible) date picker calendar.
|
||
|
||
Signals:
|
||
date_changed: Emitted when date changes.
|
||
"""
|
||
|
||
date_changed = pyqtSignal(object)
|
||
|
||
def __init__(
|
||
self,
|
||
parent: Optional[QWidget] = None,
|
||
value: Optional[date] = None,
|
||
):
|
||
super().__init__(parent)
|
||
StyleMixin.__init__(self)
|
||
|
||
self._value = value
|
||
|
||
layout = QVBoxLayout(self)
|
||
layout.setContentsMargins(0, 0, 0, 0)
|
||
|
||
self._calendar = QCalendarWidget()
|
||
self._calendar.setGridVisible(True)
|
||
|
||
if value:
|
||
self._calendar.setSelectedDate(QDate(value.year, value.month, value.day))
|
||
|
||
self._calendar.selectionChanged.connect(self._on_selection_changed)
|
||
|
||
layout.addWidget(self._calendar)
|
||
|
||
self.add_class("fakemui-static-datepicker")
|
||
|
||
self._calendar.setStyleSheet("""
|
||
QCalendarWidget {
|
||
background: white;
|
||
}
|
||
QCalendarWidget QToolButton {
|
||
color: #333;
|
||
background: transparent;
|
||
border: none;
|
||
padding: 4px 8px;
|
||
}
|
||
QCalendarWidget QToolButton:hover {
|
||
background: #f5f5f5;
|
||
border-radius: 4px;
|
||
}
|
||
QCalendarWidget QWidget#qt_calendar_navigationbar {
|
||
background: #fafafa;
|
||
}
|
||
QCalendarWidget QAbstractItemView:enabled {
|
||
selection-background-color: #1976d2;
|
||
selection-color: white;
|
||
}
|
||
""")
|
||
|
||
def _on_selection_changed(self):
|
||
"""Handle selection change."""
|
||
qdate = self._calendar.selectedDate()
|
||
self._value = date(qdate.year(), qdate.month(), qdate.day())
|
||
self.date_changed.emit(self._value)
|
||
|
||
@property
|
||
def value(self) -> Optional[date]:
|
||
return self._value
|
||
|
||
@value.setter
|
||
def value(self, val: Optional[date]):
|
||
self._value = val
|
||
if val:
|
||
self._calendar.setSelectedDate(QDate(val.year, val.month, val.day))
|
||
self.date_changed.emit(val)
|
||
|
||
|
||
# Aliases
|
||
DesktopDatePicker = DatePicker
|
||
MobileDatePicker = DatePicker
|
||
CalendarPicker = StaticDatePicker
|
||
|
||
|
||
class ClockPicker(QFrame, StyleMixin):
|
||
"""
|
||
A clock picker widget.
|
||
|
||
Signals:
|
||
time_changed: Emitted when time changes.
|
||
"""
|
||
|
||
time_changed = pyqtSignal(object)
|
||
|
||
def __init__(
|
||
self,
|
||
parent: Optional[QWidget] = None,
|
||
value: Optional[time] = None,
|
||
):
|
||
super().__init__(parent)
|
||
StyleMixin.__init__(self)
|
||
|
||
self._value = value
|
||
|
||
layout = QVBoxLayout(self)
|
||
layout.setContentsMargins(0, 0, 0, 0)
|
||
|
||
self._time_picker = TimePicker(value=value)
|
||
self._time_picker.time_changed.connect(self._on_time_changed)
|
||
|
||
layout.addWidget(self._time_picker)
|
||
|
||
self.add_class("fakemui-clock-picker")
|
||
|
||
def _on_time_changed(self, new_time: Optional[time]):
|
||
"""Handle time change."""
|
||
self._value = new_time
|
||
self.time_changed.emit(new_time)
|
||
|
||
@property
|
||
def value(self) -> Optional[time]:
|
||
return self._value
|
||
|
||
@value.setter
|
||
def value(self, val: Optional[time]):
|
||
self._value = val
|
||
self._time_picker.value = val
|
||
self.time_changed.emit(val)
|
||
|
||
|
||
# =============================================================================
|
||
# Exports
|
||
# =============================================================================
|
||
|
||
__all__ = [
|
||
# DataGrid
|
||
"DataGrid",
|
||
"DataGridPro",
|
||
"DataGridPremium",
|
||
"DataGridColumn",
|
||
# Date/Time Pickers
|
||
"DatePicker",
|
||
"TimePicker",
|
||
"DateTimePicker",
|
||
"DesktopDatePicker",
|
||
"MobileDatePicker",
|
||
"StaticDatePicker",
|
||
"CalendarPicker",
|
||
"ClockPicker",
|
||
]
|