Source code for qtframework.widgets.badge

"""Badge widget for displaying status indicators and labels."""

from __future__ import annotations

from enum import Enum
from typing import TYPE_CHECKING

from PySide6.QtCore import Qt
from PySide6.QtGui import QPalette
from PySide6.QtWidgets import QApplication, QLabel

from qtframework.utils.logger import get_logger


if TYPE_CHECKING:
    from PySide6.QtWidgets import QWidget

logger = get_logger(__name__)


[docs] class BadgeVariant(Enum): """Badge variant types.""" DEFAULT = "default" PRIMARY = "primary" SECONDARY = "secondary" SUCCESS = "success" WARNING = "warning" DANGER = "danger" INFO = "info" LIGHT = "light" DARK = "dark"
[docs] class Badge(QLabel): """A badge widget for displaying status indicators, counts, or labels. Badges are small labels that can be used to show status, counts, or categorization. They are fully theme-aware and get their colors from the current theme's badge color tokens. """ def __init__( self, text: str = "", variant: BadgeVariant | str = BadgeVariant.DEFAULT, parent: QWidget | None = None, ) -> None: """Initialize the badge. Args: text: The badge text variant: The badge variant (color scheme) parent: Parent widget """ super().__init__(text, parent) # Convert string to enum if needed if isinstance(variant, str): try: variant = BadgeVariant(variant) except ValueError: variant = BadgeVariant.DEFAULT self._variant = variant self._setup_widget() self._apply_variant_style() self._connect_theme_signals() def _setup_widget(self) -> None: """Setup the widget properties.""" # Set alignment self.setAlignment(Qt.AlignmentFlag.AlignCenter) # Set size policy to fit content self.setScaledContents(False) # Add some padding through margins self.setMargin(0) # Make font slightly smaller and bold font = self.font() font.setPointSize(font.pointSize() - 1) font.setBold(True) self.setFont(font) def _get_theme_colors(self) -> dict[str, str]: """Get colors for the current variant from the theme. Returns: Dictionary with 'bg', 'fg', and 'border' colors """ # Try to get theme manager try: app = QApplication.instance() if app and hasattr(app, "theme_manager"): theme = app.theme_manager.get_current_theme() if theme and theme.tokens and theme.tokens.components: components = theme.tokens.components # Map variant to theme token names variant_map = { BadgeVariant.DEFAULT: "default", BadgeVariant.PRIMARY: "primary", BadgeVariant.SECONDARY: "secondary", BadgeVariant.SUCCESS: "success", BadgeVariant.WARNING: "warning", BadgeVariant.DANGER: "danger", BadgeVariant.INFO: "info", BadgeVariant.LIGHT: "light", BadgeVariant.DARK: "dark", } variant_name = variant_map.get(self._variant, "default") # Get colors from theme bg = getattr(components, f"badge_{variant_name}_bg", None) fg = getattr(components, f"badge_{variant_name}_fg", None) border = getattr(components, f"badge_{variant_name}_border", None) if bg and fg and border: return {"bg": bg, "fg": fg, "border": border} except (AttributeError, TypeError) as e: logger.debug(f"Failed to get badge colors from theme: {e}") # Return fallback colors from theme's semantic tokens if badge colors not defined return self._get_fallback_colors() def _get_fallback_colors(self) -> dict[str, str]: """Get fallback colors from theme's semantic tokens or defaults. Returns: Dictionary with 'bg', 'fg', and 'border' colors """ try: app = QApplication.instance() if app and hasattr(app, "theme_manager"): theme = app.theme_manager.get_current_theme() if theme and theme.tokens: tokens = theme.tokens # Use semantic tokens as fallback fallback_map = { BadgeVariant.DEFAULT: { "bg": tokens.semantic.bg_tertiary, "fg": tokens.semantic.fg_primary, "border": tokens.semantic.border_default, }, BadgeVariant.PRIMARY: { "bg": tokens.semantic.action_primary, "fg": tokens.semantic.fg_on_accent or tokens.primitive.white, "border": tokens.semantic.action_primary, }, BadgeVariant.SECONDARY: { "bg": tokens.primitive.gray_500, "fg": tokens.primitive.white, "border": tokens.primitive.gray_600, }, BadgeVariant.SUCCESS: { "bg": tokens.semantic.feedback_success, "fg": tokens.semantic.fg_on_accent or tokens.primitive.white, "border": tokens.semantic.feedback_success, }, BadgeVariant.WARNING: { "bg": tokens.semantic.feedback_warning, "fg": tokens.primitive.black, "border": tokens.semantic.feedback_warning, }, BadgeVariant.DANGER: { "bg": tokens.semantic.feedback_error, "fg": tokens.semantic.fg_on_accent or tokens.primitive.white, "border": tokens.semantic.feedback_error, }, BadgeVariant.INFO: { "bg": tokens.semantic.feedback_info, "fg": tokens.primitive.black, "border": tokens.semantic.feedback_info, }, BadgeVariant.LIGHT: { "bg": tokens.primitive.gray_100, "fg": tokens.semantic.fg_primary, "border": tokens.semantic.border_subtle, }, BadgeVariant.DARK: { "bg": tokens.primitive.gray_900, "fg": tokens.primitive.white, "border": tokens.primitive.gray_900, }, } colors = fallback_map.get(self._variant, fallback_map[BadgeVariant.DEFAULT]) # Filter out None values and return if we have all colors if colors["bg"] and colors["fg"] and colors["border"]: return colors except (AttributeError, TypeError, KeyError) as e: logger.debug(f"Failed to get fallback colors from theme: {e}") # Last resort: use minimal theme-aware colors return self._get_minimal_colors() def _get_minimal_colors(self) -> dict[str, str]: """Get minimal colors when theme is not available. Uses only palette colors from Qt, no hardcoded hex values. Returns: Dictionary with 'bg', 'fg', and 'border' colors """ palette = self.palette() # Use palette colors - no hardcoded values! if self._variant == BadgeVariant.LIGHT: return { "bg": palette.color(QPalette.ColorRole.Window).name(), "fg": palette.color(QPalette.ColorRole.WindowText).name(), "border": palette.color(QPalette.ColorRole.Mid).name(), } if self._variant == BadgeVariant.DARK: return { "bg": palette.color(QPalette.ColorRole.Shadow).name(), "fg": palette.color(QPalette.ColorRole.BrightText).name(), "border": palette.color(QPalette.ColorRole.Dark).name(), } # For all other variants, use button colors as a reasonable default return { "bg": palette.color(QPalette.ColorRole.Button).name(), "fg": palette.color(QPalette.ColorRole.ButtonText).name(), "border": palette.color(QPalette.ColorRole.Mid).name(), } def _apply_variant_style(self) -> None: """Apply styling based on the variant using theme colors only.""" colors = self._get_theme_colors() # Apply the style self.setStyleSheet(f""" QLabel {{ background-color: {colors["bg"]}; color: {colors["fg"]}; border: 1px solid {colors["border"]}; border-radius: 4px; padding: 4px 8px; font-weight: bold; }} """) @property def variant(self) -> BadgeVariant: """Get the current variant.""" return self._variant @variant.setter def variant(self, value: BadgeVariant | str) -> None: """Set the badge variant. Args: value: The variant to set """ if isinstance(value, str): try: value = BadgeVariant(value) except ValueError: value = BadgeVariant.DEFAULT self._variant = value self._apply_variant_style()
[docs] def set_count(self, count: int) -> None: """Set the badge to display a count. Args: count: The count to display """ if count > 999: self.setText("999+") elif count > 99: self.setText(f"{count}") else: self.setText(str(count))
[docs] def set_rounded(self, rounded: bool = True) -> None: """Set whether the badge should be fully rounded (pill-shaped). Args: rounded: Whether to make the badge pill-shaped """ if rounded: # Make it pill-shaped self.setStyleSheet( self.styleSheet().replace("border-radius: 4px", "border-radius: 12px") ) else: # Make it slightly rounded self.setStyleSheet( self.styleSheet().replace("border-radius: 12px", "border-radius: 4px") )
[docs] def refresh_style(self) -> None: """Refresh the badge style from the current theme. This should be called when the theme changes. """ self._apply_variant_style()
def _connect_theme_signals(self) -> None: """Connect to theme manager signals for automatic style updates.""" try: app = QApplication.instance() if app and hasattr(app, "theme_manager"): app.theme_manager.theme_changed.connect(self._on_theme_changed) except (AttributeError, RuntimeError) as e: logger.debug(f"Failed to connect theme signals: {e}") def _on_theme_changed(self, theme_name: str) -> None: """Handle theme change event. Args: theme_name: Name of the new theme """ self.refresh_style()