Widgets.py PySideAbdhUI — Custom Widget Collection
Widgets.py is a companion module to the PySideAbdhUI framework that provides a collection
of reusable custom widgets. Each widget extends a standard PySide6/Qt widget with additional
functionality tailored for the framework's UI patterns. The module contains four distinct
classes that serve different purposes within the application:
- StackedWidget — An animated page-navigation container that extends
QStackedWidgetwith smooth slide transitions, navigation history (back/forward/first/last), and an optional type-based deduplication system for pages. - Separator — A lightweight visual divider widget extending
QFrame, supporting horizontal or vertical orientation with customizable stroke width and color. - Label — A signal-emitting label extending
QLabelthat fires atextChangedsignal whenever its text content is modified, enabling reactive UI updates. - RingProgress — A fully custom-painted circular progress indicator extending
QWidget, featuring a configurable ring arc, background track, percentage text overlay, and customizable colors and dimensions. - SearchBox — An animated search input extending
QLineEditthat collapses to a compact icon when unfocused and smoothly expands on focus, using a custom Qt property animation.
Together, these widgets form the building blocks used by the AbdhWindow class and other
framework components. They are designed to be self-contained, styleable through Qt style sheets,
and composable within the framework's layout system.
Dependencies
The module imports from both the standard PySide6 library and an internal utility module. Understanding these dependencies is important for comprehending the full capabilities of each widget:
| Module | Components Used | Purpose |
|---|---|---|
PySide6.QtWidgets |
QLineEdit, QStackedWidget, QLabel, QWidget, QFrame |
Base classes that each custom widget inherits from. |
PySide6.QtCore |
Signal, QPropertyAnimation, QRect, QEasingCurve, QParallelAnimationGroup, Qt, Property |
Signal/slot system, animation framework, geometry types, custom property system for animations. |
PySide6.QtGui |
QAction, QIcon, QPainter, QPen, QColor, QFont |
Custom painting (RingProgress), action/icon system (SearchBox), color and font handling. |
.utils |
get_icon |
Internal utility for loading SVG/icon resources by name, used in SearchBox. |
StackedWidget
StackedWidget
QStackedWidget
An animated page-navigation container that extends QStackedWidget with smooth
horizontal slide transitions between pages. Instead of the default instant-swap behavior
of QStackedWidget, this class animates the current page sliding out while the next
page slides in from the opposite direction, using QPropertyAnimation on each
widget's geometry property. The direction of the slide (left or right) is determined
by whether the target index is higher or lower than the current index.
The widget also provides a full navigation API: go_next, go_back,
go_first, go_last, and goto_index. A add_page
method handles page insertion with an optional deduplication feature that removes existing
pages of the same Python type before adding the new one. An animating flag prevents
overlapping animations, ensuring that rapid navigation clicks do not cause visual glitches.
Constructor
StackedWidget.__init__(self)
Public
Initializes the stacked widget by calling super().__init__(), then configures
animation and visual properties. The CSS class stack is assigned via
setProperty('class', 'stack') for external stylesheet targeting. A solid white
background is set using setAutoFillBackground(True) with a white palette color
to prevent flickering during animated transitions — without this, the transparent
background of the parent would be visible between page slides, causing a visual glitch.
| Attribute | Type | Default | Description |
|---|---|---|---|
animation_duration | int | 400 | Duration in milliseconds for slide-in/slide-out animations. |
animating | bool | False | Guard flag that prevents overlapping animations. Set to True during animation and reset in the completion callback. |
target_index | int | 0 | The destination index for the current animation. Used by the completion callback to set the final widget index. |
Navigation API
add_page(self, page: QWidget, allow_same_tyoes: bool = True)
Public
Adds a page widget to the stacked container and automatically navigates to it.
The method supports an optional deduplication feature: when allow_same_tyoes
is False, it scans all existing pages and removes any widget that has the
same Python type as the new page before inserting it. This is useful for ensuring
that only one instance of a particular page type exists at any time — for example,
preventing multiple "Settings" or "Profile" pages from accumulating in the stack.
When deduplication is active, the scan iterates backwards through the widget list
to safely remove items without invalidating indices. Removed widgets are deleted
via deleteLater() to ensure proper cleanup. The new page is given
setAutoFillBackground(True) to prevent flickering during slide animations.
After adding, go_last() is called to navigate to the newly added page with
a slide animation.
| Parameter | Type | Default | Description |
|---|---|---|---|
page | QWidget | — | The page widget to add to the stack. |
allow_same_tyoes | bool | True | If False, removes any existing widget of the same type before adding. Note: the parameter name contains a typo ("tyoes" instead of "types"). |
Known Issue: The parameter name allow_same_tyoes contains a typo — it should be allow_same_types. Additionally, when allow_same_tyoes is False, the code contains a nested loop bug: the outer for i in range(self.count()) loop does nothing because the inner loop immediately shadows the variable i and performs the actual removal. The outer loop's body has no effect.
go_next(self)
Public
Navigates to the next page in the stack (current index + 1). If the current page
is already the last one, no action is taken — the boundary check new_index < self.count()
prevents out-of-bounds navigation. The transition is animated.
go_back(self)
Public
Navigates to the previous page in the stack (current index - 1). If the current
page is already the first one, no action is taken — the boundary check
new_index >= 0 prevents out-of-bounds navigation. The transition is animated.
goto_index(self, index: int)
Public
Navigates directly to the page at the specified index with an animated transition.
Boundary validation is handled by setCurrentIndexAnimated().
| Parameter | Type | Description |
|---|---|---|
index | int | The zero-based index of the target page. |
go_last(self)
Public
Navigates to the last page in the stack (self.count() - 1). This is
called automatically by add_page() after a new page is inserted.
go_first(self)
Public
Navigates to the first page in the stack (index 0) with an animated transition.
Animation System
The slide transition system is the core feature of StackedWidget. It replaces the
default instant-swap behavior of QStackedWidget with a smooth horizontal slide
animation. The system works by manually managing widget geometry through
QPropertyAnimation, positioning the incoming page off-screen and animating both
pages simultaneously in a QParallelAnimationGroup.
setCurrentIndexAnimated(self, index: int)
Public
Initiates an animated transition to the page at the specified index. This method performs three guard checks before proceeding:
- Index bounds: If
index < 0orindex >= self.count(), the call is ignored. - Same index: If the target index equals the current index, no animation is needed.
- Animation guard: If
self.animatingisTrue, the call is ignored to prevent overlapping animations.
Once the guards pass, the current widget is hidden, the direction is determined
(+1 for forward navigation, -1 for backward), and the animation
is delegated to setCurrentWidgetAnimated().
| Parameter | Type | Description |
|---|---|---|
index | int | The target page index to navigate to. |
setCurrentWidgetAnimated(self, next_widget: QWidget, direction: int = -1)
Public
Performs the actual animated slide transition between the current widget and the
next widget. The method sets up two parallel QPropertyAnimation objects
on the geometry property of each widget:
- Outgoing animation: The current widget starts at
QRect(0, 0, width, height)and slides toQRect(-width*direction, 0, width, height). Whendirection = +1(forward), it slides left; whendirection = -1(backward), it slides right. - Incoming animation: The next widget starts at
QRect(width*direction, 0, width, height)(off-screen) and slides toQRect(0, 0, width, height)(center).
Both animations run in a QParallelAnimationGroup with OutCubic
easing over animation_duration (400ms). The animating flag is set
to True before starting and reset in _on_animation_finished().
The next widget is raised to the top of the Z-order with raise_() so it
paints above the outgoing widget during the transition.
| Parameter | Type | Default | Description |
|---|---|---|---|
next_widget | QWidget | — | The widget to animate into view. |
direction | int | -1 | Slide direction: +1 for forward (right), -1 for backward (left). The default is -1, which means direct calls slide from left by default. |
Note: The current widget is hidden with hide() at the start of the animation. This is slightly unusual — the widget is hidden but its geometry is still being animated. Since the animation operates on the widget's geometry property directly, it continues to animate even though the widget is not visible. The visual effect is that only the incoming widget is visible during the transition, which may cause the background to show through.
_on_animation_finished(self)
Private
Callback invoked when the parallel animation group finishes. It performs the following cleanup steps:
- Set final index: Calls
self.setCurrentIndex(self.target_index)to update the internal state to the target page. - Reset animation flag: Sets
self.animating = Falseto allow new navigation operations. - Clean up animation group: Calls
self.animation_group.deleteLater()to free the animation objects. - Reset widget geometry: Explicitly sets the current widget's geometry to fill the entire stacked widget area, calls
updateGeometry()andadjustSize(), and activates the widget's layout to ensure proper sizing after the animation.
This cleanup is essential because the animation manipulates geometry directly, which can leave widgets in an inconsistent layout state if not properly reset.
Resize Handling
resizeEvent(self, event)
Override
Overrides the default resize event to ensure the current page's layout is
re-activated when the stacked widget is resized. This is necessary because
during an animated transition, widget geometries are managed manually and may
not automatically adjust to the new container size. When the animation is not
running (not self.animating), the current widget's layout is explicitly
activated via layout.activate().
Tip: A commented-out line #current.setGeometry(0, 0, self.width(), self.height()) exists in the source, suggesting that direct geometry setting was previously used but replaced with layout activation for a more flexible approach that respects size policies and stretch factors.
Separator
Separator
QFrame
A simple visual divider widget used to create horizontal or vertical separator lines
in the UI. It extends QFrame and uses the built-in HLine or
VLine frame shapes for native rendering, then applies a custom style sheet
to control the line's color and thickness. The separator is configured with
Plain frame shadow (no 3D effect) for a flat, modern appearance.
Separator.__init__(self, orientation: str = 'horizontal', stroke: int = 1, color: str = '#888888D1', parent=None)
Public
Creates a separator line with the specified orientation, stroke width, and color.
The orientation determines whether a horizontal (HLine) or vertical
(VLine) frame shape is used. The color is applied via a style sheet that
sets both color and background-color properties, and constrains
the maximum height to the stroke width in pixels for horizontal separators.
| Parameter | Type | Default | Description |
|---|---|---|---|
orientation | str | 'horizontal' | Line direction. Accepts 'horizontal' (HLine) or any other value (VLine). |
stroke | int | 1 | Line thickness in pixels. Applied as max-height in the stylesheet and as lineWidth. |
color | str | '#888888D1' | CSS color string for the line. Supports hex with alpha (#RRGGBBAA format). The default is a semi-transparent gray. |
parent | QWidget | None | Optional parent widget. |
Note: The default color #888888D1 uses 8-digit hex with an alpha channel (D1 = ~82% opacity), giving the separator a subtle, non-intrusive appearance that works well against both light and dark backgrounds.
Label
Label
QLabel
A signal-emitting extension of QLabel that adds a textChanged
signal. Standard QLabel does not emit any signal when its text is changed
programmatically, which makes it difficult to implement reactive UI patterns where
other widgets need to respond to label text updates. The Label class solves
this by overriding setText() to compare the new text with the current text
and emit the textChanged signal only when the text actually changes, avoiding
unnecessary signal emissions for redundant updates.
Label.__init__(self, text: str = '')
Public
Initializes the label with optional text. The text is set via super().setText(text)
in the constructor rather than passed to QLabel.__init__() to ensure
consistency with the overridden setText() method.
| Parameter | Type | Default | Description |
|---|---|---|---|
text | str | '' | Initial text content for the label. |
textChanged = Signal(str)
Signal
A custom PySide6 signal that is emitted whenever the label's text is changed via
setText(). The signal carries the new text as a str parameter,
allowing connected slots to react to the updated content. The signal is only emitted
when the new text differs from the current text, preventing infinite loops in
bidirectional data binding scenarios.
setText(self, text: str)
Override
Overrides QLabel.setText() to add change detection. The method compares the
new text with the current text returned by self.text(). If they differ, it
calls super().setText(text) to update the label and then emits the
textChanged signal with the new text. If the text is the same, no action
is taken, which prevents unnecessary signal emissions and potential infinite loops
in signal-slot chains.
| Parameter | Type | Description |
|---|---|---|
text | str | The new text to set on the label. |
RingProgress
RingProgress
QWidget
A custom-painted circular progress indicator that renders a ring-shaped arc showing
the current progress value as a percentage. Unlike slider or progress bar widgets
provided by Qt, RingProgress draws its entire visual representation using
QPainter, giving full control over the appearance. The widget consists of
three visual layers: a background ring (the track), a colored progress arc, and an
optional percentage text label rendered in the center.
All visual properties — ring color, background color, text color, ring width, and
font size — are configurable through setter methods. Calling any setter triggers
self.update() to schedule a repaint, ensuring the visual state always
reflects the current property values.
Constructor
RingProgress.__init__(self, parent=None)
Public
Initializes the progress ring with default property values. The minimum widget size is set to 120×120 pixels to ensure the ring is always large enough to be visually meaningful and to prevent layout issues where the widget might be squeezed to zero size.
| Attribute | Type | Default | Description |
|---|---|---|---|
_value | int | 0 | Current progress value, clamped between _min and _max. |
_min | int | 0 | Minimum value in the progress range. |
_max | int | 100 | Maximum value in the progress range. |
_ring_color | QColor | #4CAF50 | Color of the progress arc (Material Design green). |
_bg_color | QColor | #E0E0E0 | Color of the background ring track (light gray). |
_text_color | QColor | #333333 | Color of the percentage text overlay (dark gray). |
_ring_width | int | 10 | Width of the ring arc in pixels. |
_show_text | bool | True | Whether the percentage text is displayed in the center. |
_font_size | int | 20 | Point size of the percentage text font. |
Methods
setValue(self, val: int)
Public
Sets the current progress value. The value is clamped to the valid range
[_min, _max] using max(self._min, min(self._max, val)).
After setting, self.update() is called to schedule a repaint of the widget.
| Parameter | Type | Description |
|---|---|---|
val | int | The new progress value. Will be clamped to the current range. |
value(self) -> int
Public
Returns the current progress value.
int — The current progress value.setRange(self, min_val: int, max_val: int)
Public
Sets the minimum and maximum values for the progress range. This changes the
scale against which the current value is measured. For example, setting
setRange(0, 50) means a value of 25 represents 50% progress. After
setting, self.update() schedules a repaint.
| Parameter | Type | Description |
|---|---|---|
min_val | int | The minimum value of the range. |
max_val | int | The maximum value of the range. |
setRingColor(self, color)
Public
Sets the color of the progress arc. The input is converted to a QColor
object, which accepts CSS color strings ('#FF5722', 'red',
'rgb(255,87,34)') or existing QColor objects.
| Parameter | Type | Description |
|---|---|---|
color | str or QColor | The new color for the progress arc. |
setBackgroundColor(self, color)
Public
Sets the color of the background ring track that the progress arc fills over. This is typically a muted or semi-transparent color that provides visual context for the unfilled portion of the ring.
| Parameter | Type | Description |
|---|---|---|
color | str or QColor | The new color for the background track. |
setRingWidth(self, width: int)
Public
Sets the stroke width of the ring arc in pixels. Larger values create a thicker
ring, while smaller values create a thinner one. The ring width also affects the
inset of the arc from the widget boundary — the arc is drawn inside the widget
rectangle with a margin equal to _ring_width on all sides.
| Parameter | Type | Description |
|---|---|---|
width | int | The new ring width in pixels. |
setShowText(self, visible: bool)
Public
Controls whether the percentage text is rendered in the center of the ring.
When False, only the arc is drawn, creating a clean, icon-style progress
indicator. When True, the current value followed by a percent sign is
rendered at the center.
| Parameter | Type | Description |
|---|---|---|
visible | bool | Whether to show the percentage text overlay. |
Custom Painting
paintEvent(self, event)
Override
Renders the ring progress indicator using QPainter. The painting process
consists of three layers, drawn in order:
1. Background Ring (Track)
A full 360-degree arc is drawn using the _bg_color pen with
RoundCap style for smooth endpoints. The arc rectangle is the widget
rectangle inset by _ring_width on all sides, ensuring the ring does not
overflow the widget boundary. The arc is drawn with drawArc(rect, 0, 360*16)
— Qt uses 1/16th of a degree as the unit for arc angles.
2. Progress Arc
The progress fraction is calculated as (_value - _min) / (_max - _min),
producing a value between 0.0 and 1.0. This fraction is multiplied by 360×16
to get the span angle in Qt's 1/16-degree units. The progress arc is drawn starting
from 90×16 (the 12 o'clock position) with a negative span angle, meaning it
progresses clockwise. If _max == _min, the fraction defaults to 0 to
prevent division by zero.
3. Percentage Text
If _show_text is True, the current value followed by a
percent sign (f"{_value}%") is rendered at the center of the widget
using drawText(self.rect(), Qt.AlignCenter, text). The font size is
controlled by _font_size.
SearchBox
SearchBox
QLineEdit
An animated search input that collapses to a compact icon when unfocused and
smoothly expands to a full-width input field when focused. This pattern is common
in modern UIs where search functionality should be accessible but not consume
valuable horizontal space when not in use. The animation is implemented using a
custom Qt property (expandingWidth) that is animated by
QPropertyAnimation, allowing the width change to be smoothly interpolated
rather than jumping instantly between states.
The search box features a leading search icon (loaded via get_icon("search")),
placeholder text that is only visible when expanded, and a built-in clear button
provided by setClearButtonEnabled(True). When the user types text and
then clicks away, the box remains expanded — it only collapses if the field is
empty, preserving the user's input.
Constructor
SearchBox.__init__(self, parent=None, collapsed_width: int = 32, expanded_width: int = 200, duration: int = 300)
Public
Initializes the search box with configurable collapsed and expanded widths, and
animation duration. The widget starts in its collapsed state (fixed width =
collapsed_width). A QAction with a search icon is added at
the leading position, and placeholder text "Search…" is set. A reusable
QPropertyAnimation targeting the custom expandingWidth property
is created with OutCubic easing for a natural deceleration effect.
| Parameter | Type | Default | Description |
|---|---|---|---|
parent | QWidget | None | Optional parent widget. |
collapsed_width | int | 32 | Width in pixels when the search box is collapsed (icon-only). Typically just enough to show the search icon. |
expanded_width | int | 200 | Width in pixels when the search box is expanded (full input). Large enough for comfortable text entry. |
duration | int | 300 | Duration of the expand/collapse animation in milliseconds. |
Custom Property: expandingWidth
expandingWidth = Property(int, getExpandingWidth, setExpandingWidth)
Property
A custom Qt property that allows QPropertyAnimation to animate the
widget's width. Qt's property animation system requires the target property to be
defined as a Property with getter and setter methods. The
expandingWidth property wraps the widget's width() and
setFixedWidth() calls, making it animatable.
Getter: getExpandingWidth(self) -> int
Returns the current width of the widget via self.width().
Setter: setExpandingWidth(self, width: int)
Sets the widget's fixed width to the given value via self.setFixedWidth(width).
Using setFixedWidth ensures the widget does not change size during the
animation due to layout constraints.
Focus-Driven Animation
focusInEvent(self, event)
Override
When the search box receives focus (clicked or tabbed into), it animates
from the collapsed width to the expanded width. The animation uses the reusable
_animation object with OutCubic easing, creating a smooth
expansion that decelerates as it reaches the target width.
focusOutEvent(self, event)
Override
When the search box loses focus, it conditionally collapses back to the collapsed
width. The collapse only occurs if the text field is empty (checked via
self.text().strip()). If the user has typed text, the search box
remains expanded to keep the entered content visible. This is a deliberate UX
decision — collapsing with text would hide the user's input and create confusion.
_animate_to(self, target_width: int)
Private
Core animation method that drives the width transition. It stops any currently
running animation on the reusable _animation object, sets the start
value to the current width and the end value to the target width, then starts
the animation. Stopping the previous animation before starting a new one prevents
conflicts if the user rapidly focuses and unfocuses the widget.
| Parameter | Type | Description |
|---|---|---|
target_width | int | The width to animate to (either _collapsed_width or _expanded_width). |
Full Method Index
| Class | Method | Visibility | Brief Description |
|---|---|---|---|
| StackedWidget | __init__ | Public | Initialize with animation config and white background |
add_page | Public | Add page with optional type deduplication | |
go_next | Public | Navigate to next page (animated) | |
go_back | Public | Navigate to previous page (animated) | |
goto_index | Public | Navigate to specific index (animated) | |
go_last | Public | Navigate to last page (animated) | |
go_first | Public | Navigate to first page (animated) | |
setCurrentIndexAnimated | Public | Initiate animated transition by index | |
setCurrentWidgetAnimated | Public | Perform parallel slide animation | |
_on_animation_finished | Private | Finalize animation, reset geometry | |
| Separator | __init__ | Public | Create horizontal/vertical line with custom style |
| Label | __init__ | Public | Initialize label with optional text |
setText | Override | Set text and emit textChanged signal on change | |
| RingProgress | __init__ | Public | Initialize with default colors and dimensions |
setValue | Public | Set progress value (clamped to range) | |
value | Public | Get current progress value | |
setRange | Public | Set min/max range for progress scale | |
setRingColor | Public | Set progress arc color | |
setBackgroundColor | Public | Set background track color | |
setRingWidth | Public | Set ring stroke width in pixels | |
setShowText | Public | Toggle percentage text visibility | |
paintEvent | Override | Custom paint: background ring, progress arc, text | |
| SearchBox | __init__ | Public | Initialize with collapsed/expanded widths |
getExpandingWidth | Public | Getter for custom property (returns width) | |
setExpandingWidth | Public | Setter for custom property (sets fixed width) | |
focusInEvent | Override | Expand on focus | |
focusOutEvent | Override | Collapse on unfocus (if empty) | |
_animate_to | Private | Animate width to target value |