Coverage for src / ezqt_widgets / widgets / label / clickable_tag_label.py: 97.87%
123 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-31 10:03 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-31 10:03 +0000
1# ///////////////////////////////////////////////////////////////
2# CLICKABLE_TAG_LABEL - Clickable Tag Label Widget
3# Project: ezqt_widgets
4# ///////////////////////////////////////////////////////////////
6"""
7Clickable tag label widget module.
9Provides a tag-like clickable label with toggleable state for PySide6
10applications.
11"""
13from __future__ import annotations
15# ///////////////////////////////////////////////////////////////
16# IMPORTS
17# ///////////////////////////////////////////////////////////////
18# Standard library imports
19from typing import Any
21# Third-party imports
22from PySide6.QtCore import QSize, Qt, Signal
23from PySide6.QtGui import QFont, QKeyEvent, QMouseEvent
24from PySide6.QtWidgets import QFrame, QHBoxLayout, QLabel, QSizePolicy
26# Local imports
27from ...types import WidgetParent
29# ///////////////////////////////////////////////////////////////
30# CLASSES
31# ///////////////////////////////////////////////////////////////
34class ClickableTagLabel(QFrame):
35 """Tag-like clickable label with toggleable state.
37 Features:
38 - Clickable tag with enabled/disabled state
39 - Emits signals on click and state change
40 - Customizable text, font, min width/height
41 - Customizable status color (traditional name or hex)
42 - QSS-friendly (type/class/status properties)
43 - Automatic minimum size calculation
44 - Keyboard focus and accessibility
46 Args:
47 name: Text to display in the tag (default: "").
48 enabled: Initial state (default: False).
49 status_color: Color when selected (default: "#0078d4").
50 min_width: Minimum width (default: None, auto-calculated).
51 min_height: Minimum height (default: None, auto-calculated).
52 parent: Parent widget (default: None).
53 *args: Additional arguments passed to QFrame.
54 **kwargs: Additional keyword arguments passed to QFrame.
56 Signals:
57 clicked(): Emitted when the tag is clicked.
58 toggleKeyword(str): Emitted with the tag name when toggled.
59 stateChanged(bool): Emitted when the enabled state changes.
61 Example:
62 >>> from ezqt_widgets import ClickableTagLabel
63 >>> tag = ClickableTagLabel(name="Python", enabled=False, status_color="#0078d4")
64 >>> tag.stateChanged.connect(lambda state: print(f"Active: {state}"))
65 >>> tag.toggleKeyword.connect(lambda kw: print(f"Toggled: {kw}"))
66 >>> tag.show()
67 """
69 clicked = Signal()
70 toggleKeyword = Signal(str)
71 stateChanged = Signal(bool)
73 # ///////////////////////////////////////////////////////////////
74 # INIT
75 # ///////////////////////////////////////////////////////////////
77 def __init__(
78 self,
79 name: str = "",
80 enabled: bool = False,
81 status_color: str = "#0078d4",
82 min_width: int | None = None,
83 min_height: int | None = None,
84 parent: WidgetParent = None,
85 *args: Any,
86 **kwargs: Any,
87 ) -> None:
88 """Initialize the clickable tag label."""
89 super().__init__(parent, *args, **kwargs)
91 self.setProperty("type", "ClickableTagLabel")
93 # Initialize properties
94 self._name: str = name
95 self._enabled: bool = enabled
96 self._status_color: str = status_color
97 self._min_width: int | None = min_width
98 self._min_height: int | None = min_height
100 # Setup UI
101 self._setup_ui()
102 self._update_display()
104 # ------------------------------------------------
105 # PRIVATE METHODS
106 # ------------------------------------------------
108 def _setup_ui(self) -> None:
109 """Setup the user interface components."""
110 self.setFrameShape(QFrame.Shape.NoFrame)
111 self.setFrameShadow(QFrame.Shadow.Raised)
112 self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
113 self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
114 self.setCursor(Qt.CursorShape.PointingHandCursor)
115 self.setContentsMargins(4, 0, 4, 0)
116 self.setFixedHeight(20)
118 self._layout = QHBoxLayout(self)
119 self._layout.setObjectName("status_HLayout")
120 self._layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
121 self._layout.setContentsMargins(0, 0, 0, 0)
122 self._layout.setSpacing(12)
124 self._label = QLabel()
125 self._label.setObjectName("tag")
126 self._label.setFont(QFont("Segoe UI", 8))
127 self._label.setLineWidth(0)
128 self._label.setSizePolicy(
129 QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred
130 )
131 self._label.setAlignment(Qt.AlignmentFlag.AlignHCenter)
133 self._layout.addWidget(self._label, 0, Qt.AlignmentFlag.AlignTop)
135 if self._min_width:
136 self.setMinimumWidth(self._min_width)
137 if self._min_height:
138 self.setMinimumHeight(self._min_height)
140 def _update_display(self) -> None:
141 """Update the display based on current state."""
142 self._label.setText(self._name)
143 self.setObjectName(self._name)
145 if self._enabled:
146 self.setProperty("status", "selected")
147 self._label.setStyleSheet(
148 f"color: {self._status_color}; background-color: transparent; border: none;"
149 )
150 else:
151 self.setProperty("status", "unselected")
152 self._label.setStyleSheet(
153 "color: rgb(86, 86, 86); background-color: transparent; border: none;"
154 )
156 self.refreshStyle()
157 self.adjustSize()
159 # ///////////////////////////////////////////////////////////////
160 # PROPERTIES
161 # ///////////////////////////////////////////////////////////////
163 @property
164 def name(self) -> str:
165 """Get the tag text.
167 Returns:
168 The current tag text.
169 """
170 return self._name
172 @name.setter
173 def name(self, value: str) -> None:
174 """Set the tag text.
176 Args:
177 value: The new tag text.
178 """
179 self._name = str(value)
180 self._update_display()
181 self.updateGeometry()
183 @property
184 def enabled(self) -> bool:
185 """Get the enabled state.
187 Returns:
188 True if enabled, False otherwise.
189 """
190 return self._enabled
192 @enabled.setter
193 def enabled(self, value: bool) -> None:
194 """Set the enabled state.
196 Args:
197 value: The new enabled state.
198 """
199 if value != self._enabled: 199 ↛ exitline 199 didn't return from function 'enabled' because the condition on line 199 was always true
200 self._enabled = bool(value)
201 self._update_display()
202 self.stateChanged.emit(self._enabled)
204 @property
205 def status_color(self) -> str:
206 """Get the status color.
208 Returns:
209 The current status color.
210 """
211 return self._status_color
213 @status_color.setter
214 def status_color(self, value: str) -> None:
215 """Set the status color.
217 Args:
218 value: The new status color.
219 """
220 self._status_color = str(value)
221 if self._enabled:
222 self._label.setStyleSheet(
223 f"color: {value}; background-color: transparent; border: none;"
224 )
225 self.refreshStyle()
227 @property
228 def min_width(self) -> int | None:
229 """Get the minimum width.
231 Returns:
232 The minimum width, or None if not set.
233 """
234 return self._min_width
236 @min_width.setter
237 def min_width(self, value: int | None) -> None:
238 """Set the minimum width.
240 Args:
241 value: The minimum width, or None to auto-calculate.
242 """
243 self._min_width = value
244 if value:
245 self.setMinimumWidth(value)
246 self.updateGeometry()
248 @property
249 def min_height(self) -> int | None:
250 """Get the minimum height.
252 Returns:
253 The minimum height, or None if not set.
254 """
255 return self._min_height
257 @min_height.setter
258 def min_height(self, value: int | None) -> None:
259 """Set the minimum height.
261 Args:
262 value: The minimum height, or None to auto-calculate.
263 """
264 self._min_height = value
265 if value:
266 self.setMinimumHeight(value)
267 self.updateGeometry()
269 # ///////////////////////////////////////////////////////////////
270 # EVENT HANDLERS
271 # ///////////////////////////////////////////////////////////////
273 def mousePressEvent(self, event: QMouseEvent) -> None:
274 """Handle mouse press events.
276 Args:
277 event: The mouse event.
278 """
279 if event.button() == Qt.MouseButton.LeftButton:
280 self.enabled = not self.enabled
281 self.clicked.emit()
282 self.toggleKeyword.emit(self._name)
283 super().mousePressEvent(event)
285 def keyPressEvent(self, event: QKeyEvent) -> None:
286 """Handle key press events.
288 Args:
289 event: The key event.
290 """
291 if event.key() in [Qt.Key.Key_Space, Qt.Key.Key_Return, Qt.Key.Key_Enter]: 291 ↛ 296line 291 didn't jump to line 296 because the condition on line 291 was always true
292 self.enabled = not self.enabled
293 self.clicked.emit()
294 self.toggleKeyword.emit(self._name)
295 else:
296 super().keyPressEvent(event)
298 # ///////////////////////////////////////////////////////////////
299 # OVERRIDE METHODS
300 # ///////////////////////////////////////////////////////////////
302 def sizeHint(self) -> QSize:
303 """Return the recommended size for the widget.
305 Returns:
306 The recommended size.
307 """
308 return QSize(80, 24)
310 def minimumSizeHint(self) -> QSize:
311 """Return the minimum size for the widget.
313 Returns:
314 The minimum size hint.
315 """
316 font_metrics = self._label.fontMetrics()
317 text_width = font_metrics.horizontalAdvance(self._name)
318 min_width = self._min_width if self._min_width is not None else text_width + 16
319 min_height = (
320 self._min_height
321 if self._min_height is not None
322 else max(font_metrics.height() + 8, 20)
323 )
325 return QSize(min_width, min_height)
327 # ///////////////////////////////////////////////////////////////
328 # STYLE METHODS
329 # ///////////////////////////////////////////////////////////////
331 def refreshStyle(self) -> None:
332 """Refresh the widget style.
334 Useful after dynamic stylesheet changes.
335 """
336 self.style().unpolish(self)
337 self.style().polish(self)
338 self.update()
341# ///////////////////////////////////////////////////////////////
342# PUBLIC API
343# ///////////////////////////////////////////////////////////////
345__all__ = ["ClickableTagLabel"]