EU-Utility/core/plugin_ui_components.py

663 lines
21 KiB
Python

"""
EU-Utility - Plugin UI Components
Reusable UI components for plugins with EU styling.
"""
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QFrame, QProgressBar, QTableWidget, QTableWidgetItem,
QLineEdit, QComboBox, QScrollArea, QGridLayout,
QSizePolicy, QSpacerItem, QHeaderView
)
from PyQt6.QtCore import Qt, pyqtSignal, QSize
from PyQt6.QtGui import QColor
from core.eu_styles import (
get_color, get_all_colors, get_button_style, get_input_style,
get_table_style, get_card_style, get_progress_bar_style,
EU_TYPOGRAPHY, EU_SPACING, EU_SIZES
)
from core.icon_manager import get_icon_manager
class EUCard(QFrame):
"""Styled card container."""
def __init__(self, title: str = None, parent=None):
super().__init__(parent)
self.title = title
self._setup_ui()
def _setup_ui(self):
"""Setup card UI."""
c = get_all_colors()
self.setObjectName("euCard")
self.setStyleSheet(f"""
QFrame#euCard {{
background-color: {c['bg_secondary']};
border: 1px solid {c['border_default']};
border-radius: {EU_SIZES['radius_lg']};
}}
""")
self.layout = QVBoxLayout(self)
self.layout.setContentsMargins(16, 16, 16, 16)
self.layout.setSpacing(12)
if self.title:
title_label = QLabel(self.title)
title_label.setStyleSheet(f"""
color: {c['text_primary']};
font-size: {EU_TYPOGRAPHY['size_base']};
font-weight: {EU_TYPOGRAPHY['weight_semibold']};
""")
self.layout.addWidget(title_label)
def add_widget(self, widget):
"""Add widget to card."""
self.layout.addWidget(widget)
def add_layout(self, layout):
"""Add layout to card."""
self.layout.addLayout(layout)
class EUStatCard(QFrame):
"""Statistic card with icon, value, and label."""
def __init__(self, icon: str, label: str, value: str = "0", parent=None):
super().__init__(parent)
self.icon_text = icon
self.label_text = label
self.value_text = value
self._setup_ui()
def _setup_ui(self):
"""Setup stat card UI."""
c = get_all_colors()
self.setStyleSheet(f"""
QFrame {{
background-color: {c['bg_secondary']};
border: 1px solid {c['border_default']};
border-radius: {EU_SIZES['radius_lg']};
padding: 16px;
}}
""")
layout = QHBoxLayout(self)
layout.setContentsMargins(16, 16, 16, 16)
layout.setSpacing(16)
# Icon
self.icon_label = QLabel(self.icon_text)
self.icon_label.setStyleSheet(f"""
font-size: 24px;
min-width: 40px;
""")
layout.addWidget(self.icon_label)
# Text content
text_layout = QVBoxLayout()
text_layout.setSpacing(4)
self.value_label = QLabel(self.value_text)
self.value_label.setStyleSheet(f"""
color: {c['text_primary']};
font-size: {EU_TYPOGRAPHY['size_2xl']};
font-weight: {EU_TYPOGRAPHY['weight_bold']};
""")
text_layout.addWidget(self.value_label)
self.label_widget = QLabel(self.label_text)
self.label_widget.setStyleSheet(f"""
color: {c['text_secondary']};
font-size: {EU_TYPOGRAPHY['size_sm']};
""")
text_layout.addWidget(self.label_widget)
layout.addLayout(text_layout)
layout.addStretch()
def set_value(self, value: str):
"""Update the value."""
self.value_text = value
self.value_label.setText(value)
def set_icon(self, icon: str):
"""Update the icon."""
self.icon_text = icon
self.icon_label.setText(icon)
class EUProgressCard(QFrame):
"""Progress card with label and progress bar."""
def __init__(self, label: str, max_value: float = 100, parent=None):
super().__init__(parent)
self.label_text = label
self.max_value = max_value
self.current_value = 0
self._setup_ui()
def _setup_ui(self):
"""Setup progress card UI."""
c = get_all_colors()
self.setStyleSheet(f"""
QFrame {{
background-color: {c['bg_secondary']};
border: 1px solid {c['border_default']};
border-radius: {EU_SIZES['radius_lg']};
}}
""")
layout = QVBoxLayout(self)
layout.setContentsMargins(16, 16, 16, 16)
layout.setSpacing(12)
# Header with label and value
header = QHBoxLayout()
self.label_widget = QLabel(self.label_text)
self.label_widget.setStyleSheet(f"""
color: {c['text_primary']};
font-size: {EU_TYPOGRAPHY['size_sm']};
font-weight: {EU_TYPOGRAPHY['weight_medium']};
""")
header.addWidget(self.label_widget)
header.addStretch()
self.value_label = QLabel(f"0 / {self.max_value}")
self.value_label.setStyleSheet(f"""
color: {c['text_secondary']};
font-size: {EU_TYPOGRAPHY['size_sm']};
""")
header.addWidget(self.value_label)
layout.addLayout(header)
# Progress bar
self.progress = QProgressBar()
self.progress.setMaximum(int(self.max_value))
self.progress.setValue(0)
self.progress.setTextVisible(False)
self.progress.setFixedHeight(8)
self.progress.setStyleSheet(get_progress_bar_style())
layout.addWidget(self.progress)
def set_value(self, value: float):
"""Update progress value."""
self.current_value = value
self.progress.setValue(int(value))
self.value_label.setText(f"{value:.1f} / {self.max_value}")
# Change color based on percentage
percentage = (value / self.max_value) * 100
c = get_all_colors()
if percentage >= 90:
self.progress.setStyleSheet(f"""
QProgressBar {{
background-color: {c['progress_bg']};
border: none;
border-radius: 4px;
height: 8px;
}}
QProgressBar::chunk {{
background-color: {c['accent_red']};
border-radius: 4px;
}}
""")
elif percentage >= 75:
self.progress.setStyleSheet(f"""
QProgressBar {{
background-color: {c['progress_bg']};
border: none;
border-radius: 4px;
height: 8px;
}}
QProgressBar::chunk {{
background-color: {c['accent_gold']};
border-radius: 4px;
}}
""")
class EUSearchBar(QFrame):
"""Styled search bar with icon."""
search_triggered = pyqtSignal(str)
def __init__(self, placeholder: str = "Search...", parent=None):
super().__init__(parent)
self.placeholder = placeholder
self.icon_manager = get_icon_manager()
self._setup_ui()
def _setup_ui(self):
"""Setup search bar UI."""
c = get_all_colors()
self.setStyleSheet(f"""
QFrame {{
background-color: {c['bg_secondary']};
border: 1px solid {c['border_default']};
border-radius: {EU_SIZES['radius_lg']};
}}
""")
layout = QHBoxLayout(self)
layout.setContentsMargins(12, 8, 12, 8)
layout.setSpacing(12)
# Search icon
icon_label = QLabel()
icon_pixmap = self.icon_manager.get_pixmap('search', size=16)
icon_label.setPixmap(icon_pixmap)
icon_label.setFixedSize(16, 16)
icon_label.setStyleSheet(f"color: {c['text_muted']};")
layout.addWidget(icon_label)
# Input field
self.input = QLineEdit()
self.input.setPlaceholderText(self.placeholder)
self.input.setStyleSheet(f"""
QLineEdit {{
background-color: transparent;
border: none;
color: {c['text_primary']};
font-size: {EU_TYPOGRAPHY['size_base']};
padding: 4px;
}}
""")
self.input.returnPressed.connect(self._on_search)
layout.addWidget(self.input, 1)
# Search button
self.search_btn = QPushButton("Search")
self.search_btn.setStyleSheet(get_button_style('primary', 'sm'))
self.search_btn.clicked.connect(self._on_search)
layout.addWidget(self.search_btn)
def _on_search(self):
"""Handle search."""
query = self.input.text().strip()
if query:
self.search_triggered.emit(query)
def get_text(self) -> str:
"""Get current search text."""
return self.input.text()
def clear(self):
"""Clear search input."""
self.input.clear()
class EUDataTable(QTableWidget):
"""Styled data table with EU theme."""
def __init__(self, columns: list[str], parent=None):
super().__init__(parent)
self.column_names = columns
self._setup_ui()
def _setup_ui(self):
"""Setup table UI."""
self.setColumnCount(len(self.column_names))
self.setHorizontalHeaderLabels(self.column_names)
self.setStyleSheet(get_table_style())
# Configure headers
header = self.horizontalHeader()
header.setStretchLastSection(True)
header.setDefaultSectionSize(120)
# Selection behavior
self.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
self.setSelectionMode(QTableWidget.SelectionMode.SingleSelection)
self.setAlternatingRowColors(True)
# Hide vertical header
self.verticalHeader().setVisible(False)
def add_row(self, values: list, row_data=None):
"""Add a row to the table."""
row = self.rowCount()
self.insertRow(row)
for col, value in enumerate(values):
item = QTableWidgetItem(str(value))
item.setData(Qt.ItemDataRole.UserRole, row_data)
self.setItem(row, col, item)
return row
def clear_data(self):
"""Clear all data rows."""
self.setRowCount(0)
def get_selected_row_data(self):
"""Get data from selected row."""
selected = self.selectedItems()
if selected:
return selected[0].data(Qt.ItemDataRole.UserRole)
return None
class EUActionBar(QFrame):
"""Action bar with primary and secondary actions."""
def __init__(self, parent=None):
super().__init__(parent)
self._setup_ui()
def _setup_ui(self):
"""Setup action bar UI."""
c = get_all_colors()
self.setStyleSheet(f"""
QFrame {{
background-color: {c['bg_secondary']};
border-top: 1px solid {c['border_default']};
}}
""")
self.layout = QHBoxLayout(self)
self.layout.setContentsMargins(16, 12, 16, 12)
self.layout.setSpacing(12)
def add_primary_action(self, text: str, callback, icon: str = None):
"""Add primary action button."""
btn = QPushButton(f"{icon} {text}" if icon else text)
btn.setStyleSheet(get_button_style('primary'))
btn.clicked.connect(callback)
self.layout.addWidget(btn)
return btn
def add_secondary_action(self, text: str, callback, icon: str = None):
"""Add secondary action button."""
btn = QPushButton(f"{icon} {text}" if icon else text)
btn.setStyleSheet(get_button_style('secondary'))
btn.clicked.connect(callback)
self.layout.addWidget(btn)
return btn
def add_stretch(self):
"""Add stretch to push buttons to sides."""
self.layout.addStretch()
class EUTabs(QFrame):
"""Styled tab container."""
tab_changed = pyqtSignal(int)
def __init__(self, parent=None):
super().__init__(parent)
self.tabs = []
self.current_index = 0
self._setup_ui()
def _setup_ui(self):
"""Setup tabs UI."""
c = get_all_colors()
self.setStyleSheet("background: transparent;")
self.layout = QVBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setSpacing(0)
# Tab bar
self.tab_bar = QFrame()
self.tab_bar.setStyleSheet(f"""
QFrame {{
background-color: {c['bg_secondary']};
border-bottom: 1px solid {c['border_default']};
}}
""")
self.tab_bar_layout = QHBoxLayout(self.tab_bar)
self.tab_bar_layout.setContentsMargins(16, 0, 16, 0)
self.tab_bar_layout.setSpacing(0)
self.tab_bar_layout.addStretch()
self.layout.addWidget(self.tab_bar)
# Content area
self.content = QFrame()
self.content.setStyleSheet(f"""
QFrame {{
background-color: {c['bg_primary']};
}}
""")
self.content_layout = QVBoxLayout(self.content)
self.content_layout.setContentsMargins(16, 16, 16, 16)
self.layout.addWidget(self.content, 1)
def add_tab(self, title: str, widget):
"""Add a tab."""
index = len(self.tabs)
# Create tab button
btn = QPushButton(title)
btn.setCheckable(True)
btn.setStyleSheet(self._get_tab_style(False))
btn.clicked.connect(lambda: self._select_tab(index))
# Insert before stretch
self.tab_bar_layout.insertWidget(index, btn)
self.tabs.append({
'button': btn,
'widget': widget
})
# Select first tab by default
if index == 0:
self._select_tab(0)
def _select_tab(self, index: int):
"""Select a tab."""
if index < 0 or index >= len(self.tabs):
return
# Update button states
for i, tab in enumerate(self.tabs):
is_selected = i == index
tab['button'].setChecked(is_selected)
tab['button'].setStyleSheet(self._get_tab_style(is_selected))
tab['widget'].setVisible(is_selected)
# Update content
# Clear current content
while self.content_layout.count():
item = self.content_layout.takeAt(0)
if item.widget():
item.widget().setParent(None)
# Add new content
self.content_layout.addWidget(self.tabs[index]['widget'])
self.current_index = index
self.tab_changed.emit(index)
def _get_tab_style(self, selected: bool) -> str:
"""Get tab button style."""
c = get_all_colors()
if selected:
return f"""
QPushButton {{
background-color: {c['bg_primary']};
color: {c['text_primary']};
border: none;
border-bottom: 2px solid {c['accent_orange']};
padding: 12px 20px;
font-weight: {EU_TYPOGRAPHY['weight_semibold']};
}}
"""
else:
return f"""
QPushButton {{
background-color: transparent;
color: {c['text_secondary']};
border: none;
border-bottom: 2px solid transparent;
padding: 12px 20px;
}}
QPushButton:hover {{
color: {c['text_primary']};
background-color: {c['bg_hover']};
}}
"""
class EUBadge(QLabel):
"""Status badge label."""
VARIANTS = {
'default': 'bg_secondary',
'primary': 'accent_orange',
'success': 'status_success',
'warning': 'status_warning',
'error': 'status_error',
'info': 'status_info',
}
def __init__(self, text: str, variant: str = 'default', parent=None):
super().__init__(text, parent)
self.variant = variant
self._update_style()
def _update_style(self):
"""Update badge style."""
c = get_all_colors()
color_key = self.VARIANTS.get(self.variant, 'bg_secondary')
bg_color = c.get(color_key, c['bg_secondary'])
# Determine text color based on variant
if self.variant in ['primary', 'success', 'error', 'info']:
text_color = '#ffffff'
else:
text_color = c['text_primary']
self.setStyleSheet(f"""
QLabel {{
background-color: {bg_color};
color: {text_color};
padding: 4px 10px;
border-radius: {EU_SIZES['radius_full']};
font-size: {EU_TYPOGRAPHY['size_xs']};
font-weight: {EU_TYPOGRAPHY['weight_semibold']};
}}
""")
def set_variant(self, variant: str):
"""Change badge variant."""
self.variant = variant
self._update_style()
class EUEmptyState(QFrame):
"""Empty state placeholder."""
def __init__(self, icon: str, title: str, subtitle: str = None, action_text: str = None, action_callback=None, parent=None):
super().__init__(parent)
self.icon_text = icon
self.title_text = title
self.subtitle_text = subtitle
self.action_text = action_text
self.action_callback = action_callback
self._setup_ui()
def _setup_ui(self):
"""Setup empty state UI."""
c = get_all_colors()
self.setStyleSheet("background: transparent;")
layout = QVBoxLayout(self)
layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.setSpacing(16)
# Icon
icon = QLabel(self.icon_text)
icon.setStyleSheet("font-size: 48px;")
icon.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(icon)
# Title
title = QLabel(self.title_text)
title.setStyleSheet(f"""
color: {c['text_primary']};
font-size: {EU_TYPOGRAPHY['size_xl']};
font-weight: {EU_TYPOGRAPHY['weight_bold']};
""")
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(title)
# Subtitle
if self.subtitle_text:
subtitle = QLabel(self.subtitle_text)
subtitle.setStyleSheet(f"""
color: {c['text_secondary']};
font-size: {EU_TYPOGRAPHY['size_base']};
""")
subtitle.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(subtitle)
# Action button
if self.action_text and self.action_callback:
btn = QPushButton(self.action_text)
btn.setStyleSheet(get_button_style('primary'))
btn.clicked.connect(self.action_callback)
btn.setMaximumWidth(200)
layout.addWidget(btn, alignment=Qt.AlignmentFlag.AlignCenter)
# Convenience function to create standard plugin layout
def create_plugin_layout(title: str, description: str = None) -> tuple[QVBoxLayout, QWidget]:
"""
Create a standard plugin layout with title and description.
Returns:
Tuple of (layout, container_widget)
"""
c = get_all_colors()
container = QWidget()
container.setStyleSheet("background: transparent;")
layout = QVBoxLayout(container)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(20)
# Title
title_label = QLabel(title)
title_label.setStyleSheet(f"""
color: {c['text_primary']};
font-size: {EU_TYPOGRAPHY['size_xl']};
font-weight: {EU_TYPOGRAPHY['weight_bold']};
""")
layout.addWidget(title_label)
# Description
if description:
desc_label = QLabel(description)
desc_label.setStyleSheet(f"""
color: {c['text_secondary']};
font-size: {EU_TYPOGRAPHY['size_base']};
""")
desc_label.setWordWrap(True)
layout.addWidget(desc_label)
return layout, container