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