663 lines
21 KiB
Python
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
|