""" EU-Utility - UI Performance Optimizations Performance improvements for UI rendering: 1. Double buffering for smooth rendering 2. Lazy widget loading 3. Virtual scrolling for large lists 4. Style caching 5. Reduced repaints """ from typing import Optional, List, Callable, Any from functools import lru_cache from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QStackedWidget, QListWidget, QListWidgetItem, QScrollArea, QFrame ) from PyQt6.QtCore import Qt, QTimer, pyqtSignal, QSize, QRect from PyQt6.QtGui import QPixmap, QPainter, QColor, QFont, QFontMetrics from core.eu_styles import EU_COLORS class LazyWidgetLoader: """ Lazy loader for plugin UI widgets. Only creates widgets when they are first shown. """ def __init__(self, factory: Callable[[], QWidget]): self._factory = factory self._widget: Optional[QWidget] = None self._loaded = False def get_widget(self) -> QWidget: """Get or create the widget.""" if not self._loaded: self._widget = self._factory() self._loaded = True return self._widget def is_loaded(self) -> bool: """Check if widget has been loaded.""" return self._loaded def unload(self): """Unload the widget to free memory.""" if self._widget: self._widget.deleteLater() self._widget = None self._loaded = False class VirtualListWidget(QListWidget): """ Virtual list widget for large datasets. Only renders visible items. """ def __init__(self, parent=None): super().__init__(parent) self._all_items: List[Any] = [] self._item_factory: Optional[Callable[[Any], QListWidgetItem]] = None self._page_size = 50 self._current_page = 0 self._is_virtual = False # Setup scroll handling self.verticalScrollBar().valueChanged.connect(self._on_scroll) def set_virtual_mode(self, enabled: bool, item_factory: Callable[[Any], QListWidgetItem] = None): """Enable/disable virtual mode.""" self._is_virtual = enabled self._item_factory = item_factory def set_data(self, items: List[Any]): """Set the full dataset.""" self._all_items = items if self._is_virtual: self._render_visible_items() else: # Non-virtual: render all self.clear() for item_data in items: if self._item_factory: item = self._item_factory(item_data) self.addItem(item) def _render_visible_items(self): """Render only visible items.""" self.clear() start_idx = self._current_page * self._page_size end_idx = min(start_idx + self._page_size, len(self._all_items)) for i in range(start_idx, end_idx): if self._item_factory: item = self._item_factory(self._all_items[i]) self.addItem(item) def _on_scroll(self, value: int): """Handle scroll events.""" if not self._is_virtual: return # Calculate which page should be visible scrollbar = self.verticalScrollBar() if scrollbar.maximum() > 0: ratio = value / scrollbar.maximum() new_page = int(ratio * (len(self._all_items) / self._page_size)) if new_page != self._current_page: self._current_page = new_page self._render_visible_items() class CachedStyleSheet: """ Cache for computed stylesheets. Avoids recomputing styles on every update. """ _cache: dict = {} _max_size = 100 @classmethod def get(cls, style_type: str, **params) -> str: """Get cached stylesheet.""" cache_key = f"{style_type}:{hash(frozenset(params.items()))}" if cache_key not in cls._cache: stylesheet = cls._compute_style(style_type, **params) # Simple LRU: clear if too big if len(cls._cache) >= cls._max_size: cls._cache.clear() cls._cache[cache_key] = stylesheet return cls._cache[cache_key] @classmethod def _compute_style(cls, style_type: str, **params) -> str: """Compute stylesheet for given type.""" if style_type == "button": return f""" QPushButton {{ background-color: {params.get('bg', EU_COLORS['bg_panel'])}; color: {params.get('color', EU_COLORS['text_secondary'])}; border: 1px solid {params.get('border', EU_COLORS['border_subtle'])}; border-radius: {params.get('radius', 4)}px; padding: {params.get('padding_v', 4)}px {params.get('padding_h', 12)}px; font-size: {params.get('font_size', 11)}px; }} QPushButton:hover {{ background-color: {params.get('bg_hover', EU_COLORS['bg_hover'])}; border-color: {params.get('border_hover', EU_COLORS['accent_orange'])}; }} """ elif style_type == "label": return f""" QLabel {{ color: {params.get('color', EU_COLORS['text_primary'])}; font-size: {params.get('font_size', 12)}px; font-weight: {params.get('weight', 'normal')}; }} """ elif style_type == "frame": return f""" QFrame {{ background-color: {params.get('bg', EU_COLORS['bg_dark'])}; border: {params.get('border_width', 1)}px solid {params.get('border', EU_COLORS['border_medium'])}; border-radius: {params.get('radius', 6)}px; }} """ return "" @classmethod def clear_cache(cls): """Clear the stylesheet cache.""" cls._cache.clear() class DoubleBufferedWidget(QWidget): """ Widget with double buffering for smooth rendering. Reduces flickering during updates. """ def __init__(self, parent=None): super().__init__(parent) self._buffer: Optional[QPixmap] = None self._buffer_dirty = True self._buffer_size: Optional[QSize] = None def resizeEvent(self, event): """Handle resize - invalidate buffer.""" super().resizeEvent(event) self._buffer_dirty = True self._buffer = None def paintEvent(self, event): """Paint with double buffering.""" if self._buffer_dirty or self._buffer is None: self._repaint_buffer() # Draw from buffer painter = QPainter(self) if self._buffer: painter.drawPixmap(0, 0, self._buffer) painter.end() def _repaint_buffer(self): """Repaint the off-screen buffer.""" size = self.size() if size.width() <= 0 or size.height() <= 0: return self._buffer = QPixmap(size) self._buffer.fill(QColor(EU_COLORS['bg_dark'])) painter = QPainter(self._buffer) self._draw_content(painter) painter.end() self._buffer_dirty = False def _draw_content(self, painter: QPainter): """Override this to draw content.""" pass def invalidate_buffer(self): """Mark buffer as needing repaint.""" self._buffer_dirty = True self.update() class ThrottledUpdater: """ Throttles UI updates to a maximum frequency. Useful for high-frequency data updates. """ def __init__(self, update_func: Callable, min_interval_ms: int = 100): self.update_func = update_func self.min_interval_ms = min_interval_ms self._pending = False self._last_update = 0 self._timer = QTimer() self._timer.setSingleShot(True) self._timer.timeout.connect(self._do_update) def trigger(self): """Trigger an update (may be throttled).""" import time now = int(time.time() * 1000) if now - self._last_update >= self.min_interval_ms: # Update immediately self._do_update() elif not self._pending: # Schedule update self._pending = True delay = self.min_interval_ms - (now - self._last_update) self._timer.start(delay) def _do_update(self): """Perform the actual update.""" import time self._last_update = int(time.time() * 1000) self._pending = False self.update_func() class FontCache: """ Cache for QFont objects to avoid recreation. """ _cache: dict = {} @classmethod def get(cls, family: str = "Segoe UI", size: int = 12, weight: int = QFont.Weight.Normal, bold: bool = False) -> QFont: """Get cached font.""" cache_key = (family, size, weight, bold) if cache_key not in cls._cache: font = QFont(family, size, weight) font.setBold(bold) cls._cache[cache_key] = font return cls._cache[cache_key] @classmethod def get_metrics(cls, font: QFont) -> QFontMetrics: """Get font metrics (cached per font).""" cache_key = id(font) if cache_key not in cls._cache: cls._cache[cache_key] = QFontMetrics(font) return cls._cache[cache_key] class OptimizedStackedWidget(QStackedWidget): """ Stacked widget with lazy loading and memory management. """ def __init__(self, parent=None): super().__init__(parent) self._lazy_loaders: dict = {} self._max_loaded = 5 # Maximum widgets to keep loaded def add_lazy_widget(self, index: int, loader: LazyWidgetLoader): """Add a lazily-loaded widget.""" self._lazy_loaders[index] = loader # Add placeholder placeholder = QWidget() self.addWidget(placeholder) def setCurrentIndex(self, index: int): """Switch to widget, loading if necessary.""" # Load if needed if index in self._lazy_loaders: loader = self._lazy_loaders[index] if not loader.is_loaded(): # Load the widget widget = loader.get_widget() old_widget = self.widget(index) self.insertWidget(index, widget) self.removeWidget(old_widget) old_widget.deleteLater() # Unload old widgets if too many loaded self._unload_old_widgets() super().setCurrentIndex(index) def _unload_old_widgets(self): """Unload least recently used widgets.""" loaded = [ (idx, loader) for idx, loader in self._lazy_loaders.items() if loader.is_loaded() ] if len(loaded) > self._max_loaded: # Unload oldest (simple FIFO) for idx, loader in loaded[:-self._max_loaded]: if idx != self.currentIndex(): loader.unload() # Restore placeholder placeholder = QWidget() old_widget = self.widget(idx) self.insertWidget(idx, placeholder) self.removeWidget(old_widget) class DebouncedLineEdit: """ Line edit with debounced text change signals. Reduces processing for rapid text input. """ def __init__(self, callback: Callable[[str], None], delay_ms: int = 300): from PyQt6.QtWidgets import QLineEdit self.widget = QLineEdit() self.callback = callback self.delay_ms = delay_ms self._timer = QTimer() self._timer.setSingleShot(True) self._timer.timeout.connect(self._emit_text) self.widget.textChanged.connect(self._on_text_changed) self._pending_text = "" def _on_text_changed(self, text: str): """Handle text change with debounce.""" self._pending_text = text self._timer.start(self.delay_ms) def _emit_text(self): """Emit the debounced text.""" self.callback(self._pending_text) def set_text(self, text: str): """Set text without triggering debounce.""" self._timer.stop() self.widget.setText(text) # ========== Usage Example ========== if __name__ == "__main__": import sys from PyQt6.QtWidgets import QApplication app = QApplication(sys.argv) # Test cached stylesheet style1 = CachedStyleSheet.get("button", bg="#333", color="#fff") style2 = CachedStyleSheet.get("button", bg="#333", color="#fff") print(f"Stylesheet cached: {style1 is style2}") # Test font cache font1 = FontCache.get("Arial", 12) font2 = FontCache.get("Arial", 12) print(f"Font cached: {font1 is font2}") print("UI optimizations loaded successfully!")