Skip to content

Miscellaneous Widgets

Utility widgets: animated circular timer, drag-and-drop list, option selector, theme-aware icon, toggleable icon, modern toggle switch, animated notification banner, and collapsible accordion section.


CircularTimer

A QWidget that draws an animated arc representing elapsed time. The arc grows clockwise from the 12 o'clock position.

Signals:

Signal Signature Emitted when
timerReset () resetTimer() is called
clicked () The widget is clicked
cycleCompleted () One full cycle ends (whether or not loop is True)

Constructor parameters:

Parameter Type Default Description
parent QWidget \| None None Parent widget
duration int 5000 Total cycle duration in milliseconds
ring_color QColor \| str "#0078d4" Color of the progress arc; supports hex, rgb(), rgba(), and named colors
node_color QColor \| str "#2d2d2d" Fill color of the inner circle
ring_width_mode "small" \| "medium" \| "large" "medium" Dynamic arc thickness preset
pen_width int \| float \| None None Explicit arc thickness; takes priority over ring_width_mode if set
loop bool False Whether the animation restarts automatically at the end of each cycle

Properties:

Property Type Description
duration int Gets or sets the cycle duration in milliseconds
elapsed int Gets or sets the elapsed time in milliseconds
running bool Read-only; True while the timer is active
ring_color QColor Gets or sets the arc color
node_color QColor Gets or sets the inner circle color
ring_width_mode str Gets or sets the thickness preset
pen_width float \| None Gets or sets an explicit arc thickness
loop bool Gets or sets whether the timer loops

Methods:

Method Signature Description
startTimer() () -> None Starts the animation from the current elapsed position
stopTimer() () -> None Stops the animation and resets elapsed to 0
resetTimer() () -> None Resets elapsed to 0 and emits timerReset without stopping
refreshStyle() () -> None Re-applies the QSS stylesheet

Example:

from PySide6.QtWidgets import QApplication
from ezqt_widgets import CircularTimer

app = QApplication([])

timer = CircularTimer(duration=10000, ring_color="#0078d4", loop=True)
timer.cycleCompleted.connect(lambda: print("cycle done"))
timer.clicked.connect(timer.resetTimer)
timer.startTimer()
timer.show()

app.exec()

CircularTimer

CircularTimer(parent: WidgetParent = None, duration: int = 5000, ring_color: ColorType = '#0078d4', node_color: ColorType = '#2d2d2d', ring_width_mode: Literal['small', 'medium', 'large'] = 'medium', pen_width: int | float | None = None, loop: bool = False, *args: Any, **kwargs: Any)

Bases: QWidget

Animated circular timer for indicating progress or elapsed time.

Features
  • Animated circular progress indicator
  • Customizable colors for ring and center
  • Configurable duration and loop mode
  • Click events for interaction
  • Smooth animation with configurable frame rate
PARAMETER DESCRIPTION
parent

Parent widget (default: None).

TYPE: WidgetParent DEFAULT: None

duration

Total animation duration in milliseconds (default: 5000).

TYPE: int DEFAULT: 5000

ring_color

Color of the progress arc (default: "#0078d4"). Supports: hex (#ff0000), rgb(255,0,0), rgba(255,0,0,0.5), names (red).

TYPE: ColorType DEFAULT: '#0078d4'

node_color

Color of the center (default: "#2d2d2d"). Supports: hex (#ffffff), rgb(255,255,255), rgba(255,255,255,0.8), names (white).

TYPE: ColorType DEFAULT: '#2d2d2d'

ring_width_mode

"small", "medium" (default), or "large". Controls the dynamic thickness of the arc.

TYPE: Literal['small', 'medium', 'large'] DEFAULT: 'medium'

pen_width

Thickness of the arc (takes priority over ring_width_mode if set).

TYPE: int | float | None DEFAULT: None

loop

If True, the timer loops automatically at each cycle (default: False).

TYPE: bool DEFAULT: False

*args

Additional arguments passed to QWidget.

TYPE: Any DEFAULT: ()

**kwargs

Additional keyword arguments passed to QWidget.

TYPE: Any DEFAULT: {}

Signals

timerReset(): Emitted when the timer is reset. clicked(): Emitted when the widget is clicked. cycleCompleted(): Emitted at each end of cycle (even if loop=False).

Example

from ezqt_widgets import CircularTimer timer = CircularTimer(duration=10000, ring_color="#0078d4", loop=True) timer.cycleCompleted.connect(lambda: print("cycle done")) timer.clicked.connect(timer.reset) timer.start() timer.show()

Initialize the circular timer.

Source code in src/ezqt_widgets/widgets/misc/circular_timer.py
def __init__(
    self,
    parent: WidgetParent = None,
    duration: int = 5000,
    ring_color: ColorType = "#0078d4",
    node_color: ColorType = "#2d2d2d",
    ring_width_mode: Literal["small", "medium", "large"] = "medium",
    pen_width: int | float | None = None,
    loop: bool = False,
    *args: Any,
    **kwargs: Any,
) -> None:
    """Initialize the circular timer."""
    super().__init__(parent, *args, **kwargs)
    self.setProperty("type", "CircularTimer")

    # Initialize properties
    self._duration: int = duration
    self._elapsed: int = 0
    self._running: bool = False
    self._ring_color: QColor = _parse_css_color(ring_color)
    self._node_color: QColor = _parse_css_color(node_color)
    self._ring_width_mode: str = ring_width_mode
    self._pen_width: float | None = pen_width
    self._loop: bool = bool(loop)
    self._last_update: float | None = None
    self._interval: int = 16  # ~60 FPS

    # Setup timer
    self._timer = QTimer(self)
    self._timer.timeout.connect(self._on_timer)

duration property writable

duration: int

Get the total duration.

RETURNS DESCRIPTION
int

The total duration in milliseconds.

elapsed property writable

elapsed: int

Get the elapsed time.

RETURNS DESCRIPTION
int

The elapsed time in milliseconds.

running property

running: bool

Get whether the timer is running.

RETURNS DESCRIPTION
bool

True if running, False otherwise.

ring_color property writable

ring_color: QColor

Get the ring color.

RETURNS DESCRIPTION
QColor

The current ring color.

node_color property writable

node_color: QColor

Get the node color.

RETURNS DESCRIPTION
QColor

The current node color.

ring_width_mode property writable

ring_width_mode: str

Get the ring width mode.

RETURNS DESCRIPTION
str

The current ring width mode ("small", "medium", or "large").

pen_width property writable

pen_width: float | None

Get the pen width.

RETURNS DESCRIPTION
float | None

The pen width, or None if using ring_width_mode.

loop property writable

loop: bool

Get whether the timer loops.

RETURNS DESCRIPTION
bool

True if looping, False otherwise.

mousePressEvent

mousePressEvent(_event: QMouseEvent) -> None

Handle mouse press events.

PARAMETER DESCRIPTION
_event

The mouse event (unused but required by signature).

TYPE: QMouseEvent

Source code in src/ezqt_widgets/widgets/misc/circular_timer.py
def mousePressEvent(self, _event: QMouseEvent) -> None:
    """Handle mouse press events.

    Args:
        _event: The mouse event (unused but required by signature).
    """
    self.clicked.emit()

startTimer

startTimer() -> None

Start the circular timer.

Source code in src/ezqt_widgets/widgets/misc/circular_timer.py
def startTimer(self) -> None:  # type: ignore[override]
    """Start the circular timer."""
    self.stopTimer()  # Always stop before starting
    self._running = True
    self._last_update = None
    self._timer.start(self._interval)

stopTimer

stopTimer() -> None

Stop the circular timer.

Source code in src/ezqt_widgets/widgets/misc/circular_timer.py
def stopTimer(self) -> None:
    """Stop the circular timer."""
    self.resetTimer()  # Always reset to zero
    self._running = False
    self._timer.stop()

resetTimer

resetTimer() -> None

Reset the circular timer.

Source code in src/ezqt_widgets/widgets/misc/circular_timer.py
def resetTimer(self) -> None:
    """Reset the circular timer."""
    self._elapsed = 0
    self._last_update = None
    self.timerReset.emit()
    self.update()

minimumSizeHint

minimumSizeHint() -> QSize

Get the recommended minimum size for the widget.

RETURNS DESCRIPTION
QSize

The minimum size hint.

Source code in src/ezqt_widgets/widgets/misc/circular_timer.py
def minimumSizeHint(self) -> QSize:
    """Get the recommended minimum size for the widget.

    Returns:
        The minimum size hint.
    """
    return QSize(24, 24)

paintEvent

paintEvent(_event: QPaintEvent) -> None

Draw the animated circular timer.

PARAMETER DESCRIPTION
_event

The paint event (unused but required by signature).

TYPE: QPaintEvent

Source code in src/ezqt_widgets/widgets/misc/circular_timer.py
def paintEvent(self, _event: QPaintEvent) -> None:
    """Draw the animated circular timer.

    Args:
        _event: The paint event (unused but required by signature).
    """
    painter = QPainter(self)
    painter.setRenderHint(QPainter.RenderHint.Antialiasing)
    size = min(self.width(), self.height())

    # Pen width (dynamic mode or fixed value)
    if self._pen_width is not None:
        pen_width = int(self._pen_width)
    else:
        if self._ring_width_mode == "small":
            pen_width = int(max(size * 0.12, 3))
        elif self._ring_width_mode == "large":
            pen_width = int(max(size * 0.28, 3))
        else:  # medium
            pen_width = int(max(size * 0.18, 3))

    # Node circle (precise centering)
    center = size / 2
    node_radius = (size - 2 * pen_width) / 2 - pen_width / 2
    if node_radius > 0:
        painter.setPen(Qt.PenStyle.NoPen)
        painter.setBrush(self._node_color)
        painter.drawEllipse(
            int(center - node_radius),
            int(center - node_radius),
            int(2 * node_radius),
            int(2 * node_radius),
        )

    # Ring arc (clockwise, starting at 12 o'clock)
    painter.setPen(
        QPen(
            self._ring_color,
            pen_width,
            Qt.PenStyle.SolidLine,
            Qt.PenCapStyle.RoundCap,
        )
    )
    angle = int((self._elapsed / self._duration) * 360 * 16)
    painter.drawArc(
        pen_width,
        pen_width,
        int(size - 2 * pen_width),
        int(size - 2 * pen_width),
        90 * 16,
        -angle,  # clockwise
    )

refreshStyle

refreshStyle() -> None

Refresh the widget's style.

Useful after dynamic stylesheet changes.

Source code in src/ezqt_widgets/widgets/misc/circular_timer.py
def refreshStyle(self) -> None:
    """Refresh the widget's style.

    Useful after dynamic stylesheet changes.
    """
    self.style().unpolish(self)
    self.style().polish(self)
    self.update()

DraggableItem

A QFrame representing a single item in a DraggableList. It embeds a HoverLabel whose hover icon triggers removal.

Signals:

Signal Signature Emitted when
itemClicked (str) The item is clicked; the string is item_id
itemRemoved (str) The removal icon is clicked; the string is item_id

Constructor parameters:

Parameter Type Default Description
item_id str Unique identifier for this item (required)
text str Text displayed in the item (required)
parent QWidget \| None None Parent widget
icon QIcon \| QPixmap \| str \| None None Icon for the HoverLabel; defaults to an icons8 drag icon
compact bool False Compact display mode (reduced height: 24–32 px vs 40–60 px)

Properties:

Property Type Description
icon_color str Gets or sets the icon color of the internal HoverLabel
compact bool Gets or sets compact mode; adjusts height constraints

Methods:

Method Signature Description
refreshStyle() () -> None Re-applies the QSS stylesheet

Usage with DraggableList

DraggableItem is designed to be managed by DraggableList. Use DraggableList.addItem() rather than instantiating DraggableItem directly in most cases.

DraggableItem

DraggableItem(item_id: str, text: str, parent: WidgetParent = None, icon: IconSourceExtended = None, compact: bool = False, **kwargs: Any)

Bases: QFrame

Draggable item widget for DraggableList.

This item can be moved by drag & drop and always contains a HoverLabel for a consistent interface.

PARAMETER DESCRIPTION
item_id

Unique identifier for the item.

TYPE: str

text

Text to display in the item.

TYPE: str

parent

Parent widget (default: None).

TYPE: WidgetParent DEFAULT: None

icon

Icon for the item (default: None, uses default icon).

TYPE: IconSourceExtended DEFAULT: None

compact

Whether to display in compact mode (default: False).

TYPE: bool DEFAULT: False

**kwargs

Additional keyword arguments passed to HoverLabel.

TYPE: Any DEFAULT: {}

Signals

itemClicked(str): Emitted when the item is clicked. itemRemoved(str): Emitted when the item is removed.

Example

from ezqt_widgets import DraggableItem item = DraggableItem(item_id="item-1", text="First item") item.itemClicked.connect(lambda id: print(f"Clicked: {id}")) item.itemRemoved.connect(lambda id: print(f"Removed: {id}")) item.show()

Initialize the draggable item.

Source code in src/ezqt_widgets/widgets/misc/draggable_list.py
def __init__(
    self,
    item_id: str,
    text: str,
    parent: WidgetParent = None,
    icon: IconSourceExtended = None,
    compact: bool = False,
    **kwargs: Any,
) -> None:
    """Initialize the draggable item."""
    super().__init__(parent)
    self.setProperty("type", "DraggableItem")

    # Initialize attributes
    self._item_id = item_id
    self._text = text
    self._is_dragging = False
    self._drag_start_pos = QPoint()
    self._compact = compact

    # Configure widget
    self.setFrameShape(QFrame.Shape.Box)
    self.setLineWidth(1)
    self.setMidLineWidth(0)
    self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)

    # Height based on compact mode
    if self._compact:
        self.setMinimumHeight(24)
        self.setMaximumHeight(32)
    else:
        self.setMinimumHeight(40)
        self.setMaximumHeight(60)

    # Main layout
    layout = QHBoxLayout(self)
    if self._compact:
        layout.setContentsMargins(6, 2, 6, 2)  # Reduced margins in compact mode
    else:
        layout.setContentsMargins(8, 4, 8, 4)  # Normal margins
    layout.setSpacing(8)

    # Default icon for drag & drop if no icon is provided
    if icon is None:
        icon = "https://img.icons8.com/?size=100&id=8329&format=png&color=000000"

    # Content widget (HoverLabel with removal icon)
    icon_size = QSize(16, 16) if self._compact else QSize(20, 20)
    icon_padding = 2 if self._compact else 4

    self._content_widget = HoverLabel(
        text=text,
        icon=icon,  # Trash icon for removal
        icon_size=icon_size,
        icon_padding=icon_padding,
        **kwargs,
    )
    self._content_widget.hoverIconClicked.connect(self._on_remove_clicked)

    # Icon color property
    self._icon_color = "grey"
    # Apply initial color
    self._content_widget.icon_color = self._icon_color

    # Add widget to layout (takes full width)
    layout.addWidget(self._content_widget)

item_id property

item_id: str

Get the item identifier.

RETURNS DESCRIPTION
str

The unique identifier of the item.

text property

text: str

Get the item text.

RETURNS DESCRIPTION
str

The display text of the item.

content_widget property

content_widget: HoverLabel

Get the inner HoverLabel widget.

RETURNS DESCRIPTION
HoverLabel

The HoverLabel used for display and interaction.

icon_color property writable

icon_color: str

Get the icon color of the HoverLabel.

RETURNS DESCRIPTION
str

The current icon color.

compact property writable

compact: bool

Get the compact mode.

RETURNS DESCRIPTION
bool

True if compact mode is enabled, False otherwise.

mousePressEvent

mousePressEvent(event: QMouseEvent) -> None

Handle mouse press events for drag start.

PARAMETER DESCRIPTION
event

The mouse event.

TYPE: QMouseEvent

Source code in src/ezqt_widgets/widgets/misc/draggable_list.py
def mousePressEvent(self, event: QMouseEvent) -> None:
    """Handle mouse press events for drag start.

    Args:
        event: The mouse event.
    """
    if event.button() == Qt.MouseButton.LeftButton:
        self._drag_start_pos = event.position().toPoint()
    super().mousePressEvent(event)

mouseMoveEvent

mouseMoveEvent(event: QMouseEvent) -> None

Handle mouse movement for drag & drop.

PARAMETER DESCRIPTION
event

The mouse event.

TYPE: QMouseEvent

Source code in src/ezqt_widgets/widgets/misc/draggable_list.py
def mouseMoveEvent(self, event: QMouseEvent) -> None:
    """Handle mouse movement for drag & drop.

    Args:
        event: The mouse event.
    """
    if not (event.buttons() & Qt.MouseButton.LeftButton):
        return

    if not self._is_dragging:
        if (
            event.position().toPoint() - self._drag_start_pos
        ).manhattanLength() < 10:
            return

        self._is_dragging = True
        self.setProperty("dragging", True)
        self.style().unpolish(self)
        self.style().polish(self)

        # Create drag
        drag = QDrag(self)
        mime_data = QMimeData()
        mime_data.setText(self._item_id)
        drag.setMimeData(mime_data)

        # Execute drag
        drag.exec(Qt.DropAction.MoveAction)

        # Cleanup after drag
        self._is_dragging = False
        self.setProperty("dragging", False)
        self.style().unpolish(self)
        self.style().polish(self)

mouseReleaseEvent

mouseReleaseEvent(event: QMouseEvent) -> None

Handle mouse release events for drag end.

PARAMETER DESCRIPTION
event

The mouse event.

TYPE: QMouseEvent

Source code in src/ezqt_widgets/widgets/misc/draggable_list.py
def mouseReleaseEvent(self, event: QMouseEvent) -> None:
    """Handle mouse release events for drag end.

    Args:
        event: The mouse event.
    """
    self._is_dragging = False
    self.setProperty("dragging", False)
    self.style().unpolish(self)
    self.style().polish(self)
    super().mouseReleaseEvent(event)

sizeHint

sizeHint() -> QSize

Get the recommended size for the widget based on content.

RETURNS DESCRIPTION
QSize

The recommended size.

Source code in src/ezqt_widgets/widgets/misc/draggable_list.py
def sizeHint(self) -> QSize:
    """Get the recommended size for the widget based on content.

    Returns:
        The recommended size.
    """
    # Get suggested size from HoverLabel
    content_size = self._content_widget.sizeHint()

    # Add layout margins and padding
    layout = self.layout()
    if layout is None:
        return QSize(content_size.width(), content_size.height())
    layout_margins = layout.contentsMargins()

    # Calculate total width
    total_width = (
        content_size.width() + layout_margins.left() + layout_margins.right()
    )

    # Calculate total height based on compact mode
    if self._compact:
        min_height = max(
            24,
            content_size.height() + layout_margins.top() + layout_margins.bottom(),
        )
        max_height = 32
    else:
        min_height = max(
            40,
            content_size.height() + layout_margins.top() + layout_margins.bottom(),
        )
        max_height = 60

    return QSize(total_width, min(min_height, max_height))

minimumSizeHint

minimumSizeHint() -> QSize

Get the minimum size for the widget.

RETURNS DESCRIPTION
QSize

The minimum size hint.

Source code in src/ezqt_widgets/widgets/misc/draggable_list.py
def minimumSizeHint(self) -> QSize:
    """Get the minimum size for the widget.

    Returns:
        The minimum size hint.
    """
    # Get minimum size from HoverLabel
    content_min_size = self._content_widget.minimumSizeHint()

    # Add layout margins
    layout = self.layout()
    if layout is None:
        return QSize(content_min_size.width(), content_min_size.height())
    layout_margins = layout.contentsMargins()

    # Minimum width based on content + margins
    min_width = (
        content_min_size.width() + layout_margins.left() + layout_margins.right()
    )

    # Minimum height based on compact mode
    min_height = 24 if self._compact else 40

    return QSize(min_width, min_height)

refreshStyle

refreshStyle() -> None

Refresh the widget's style.

Useful after dynamic stylesheet changes.

Source code in src/ezqt_widgets/widgets/misc/draggable_list.py
def refreshStyle(self) -> None:
    """Refresh the widget's style.

    Useful after dynamic stylesheet changes.
    """
    self.style().unpolish(self)
    self.style().polish(self)
    self.update()

DraggableList

A QWidget containing a scrollable list of DraggableItem instances that can be reordered by drag-and-drop and removed individually.

Signals:

Signal Signature Emitted when
itemMoved (str, int, int) An item is dropped at a new position; args are item_id, old_position, new_position
itemRemoved (str, int) An item is removed; args are item_id, position
itemAdded (str, int) An item is added; args are item_id, position
itemClicked (str) An item is clicked; the string is item_id
orderChanged (list) The item order changes; the list is the new ordered item_id list

Constructor parameters:

Parameter Type Default Description
parent QWidget \| None None Parent widget
items list[str] \| None None Initial list of item identifiers
allow_drag_drop bool True Whether drag-and-drop reordering is permitted
allow_remove bool True Whether items can be removed via the hover icon
max_height int 300 Maximum widget height in pixels
min_width int 150 Minimum widget width in pixels
compact bool False Compact display mode for all items

Properties:

Property Type Description
items list[str] Gets or sets the full item list (returns a copy); setting rebuilds all item widgets
item_count int Read-only; number of items currently in the list
allow_drag_drop bool Gets or sets whether drag-and-drop is allowed
allow_remove bool Gets or sets whether item removal is allowed; updates all existing items
icon_color str Gets or sets the icon color for all items
compact bool Gets or sets compact mode for all items
min_width int Gets or sets the minimum widget width

Methods:

Method Signature Description
addItem() (item_id: str, text: str \| None) -> None Adds an item; text defaults to item_id if None; no-op if item_id already exists
removeItem() (item_id: str) -> bool Removes the item with the given id; returns True if found and removed
clearItems() () -> None Removes all items and emits orderChanged([])
moveItem() (item_id: str, new_position: int) -> bool Moves an item to a new 0-based position; returns True on success
getItemPosition() (item_id: str) -> int Returns the 0-based position of the item, or -1 if not found
refreshStyle() () -> None Re-applies the QSS stylesheet

Example:

from PySide6.QtWidgets import QApplication
from ezqt_widgets import DraggableList

app = QApplication([])

task_list = DraggableList(
    items=["design", "implement", "test", "deploy"],
    allow_drag_drop=True,
    allow_remove=True,
    compact=False,
)
task_list.itemMoved.connect(
    lambda id, old, new: print(f"Moved '{id}' from {old} to {new}")
)
task_list.itemRemoved.connect(
    lambda id, pos: print(f"Removed '{id}' at position {pos}")
)
task_list.show()

app.exec()

DraggableList

DraggableList(parent: WidgetParent = None, items: list[str] | None = None, allow_drag_drop: bool = True, allow_remove: bool = True, max_height: int = 300, min_width: int = 150, compact: bool = False, *args: Any, **kwargs: Any)

Bases: QWidget

List widget with reorderable items via drag & drop and removal.

This widget allows managing a list of items that users can reorder by drag & drop and remove individually.

Features
  • List of items reorderable by drag & drop
  • Item removal via HoverLabel (red icon on hover)
  • Consistent interface with HoverLabel for all items
  • Signals for reordering and removal events
  • Smooth and intuitive interface
  • Appearance customization
  • Automatic item order management
  • Integrated removal icon in HoverLabel
Use cases
  • Reorderable task list
  • Option selector with customizable order
  • File management interface
  • Priority-ordered element configuration
PARAMETER DESCRIPTION
parent

The parent widget (default: None).

TYPE: WidgetParent DEFAULT: None

items

Initial list of items (default: []).

TYPE: list[str] | None DEFAULT: None

allow_drag_drop

Allow drag & drop for reordering (default: True).

TYPE: bool DEFAULT: True

allow_remove

Allow item removal via HoverLabel (default: True).

TYPE: bool DEFAULT: True

max_height

Maximum height of the widget (default: 300).

TYPE: int DEFAULT: 300

min_width

Minimum width of the widget (default: 150).

TYPE: int DEFAULT: 150

compact

Display items in compact mode (reduced height) (default: False).

TYPE: bool DEFAULT: False

*args

Additional arguments passed to item widgets.

TYPE: Any DEFAULT: ()

**kwargs

Additional keyword arguments passed to item widgets.

TYPE: Any DEFAULT: {}

Signals

itemMoved(str, int, int): Emitted when an item is moved (item_id, old_position, new_position). itemRemoved(str, int): Emitted when an item is removed (item_id, position). itemAdded(str, int): Emitted when an item is added (item_id, position). itemClicked(str): Emitted when an item is clicked (item_id). orderChanged(list): Emitted when the item order changes (new ordered list).

Example

draggable_list = DraggableList( ... items=["Item 1", "Item 2", "Item 3"], ... icon="https://img.icons8.com/?size=100&id=8329&format=png&color=000000" ... ) draggable_list.itemMoved.connect( ... lambda item_id, old_pos, new_pos: print(f"Moved {item_id} from {old_pos} to {new_pos}") ... ) draggable_list.itemRemoved.connect( ... lambda item_id, pos: print(f"Removed {item_id} at {pos}") ... )

Initialize the draggable list.

Source code in src/ezqt_widgets/widgets/misc/draggable_list.py
def __init__(
    self,
    parent: WidgetParent = None,
    items: list[str] | None = None,
    allow_drag_drop: bool = True,
    allow_remove: bool = True,
    max_height: int = 300,
    min_width: int = 150,
    compact: bool = False,
    *args: Any,  # noqa: ARG002
    **kwargs: Any,
) -> None:
    """Initialize the draggable list."""
    super().__init__(parent)
    self.setProperty("type", "DraggableList")

    # Initialize attributes
    self._items: list[str] = items or []
    self._allow_drag_drop: bool = allow_drag_drop
    self._allow_remove: bool = allow_remove
    self._max_height: int = max_height
    self._min_width: int = min_width
    self._compact: bool = compact
    self._item_widgets: dict[str, DraggableItem] = {}
    self._kwargs = kwargs
    self._icon_color = "grey"  # Default icon color

    # Configure widget
    self.setAcceptDrops(True)
    self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
    self.setMinimumWidth(min_width)
    self.setMaximumHeight(max_height)

    # Main layout
    layout = QVBoxLayout(self)
    layout.setContentsMargins(8, 8, 8, 8)
    layout.setSpacing(4)

    # Scroll area
    self._scroll_area = QScrollArea()
    self._scroll_area.setWidgetResizable(True)
    self._scroll_area.setHorizontalScrollBarPolicy(
        Qt.ScrollBarPolicy.ScrollBarAlwaysOff
    )
    self._scroll_area.setVerticalScrollBarPolicy(
        Qt.ScrollBarPolicy.ScrollBarAsNeeded
    )
    self._scroll_area.setFrameShape(QFrame.Shape.NoFrame)

    # Container widget for items
    self._container_widget = QWidget()
    self._container_layout = QVBoxLayout(self._container_widget)
    self._container_layout.setContentsMargins(0, 0, 0, 0)
    self._container_layout.setSpacing(4)
    self._container_layout.addStretch()  # Flexible space at the end

    self._scroll_area.setWidget(self._container_widget)
    layout.addWidget(self._scroll_area)

    # Initialize items
    self._create_items()

items property writable

items: list[str]

Get the list of items.

RETURNS DESCRIPTION
list[str]

A copy of the current items list.

item_count property

item_count: int

Get the number of items in the list.

RETURNS DESCRIPTION
int

The number of items (read-only).

allow_drag_drop property writable

allow_drag_drop: bool

Get whether drag & drop is allowed.

RETURNS DESCRIPTION
bool

True if drag & drop is allowed, False otherwise.

allow_remove property writable

allow_remove: bool

Get whether item removal is allowed.

RETURNS DESCRIPTION
bool

True if removal is allowed, False otherwise.

icon_color property writable

icon_color: str

Get the icon color of the items.

RETURNS DESCRIPTION
str

The current icon color.

compact property writable

compact: bool

Get the compact mode.

RETURNS DESCRIPTION
bool

True if compact mode is enabled, False otherwise.

min_width property writable

min_width: int

Get the minimum width of the widget.

RETURNS DESCRIPTION
int

The minimum width.

addItem

addItem(item_id: str, text: str | None = None) -> None

Add an item to the list.

PARAMETER DESCRIPTION
item_id

Unique identifier for the item.

TYPE: str

text

Text to display (uses item_id if None).

TYPE: str | None DEFAULT: None

Source code in src/ezqt_widgets/widgets/misc/draggable_list.py
def addItem(self, item_id: str, text: str | None = None) -> None:
    """Add an item to the list.

    Args:
        item_id: Unique identifier for the item.
        text: Text to display (uses item_id if None).
    """
    if item_id in self._items:
        return  # Item already present

    text = text or item_id
    self._items.append(item_id)

    # Create widget
    item_widget = DraggableItem(
        item_id=item_id, text=text, compact=self._compact, **self._kwargs
    )

    # Connect signals
    item_widget.itemRemoved.connect(self._on_item_removed)

    # Hide removal icon if necessary
    if not self._allow_remove:
        item_widget.content_widget.icon_enabled = False

    # Add to layout (before stretch)
    self._container_layout.insertWidget(len(self._items) - 1, item_widget)
    self._item_widgets[item_id] = item_widget

    # Emit signal
    self.itemAdded.emit(item_id, len(self._items) - 1)
    self.orderChanged.emit(self._items.copy())

removeItem

removeItem(item_id: str) -> bool

Remove an item from the list.

PARAMETER DESCRIPTION
item_id

Identifier of the item to remove.

TYPE: str

RETURNS DESCRIPTION
bool

True if the item was removed, False otherwise.

Source code in src/ezqt_widgets/widgets/misc/draggable_list.py
def removeItem(self, item_id: str) -> bool:
    """Remove an item from the list.

    Args:
        item_id: Identifier of the item to remove.

    Returns:
        True if the item was removed, False otherwise.
    """
    if item_id not in self._items:
        return False

    # Remove from list
    position = self._items.index(item_id)
    self._items.remove(item_id)

    # Remove widget
    if item_id in self._item_widgets:
        widget = self._item_widgets[item_id]
        self._container_layout.removeWidget(widget)
        widget.deleteLater()
        del self._item_widgets[item_id]

    # Emit signals
    self.itemRemoved.emit(item_id, position)
    self.orderChanged.emit(self._items.copy())

    return True

clearItems

clearItems() -> None

Remove all items from the list.

Source code in src/ezqt_widgets/widgets/misc/draggable_list.py
def clearItems(self) -> None:
    """Remove all items from the list."""
    # Clean up widgets
    for widget in self._item_widgets.values():
        self._container_layout.removeWidget(widget)
        widget.deleteLater()
    self._item_widgets.clear()

    # Clear list
    self._items.clear()

    # Emit signal
    self.orderChanged.emit([])

moveItem

moveItem(item_id: str, new_position: int) -> bool

Move an item to a new position.

PARAMETER DESCRIPTION
item_id

Identifier of the item to move.

TYPE: str

new_position

New position (0-based).

TYPE: int

RETURNS DESCRIPTION
bool

True if the item was moved, False otherwise.

Source code in src/ezqt_widgets/widgets/misc/draggable_list.py
def moveItem(self, item_id: str, new_position: int) -> bool:
    """Move an item to a new position.

    Args:
        item_id: Identifier of the item to move.
        new_position: New position (0-based).

    Returns:
        True if the item was moved, False otherwise.
    """
    if item_id not in self._items:
        return False

    old_position = self._items.index(item_id)
    if old_position == new_position:
        return True

    # Move in list
    self._items.pop(old_position)
    self._items.insert(new_position, item_id)

    # Move widget
    if item_id in self._item_widgets:
        widget = self._item_widgets[item_id]
        self._container_layout.removeWidget(widget)
        self._container_layout.insertWidget(new_position, widget)

    # Emit signals
    self.itemMoved.emit(item_id, old_position, new_position)
    self.orderChanged.emit(self._items.copy())

    return True

getItemPosition

getItemPosition(item_id: str) -> int

Get the position of an item.

PARAMETER DESCRIPTION
item_id

Identifier of the item.

TYPE: str

RETURNS DESCRIPTION
int

Position of the item (-1 if not found).

Source code in src/ezqt_widgets/widgets/misc/draggable_list.py
def getItemPosition(self, item_id: str) -> int:
    """Get the position of an item.

    Args:
        item_id: Identifier of the item.

    Returns:
        Position of the item (-1 if not found).
    """
    try:
        return self._items.index(item_id)
    except ValueError:
        return -1

dragEnterEvent

dragEnterEvent(event: QDragEnterEvent) -> None

Handle drag enter events.

PARAMETER DESCRIPTION
event

The drag enter event.

TYPE: QDragEnterEvent

Source code in src/ezqt_widgets/widgets/misc/draggable_list.py
def dragEnterEvent(self, event: QDragEnterEvent) -> None:
    """Handle drag enter events.

    Args:
        event: The drag enter event.
    """
    if self._allow_drag_drop and event.mimeData().hasText():
        event.acceptProposedAction()

dragMoveEvent

dragMoveEvent(event: QDragMoveEvent) -> None

Handle drag move events.

PARAMETER DESCRIPTION
event

The drag move event.

TYPE: QDragMoveEvent

Source code in src/ezqt_widgets/widgets/misc/draggable_list.py
def dragMoveEvent(self, event: QDragMoveEvent) -> None:
    """Handle drag move events.

    Args:
        event: The drag move event.
    """
    if self._allow_drag_drop and event.mimeData().hasText():
        event.acceptProposedAction()

dropEvent

dropEvent(event: QDropEvent) -> None

Handle drop events.

PARAMETER DESCRIPTION
event

The drop event.

TYPE: QDropEvent

Source code in src/ezqt_widgets/widgets/misc/draggable_list.py
def dropEvent(self, event: QDropEvent) -> None:
    """Handle drop events.

    Args:
        event: The drop event.
    """
    if not self._allow_drag_drop:
        return

    item_id = event.mimeData().text()
    if item_id not in self._items:
        return

    # Calculate new position
    drop_pos = event.position().toPoint()
    new_position = self._calculate_drop_position(drop_pos)

    # Move item
    self.moveItem(item_id, new_position)

    event.acceptProposedAction()

sizeHint

sizeHint() -> QSize

Get the recommended size for the widget based on content.

RETURNS DESCRIPTION
QSize

The recommended size.

Source code in src/ezqt_widgets/widgets/misc/draggable_list.py
def sizeHint(self) -> QSize:
    """Get the recommended size for the widget based on content.

    Returns:
        The recommended size.
    """
    # Calculate maximum width of items
    max_item_width = 0

    if self._item_widgets:
        # Get maximum width of existing items
        item_widths = [
            widget.sizeHint().width() for widget in self._item_widgets.values()
        ]
        max_item_width = max(item_widths) if item_widths else 0

    # Use minimum width only if necessary
    if max_item_width < self._min_width:
        max_item_width = self._min_width

    # Add main widget margins
    margins = self.contentsMargins()
    total_width = max_item_width + margins.left() + margins.right()

    # Calculate height based on number of items
    item_height = 50  # Approximate item height
    spacing = 4  # Spacing between items
    total_items_height = len(self._item_widgets) * (item_height + spacing)

    # Add margins and limit to maximum height
    total_height = min(
        total_items_height + margins.top() + margins.bottom(), self._max_height
    )

    return QSize(total_width, max(200, total_height))

minimumSizeHint

minimumSizeHint() -> QSize

Get the minimum size for the widget.

RETURNS DESCRIPTION
QSize

The minimum size hint.

Source code in src/ezqt_widgets/widgets/misc/draggable_list.py
def minimumSizeHint(self) -> QSize:
    """Get the minimum size for the widget.

    Returns:
        The minimum size hint.
    """
    # Minimum width based on items or configured minimum width
    min_width = 0

    if self._item_widgets:
        # Get minimum width of existing items
        item_min_widths = [
            widget.minimumSizeHint().width()
            for widget in self._item_widgets.values()
        ]
        min_width = max(item_min_widths) if item_min_widths else 0

    # Use minimum width only if necessary
    if min_width < self._min_width:
        min_width = self._min_width

    # Add margins
    margins = self.contentsMargins()
    total_width = min_width + margins.left() + margins.right()

    # Minimum height based on at least one item
    item_min_height = 40  # Minimum item height
    spacing = 4  # Spacing
    min_height = item_min_height + spacing + margins.top() + margins.bottom()

    return QSize(total_width, min_height)

refreshStyle

refreshStyle() -> None

Refresh the widget's style.

Useful after dynamic stylesheet changes.

Source code in src/ezqt_widgets/widgets/misc/draggable_list.py
def refreshStyle(self) -> None:
    """Refresh the widget's style.

    Useful after dynamic stylesheet changes.
    """
    self.style().unpolish(self)
    self.style().polish(self)
    self.update()

OptionSelector

A QFrame displaying a horizontal or vertical row of text options with an animated selector rectangle that slides to the chosen option.

Signals:

Signal Signature Emitted when
clicked () Any option is clicked
valueChanged (str) The selected option text changes
valueIdChanged (int) The selected option index changes

Constructor parameters:

Parameter Type Default Description
items list[str] List of option texts to display (required)
default_id int 0 Index of the initially selected option
min_width int \| None None Minimum width
min_height int \| None None Minimum height
orientation str "horizontal" Layout: "horizontal" or "vertical"
animation_duration int 300 Selector slide animation duration in milliseconds
parent QWidget \| None None Parent widget

Properties:

Property Type Description
value str Gets or sets the selected option by text
value_id int Gets or sets the selected option by index
options list[str] Read-only; returns a copy of the options list
default_id int Gets or sets the default selection index
selected_option FramedLabel \| None Read-only; the currently selected option widget
orientation str Gets or sets the layout orientation
min_width int \| None Gets or sets minimum width
min_height int \| None Gets or sets minimum height
animation_duration int Gets or sets the animation duration in milliseconds

Methods:

Method Signature Description
initializeSelector() (default_id: int) -> None Positions the selector at the given index without animation
addOption() (option_id: int, option_text: str) -> None Adds a new option at the given id position
toggleSelection() (option_id: int) -> None Selects an option by id and animates the selector
moveSelector() (option: FramedLabel) -> None Animates the selector to the given option widget
refreshStyle() () -> None Re-applies the QSS stylesheet

Example:

from PySide6.QtWidgets import QApplication
from ezqt_widgets import OptionSelector

app = QApplication([])

selector = OptionSelector(
    items=["Day", "Week", "Month", "Year"],
    default_id=0,
    orientation="horizontal",
    animation_duration=250,
)
selector.valueChanged.connect(lambda v: print(f"Selected: {v}"))
selector.show()

app.exec()

OptionSelector

OptionSelector(items: list[str], default_id: int = 0, min_width: int | None = None, min_height: int | None = None, orientation: str = 'horizontal', animation_duration: int = 300, parent: WidgetParent = None, *args: Any, **kwargs: Any)

Bases: QFrame

Option selector widget with animated selector.

Features
  • Multiple selectable options displayed as labels
  • Animated selector that moves between options
  • Single selection mode (radio behavior)
  • Configurable default selection by ID (index)
  • Smooth animations with easing curves
  • Click events for option selection
  • Uses IDs internally for robust value handling
PARAMETER DESCRIPTION
items

List of option texts to display.

TYPE: list[str]

default_id

Default selected option ID (index) (default: 0).

TYPE: int DEFAULT: 0

min_width

Minimum width constraint for the widget (default: None).

TYPE: int | None DEFAULT: None

min_height

Minimum height constraint for the widget (default: None).

TYPE: int | None DEFAULT: None

orientation

Layout orientation: "horizontal" or "vertical" (default: "horizontal").

TYPE: str DEFAULT: 'horizontal'

animation_duration

Duration of the selector animation in milliseconds (default: 300).

TYPE: int DEFAULT: 300

parent

The parent widget (default: None).

TYPE: WidgetParent DEFAULT: None

*args

Additional arguments passed to QFrame.

TYPE: Any DEFAULT: ()

**kwargs

Additional keyword arguments passed to QFrame.

TYPE: Any DEFAULT: {}

Signals

clicked(): Emitted when an option is clicked. valueChanged(str): Emitted when the selected value changes. valueIdChanged(int): Emitted when the selected value ID changes.

Example

from ezqt_widgets import OptionSelector selector = OptionSelector(items=["Day", "Week", "Month"], default_id=0) selector.valueChanged.connect(lambda v: print(f"Selected: {v}")) selector.show()

Initialize the option selector.

Source code in src/ezqt_widgets/widgets/misc/option_selector.py
def __init__(
    self,
    items: list[str],
    default_id: int = 0,
    min_width: int | None = None,
    min_height: int | None = None,
    orientation: str = "horizontal",
    animation_duration: int = 300,
    parent: WidgetParent = None,
    *args: Any,
    **kwargs: Any,
) -> None:
    """Initialize the option selector."""
    super().__init__(parent, *args, **kwargs)
    self.setProperty("type", "OptionSelector")
    self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)

    # Initialize variables
    self._value_id = 0
    self._options_list = items
    self._default_id = default_id
    self._options: dict[int, FramedLabel] = {}
    self._selector_animation: QPropertyAnimation | None = None
    self._min_width = min_width
    self._min_height = min_height
    self._orientation = orientation.lower()
    self._animation_duration = animation_duration

    # Setup grid layout
    self._grid = QGridLayout(self)
    self._grid.setObjectName("grid")
    self._grid.setSpacing(4)
    self._grid.setContentsMargins(4, 4, 4, 4)
    self._grid.setAlignment(Qt.AlignmentFlag.AlignCenter)

    # Create selector
    self._selector = QFrame(self)
    self._selector.setObjectName("selector")
    self._selector.setProperty("type", "OptionSelector_Selector")

    # Add options
    for i, option_text in enumerate(self._options_list):
        self.addOption(option_id=i, option_text=option_text)

    # Initialize selector
    if self._options_list:
        self.initializeSelector(self._default_id)

value property writable

value: str

Get or set the currently selected option text.

RETURNS DESCRIPTION
str

The currently selected option text, or empty string if none.

value_id property writable

value_id: int

Get or set the currently selected option ID.

RETURNS DESCRIPTION
int

The currently selected option ID.

options property

options: list[str]

Get the list of available options.

RETURNS DESCRIPTION
list[str]

A copy of the options list.

default_id property writable

default_id: int

Get or set the default option ID.

RETURNS DESCRIPTION
int

The default option ID.

selected_option property

selected_option: FramedLabel | None

Get the currently selected option widget.

RETURNS DESCRIPTION
FramedLabel | None

The selected option widget, or None if none selected.

orientation property writable

orientation: str

Get or set the orientation of the selector.

RETURNS DESCRIPTION
str

The current orientation ("horizontal" or "vertical").

min_width property writable

min_width: int | None

Get or set the minimum width of the widget.

RETURNS DESCRIPTION
int | None

The minimum width, or None if not set.

min_height property writable

min_height: int | None

Get or set the minimum height of the widget.

RETURNS DESCRIPTION
int | None

The minimum height, or None if not set.

animation_duration property writable

animation_duration: int

Get or set the animation duration in milliseconds.

RETURNS DESCRIPTION
int

The animation duration in milliseconds.

initializeSelector

initializeSelector(default_id: int = 0) -> None

Initialize the selector with default position.

PARAMETER DESCRIPTION
default_id

The default option ID to select.

TYPE: int DEFAULT: 0

Source code in src/ezqt_widgets/widgets/misc/option_selector.py
def initializeSelector(self, default_id: int = 0) -> None:
    """Initialize the selector with default position.

    Args:
        default_id: The default option ID to select.
    """
    if 0 <= default_id < len(self._options_list):
        self._default_id = default_id
        selected_option = self._options.get(default_id)

        if selected_option:
            self._value_id = default_id

            default_pos = self._grid.indexOf(selected_option)
            self._grid.addWidget(self._selector, 0, default_pos)
            self._selector.lower()  # Ensure selector stays below
            self._selector.update()  # Force refresh if needed

addOption

addOption(option_id: int, option_text: str) -> None

Add a new option to the selector.

PARAMETER DESCRIPTION
option_id

The ID for the option.

TYPE: int

option_text

The text to display for the option.

TYPE: str

Source code in src/ezqt_widgets/widgets/misc/option_selector.py
def addOption(self, option_id: int, option_text: str) -> None:
    """Add a new option to the selector.

    Args:
        option_id: The ID for the option.
        option_text: The text to display for the option.
    """
    # Create option label
    option = _SelectableOptionLabel(option_text.capitalize(), option_id, self, self)
    option.setObjectName(f"opt_{option_id}")
    option.setFrameShape(QFrame.Shape.NoFrame)
    option.setFrameShadow(QFrame.Shadow.Raised)
    option.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
    option.setProperty("type", "OptionSelector_Option")

    # Add to grid based on orientation
    option_index = len(self._options.items())
    if self._orientation == "horizontal":
        self._grid.addWidget(option, 0, option_index)
    else:  # vertical
        self._grid.addWidget(option, option_index, 0)

    # Store option
    self._options[option_id] = option

    # Update options list
    if option_id >= len(self._options_list):
        # Add empty elements if necessary
        while len(self._options_list) <= option_id:
            self._options_list.append("")
    self._options_list[option_id] = option_text

toggleSelection

toggleSelection(option_id: int) -> None

Handle option selection.

PARAMETER DESCRIPTION
option_id

The ID of the option to select.

TYPE: int

Source code in src/ezqt_widgets/widgets/misc/option_selector.py
def toggleSelection(self, option_id: int) -> None:
    """Handle option selection.

    Args:
        option_id: The ID of the option to select.
    """
    if option_id != self._value_id:
        self._value_id = option_id
        self.clicked.emit()
        self.valueChanged.emit(self.value)
        self.valueIdChanged.emit(option_id)
        self.moveSelector(self._options[option_id])

moveSelector

moveSelector(option: FramedLabel) -> None

Animate the selector to the selected option.

PARAMETER DESCRIPTION
option

The option widget to move the selector to.

TYPE: FramedLabel

Source code in src/ezqt_widgets/widgets/misc/option_selector.py
def moveSelector(self, option: FramedLabel) -> None:
    """Animate the selector to the selected option.

    Args:
        option: The option widget to move the selector to.
    """
    start_geometry = self._selector.geometry()
    end_geometry = option.geometry()

    # Create geometry animation
    self._selector_animation = QPropertyAnimation(self._selector, b"geometry")
    self._selector_animation.setDuration(self._animation_duration)
    self._selector_animation.setStartValue(start_geometry)
    self._selector_animation.setEndValue(end_geometry)
    self._selector_animation.setEasingCurve(QEasingCurve.Type.OutCubic)

    # Ensure selector stays below
    self._selector.lower()

    # Start animation
    self._selector_animation.start()

sizeHint

sizeHint() -> QSize

Get the recommended size for the widget.

RETURNS DESCRIPTION
QSize

The recommended size.

Source code in src/ezqt_widgets/widgets/misc/option_selector.py
def sizeHint(self) -> QSize:
    """Get the recommended size for the widget.

    Returns:
        The recommended size.
    """
    return QSize(200, 40)

minimumSizeHint

minimumSizeHint() -> QSize

Get the minimum size hint for the widget.

RETURNS DESCRIPTION
QSize

The minimum size hint.

Source code in src/ezqt_widgets/widgets/misc/option_selector.py
def minimumSizeHint(self) -> QSize:
    """Get the minimum size hint for the widget.

    Returns:
        The minimum size hint.
    """
    # Calculate options dimensions
    max_option_width = 0
    max_option_height = 0

    for option_text in self._options_list:
        # Estimate text width using font metrics
        font_metrics = self.fontMetrics()
        text_width = font_metrics.horizontalAdvance(option_text.capitalize())

        # Add padding and margins
        option_width = text_width + 16  # 8px padding on each side
        option_height = max(font_metrics.height() + 8, 30)  # 4px padding top/bottom

        max_option_width = max(max_option_width, option_width)
        max_option_height = max(max_option_height, option_height)

    # Calculate total dimensions based on orientation
    if self._orientation == "horizontal":
        # Horizontal: options side by side with individual widths
        total_width = 0
        for option_text in self._options_list:
            font_metrics = self.fontMetrics()
            text_width = font_metrics.horizontalAdvance(option_text.capitalize())
            option_width = text_width + 16  # 8px padding on each side
            total_width += option_width
        total_width += (len(self._options_list) - 1) * self._grid.spacing()
        total_height = max_option_height
    else:
        # Vertical: options stacked
        total_width = max_option_width
        total_height = max_option_height * len(self._options_list)
        total_height += (len(self._options_list) - 1) * self._grid.spacing()

    # Add grid margins
    total_width += 8  # Grid margins (4px on each side)
    total_height += 8  # Grid margins (4px on each side)

    # Apply minimum constraints
    min_width = self._min_width if self._min_width is not None else total_width
    min_height = self._min_height if self._min_height is not None else total_height

    return QSize(max(min_width, total_width), max(min_height, total_height))

refreshStyle

refreshStyle() -> None

Refresh the widget's style.

Useful after dynamic stylesheet changes.

Source code in src/ezqt_widgets/widgets/misc/option_selector.py
def refreshStyle(self) -> None:
    """Refresh the widget's style.

    Useful after dynamic stylesheet changes.
    """
    self.style().unpolish(self)
    self.style().polish(self)
    self.update()

ThemeIcon

A QIcon subclass that recolors itself to white (dark theme) or black (light theme) when the active theme changes. Custom colors for each theme can be specified.

Constructor parameters:

Parameter Type Default Description
icon QIcon \| QPixmap \| str Source icon (required); None raises TypeError
theme str "dark" Initial theme: "dark" or "light"
dark_color QColor \| str \| None None Color applied in dark theme; defaults to white
light_color QColor \| str \| None None Color applied in light theme; defaults to black

Properties:

Property Type Description
theme str Gets or sets the active theme; setting recolors the icon immediately
original_icon QIcon Gets or sets the source icon; setting triggers a recolor

Methods:

Method Signature Description
setTheme() (theme: str) -> None Convenience alias for setting the theme property
from_source() (source, theme, dark_color, light_color) -> ThemeIcon \| None Class method; returns None if source is None, otherwise wraps it in a ThemeIcon

Example:

from ezqt_widgets import ThemeIcon

# Auto dark/light colors (white in dark mode, black in light mode)
icon = ThemeIcon("path/to/icon.png", theme="dark")

# Custom colors
icon = ThemeIcon(
    "path/to/icon.svg",
    dark_color="#FFFFFF",
    light_color="#333333",
)

# Switch theme at runtime
icon.setTheme("light")

# Factory method — returns None if source is None
themed = ThemeIcon.from_source("path/to/icon.png", theme="dark")

ThemeIcon

ThemeIcon(icon: IconSourceExtended, theme: str = 'dark', dark_color: QColor | str | None = None, light_color: QColor | str | None = None)

Bases: QIcon

QIcon subclass with automatic theme-based color adaptation.

This icon adapts its color based on the specified theme
  • Dark theme: icon rendered in the resolved dark color.
  • Light theme: icon rendered in the resolved light color.

The icon can be updated dynamically by calling :meth:setTheme when the application theme changes.

PARAMETER DESCRIPTION
icon

The source icon (QIcon, QPixmap, or path string).

TYPE: IconSourceExtended

theme

The initial theme ("dark" or "light", default: "dark").

TYPE: str DEFAULT: 'dark'

dark_color

Optional color for dark theme (hex or rgb(...) string).

TYPE: QColor | str | None DEFAULT: None

light_color

Optional color for light theme (hex or rgb(...) string).

TYPE: QColor | str | None DEFAULT: None

RAISES DESCRIPTION
TypeError

If icon is None.

Properties

theme: Get or set the active theme. original_icon: Get or set the source icon.

Example

from ezqt_widgets.widgets.misc.theme_icon import ThemeIcon

Basic usage with automatic white/black color adaptation

icon = ThemeIcon("path/to/icon.png", theme="dark") button.setIcon(icon)

Custom colors for each theme

icon = ThemeIcon("icon.png", dark_color="#FFFFFF", light_color="#333333")

Factory method from any source (QIcon, QPixmap, path, or None)

themed = ThemeIcon.from_source("icon.svg", theme="light")

Adapt to a new theme dynamically

icon.setTheme("light")

Initialize the theme icon.

PARAMETER DESCRIPTION
icon

The source icon (QIcon, QPixmap, or path string).

TYPE: IconSourceExtended

theme

The initial theme ("dark" or "light").

TYPE: str DEFAULT: 'dark'

dark_color

Optional color for dark theme (hex or rgb(...) string).

TYPE: QColor | str | None DEFAULT: None

light_color

Optional color for light theme (hex or rgb(...) string).

TYPE: QColor | str | None DEFAULT: None

RAISES DESCRIPTION
TypeError

If icon is None.

Source code in src/ezqt_widgets/widgets/misc/theme_icon.py
def __init__(
    self,
    icon: IconSourceExtended,
    theme: str = "dark",
    dark_color: QColor | str | None = None,
    light_color: QColor | str | None = None,
) -> None:
    """Initialize the theme icon.

    Args:
        icon: The source icon (``QIcon``, ``QPixmap``, or path string).
        theme: The initial theme (``"dark"`` or ``"light"``).
        dark_color: Optional color for dark theme (hex or ``rgb(...)`` string).
        light_color: Optional color for light theme (hex or ``rgb(...)`` string).

    Raises:
        TypeError: If ``icon`` is ``None``.
    """
    super().__init__()
    self._original_icon: QIcon = self._to_qicon(icon)
    self._theme: str = theme
    self._dark_color, self._light_color = self._resolve_theme_colors(
        dark_color, light_color
    )
    self._update_icon()

theme property writable

theme: str

Get the current theme.

RETURNS DESCRIPTION
str

The current theme ("dark" or "light").

original_icon property writable

original_icon: QIcon

Get the original (uncolored) source icon.

RETURNS DESCRIPTION
QIcon

The original icon.

from_source classmethod

from_source(source: IconSourceExtended, theme: str = 'dark', dark_color: QColor | str | None = None, light_color: QColor | str | None = None) -> ThemeIcon | None

Create a ThemeIcon from any supported source.

PARAMETER DESCRIPTION
source

The icon source (ThemeIcon, QIcon, QPixmap, or path string).

TYPE: IconSourceExtended

theme

The initial theme ("dark" or "light").

TYPE: str DEFAULT: 'dark'

dark_color

Optional color for dark theme (hex or rgb(...) string).

TYPE: QColor | str | None DEFAULT: None

light_color

Optional color for light theme (hex or rgb(...) string).

TYPE: QColor | str | None DEFAULT: None

RETURNS DESCRIPTION
ThemeIcon | None

A ThemeIcon instance or None if source is None.

Source code in src/ezqt_widgets/widgets/misc/theme_icon.py
@classmethod
def from_source(
    cls,
    source: IconSourceExtended,
    theme: str = "dark",
    dark_color: QColor | str | None = None,
    light_color: QColor | str | None = None,
) -> ThemeIcon | None:
    """Create a ThemeIcon from any supported source.

    Args:
        source: The icon source (ThemeIcon, QIcon, QPixmap, or path string).
        theme: The initial theme (``"dark"`` or ``"light"``).
        dark_color: Optional color for dark theme (hex or ``rgb(...)`` string).
        light_color: Optional color for light theme (hex or ``rgb(...)`` string).

    Returns:
        A ThemeIcon instance or None if ``source`` is None.
    """
    if source is None:
        return None
    if isinstance(source, cls):
        return source
    return cls(
        source,
        theme=theme,
        dark_color=dark_color,
        light_color=light_color,
    )

setTheme

setTheme(theme: str) -> None

Update the icon color for the given theme.

Convenience method equivalent to setting the :attr:theme property.

PARAMETER DESCRIPTION
theme

The new theme ("dark" or "light").

TYPE: str

Source code in src/ezqt_widgets/widgets/misc/theme_icon.py
def setTheme(self, theme: str) -> None:
    """Update the icon color for the given theme.

    Convenience method equivalent to setting the :attr:`theme` property.

    Args:
        theme: The new theme (``"dark"`` or ``"light"``).
    """
    self.theme = theme

ToggleIcon

A QLabel that alternates between two icons representing an "opened" and a "closed" state. When no custom icons are provided, a built-in painted triangle arrow is used.

Signals:

Signal Signature Emitted when
stateChanged (str) The state changes; value is "opened" or "closed"
clicked () The widget is clicked

Constructor parameters:

Parameter Type Default Description
parent QWidget \| None None Parent widget
opened_icon QIcon \| QPixmap \| str \| None None Icon shown in the opened state; uses painted arrow if None
closed_icon QIcon \| QPixmap \| str \| None None Icon shown in the closed state; uses painted arrow if None
icon_size int 16 Icon size in pixels (square)
icon_color QColor \| str \| None None Color applied to icons; defaults to semi-transparent white
initial_state str "closed" Starting state: "opened" or "closed"
min_width int \| None None Minimum width
min_height int \| None None Minimum height

Properties:

Property Type Description
state str Gets or sets the current state ("opened" or "closed")
opened_icon QPixmap \| None Gets or sets the opened-state icon
closed_icon QPixmap \| None Gets or sets the closed-state icon
icon_size int Gets or sets the icon size in pixels
icon_color QColor Gets or sets the color applied to icons
min_width int \| None Gets or sets minimum width
min_height int \| None Gets or sets minimum height

Methods:

Method Signature Description
toggleState() () -> None Switches between "opened" and "closed"
setStateOpened() () -> None Forces state to "opened"
setStateClosed() () -> None Forces state to "closed"
isOpened() () -> bool Returns True if state is "opened"
isClosed() () -> bool Returns True if state is "closed"
refreshStyle() () -> None Re-applies the QSS stylesheet

Keyboard support: Space, Enter, and Return keys toggle the state when the widget has focus.

Example:

from PySide6.QtWidgets import QApplication
from ezqt_widgets import ToggleIcon

app = QApplication([])

toggle = ToggleIcon(initial_state="closed", icon_size=20)
toggle.stateChanged.connect(lambda s: print(f"State: {s}"))
toggle.toggleState()  # now "opened"
toggle.show()

app.exec()

ToggleIcon

ToggleIcon(parent: WidgetParent = None, opened_icon: IconSourceExtended = None, closed_icon: IconSourceExtended = None, icon_size: int = 16, icon_color: ColorType | None = None, initial_state: str = 'closed', min_width: int | None = None, min_height: int | None = None, *args: Any, **kwargs: Any)

Bases: QLabel

Label with toggleable icons to indicate an open/closed state.

Features
  • Toggleable icons for open/closed states
  • Custom icons or default painted icons
  • Configurable icon size and color
  • Click and keyboard events for toggling
  • Property-based state management
PARAMETER DESCRIPTION
parent

Parent widget (default: None).

TYPE: WidgetParent DEFAULT: None

opened_icon

Icon to display when state is "opened" (ThemeIcon, QIcon, QPixmap, path, or URL). If None, uses paintEvent (default: None).

TYPE: IconSourceExtended DEFAULT: None

closed_icon

Icon to display when state is "closed" (ThemeIcon, QIcon, QPixmap, path, or URL). If None, uses paintEvent (default: None).

TYPE: IconSourceExtended DEFAULT: None

icon_size

Icon size in pixels (default: 16).

TYPE: int DEFAULT: 16

icon_color

Color to apply to icons (default: white with 0.5 opacity).

TYPE: ColorType | None DEFAULT: None

initial_state

Initial state ("opened" or "closed", default: "closed").

TYPE: str DEFAULT: 'closed'

min_width

Minimum width of the widget (default: None).

TYPE: int | None DEFAULT: None

min_height

Minimum height of the widget (default: None).

TYPE: int | None DEFAULT: None

*args

Additional arguments passed to QLabel.

TYPE: Any DEFAULT: ()

**kwargs

Additional keyword arguments passed to QLabel.

TYPE: Any DEFAULT: {}

Signals

stateChanged(str): Emitted when the state changes ("opened" or "closed"). clicked(): Emitted when the widget is clicked.

Example

from ezqt_widgets import ToggleIcon toggle = ToggleIcon(initial_state="closed", icon_size=20) toggle.stateChanged.connect(lambda s: print(f"State: {s}")) toggle.toggleState() toggle.show()

Initialize the toggle icon.

Source code in src/ezqt_widgets/widgets/misc/toggle_icon.py
def __init__(
    self,
    parent: WidgetParent = None,
    opened_icon: IconSourceExtended = None,
    closed_icon: IconSourceExtended = None,
    icon_size: int = 16,
    icon_color: ColorType | None = None,
    initial_state: str = "closed",
    min_width: int | None = None,
    min_height: int | None = None,
    *args: Any,
    **kwargs: Any,
) -> None:
    """Initialize the toggle icon."""
    super().__init__(parent, *args, **kwargs)
    self.setProperty("type", "ToggleIcon")

    # Initialize variables
    self._icon_size = icon_size
    self._icon_color = (
        QColor(255, 255, 255, 128) if icon_color is None else QColor(icon_color)
    )
    self._min_width = min_width
    self._min_height = min_height
    self._state = initial_state

    # Setup icons
    self._use_custom_icons = opened_icon is not None or closed_icon is not None

    if self._use_custom_icons:
        # Use provided icons
        self._opened_icon = (
            _load_icon_from_source(
                opened_icon, QSize(self._icon_size, self._icon_size)
            )
            if opened_icon is not None
            else None
        )
        self._closed_icon = (
            _load_icon_from_source(
                closed_icon, QSize(self._icon_size, self._icon_size)
            )
            if closed_icon is not None
            else None
        )
    else:
        # Use paintEvent to draw icons
        self._opened_icon = None
        self._closed_icon = None

    # Setup widget
    self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
    self._update_icon()
    self._apply_initial_state()

state property writable

state: str

Get the current state.

RETURNS DESCRIPTION
str

The current state ("opened" or "closed").

opened_icon property writable

opened_icon: QPixmap | None

Get or set the opened state icon.

RETURNS DESCRIPTION
QPixmap | None

The opened icon pixmap, or None if using default.

closed_icon property writable

closed_icon: QPixmap | None

Get or set the closed state icon.

RETURNS DESCRIPTION
QPixmap | None

The closed icon pixmap, or None if using default.

icon_size property writable

icon_size: int

Get or set the icon size.

RETURNS DESCRIPTION
int

The current icon size in pixels.

icon_color property writable

icon_color: QColor

Get or set the icon color.

RETURNS DESCRIPTION
QColor

The current icon color.

min_width property writable

min_width: int | None

Get or set the minimum width.

RETURNS DESCRIPTION
int | None

The minimum width, or None if not set.

min_height property writable

min_height: int | None

Get or set the minimum height.

RETURNS DESCRIPTION
int | None

The minimum height, or None if not set.

mousePressEvent

mousePressEvent(event: QMouseEvent) -> None

Handle mouse press events.

PARAMETER DESCRIPTION
event

The mouse event.

TYPE: QMouseEvent

Source code in src/ezqt_widgets/widgets/misc/toggle_icon.py
def mousePressEvent(self, event: QMouseEvent) -> None:
    """Handle mouse press events.

    Args:
        event: The mouse event.
    """
    self.toggleState()
    self.clicked.emit()
    super().mousePressEvent(event)

keyPressEvent

keyPressEvent(event: QKeyEvent) -> None

Handle key press events.

PARAMETER DESCRIPTION
event

The key event.

TYPE: QKeyEvent

Source code in src/ezqt_widgets/widgets/misc/toggle_icon.py
def keyPressEvent(self, event: QKeyEvent) -> None:
    """Handle key press events.

    Args:
        event: The key event.
    """
    if event.key() in [
        Qt.Key.Key_Return,
        Qt.Key.Key_Enter,
        Qt.Key.Key_Space,
    ]:
        self.toggleState()
        self.clicked.emit()
    super().keyPressEvent(event)

paintEvent

paintEvent(event: QPaintEvent) -> None

Draw the icon if no custom icon is provided, centered in a square.

PARAMETER DESCRIPTION
event

The paint event.

TYPE: QPaintEvent

Source code in src/ezqt_widgets/widgets/misc/toggle_icon.py
def paintEvent(self, event: QPaintEvent) -> None:
    """Draw the icon if no custom icon is provided, centered in a square.

    Args:
        event: The paint event.
    """
    if not self._use_custom_icons:
        painter = QPainter(self)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)
        try:
            rect = self.rect()
            # Calculate centered square
            side = min(rect.width(), rect.height())
            x0 = rect.center().x() - side // 2
            y0 = rect.center().y() - side // 2
            square = QRectF(x0, y0, side, side)
            center_x = square.center().x()
            center_y = square.center().y()
            arrow_size = max(2, self._icon_size // 4)
            painter.setPen(Qt.PenStyle.NoPen)
            painter.setBrush(self._icon_color)
            if self._state == "opened":
                points = [
                    QPointF(center_x - arrow_size, center_y - arrow_size // 2),
                    QPointF(center_x + arrow_size, center_y - arrow_size // 2),
                    QPointF(center_x, center_y + arrow_size // 2),
                ]
            else:
                points = [
                    QPointF(center_x - arrow_size, center_y + arrow_size // 2),
                    QPointF(center_x + arrow_size, center_y + arrow_size // 2),
                    QPointF(center_x, center_y - arrow_size // 2),
                ]
            painter.drawPolygon(points)
        finally:
            painter.end()
    else:
        super().paintEvent(event)

minimumSizeHint

minimumSizeHint() -> QSize

Calculate a minimum square size based on icon and margins.

RETURNS DESCRIPTION
QSize

The minimum size hint.

Source code in src/ezqt_widgets/widgets/misc/toggle_icon.py
def minimumSizeHint(self) -> QSize:
    """Calculate a minimum square size based on icon and margins.

    Returns:
        The minimum size hint.
    """
    icon_size = self._icon_size
    margins = self.contentsMargins()
    base = icon_size + max(
        margins.left() + margins.right(),
        margins.top() + margins.bottom(),
    )
    min_side = base
    if self._min_width is not None:
        min_side = max(min_side, self._min_width)
    if self._min_height is not None:
        min_side = max(min_side, self._min_height)
    return QSize(min_side, min_side)

setTheme

setTheme(theme: str) -> None

Update all icons' color for the given theme.

Can be connected directly to a theme-change signal to keep icons in sync with the application's color scheme.

PARAMETER DESCRIPTION
theme

The new theme ("dark" or "light").

TYPE: str

Source code in src/ezqt_widgets/widgets/misc/toggle_icon.py
def setTheme(self, theme: str) -> None:
    """Update all icons' color for the given theme.

    Can be connected directly to a theme-change signal to keep
    icons in sync with the application's color scheme.

    Args:
        theme: The new theme (``"dark"`` or ``"light"``).
    """
    if isinstance(self._opened_icon, ThemeIcon):
        self._opened_icon.setTheme(theme)
    if isinstance(self._closed_icon, ThemeIcon):
        self._closed_icon.setTheme(theme)
    self._update_icon()

toggleState

toggleState() -> None

Toggle between opened and closed states.

Source code in src/ezqt_widgets/widgets/misc/toggle_icon.py
def toggleState(self) -> None:
    """Toggle between opened and closed states."""
    self.state = "opened" if self._state == "closed" else "closed"

setStateOpened

setStateOpened() -> None

Force the state to opened.

Source code in src/ezqt_widgets/widgets/misc/toggle_icon.py
def setStateOpened(self) -> None:
    """Force the state to opened."""
    self.state = "opened"

setStateClosed

setStateClosed() -> None

Force the state to closed.

Source code in src/ezqt_widgets/widgets/misc/toggle_icon.py
def setStateClosed(self) -> None:
    """Force the state to closed."""
    self.state = "closed"

isOpened

isOpened() -> bool

Check if the state is opened.

RETURNS DESCRIPTION
bool

True if opened, False otherwise.

Source code in src/ezqt_widgets/widgets/misc/toggle_icon.py
def isOpened(self) -> bool:
    """Check if the state is opened.

    Returns:
        True if opened, False otherwise.
    """
    return self._state == "opened"

isClosed

isClosed() -> bool

Check if the state is closed.

RETURNS DESCRIPTION
bool

True if closed, False otherwise.

Source code in src/ezqt_widgets/widgets/misc/toggle_icon.py
def isClosed(self) -> bool:
    """Check if the state is closed.

    Returns:
        True if closed, False otherwise.
    """
    return self._state == "closed"

refreshStyle

refreshStyle() -> None

Refresh the widget style.

Useful after dynamic stylesheet changes.

Source code in src/ezqt_widgets/widgets/misc/toggle_icon.py
def refreshStyle(self) -> None:
    """Refresh the widget style.

    Useful after dynamic stylesheet changes.
    """
    self.style().unpolish(self)
    self.style().polish(self)
    self.update()

ToggleSwitch

A QWidget that renders an animated toggle switch with a sliding circle. Clicking or setting checked triggers a smooth animation.

Signals:

Signal Signature Emitted when
toggled (bool) The checked state changes; value is the new state

Constructor parameters:

Parameter Type Default Description
parent QWidget \| None None Parent widget
checked bool False Initial checked state
width int 50 Width of the switch in pixels (minimum 20)
height int 24 Height of the switch in pixels (minimum 12)
animation bool True Whether the circle slides with animation

Properties:

Property Type Description
checked bool Gets or sets the toggle state; emits toggled and animates on change
width int Gets or sets the switch width
height int Gets or sets the switch height
animation bool Gets or sets whether animation is enabled

Methods:

Method Signature Description
toggle() () -> None Inverts the current checked state
refreshStyle() () -> None Re-applies the QSS stylesheet

Default colors (not configurable via constructor):

Element Off state On state
Background rgb(44, 49, 58) rgb(150, 205, 50)
Circle rgb(255, 255, 255) rgb(255, 255, 255)
Border rgb(52, 59, 72) rgb(52, 59, 72)

Apply a QSS stylesheet to the parent or the application to override these colors.

Example:

from PySide6.QtWidgets import QApplication
from ezqt_widgets import ToggleSwitch

app = QApplication([])

switch = ToggleSwitch(checked=False, width=50, height=24, animation=True)
switch.toggled.connect(lambda on: print(f"On: {on}"))
switch.checked = True  # animates to the on position
switch.show()

app.exec()

ToggleSwitch

ToggleSwitch(parent: WidgetParent = None, checked: bool = False, width: int = 50, height: int = 24, animation: bool = True, *args: Any, **kwargs: Any)

Bases: QWidget

Modern toggle switch widget with animated sliding circle.

Features
  • Smooth animation when toggling
  • Customizable colors for on/off states
  • Configurable size and border radius
  • Click to toggle functionality
  • Property-based access to state
  • Signal emitted on state change
PARAMETER DESCRIPTION
parent

The parent widget (default: None).

TYPE: WidgetParent DEFAULT: None

checked

Initial state of the toggle (default: False).

TYPE: bool DEFAULT: False

width

Width of the toggle switch (default: 50).

TYPE: int DEFAULT: 50

height

Height of the toggle switch (default: 24).

TYPE: int DEFAULT: 24

animation

Whether to animate the toggle (default: True).

TYPE: bool DEFAULT: True

*args

Additional arguments passed to QWidget.

TYPE: Any DEFAULT: ()

**kwargs

Additional keyword arguments passed to QWidget.

TYPE: Any DEFAULT: {}

Signals

toggled(bool): Emitted when the toggle state changes.

Example

from ezqt_widgets import ToggleSwitch switch = ToggleSwitch(checked=False, width=50, height=24) switch.toggled.connect(lambda state: print(f"On: {state}")) switch.checked = True switch.show()

Initialize the toggle switch.

Source code in src/ezqt_widgets/widgets/misc/toggle_switch.py
def __init__(
    self,
    parent: WidgetParent = None,
    checked: bool = False,
    width: int = 50,
    height: int = 24,
    animation: bool = True,
    *args: Any,
    **kwargs: Any,
) -> None:
    """Initialize the toggle switch."""
    super().__init__(parent, *args, **kwargs)

    # Initialize properties
    self._checked: bool = checked
    self._width: int = width
    self._height: int = height
    self._animation: bool = animation
    self._circle_radius: int = (height - 4) // 2  # Circle radius with 2px margin
    self._animation_duration: int = 200

    # Colors
    self._bg_color_off: QColor = QColor(44, 49, 58)  # Default dark theme
    self._bg_color_on: QColor = QColor(150, 205, 50)  # Default accent color
    self._circle_color: QColor = QColor(255, 255, 255)
    self._border_color: QColor = QColor(52, 59, 72)

    # Initialize position
    self._circle_position: int = self._get_circle_position()

    # Setup animation
    self._setup_animation()

    # Setup widget
    self._setup_widget()

checked property writable

checked: bool

Get the toggle state.

RETURNS DESCRIPTION
bool

True if checked, False otherwise.

width property writable

width: int

Get the width of the toggle.

RETURNS DESCRIPTION
int

The current width in pixels.

height property writable

height: int

Get the height of the toggle.

RETURNS DESCRIPTION
int

The current height in pixels.

animation property writable

animation: bool

Get whether animation is enabled.

RETURNS DESCRIPTION
bool

True if animation is enabled, False otherwise.

toggle

toggle() -> None

Toggle the switch state.

Source code in src/ezqt_widgets/widgets/misc/toggle_switch.py
def toggle(self) -> None:
    """Toggle the switch state."""
    self.checked = not self._checked

mousePressEvent

mousePressEvent(event: QMouseEvent) -> None

Handle mouse press events.

PARAMETER DESCRIPTION
event

The mouse event.

TYPE: QMouseEvent

Source code in src/ezqt_widgets/widgets/misc/toggle_switch.py
def mousePressEvent(self, event: QMouseEvent) -> None:
    """Handle mouse press events.

    Args:
        event: The mouse event.
    """
    if event.button() == Qt.MouseButton.LeftButton:
        self.toggle()

paintEvent

paintEvent(_event: QPaintEvent) -> None

Custom paint event to draw the toggle switch.

PARAMETER DESCRIPTION
_event

The paint event (unused but required by signature).

TYPE: QPaintEvent

Source code in src/ezqt_widgets/widgets/misc/toggle_switch.py
def paintEvent(self, _event: QPaintEvent) -> None:
    """Custom paint event to draw the toggle switch.

    Args:
        _event: The paint event (unused but required by signature).
    """
    painter = QPainter(self)
    painter.setRenderHint(QPainter.RenderHint.Antialiasing)

    # Draw background
    bg_color = self._bg_color_on if self._checked else self._bg_color_off
    painter.setPen(QPen(self._border_color, 1))
    painter.setBrush(QBrush(bg_color))
    painter.drawRoundedRect(
        0,
        0,
        self._width,
        self._height,
        self._height // 2,
        self._height // 2,
    )

    # Draw circle
    circle_x = self._circle_position
    circle_y = (self._height - self._circle_radius * 2) // 2
    circle_rect = QRect(
        circle_x, circle_y, self._circle_radius * 2, self._circle_radius * 2
    )

    painter.setPen(Qt.PenStyle.NoPen)
    painter.setBrush(QBrush(self._circle_color))
    painter.drawEllipse(
        circle_x, circle_y, circle_rect.width(), circle_rect.height()
    )

sizeHint

sizeHint() -> QSize

Return the recommended size for the widget.

RETURNS DESCRIPTION
QSize

The recommended size.

Source code in src/ezqt_widgets/widgets/misc/toggle_switch.py
def sizeHint(self) -> QSize:
    """Return the recommended size for the widget.

    Returns:
        The recommended size.
    """
    return QSize(self._width, self._height)

minimumSizeHint

minimumSizeHint() -> QSize

Return the minimum size for the widget.

RETURNS DESCRIPTION
QSize

The minimum size hint.

Source code in src/ezqt_widgets/widgets/misc/toggle_switch.py
def minimumSizeHint(self) -> QSize:
    """Return the minimum size for the widget.

    Returns:
        The minimum size hint.
    """
    return QSize(self._width, self._height)

refreshStyle

refreshStyle() -> None

Refresh the widget style.

Useful after dynamic stylesheet changes.

Source code in src/ezqt_widgets/widgets/misc/toggle_switch.py
def refreshStyle(self) -> None:
    """Refresh the widget style.

    Useful after dynamic stylesheet changes.
    """
    self.style().unpolish(self)
    self.style().polish(self)
    self.update()

NotificationBanner

An animated slide-down notification banner QWidget that overlays the top of a parent widget. The banner can auto-dismiss after a configurable duration, and always provides a manual close button.

NotificationLevel

NotificationLevel is an Enum that sets the visual severity of the banner.

Member Value Background color
INFO "INFO" #3b82f6 (blue)
WARNING "WARNING" #f59e0b (amber)
ERROR "ERROR" #ef4444 (red)
SUCCESS "SUCCESS" #22c55e (green)
from ezqt_widgets import NotificationLevel

level = NotificationLevel.SUCCESS

Signals:

Signal Signature Emitted when
dismissed () The banner is hidden, whether by auto-dismiss or manually

Constructor parameters:

Parameter Type Default Description
parent QWidget The parent widget that hosts the banner overlay (required, not None)

Methods:

Method Signature Description
showNotification() (message: str, level: NotificationLevel, duration: int) -> None Displays the banner with the given message, level, and duration
refreshStyle() () -> None Re-applies the QSS stylesheet

showNotification() parameters:

Parameter Type Default Description
message str Text displayed in the banner (required)
level NotificationLevel NotificationLevel.INFO Severity level controlling background color and icon
duration int 3000 Display duration in milliseconds. Pass 0 for permanent (manual-only dismiss)

Behavior notes:

  • The banner is 48 px tall and spans the full width of the parent widget.
  • Slide-in and slide-out use a QPropertyAnimation on geometry with an OutCubic easing curve (250 ms).
  • Calling showNotification() while a banner is already visible cancels the previous auto-dismiss timer and replaces the content immediately.
  • The banner tracks parent resize events via an event filter and repositions itself automatically while visible.
  • The parent argument is mandatory. Passing None will raise an AttributeError at initialization.

Example:

from PySide6.QtWidgets import QApplication, QWidget
from ezqt_widgets import NotificationBanner, NotificationLevel

app = QApplication([])

window = QWidget()
window.resize(600, 400)

banner = NotificationBanner(parent=window)
banner.dismissed.connect(lambda: print("Banner dismissed"))

# Show a success banner that auto-dismisses after 4 seconds
banner.showNotification(
    "Configuration saved successfully.",
    NotificationLevel.SUCCESS,
    duration=4000,
)

# Show a permanent error banner (requires manual close)
banner.showNotification(
    "Connection lost. Check network settings.",
    NotificationLevel.ERROR,
    duration=0,
)

window.show()
app.exec()

NotificationBanner

NotificationBanner(parent: QWidget)

Bases: QWidget

Animated slide-down notification banner overlaying a parent widget.

The banner slides in from the top of its parent widget and can auto-dismiss after a configurable duration. A close button is always visible for manual dismissal. The banner repositions itself when the parent is resized via event filtering.

Features
  • Slide-down animation via QPropertyAnimation on geometry
  • Four severity levels: INFO, WARNING, ERROR, SUCCESS
  • Auto-dismiss via QTimer when duration > 0
  • Manual close button (×)
  • Level icon via inline SVG rendered to ThemeIcon
  • Parent resize tracking via event filter
PARAMETER DESCRIPTION
parent

The parent widget inside which the banner is displayed. Must be a valid QWidget (not None).

TYPE: QWidget

Signals

dismissed(): Emitted when the banner is hidden (any cause).

Example

from ezqt_widgets import NotificationBanner, NotificationLevel banner = NotificationBanner(parent=main_widget) banner.dismissed.connect(lambda: print("Banner closed")) banner.showNotification("File saved!", NotificationLevel.SUCCESS)

Initialize the notification banner.

PARAMETER DESCRIPTION
parent

The parent widget that hosts the banner overlay.

TYPE: QWidget

Source code in src/ezqt_widgets/widgets/misc/notification_banner.py
def __init__(self, parent: QWidget) -> None:
    """Initialize the notification banner.

    Args:
        parent: The parent widget that hosts the banner overlay.
    """
    super().__init__(parent)
    self.setProperty("type", "NotificationBanner")

    # Initialize private state
    self._duration: int = 3000
    self._dismiss_timer: QTimer | None = None
    self._animation: QPropertyAnimation | None = None

    # Build UI before hiding
    self._setup_widget()

    # Start hidden
    self.setGeometry(0, 0, parent.width(), 0)
    self.hide()

    # Install event filter on parent to track resizes
    parent.installEventFilter(self)

showNotification

showNotification(message: str, level: NotificationLevel = INFO, duration: int = 3000) -> None

Display a notification banner with the given message and level.

PARAMETER DESCRIPTION
message

The text to display in the banner.

TYPE: str

level

The severity level (default: NotificationLevel.INFO).

TYPE: NotificationLevel DEFAULT: INFO

duration

Display duration in milliseconds. Use 0 for a permanent banner that requires manual dismissal (default: 3000).

TYPE: int DEFAULT: 3000

Source code in src/ezqt_widgets/widgets/misc/notification_banner.py
def showNotification(
    self,
    message: str,
    level: NotificationLevel = NotificationLevel.INFO,
    duration: int = 3000,
) -> None:
    """Display a notification banner with the given message and level.

    Args:
        message: The text to display in the banner.
        level: The severity level (default: NotificationLevel.INFO).
        duration: Display duration in milliseconds. Use 0 for a
            permanent banner that requires manual dismissal
            (default: 3000).
    """
    self._stop_timer()
    self._duration = duration

    self._message_label.setText(message)
    self._apply_level_style(level)
    self._slide_in()

    if duration > 0:
        self._dismiss_timer = QTimer(self)
        self._dismiss_timer.setSingleShot(True)
        self._dismiss_timer.timeout.connect(self._dismiss)
        self._dismiss_timer.start(duration)

eventFilter

eventFilter(obj: object, event: QEvent) -> bool

Track parent resize events to reposition the banner.

PARAMETER DESCRIPTION
obj

The object that generated the event.

TYPE: object

event

The event.

TYPE: QEvent

RETURNS DESCRIPTION
bool

False to allow normal event propagation.

Source code in src/ezqt_widgets/widgets/misc/notification_banner.py
def eventFilter(self, obj: object, event: QEvent) -> bool:
    """Track parent resize events to reposition the banner.

    Args:
        obj: The object that generated the event.
        event: The event.

    Returns:
        False to allow normal event propagation.
    """
    if obj is self.parentWidget() and event.type() == QEvent.Type.Resize:
        parent = self.parentWidget()
        if parent is not None and self.isVisible():
            self.setGeometry(0, 0, parent.width(), _BANNER_HEIGHT)
    return False

refreshStyle

refreshStyle() -> None

Refresh the widget style.

Useful after dynamic stylesheet changes.

Source code in src/ezqt_widgets/widgets/misc/notification_banner.py
def refreshStyle(self) -> None:
    """Refresh the widget style.

    Useful after dynamic stylesheet changes.
    """
    self.style().unpolish(self)
    self.style().polish(self)
    self.update()

CollapsibleSection

An accordion-style QWidget with a clickable header and smooth expand/collapse animation. The header is always visible; clicking it toggles the content area between its natural height and zero.

Signals:

Signal Signature Emitted when
expandedChanged (bool) The expanded state changes; value is the new state

Constructor parameters:

Parameter Type Default Description
parent QWidget \| None None Parent widget
title str "" Header title text
expanded bool True Initial expanded state

Properties:

Property Type Description
title str Gets or sets the header title text
is_expanded bool Read-only; True if the content area is currently expanded

Methods:

Method Signature Description
setContentWidget() (widget: QWidget) -> None Sets the widget displayed in the collapsible area; replaces any previous content
expand() () -> None Expands the content area with animation; no-op if already expanded
collapse() () -> None Collapses the content area with animation; no-op if already collapsed
toggle() () -> None Toggles between expanded and collapsed states
setTheme() (theme: str) -> None Updates the chevron icon color; connect to a themeChanged signal
refreshStyle() () -> None Re-applies the QSS stylesheet

Behavior notes:

  • The expand/collapse animation operates on maximumHeight of the content area using QPropertyAnimation with an InOutCubic easing curve (200 ms).
  • expandedChanged is emitted before the animation completes.
  • Calling setContentWidget() replaces any previously set widget and re-applies the current expanded/collapsed state without animation.
  • The header chevron uses a ToggleIcon and reflects the current state (chevron-down when expanded, chevron-right when collapsed).
  • setTheme() propagates the theme to the internal ToggleIcon chevron.

Example:

from PySide6.QtWidgets import QApplication, QWidget, QFormLayout, QLineEdit
from ezqt_widgets import CollapsibleSection

app = QApplication([])

# Build a form to use as content
form_widget = QWidget()
form_layout = QFormLayout(form_widget)
form_layout.addRow("Host:", QLineEdit("localhost"))
form_layout.addRow("Port:", QLineEdit("5432"))

section = CollapsibleSection(title="Database settings", expanded=False)
section.setContentWidget(form_widget)
section.expandedChanged.connect(lambda e: print(f"Expanded: {e}"))
section.show()

app.exec()

CollapsibleSection

CollapsibleSection(parent: WidgetParent = None, *, title: str = '', expanded: bool = True)

Bases: QWidget

Accordion-style section widget with animated expand/collapse.

The header is always visible. Clicking anywhere on the header (or calling toggle()) animates the content area between 0 height and its natural size hint height.

Features
  • Clickable header with title label and ToggleIcon chevron
  • Smooth height animation via QPropertyAnimation on maximumHeight
  • Supports an arbitrary QWidget as content via setContentWidget()
  • expand()/collapse()/toggle() public API
  • Theme propagation to the ToggleIcon chevron
PARAMETER DESCRIPTION
parent

The parent widget (default: None).

TYPE: WidgetParent DEFAULT: None

title

Header title text (default: "").

TYPE: str DEFAULT: ''

expanded

Initial expanded state (default: True).

TYPE: bool DEFAULT: True

Properties

title: Get or set the header title text. is_expanded: Get the current expanded state.

Signals

expandedChanged(bool): Emitted when the expanded state changes.

Example

from ezqt_widgets import CollapsibleSection section = CollapsibleSection(title="Settings", expanded=False) section.setContentWidget(my_form_widget) section.expandedChanged.connect(lambda e: print(f"Expanded: {e}")) section.show()

Initialize the collapsible section.

Source code in src/ezqt_widgets/widgets/misc/collapsible_section.py
def __init__(
    self,
    parent: WidgetParent = None,
    *,
    title: str = "",
    expanded: bool = True,
) -> None:
    """Initialize the collapsible section."""
    super().__init__(parent)
    self.setProperty("type", "CollapsibleSection")

    # Initialize private state
    self._expanded: bool = expanded
    self._content_widget: QWidget | None = None
    self._animation: QPropertyAnimation | None = None

    # Setup UI
    self._setup_widget(title)
    self._setup_animation()

    # Apply initial state without animation
    self._apply_initial_state()

title property writable

title: str

Get the header title text.

RETURNS DESCRIPTION
str

The current title string.

is_expanded property

is_expanded: bool

Get the current expanded state.

RETURNS DESCRIPTION
bool

True if the section is expanded, False if collapsed.

setContentWidget

setContentWidget(widget: QWidget) -> None

Set the widget displayed in the collapsible content area.

Replaces any previously set content widget. The section keeps its current expanded/collapsed state.

PARAMETER DESCRIPTION
widget

The widget to display as content.

TYPE: QWidget

Source code in src/ezqt_widgets/widgets/misc/collapsible_section.py
def setContentWidget(self, widget: QWidget) -> None:
    """Set the widget displayed in the collapsible content area.

    Replaces any previously set content widget. The section keeps
    its current expanded/collapsed state.

    Args:
        widget: The widget to display as content.
    """
    # Remove previous content
    if self._content_widget is not None:
        self._content_layout.removeWidget(self._content_widget)
        self._content_widget.setParent(None)

    self._content_widget = widget
    self._content_layout.addWidget(widget)

    # Re-apply state to reflect new content height
    self._apply_initial_state()

expand

expand() -> None

Expand the content area with animation.

Source code in src/ezqt_widgets/widgets/misc/collapsible_section.py
def expand(self) -> None:
    """Expand the content area with animation."""
    if self._expanded:
        return
    self._expanded = True
    self._toggle_icon.setStateOpened()
    self._run_animation(expanding=True)
    self.expandedChanged.emit(True)

collapse

collapse() -> None

Collapse the content area with animation.

Source code in src/ezqt_widgets/widgets/misc/collapsible_section.py
def collapse(self) -> None:
    """Collapse the content area with animation."""
    if not self._expanded:
        return
    self._expanded = False
    self._toggle_icon.setStateClosed()
    self._run_animation(expanding=False)
    self.expandedChanged.emit(False)

toggle

toggle() -> None

Toggle between expanded and collapsed states.

Source code in src/ezqt_widgets/widgets/misc/collapsible_section.py
def toggle(self) -> None:
    """Toggle between expanded and collapsed states."""
    if self._expanded:
        self.collapse()
    else:
        self.expand()

setTheme

setTheme(theme: str) -> None

Update the toggle icon color for the given theme.

Can be connected directly to a theme-change signal to keep the icon in sync with the application's color scheme.

PARAMETER DESCRIPTION
theme

The new theme ("dark" or "light").

TYPE: str

Source code in src/ezqt_widgets/widgets/misc/collapsible_section.py
def setTheme(self, theme: str) -> None:
    """Update the toggle icon color for the given theme.

    Can be connected directly to a theme-change signal to keep
    the icon in sync with the application's color scheme.

    Args:
        theme: The new theme (``"dark"`` or ``"light"``).
    """
    self._toggle_icon.setTheme(theme)

refreshStyle

refreshStyle() -> None

Refresh the widget style.

Useful after dynamic stylesheet changes.

Source code in src/ezqt_widgets/widgets/misc/collapsible_section.py
def refreshStyle(self) -> None:
    """Refresh the widget style.

    Useful after dynamic stylesheet changes.
    """
    self.style().unpolish(self)
    self.style().polish(self)
    self.update()