Skip to content

API Reference

This page contains the automatic API reference for the qextrawidgets package.

Core

Regexs

Utils

QColorUtils

Utility class for color-related operations.

Source code in source/qextrawidgets/core/utils/color_utils.py
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class QColorUtils:
    """Utility class for color-related operations."""

    @staticmethod
    def getContrastingTextColor(bg_color: QColor) -> QColor:
        """Returns Qt.black or Qt.white depending on the background color luminance.

        Formula based on human perception (NTSC conversion formula).

        Args:
            bg_color (QColor): Background color to calculate contrast against.

        Returns:
            QColor: Contrasting text color (Black or White).
        """
        r = bg_color.red()
        g = bg_color.green()
        b = bg_color.blue()

        # Calculate weighted brightness
        # 0.299R + 0.587G + 0.114B
        luminance = (0.299 * r) + (0.587 * g) + (0.114 * b)

        # Common threshold is 128 (half of 255).
        # If brighter than 128, background is light -> Black Text
        # If darker, background is dark -> White Text
        return QColor(Qt.GlobalColor.black) if luminance > 128 else QColor(Qt.GlobalColor.white)

getContrastingTextColor(bg_color) staticmethod

Returns Qt.black or Qt.white depending on the background color luminance.

Formula based on human perception (NTSC conversion formula).

Parameters:

Name Type Description Default
bg_color QColor

Background color to calculate contrast against.

required

Returns:

Name Type Description
QColor QColor

Contrasting text color (Black or White).

Source code in source/qextrawidgets/core/utils/color_utils.py
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@staticmethod
def getContrastingTextColor(bg_color: QColor) -> QColor:
    """Returns Qt.black or Qt.white depending on the background color luminance.

    Formula based on human perception (NTSC conversion formula).

    Args:
        bg_color (QColor): Background color to calculate contrast against.

    Returns:
        QColor: Contrasting text color (Black or White).
    """
    r = bg_color.red()
    g = bg_color.green()
    b = bg_color.blue()

    # Calculate weighted brightness
    # 0.299R + 0.587G + 0.114B
    luminance = (0.299 * r) + (0.587 * g) + (0.114 * b)

    # Common threshold is 128 (half of 255).
    # If brighter than 128, background is light -> Black Text
    # If darker, background is dark -> White Text
    return QColor(Qt.GlobalColor.black) if luminance > 128 else QColor(Qt.GlobalColor.white)

QEmojiFinder

Utility class for finding emojis and aliases in text using QRegularExpression.

Source code in source/qextrawidgets/core/utils/emoji_finder.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class QEmojiFinder:
    """Utility class for finding emojis and aliases in text using QRegularExpression."""
    @classmethod
    def findEmojis(cls, text: str) -> typing.Generator[QRegularExpressionMatch, None, None]:
        """Finds all Unicode emojis in the given text.

        Args:
            text (str): The text to scan.

        Yields:
            Generator[QRegularExpressionMatch]: Matches for each emoji found.
        """
        regex = QEmojiRegex()
        iterator = regex.globalMatch(text)
        while iterator.hasNext():
            yield iterator.next()

    @classmethod
    def findEmojiAliases(cls, text: str) -> typing.Generator[typing.Tuple[EmojiChar, QRegularExpressionMatch], None, None]:
        """Finds all text aliases (e.g., :smile:) in the given text.

        Args:
            text (str): The text to scan.

        Yields:
            Generator[Tuple[EmojiChar, QRegularExpressionMatch]]: Tuples of EmojiChar data and their matches.
        """
        regex = QRegularExpression(R"(:\w+:)")
        iterator = regex.globalMatch(text)
        while iterator.hasNext():
            match = iterator.next()
            first_captured = match.captured(0)
            alias = first_captured[1:-1]
            emoji = find_by_shortname(alias)
            if len(emoji) == 1:
                yield emoji[0], match

findEmojiAliases(text) classmethod

Finds all text aliases (e.g., :smile:) in the given text.

Parameters:

Name Type Description Default
text str

The text to scan.

required

Yields:

Type Description
Tuple[EmojiChar, QRegularExpressionMatch]

Generator[Tuple[EmojiChar, QRegularExpressionMatch]]: Tuples of EmojiChar data and their matches.

Source code in source/qextrawidgets/core/utils/emoji_finder.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@classmethod
def findEmojiAliases(cls, text: str) -> typing.Generator[typing.Tuple[EmojiChar, QRegularExpressionMatch], None, None]:
    """Finds all text aliases (e.g., :smile:) in the given text.

    Args:
        text (str): The text to scan.

    Yields:
        Generator[Tuple[EmojiChar, QRegularExpressionMatch]]: Tuples of EmojiChar data and their matches.
    """
    regex = QRegularExpression(R"(:\w+:)")
    iterator = regex.globalMatch(text)
    while iterator.hasNext():
        match = iterator.next()
        first_captured = match.captured(0)
        alias = first_captured[1:-1]
        emoji = find_by_shortname(alias)
        if len(emoji) == 1:
            yield emoji[0], match

findEmojis(text) classmethod

Finds all Unicode emojis in the given text.

Parameters:

Name Type Description Default
text str

The text to scan.

required

Yields:

Type Description
QRegularExpressionMatch

Generator[QRegularExpressionMatch]: Matches for each emoji found.

Source code in source/qextrawidgets/core/utils/emoji_finder.py
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@classmethod
def findEmojis(cls, text: str) -> typing.Generator[QRegularExpressionMatch, None, None]:
    """Finds all Unicode emojis in the given text.

    Args:
        text (str): The text to scan.

    Yields:
        Generator[QRegularExpressionMatch]: Matches for each emoji found.
    """
    regex = QEmojiRegex()
    iterator = regex.globalMatch(text)
    while iterator.hasNext():
        yield iterator.next()

QEmojiFonts

Utility class for loading and accessing emoji fonts.

Source code in source/qextrawidgets/core/utils/emoji_fonts.py
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class QEmojiFonts:
    """Utility class for loading and accessing emoji fonts."""

    TwemojiFontFamily = None

    @classmethod
    def loadTwemojiFont(cls) -> str:
        """Loads the bundled Twemoji font into the application font database.

        Returns:
            str: The loaded font family name.
        """
        if not cls.TwemojiFontFamily:
            root_folder_path = Path(__file__).parent.parent.parent
            fonts_folder_path = root_folder_path / "fonts"
            file_path = fonts_folder_path / "Twemoji-17.0.2.ttf"

            id_ = QFontDatabase.addApplicationFont(str(file_path))
            family = QFontDatabase.applicationFontFamilies(id_)[0]

            cls.TwemojiFontFamily = family

        return cls.TwemojiFontFamily

    @classmethod
    def twemojiFont(cls) -> QFont:
        """Returns a QFont object using the Twemoji font family.

        Returns:
            QFont: The Twemoji font.
        """
        return QFont(cls.loadTwemojiFont())

loadTwemojiFont() classmethod

Loads the bundled Twemoji font into the application font database.

Returns:

Name Type Description
str str

The loaded font family name.

Source code in source/qextrawidgets/core/utils/emoji_fonts.py
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@classmethod
def loadTwemojiFont(cls) -> str:
    """Loads the bundled Twemoji font into the application font database.

    Returns:
        str: The loaded font family name.
    """
    if not cls.TwemojiFontFamily:
        root_folder_path = Path(__file__).parent.parent.parent
        fonts_folder_path = root_folder_path / "fonts"
        file_path = fonts_folder_path / "Twemoji-17.0.2.ttf"

        id_ = QFontDatabase.addApplicationFont(str(file_path))
        family = QFontDatabase.applicationFontFamilies(id_)[0]

        cls.TwemojiFontFamily = family

    return cls.TwemojiFontFamily

twemojiFont() classmethod

Returns a QFont object using the Twemoji font family.

Returns:

Name Type Description
QFont QFont

The Twemoji font.

Source code in source/qextrawidgets/core/utils/emoji_fonts.py
30
31
32
33
34
35
36
37
@classmethod
def twemojiFont(cls) -> QFont:
    """Returns a QFont object using the Twemoji font family.

    Returns:
        QFont: The Twemoji font.
    """
    return QFont(cls.loadTwemojiFont())

QIconGenerator

Class responsible for generating Pixmaps and icons based on text/fonts.

Source code in source/qextrawidgets/core/utils/icon_generator.py
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
class QIconGenerator:
    """Class responsible for generating Pixmaps and icons based on text/fonts."""

    @staticmethod
    def calculateMaxPixelSize(text: str, font: QFont, target_size: QSize) -> int:
        """
        Calculates the maximum pixel size the font can have so the text fits within target_size.

        Args:
            text (str): Text to be measured.
            font (QFont): The font configuration (family, weight, italic).
            target_size (QSize): The available space.

        Returns:
            int: The calculated pixel size.
        """
        if not text:
            return 12  # safe fallback size

        # 1. Work with a copy to avoid altering the original font externally
        temp_font = QFont(font)

        # 2. Use an arbitrary large base size for calculation precision
        base_pixel_size = 100
        temp_font.setPixelSize(base_pixel_size)

        fm = QFontMetrics(temp_font)

        # 3. Get dimensions occupied by text at base size
        # horizontalAdvance: Total width including natural spacing
        base_width = fm.horizontalAdvance(text)
        # height: Total line height (Ascent + Descent).
        base_height = fm.height()

        if base_width == 0 or base_height == 0:
            return base_pixel_size

        # 4. Calculate scale ratio for each dimension
        width_ratio = target_size.width() / base_width
        height_ratio = target_size.height() / base_height

        # 5. The Limiting Factor is the SMALLEST ratio (to ensure it fits both width and height)
        final_scale_factor = min(width_ratio, height_ratio)

        # 6. Apply factor to base size
        new_pixel_size = int(base_pixel_size * final_scale_factor)

        # Returns at least 1 to avoid rendering errors
        return max(1, new_pixel_size)

    @classmethod
    def charToPixmap(
        cls,
        char: str,
        target_size: QSize,
        font: QFont = QFont("Arial"),
        color: QColor = QColor(Qt.GlobalColor.black),
    ) -> QPixmap:
        """
        Generates a QPixmap of a specific size containing a character rendered at the largest possible size.

        Args:
            char (str): The character to be rendered.
            target_size (QSize): The final image size (e.g., 64x64).
            font (QFont): The base font (will be resized internally).
            color (QColor): The text color.

        Returns:
            QPixmap: Transparent image with the character centered.
        """
        if target_size.isEmpty():
            return QPixmap()

        # 1. Calculate optimal font size to fill target_size
        optimal_size = cls.calculateMaxPixelSize(char, font, target_size)

        # 2. Configure font with calculated size
        render_font = QFont(font)
        render_font.setPixelSize(optimal_size)

        # 3. Create Pixmap with exact requested size
        pixmap = QPixmap(target_size)
        pixmap.fill(Qt.GlobalColor.transparent)

        # 4. Configure Painter
        painter = QPainter(pixmap)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)
        painter.setRenderHint(QPainter.RenderHint.TextAntialiasing)
        painter.setFont(render_font)
        painter.setPen(color)

        # 5. Draw text centered in Pixmap rectangle
        # Qt.AlignCenter handles X and Y positioning automatically
        rect = pixmap.rect()
        painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, char)

        painter.end()

        return pixmap

    @staticmethod
    def getCircularPixmap(pixmap: QPixmap, size: int, dpr: float = 1.0) -> QPixmap:
        """Creates a circular pixmap (center crop) with HiDPI support.

        Uses QStyle to calculate alignment for proper center cropping.

        Args:
            pixmap: Source pixmap to crop.
            size: Logical size of the output circular pixmap.
            dpr: Device Pixel Ratio for HiDPI displays (e.g., 1.0, 1.25, 2.0).

        Returns:
            QPixmap: Circular pixmap with transparent background.
        """
        if pixmap.isNull():
            return pixmap

        # 1. Configure physical size for high density (Retina/4K)
        physical_size = int(size * dpr)

        output = QPixmap(physical_size, physical_size)
        output.fill(Qt.GlobalColor.transparent)
        output.setDevicePixelRatio(dpr)

        # 2. Configure Painter
        painter = QPainter(output)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
        painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform, True)

        # 3. Apply circular clip path (using logical coordinates)
        path = QPainterPath()
        path.addEllipse(0, 0, size, size)
        painter.setClipPath(path)

        # 4. Calculate center crop using QStyle.alignedRect
        # Find the smallest side to create a square crop
        min_side = min(pixmap.width(), pixmap.height())
        crop_size = QSize(min_side, min_side)

        # QStyle automatically calculates centered rectangle within original image
        source_rect = QStyle.alignedRect(
            Qt.LayoutDirection.LeftToRight,
            Qt.AlignmentFlag.AlignCenter,
            crop_size,  # Square size we want to crop
            pixmap.rect(),  # Total rectangle of original image
        )

        # 5. Draw
        # source_rect (center of original) is drawn into target_rect (final circle)
        target_rect = QRect(0, 0, size, size)
        painter.drawPixmap(target_rect, pixmap, source_rect)

        painter.end()
        return output

    @staticmethod
    def createIconWithBackground(
        icon_name: str,
        background_color: str,
        size: int = 48,
        dpr: float = 1.0,
        icon_color: str = "white",
        scale_factor: float = 0.6,
    ) -> QPixmap:
        """Creates a high-quality (HiDPI) icon with circular background.

        Args:
            icon_name: QtAwesome icon name (e.g., 'fa5s.user').
            background_color: Background color in any Qt-supported format (e.g., '#FF5733', 'red').
            size: Logical desired size (e.g., 48).
            dpr: Device Pixel Ratio of the window (e.g., 1.0, 1.25, 2.0).
            icon_color: Icon foreground color.
            scale_factor: Icon size relative to background (0.0 to 1.0).

        Returns:
            QPixmap: High-quality pixmap with icon on circular background.
        """
        # 1. Calculate PHYSICAL size (actual pixels)
        # If size=48 and dpr=2 (4K/Retina display), create a 96x96 pixel image
        physical_width = int(size * dpr)
        physical_height = int(size * dpr)

        # 2. Create Pixmap with physical size
        final_pixmap = QPixmap(physical_width, physical_height)
        final_pixmap.fill(Qt.GlobalColor.transparent)

        # IMPORTANT: Tell the pixmap about its pixel density
        # This makes QPainter 'think' in logical coordinates (48x48)
        # while drawing in high resolution (96x96)
        final_pixmap.setDevicePixelRatio(dpr)

        # 3. Start painting
        painter = QPainter(final_pixmap)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)
        painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)

        # 4. Draw background (using logical coordinates 0..size)
        painter.setBrush(QColor(background_color))
        painter.setPen(Qt.PenStyle.NoPen)
        painter.drawEllipse(0, 0, size, size)

        # 5. Generate internal icon (QtAwesome)
        # Request high-resolution icon from qtawesome to ensure quality
        logical_icon_size = int(size * scale_factor)
        physical_icon_size = int(logical_icon_size * dpr)

        icon = qtawesome.icon(icon_name, color=icon_color)
        # Generate raw pixmap in high resolution
        icon_pixmap_high_resolution = icon.pixmap(
            physical_icon_size, physical_icon_size
        )

        # Set DPR on internal icon for proper alignment
        icon_pixmap_high_resolution.setDevicePixelRatio(dpr)

        # 6. Center with QStyle.alignedRect (using logical coordinates)
        centered_rect = QStyle.alignedRect(
            Qt.LayoutDirection.LeftToRight,
            Qt.AlignmentFlag.AlignCenter,
            QSize(logical_icon_size, logical_icon_size),  # Logical size
            QRect(0, 0, size, size),  # Logical area
        )

        # 7. Draw
        painter.drawPixmap(centered_rect, icon_pixmap_high_resolution)
        painter.end()

        return final_pixmap

calculateMaxPixelSize(text, font, target_size) staticmethod

Calculates the maximum pixel size the font can have so the text fits within target_size.

Parameters:

Name Type Description Default
text str

Text to be measured.

required
font QFont

The font configuration (family, weight, italic).

required
target_size QSize

The available space.

required

Returns:

Name Type Description
int int

The calculated pixel size.

Source code in source/qextrawidgets/core/utils/icon_generator.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
@staticmethod
def calculateMaxPixelSize(text: str, font: QFont, target_size: QSize) -> int:
    """
    Calculates the maximum pixel size the font can have so the text fits within target_size.

    Args:
        text (str): Text to be measured.
        font (QFont): The font configuration (family, weight, italic).
        target_size (QSize): The available space.

    Returns:
        int: The calculated pixel size.
    """
    if not text:
        return 12  # safe fallback size

    # 1. Work with a copy to avoid altering the original font externally
    temp_font = QFont(font)

    # 2. Use an arbitrary large base size for calculation precision
    base_pixel_size = 100
    temp_font.setPixelSize(base_pixel_size)

    fm = QFontMetrics(temp_font)

    # 3. Get dimensions occupied by text at base size
    # horizontalAdvance: Total width including natural spacing
    base_width = fm.horizontalAdvance(text)
    # height: Total line height (Ascent + Descent).
    base_height = fm.height()

    if base_width == 0 or base_height == 0:
        return base_pixel_size

    # 4. Calculate scale ratio for each dimension
    width_ratio = target_size.width() / base_width
    height_ratio = target_size.height() / base_height

    # 5. The Limiting Factor is the SMALLEST ratio (to ensure it fits both width and height)
    final_scale_factor = min(width_ratio, height_ratio)

    # 6. Apply factor to base size
    new_pixel_size = int(base_pixel_size * final_scale_factor)

    # Returns at least 1 to avoid rendering errors
    return max(1, new_pixel_size)

charToPixmap(char, target_size, font=QFont('Arial'), color=QColor(Qt.GlobalColor.black)) classmethod

Generates a QPixmap of a specific size containing a character rendered at the largest possible size.

Parameters:

Name Type Description Default
char str

The character to be rendered.

required
target_size QSize

The final image size (e.g., 64x64).

required
font QFont

The base font (will be resized internally).

QFont('Arial')
color QColor

The text color.

QColor(black)

Returns:

Name Type Description
QPixmap QPixmap

Transparent image with the character centered.

Source code in source/qextrawidgets/core/utils/icon_generator.py
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
@classmethod
def charToPixmap(
    cls,
    char: str,
    target_size: QSize,
    font: QFont = QFont("Arial"),
    color: QColor = QColor(Qt.GlobalColor.black),
) -> QPixmap:
    """
    Generates a QPixmap of a specific size containing a character rendered at the largest possible size.

    Args:
        char (str): The character to be rendered.
        target_size (QSize): The final image size (e.g., 64x64).
        font (QFont): The base font (will be resized internally).
        color (QColor): The text color.

    Returns:
        QPixmap: Transparent image with the character centered.
    """
    if target_size.isEmpty():
        return QPixmap()

    # 1. Calculate optimal font size to fill target_size
    optimal_size = cls.calculateMaxPixelSize(char, font, target_size)

    # 2. Configure font with calculated size
    render_font = QFont(font)
    render_font.setPixelSize(optimal_size)

    # 3. Create Pixmap with exact requested size
    pixmap = QPixmap(target_size)
    pixmap.fill(Qt.GlobalColor.transparent)

    # 4. Configure Painter
    painter = QPainter(pixmap)
    painter.setRenderHint(QPainter.RenderHint.Antialiasing)
    painter.setRenderHint(QPainter.RenderHint.TextAntialiasing)
    painter.setFont(render_font)
    painter.setPen(color)

    # 5. Draw text centered in Pixmap rectangle
    # Qt.AlignCenter handles X and Y positioning automatically
    rect = pixmap.rect()
    painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, char)

    painter.end()

    return pixmap

createIconWithBackground(icon_name, background_color, size=48, dpr=1.0, icon_color='white', scale_factor=0.6) staticmethod

Creates a high-quality (HiDPI) icon with circular background.

Parameters:

Name Type Description Default
icon_name str

QtAwesome icon name (e.g., 'fa5s.user').

required
background_color str

Background color in any Qt-supported format (e.g., '#FF5733', 'red').

required
size int

Logical desired size (e.g., 48).

48
dpr float

Device Pixel Ratio of the window (e.g., 1.0, 1.25, 2.0).

1.0
icon_color str

Icon foreground color.

'white'
scale_factor float

Icon size relative to background (0.0 to 1.0).

0.6

Returns:

Name Type Description
QPixmap QPixmap

High-quality pixmap with icon on circular background.

Source code in source/qextrawidgets/core/utils/icon_generator.py
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
@staticmethod
def createIconWithBackground(
    icon_name: str,
    background_color: str,
    size: int = 48,
    dpr: float = 1.0,
    icon_color: str = "white",
    scale_factor: float = 0.6,
) -> QPixmap:
    """Creates a high-quality (HiDPI) icon with circular background.

    Args:
        icon_name: QtAwesome icon name (e.g., 'fa5s.user').
        background_color: Background color in any Qt-supported format (e.g., '#FF5733', 'red').
        size: Logical desired size (e.g., 48).
        dpr: Device Pixel Ratio of the window (e.g., 1.0, 1.25, 2.0).
        icon_color: Icon foreground color.
        scale_factor: Icon size relative to background (0.0 to 1.0).

    Returns:
        QPixmap: High-quality pixmap with icon on circular background.
    """
    # 1. Calculate PHYSICAL size (actual pixels)
    # If size=48 and dpr=2 (4K/Retina display), create a 96x96 pixel image
    physical_width = int(size * dpr)
    physical_height = int(size * dpr)

    # 2. Create Pixmap with physical size
    final_pixmap = QPixmap(physical_width, physical_height)
    final_pixmap.fill(Qt.GlobalColor.transparent)

    # IMPORTANT: Tell the pixmap about its pixel density
    # This makes QPainter 'think' in logical coordinates (48x48)
    # while drawing in high resolution (96x96)
    final_pixmap.setDevicePixelRatio(dpr)

    # 3. Start painting
    painter = QPainter(final_pixmap)
    painter.setRenderHint(QPainter.RenderHint.Antialiasing)
    painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)

    # 4. Draw background (using logical coordinates 0..size)
    painter.setBrush(QColor(background_color))
    painter.setPen(Qt.PenStyle.NoPen)
    painter.drawEllipse(0, 0, size, size)

    # 5. Generate internal icon (QtAwesome)
    # Request high-resolution icon from qtawesome to ensure quality
    logical_icon_size = int(size * scale_factor)
    physical_icon_size = int(logical_icon_size * dpr)

    icon = qtawesome.icon(icon_name, color=icon_color)
    # Generate raw pixmap in high resolution
    icon_pixmap_high_resolution = icon.pixmap(
        physical_icon_size, physical_icon_size
    )

    # Set DPR on internal icon for proper alignment
    icon_pixmap_high_resolution.setDevicePixelRatio(dpr)

    # 6. Center with QStyle.alignedRect (using logical coordinates)
    centered_rect = QStyle.alignedRect(
        Qt.LayoutDirection.LeftToRight,
        Qt.AlignmentFlag.AlignCenter,
        QSize(logical_icon_size, logical_icon_size),  # Logical size
        QRect(0, 0, size, size),  # Logical area
    )

    # 7. Draw
    painter.drawPixmap(centered_rect, icon_pixmap_high_resolution)
    painter.end()

    return final_pixmap

getCircularPixmap(pixmap, size, dpr=1.0) staticmethod

Creates a circular pixmap (center crop) with HiDPI support.

Uses QStyle to calculate alignment for proper center cropping.

Parameters:

Name Type Description Default
pixmap QPixmap

Source pixmap to crop.

required
size int

Logical size of the output circular pixmap.

required
dpr float

Device Pixel Ratio for HiDPI displays (e.g., 1.0, 1.25, 2.0).

1.0

Returns:

Name Type Description
QPixmap QPixmap

Circular pixmap with transparent background.

Source code in source/qextrawidgets/core/utils/icon_generator.py
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
@staticmethod
def getCircularPixmap(pixmap: QPixmap, size: int, dpr: float = 1.0) -> QPixmap:
    """Creates a circular pixmap (center crop) with HiDPI support.

    Uses QStyle to calculate alignment for proper center cropping.

    Args:
        pixmap: Source pixmap to crop.
        size: Logical size of the output circular pixmap.
        dpr: Device Pixel Ratio for HiDPI displays (e.g., 1.0, 1.25, 2.0).

    Returns:
        QPixmap: Circular pixmap with transparent background.
    """
    if pixmap.isNull():
        return pixmap

    # 1. Configure physical size for high density (Retina/4K)
    physical_size = int(size * dpr)

    output = QPixmap(physical_size, physical_size)
    output.fill(Qt.GlobalColor.transparent)
    output.setDevicePixelRatio(dpr)

    # 2. Configure Painter
    painter = QPainter(output)
    painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
    painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform, True)

    # 3. Apply circular clip path (using logical coordinates)
    path = QPainterPath()
    path.addEllipse(0, 0, size, size)
    painter.setClipPath(path)

    # 4. Calculate center crop using QStyle.alignedRect
    # Find the smallest side to create a square crop
    min_side = min(pixmap.width(), pixmap.height())
    crop_size = QSize(min_side, min_side)

    # QStyle automatically calculates centered rectangle within original image
    source_rect = QStyle.alignedRect(
        Qt.LayoutDirection.LeftToRight,
        Qt.AlignmentFlag.AlignCenter,
        crop_size,  # Square size we want to crop
        pixmap.rect(),  # Total rectangle of original image
    )

    # 5. Draw
    # source_rect (center of original) is drawn into target_rect (final circle)
    target_rect = QRect(0, 0, size, size)
    painter.drawPixmap(target_rect, pixmap, source_rect)

    painter.end()
    return output

QSystemUtils

Utilities related to system and application settings.

Source code in source/qextrawidgets/core/utils/system_utils.py
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class QSystemUtils:
    """Utilities related to system and application settings."""

    @staticmethod
    def isDarkMode() -> bool:
        """
        Checks if the application is running in dark mode.

        Returns:
            bool: True if dark mode is active, False otherwise.
        """
        style_hints = QApplication.styleHints()
        color_scheme = style_hints.colorScheme()
        return color_scheme.value == 2

    @staticmethod
    def applyDarkMode():
        """Applies a generic dark palette."""
        QGuiApplication.styleHints().setColorScheme(Qt.ColorScheme.Dark)

    @staticmethod
    def applyLightMode():
        """Restores the default system palette (Light)."""
        # Using the default Fusion style palette is usually a clean light palette
        QGuiApplication.styleHints().setColorScheme(Qt.ColorScheme.Light)

applyDarkMode() staticmethod

Applies a generic dark palette.

Source code in source/qextrawidgets/core/utils/system_utils.py
20
21
22
23
@staticmethod
def applyDarkMode():
    """Applies a generic dark palette."""
    QGuiApplication.styleHints().setColorScheme(Qt.ColorScheme.Dark)

applyLightMode() staticmethod

Restores the default system palette (Light).

Source code in source/qextrawidgets/core/utils/system_utils.py
25
26
27
28
29
@staticmethod
def applyLightMode():
    """Restores the default system palette (Light)."""
    # Using the default Fusion style palette is usually a clean light palette
    QGuiApplication.styleHints().setColorScheme(Qt.ColorScheme.Light)

isDarkMode() staticmethod

Checks if the application is running in dark mode.

Returns:

Name Type Description
bool bool

True if dark mode is active, False otherwise.

Source code in source/qextrawidgets/core/utils/system_utils.py
 8
 9
10
11
12
13
14
15
16
17
18
@staticmethod
def isDarkMode() -> bool:
    """
    Checks if the application is running in dark mode.

    Returns:
        bool: True if dark mode is active, False otherwise.
    """
    style_hints = QApplication.styleHints()
    color_scheme = style_hints.colorScheme()
    return color_scheme.value == 2

QTwemojiImageProvider

Utility class for loading, resizing, and caching emoji images.

Source code in source/qextrawidgets/core/utils/twemoji_image_provider.py
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
class QTwemojiImageProvider:
    """Utility class for loading, resizing, and caching emoji images."""

    @staticmethod
    def getPixmap(emoji: str, margin: int, size: int, dpr: float = 1.0, source_format: str = "png") -> QPixmap:
        """Loads and returns a colorized or processed emoji pixmap.

        Uses caching to improve performance on subsequent calls.

        Args:
            emoji (str): Emoji character.
            margin (int): Margin around the emoji in pixels.
            size (int): Target logical size.
            dpr (float, optional): Device pixel ratio. Defaults to 1.0.
            source_format (str, optional): Image format (png or svg). Defaults to "png".

        Returns:
            QPixmap: The processed pixmap.
        """

        # 1. Calculate real physical size (pixels)
        target_size = int(size * dpr)

        # 2. Generate unique key for Cache
        cache_url = QTwemojiImageProvider.getUrl(char_to_unified(emoji), margin, size, dpr, source_format)

        # 3. Try to fetch from Cache
        pixmap = QPixmap()
        if QPixmapCache.find(cache_url.toString(), pixmap):
            return pixmap

        # --- CACHE MISS (Load from disk) ---

        # 4. Load using QImageReader (more efficient than QPixmap(path))
        emoji_path = str(get_emoji_path(emoji, source_format))
        if not emoji_path:
            # Fallback (Returns a transparent pixmap or placeholder in case of error)
            fallback = QPixmap(target_size, target_size)
            fallback.fill(Qt.GlobalColor.transparent)
            fallback.setDevicePixelRatio(dpr)
            return fallback

        reader = QImageReader(emoji_path)

        if reader.canRead():
            # Important for SVG: Define render size before reading
            reader.setScaledSize(QSize(target_size, target_size))

            image = reader.read()
            if not image.isNull():
                pixmap = QPixmap.fromImage(image)
                pixmap.setDevicePixelRatio(dpr)

                # Apply margin
                if margin > 0:
                    final_size = int((size + (margin * 2)) * dpr)
                    final_pixmap = QPixmap(final_size, final_size)
                    final_pixmap.setDevicePixelRatio(dpr)
                    final_pixmap.fill(Qt.GlobalColor.transparent)

                    painter = QPainter(final_pixmap)
                    painter.drawPixmap(margin, margin, pixmap)
                    painter.end()
                    pixmap = final_pixmap

                # Save to cache for future
                QPixmapCache.insert(cache_url.toString(), pixmap)
                return pixmap

        # 5. Fallback (Returns a transparent pixmap or placeholder in case of error)
        fallback = QPixmap(target_size, target_size)
        fallback.fill(Qt.GlobalColor.transparent)
        fallback.setDevicePixelRatio(dpr)
        return fallback

    @staticmethod
    def getUrl(alias: str, margin: int, size: int, dpr: float, source_format: str) -> QUrl:
        """Generates a unique QUrl key for caching an emoji pixmap.

        Args:
            alias (str): Emoji identifier (unified code or alias).
            margin (int): Margin size.
            size (QSize): Logical size.
            dpr (float): Device pixel ratio.
            source_format (str): Image format.

        Returns:
            QUrl: The generated cache key URL.
        """
        url = QUrl()
        url.setScheme("twemoji")
        url.setPath(alias)

        query_params = QUrlQuery()
        query_params.addQueryItem("margin", str(margin))
        query_params.addQueryItem("size", str(size))
        query_params.addQueryItem("dpr", str(dpr))
        query_params.addQueryItem("source_format", source_format)

        url.setQuery(query_params)

        return url

getPixmap(emoji, margin, size, dpr=1.0, source_format='png') staticmethod

Loads and returns a colorized or processed emoji pixmap.

Uses caching to improve performance on subsequent calls.

Parameters:

Name Type Description Default
emoji str

Emoji character.

required
margin int

Margin around the emoji in pixels.

required
size int

Target logical size.

required
dpr float

Device pixel ratio. Defaults to 1.0.

1.0
source_format str

Image format (png or svg). Defaults to "png".

'png'

Returns:

Name Type Description
QPixmap QPixmap

The processed pixmap.

Source code in source/qextrawidgets/core/utils/twemoji_image_provider.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
@staticmethod
def getPixmap(emoji: str, margin: int, size: int, dpr: float = 1.0, source_format: str = "png") -> QPixmap:
    """Loads and returns a colorized or processed emoji pixmap.

    Uses caching to improve performance on subsequent calls.

    Args:
        emoji (str): Emoji character.
        margin (int): Margin around the emoji in pixels.
        size (int): Target logical size.
        dpr (float, optional): Device pixel ratio. Defaults to 1.0.
        source_format (str, optional): Image format (png or svg). Defaults to "png".

    Returns:
        QPixmap: The processed pixmap.
    """

    # 1. Calculate real physical size (pixels)
    target_size = int(size * dpr)

    # 2. Generate unique key for Cache
    cache_url = QTwemojiImageProvider.getUrl(char_to_unified(emoji), margin, size, dpr, source_format)

    # 3. Try to fetch from Cache
    pixmap = QPixmap()
    if QPixmapCache.find(cache_url.toString(), pixmap):
        return pixmap

    # --- CACHE MISS (Load from disk) ---

    # 4. Load using QImageReader (more efficient than QPixmap(path))
    emoji_path = str(get_emoji_path(emoji, source_format))
    if not emoji_path:
        # Fallback (Returns a transparent pixmap or placeholder in case of error)
        fallback = QPixmap(target_size, target_size)
        fallback.fill(Qt.GlobalColor.transparent)
        fallback.setDevicePixelRatio(dpr)
        return fallback

    reader = QImageReader(emoji_path)

    if reader.canRead():
        # Important for SVG: Define render size before reading
        reader.setScaledSize(QSize(target_size, target_size))

        image = reader.read()
        if not image.isNull():
            pixmap = QPixmap.fromImage(image)
            pixmap.setDevicePixelRatio(dpr)

            # Apply margin
            if margin > 0:
                final_size = int((size + (margin * 2)) * dpr)
                final_pixmap = QPixmap(final_size, final_size)
                final_pixmap.setDevicePixelRatio(dpr)
                final_pixmap.fill(Qt.GlobalColor.transparent)

                painter = QPainter(final_pixmap)
                painter.drawPixmap(margin, margin, pixmap)
                painter.end()
                pixmap = final_pixmap

            # Save to cache for future
            QPixmapCache.insert(cache_url.toString(), pixmap)
            return pixmap

    # 5. Fallback (Returns a transparent pixmap or placeholder in case of error)
    fallback = QPixmap(target_size, target_size)
    fallback.fill(Qt.GlobalColor.transparent)
    fallback.setDevicePixelRatio(dpr)
    return fallback

getUrl(alias, margin, size, dpr, source_format) staticmethod

Generates a unique QUrl key for caching an emoji pixmap.

Parameters:

Name Type Description Default
alias str

Emoji identifier (unified code or alias).

required
margin int

Margin size.

required
size QSize

Logical size.

required
dpr float

Device pixel ratio.

required
source_format str

Image format.

required

Returns:

Name Type Description
QUrl QUrl

The generated cache key URL.

Source code in source/qextrawidgets/core/utils/twemoji_image_provider.py
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
@staticmethod
def getUrl(alias: str, margin: int, size: int, dpr: float, source_format: str) -> QUrl:
    """Generates a unique QUrl key for caching an emoji pixmap.

    Args:
        alias (str): Emoji identifier (unified code or alias).
        margin (int): Margin size.
        size (QSize): Logical size.
        dpr (float): Device pixel ratio.
        source_format (str): Image format.

    Returns:
        QUrl: The generated cache key URL.
    """
    url = QUrl()
    url.setScheme("twemoji")
    url.setPath(alias)

    query_params = QUrlQuery()
    query_params.addQueryItem("margin", str(margin))
    query_params.addQueryItem("size", str(size))
    query_params.addQueryItem("dpr", str(dpr))
    query_params.addQueryItem("source_format", source_format)

    url.setQuery(query_params)

    return url

GUI

Icons

QThemeResponsiveIcon

Bases: QIcon

QIcon wrapper that applies automatic coloring based on system theme.

The icon switches between Black and White based on the current system palette.

Source code in source/qextrawidgets/gui/icons/theme_responsive_icon.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
class QThemeResponsiveIcon(QIcon):
    """QIcon wrapper that applies automatic coloring based on system theme.

    The icon switches between Black and White based on the current system palette.
    """

    def __init__(self, source: typing.Union[str, QPixmap, QIcon, None] = None) -> None:
        """Initializes the theme responsive icon.

        Args:
            source (Union[str, QPixmap, QIcon, None]): Icon source.
        """
        if isinstance(source, QIcon):
            icon = source
        elif isinstance(source, str):
            icon = QIcon(source)
        elif isinstance(source, QPixmap):
            icon = QIcon()
            icon.addPixmap(source)
        elif source is None:
            icon = QIcon()
        else:
            raise ValueError("Invalid source type")

        self._engine = QThemeResponsiveIconEngine(icon)

        super().__init__(self._engine)

    @staticmethod
    def fromAwesome(icon_name: str, **kwargs: typing.Any) -> "QThemeResponsiveIcon":
        """Creates a theme responsive icon from a QtAwesome icon name.

        Args:
            icon_name (str): QtAwesome icon name (e.g., "fa6s.house").
            **kwargs (Any): Additional arguments for qtawesome.icon.

        Returns:
            QThemeResponsiveIcon: The created icon.
        """
        return QThemeResponsiveIcon(qtawesome.icon(icon_name, **kwargs))

    def themePixmap(self, size: QSize, mode: QIcon.Mode, state: QIcon.State, scheme: Qt.ColorScheme) -> QPixmap:
        """Returns a themed pixmap directly.

        Args:
            size (QSize): Target size.
            mode (Mode): Icon mode.
            state (State): Icon state.
            scheme (ColorScheme): System color scheme.

        Returns:
            QPixmap: The themed pixmap.
        """
        return self._engine.themePixmap(size, mode, state, scheme)

__init__(source=None)

Initializes the theme responsive icon.

Parameters:

Name Type Description Default
source Union[str, QPixmap, QIcon, None]

Icon source.

None
Source code in source/qextrawidgets/gui/icons/theme_responsive_icon.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
def __init__(self, source: typing.Union[str, QPixmap, QIcon, None] = None) -> None:
    """Initializes the theme responsive icon.

    Args:
        source (Union[str, QPixmap, QIcon, None]): Icon source.
    """
    if isinstance(source, QIcon):
        icon = source
    elif isinstance(source, str):
        icon = QIcon(source)
    elif isinstance(source, QPixmap):
        icon = QIcon()
        icon.addPixmap(source)
    elif source is None:
        icon = QIcon()
    else:
        raise ValueError("Invalid source type")

    self._engine = QThemeResponsiveIconEngine(icon)

    super().__init__(self._engine)

fromAwesome(icon_name, **kwargs) staticmethod

Creates a theme responsive icon from a QtAwesome icon name.

Parameters:

Name Type Description Default
icon_name str

QtAwesome icon name (e.g., "fa6s.house").

required
**kwargs Any

Additional arguments for qtawesome.icon.

{}

Returns:

Name Type Description
QThemeResponsiveIcon QThemeResponsiveIcon

The created icon.

Source code in source/qextrawidgets/gui/icons/theme_responsive_icon.py
38
39
40
41
42
43
44
45
46
47
48
49
@staticmethod
def fromAwesome(icon_name: str, **kwargs: typing.Any) -> "QThemeResponsiveIcon":
    """Creates a theme responsive icon from a QtAwesome icon name.

    Args:
        icon_name (str): QtAwesome icon name (e.g., "fa6s.house").
        **kwargs (Any): Additional arguments for qtawesome.icon.

    Returns:
        QThemeResponsiveIcon: The created icon.
    """
    return QThemeResponsiveIcon(qtawesome.icon(icon_name, **kwargs))

themePixmap(size, mode, state, scheme)

Returns a themed pixmap directly.

Parameters:

Name Type Description Default
size QSize

Target size.

required
mode Mode

Icon mode.

required
state State

Icon state.

required
scheme ColorScheme

System color scheme.

required

Returns:

Name Type Description
QPixmap QPixmap

The themed pixmap.

Source code in source/qextrawidgets/gui/icons/theme_responsive_icon.py
51
52
53
54
55
56
57
58
59
60
61
62
63
def themePixmap(self, size: QSize, mode: QIcon.Mode, state: QIcon.State, scheme: Qt.ColorScheme) -> QPixmap:
    """Returns a themed pixmap directly.

    Args:
        size (QSize): Target size.
        mode (Mode): Icon mode.
        state (State): Icon state.
        scheme (ColorScheme): System color scheme.

    Returns:
        QPixmap: The themed pixmap.
    """
    return self._engine.themePixmap(size, mode, state, scheme)

Items

QEmojiCategoryItem

Bases: QStandardItem

A standard item representing a category of emojis in the model.

Source code in source/qextrawidgets/gui/items/emoji_category_item.py
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class QEmojiCategoryItem(QStandardItem):
    """
    A standard item representing a category of emojis in the model.
    """

    class QEmojiCategoryDataRole(int, Enum):
        """
        Custom data roles for the category item.
        """

        CategoryRole = Qt.ItemDataRole.UserRole + 2

    def __init__(self, category: str, icon: typing.Union[QIcon, QPixmap]):
        """
        Initializes the category item.

        Args:
            category (str): The name of the category.
            icon (typing.Union[QIcon, QPixmap]): The icon representing the category.
        """
        super().__init__()
        self.setText(category)
        self.setIcon(icon)
        self.setData(
            category, role=QEmojiCategoryItem.QEmojiCategoryDataRole.CategoryRole
        )
        self.setEditable(False)

    def category(self) -> str:
        """
        Returns the category name.

        Returns:
            str: The category name.
        """
        return self.text()

QEmojiCategoryDataRole

Bases: int, Enum

Custom data roles for the category item.

Source code in source/qextrawidgets/gui/items/emoji_category_item.py
12
13
14
15
16
17
class QEmojiCategoryDataRole(int, Enum):
    """
    Custom data roles for the category item.
    """

    CategoryRole = Qt.ItemDataRole.UserRole + 2

__init__(category, icon)

Initializes the category item.

Parameters:

Name Type Description Default
category str

The name of the category.

required
icon Union[QIcon, QPixmap]

The icon representing the category.

required
Source code in source/qextrawidgets/gui/items/emoji_category_item.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def __init__(self, category: str, icon: typing.Union[QIcon, QPixmap]):
    """
    Initializes the category item.

    Args:
        category (str): The name of the category.
        icon (typing.Union[QIcon, QPixmap]): The icon representing the category.
    """
    super().__init__()
    self.setText(category)
    self.setIcon(icon)
    self.setData(
        category, role=QEmojiCategoryItem.QEmojiCategoryDataRole.CategoryRole
    )
    self.setEditable(False)

category()

Returns the category name.

Returns:

Name Type Description
str str

The category name.

Source code in source/qextrawidgets/gui/items/emoji_category_item.py
35
36
37
38
39
40
41
42
def category(self) -> str:
    """
    Returns the category name.

    Returns:
        str: The category name.
    """
    return self.text()

EmojiSkinTone

Bases: str, Enum

Skin tone modifiers (Fitzpatrick scale) supported by Unicode.

Inherits from 'str' to facilitate direct concatenation with base emojis.

Attributes:

Name Type Description
Default

Default skin tone (usually yellow/neutral). No modifier.

Light

Type 1-2: Light skin tone.

MediumLight

Type 3: Medium-light skin tone.

Medium

Type 4: Medium skin tone.

MediumDark

Type 5: Medium-dark skin tone.

Dark

Type 6: Dark skin tone.

Source code in source/qextrawidgets/gui/items/emoji_item.py
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class EmojiSkinTone(str, Enum):
    """Skin tone modifiers (Fitzpatrick scale) supported by Unicode.

    Inherits from 'str' to facilitate direct concatenation with base emojis.

    Attributes:
        Default: Default skin tone (usually yellow/neutral). No modifier.
        Light: Type 1-2: Light skin tone.
        MediumLight: Type 3: Medium-light skin tone.
        Medium: Type 4: Medium skin tone.
        MediumDark: Type 5: Medium-dark skin tone.
        Dark: Type 6: Dark skin tone.
    """

    # Default (Generally Yellow/Neutral) - Adds no code
    Default = ""

    # Type 1-2: Light Skin
    Light = "1F3FB"

    # Type 3: Medium-Light Skin
    MediumLight = "1F3FC"

    # Type 4: Medium Skin
    Medium = "1F3FD"

    # Type 5: Medium-Dark Skin
    MediumDark = "1F3FE"

    # Type 6: Dark Skin
    Dark = "1F3FF"

QEmojiItem

Bases: QStandardItem

A standard item representing a single emoji in the model.

Source code in source/qextrawidgets/gui/items/emoji_item.py
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
class QEmojiItem(QStandardItem):
    """A standard item representing a single emoji in the model."""

    class QEmojiDataRole(int, Enum):
        """
        Custom data roles for the emoji item.
        """

        SkinToneRole = Qt.ItemDataRole.UserRole + 1
        CategoryRole = Qt.ItemDataRole.UserRole + 2
        EmojiRole = Qt.ItemDataRole.UserRole + 3
        ShortNamesRole = Qt.ItemDataRole.UserRole + 4

    def __init__(self, emoji_char: EmojiChar, skin_tone: str = ""):
        """
        Initializes the emoji item.

        Args:
            emoji_char (EmojiChar): The emoji character data object.
            skin_tone (str, optional): The skin tone modifier (hex code). Defaults to "".
        """
        super().__init__()
        self.setData(emoji_char, Qt.ItemDataRole.UserRole)
        self.setData(skin_tone, self.QEmojiDataRole.SkinToneRole)
        self.setEditable(False)

    @classmethod
    def fromEmoji(cls, emoji: str, skin_tone: str = "") -> "QEmojiItem":
        """
        Create a QEmojiItem from an emoji character string.

        Args:
            emoji (str): The emoji character.
            skin_tone (str, optional): Skin tone modifier.

        Returns:
            QEmojiItem: The created item.

        Raises:
            ValueError: If the emoji is not found in the database.
        """
        emoji_char = _find_emoji_by_char(emoji)
        if not emoji_char:
            raise ValueError(f"Emoji '{emoji}' not found in emoji database.")
        return cls(emoji_char, skin_tone)

    @classmethod
    def fromEmojiShortName(cls, short_name: str, skin_tone: str = "") -> "QEmojiItem":
        """
        Create a QEmojiItem from a short name (e.g., 'smile' or ':smile:').

        Args:
            short_name (str): The short name of the emoji.
            skin_tone (str, optional): Skin tone modifier.

        Returns:
            QEmojiItem: The created item.

        Raises:
            ValueError: If the emoji is not found by short name.
        """
        emoji_char = _find_emoji_by_short_name(short_name)
        if not emoji_char:
            raise ValueError(f"Emoji with short name '{short_name}' not found.")
        return cls(emoji_char, skin_tone)

    def emojiChar(self) -> EmojiChar:
        """
        Returns the raw EmojiChar object associated with this item.

        Returns:
            EmojiChar: The emoji character data object.
        """
        return self.data(Qt.ItemDataRole.UserRole)

    def coloredEmojiChar(self) -> EmojiChar:
        """
        Returns the EmojiChar corresponding to the set skin tone, if available.
        Otherwise, returns the base EmojiChar.

        Returns:
            EmojiChar: The processed emoji character data object.
        """
        emoji_char = self.emojiChar()
        skin_tone = self.skinTone()
        if (
            skin_tone
            and self.skinToneCompatible(emoji_char)
            and skin_tone in emoji_char.skin_variations
        ):
            return emoji_char.skin_variations[skin_tone]
        return emoji_char

    @staticmethod
    def skinToneCompatible(emoji_char: EmojiChar) -> bool:
        """
        Checks if the given emoji supports skin tone variations in the library.

        Args:
            emoji_char (EmojiChar): The emoji to check.

        Returns:
            bool: True if it supports skin tone variations, False otherwise.
        """
        return any(
            skin_tone in emoji_char.skin_variations for skin_tone in EmojiSkinTone
        )

    def emoji(self) -> str:
        """Returns the emoji character.

        Returns:
            str: The emoji character.
        """
        return self.coloredEmojiChar().char

    def shortNames(self) -> typing.List[str]:
        """
        Returns a list of short names (keywords) for the emoji.

        Returns:
            typing.List[str]: List of short names.
        """
        return self.emojiChar().short_names or []

    def aliasesText(self) -> str:
        """
        Returns a string containing all short names formatted as aliases (e.g. :smile: :happy:).

        Returns:
            str: Space-separated aliases.
        """
        return " ".join(f":{a}:" for a in self.shortNames())

    def firstAlias(self) -> str:
        """
        Returns the first alias/short name of the emoji.

        Returns:
            str: The first alias, or None/IndexError if empty (though usually not empty).
        """
        return self.shortNames()[0]

    def skinTone(self) -> str:
        """
        Returns the current skin tone hex string stored in data.

        Returns:
            str: The skin tone hex string (e.g., '1F3FB') or empty string.
        """
        return self.data(self.QEmojiDataRole.SkinToneRole)

    def clone(self, /):
        """
        Creates a copy of this QEmojiItem.

        Returns:
            QEmojiItem: A new instance with the same emoji and skin tone.
        """
        return QEmojiItem(self.emojiChar(), self.skinTone())

    def data(self, role: int = Qt.ItemDataRole.UserRole) -> typing.Any:
        """
        Retrieves data for the given role.

        Args:
            role (int): The data role.

        Returns:
            typing.Any: The data associated with the role.
        """
        if role == self.QEmojiDataRole.CategoryRole:
            return self.emojiChar().category

        if role == self.QEmojiDataRole.EmojiRole:
            return self.emojiChar().char

        if role == self.QEmojiDataRole.ShortNamesRole:
            return self.shortNames()

        return super().data(role)

    def parent(self) -> typing.Optional[QEmojiCategoryItem]:  # type: ignore[override]
        """
        Returns the parent item of the emoji item.

        Returns:
            QEmojiCategoryItem: The parent category item.
        """
        item = super().parent()
        if isinstance(item, QEmojiCategoryItem):
            return item
        return None

QEmojiDataRole

Bases: int, Enum

Custom data roles for the emoji item.

Source code in source/qextrawidgets/gui/items/emoji_item.py
71
72
73
74
75
76
77
78
79
class QEmojiDataRole(int, Enum):
    """
    Custom data roles for the emoji item.
    """

    SkinToneRole = Qt.ItemDataRole.UserRole + 1
    CategoryRole = Qt.ItemDataRole.UserRole + 2
    EmojiRole = Qt.ItemDataRole.UserRole + 3
    ShortNamesRole = Qt.ItemDataRole.UserRole + 4

__init__(emoji_char, skin_tone='')

Initializes the emoji item.

Parameters:

Name Type Description Default
emoji_char EmojiChar

The emoji character data object.

required
skin_tone str

The skin tone modifier (hex code). Defaults to "".

''
Source code in source/qextrawidgets/gui/items/emoji_item.py
81
82
83
84
85
86
87
88
89
90
91
92
def __init__(self, emoji_char: EmojiChar, skin_tone: str = ""):
    """
    Initializes the emoji item.

    Args:
        emoji_char (EmojiChar): The emoji character data object.
        skin_tone (str, optional): The skin tone modifier (hex code). Defaults to "".
    """
    super().__init__()
    self.setData(emoji_char, Qt.ItemDataRole.UserRole)
    self.setData(skin_tone, self.QEmojiDataRole.SkinToneRole)
    self.setEditable(False)

aliasesText()

Returns a string containing all short names formatted as aliases (e.g. :smile: :happy:).

Returns:

Name Type Description
str str

Space-separated aliases.

Source code in source/qextrawidgets/gui/items/emoji_item.py
193
194
195
196
197
198
199
200
def aliasesText(self) -> str:
    """
    Returns a string containing all short names formatted as aliases (e.g. :smile: :happy:).

    Returns:
        str: Space-separated aliases.
    """
    return " ".join(f":{a}:" for a in self.shortNames())

clone()

Creates a copy of this QEmojiItem.

Returns:

Name Type Description
QEmojiItem

A new instance with the same emoji and skin tone.

Source code in source/qextrawidgets/gui/items/emoji_item.py
220
221
222
223
224
225
226
227
def clone(self, /):
    """
    Creates a copy of this QEmojiItem.

    Returns:
        QEmojiItem: A new instance with the same emoji and skin tone.
    """
    return QEmojiItem(self.emojiChar(), self.skinTone())

coloredEmojiChar()

Returns the EmojiChar corresponding to the set skin tone, if available. Otherwise, returns the base EmojiChar.

Returns:

Name Type Description
EmojiChar EmojiChar

The processed emoji character data object.

Source code in source/qextrawidgets/gui/items/emoji_item.py
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
def coloredEmojiChar(self) -> EmojiChar:
    """
    Returns the EmojiChar corresponding to the set skin tone, if available.
    Otherwise, returns the base EmojiChar.

    Returns:
        EmojiChar: The processed emoji character data object.
    """
    emoji_char = self.emojiChar()
    skin_tone = self.skinTone()
    if (
        skin_tone
        and self.skinToneCompatible(emoji_char)
        and skin_tone in emoji_char.skin_variations
    ):
        return emoji_char.skin_variations[skin_tone]
    return emoji_char

data(role=Qt.ItemDataRole.UserRole)

Retrieves data for the given role.

Parameters:

Name Type Description Default
role int

The data role.

UserRole

Returns:

Type Description
Any

typing.Any: The data associated with the role.

Source code in source/qextrawidgets/gui/items/emoji_item.py
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
def data(self, role: int = Qt.ItemDataRole.UserRole) -> typing.Any:
    """
    Retrieves data for the given role.

    Args:
        role (int): The data role.

    Returns:
        typing.Any: The data associated with the role.
    """
    if role == self.QEmojiDataRole.CategoryRole:
        return self.emojiChar().category

    if role == self.QEmojiDataRole.EmojiRole:
        return self.emojiChar().char

    if role == self.QEmojiDataRole.ShortNamesRole:
        return self.shortNames()

    return super().data(role)

emoji()

Returns the emoji character.

Returns:

Name Type Description
str str

The emoji character.

Source code in source/qextrawidgets/gui/items/emoji_item.py
176
177
178
179
180
181
182
def emoji(self) -> str:
    """Returns the emoji character.

    Returns:
        str: The emoji character.
    """
    return self.coloredEmojiChar().char

emojiChar()

Returns the raw EmojiChar object associated with this item.

Returns:

Name Type Description
EmojiChar EmojiChar

The emoji character data object.

Source code in source/qextrawidgets/gui/items/emoji_item.py
134
135
136
137
138
139
140
141
def emojiChar(self) -> EmojiChar:
    """
    Returns the raw EmojiChar object associated with this item.

    Returns:
        EmojiChar: The emoji character data object.
    """
    return self.data(Qt.ItemDataRole.UserRole)

firstAlias()

Returns the first alias/short name of the emoji.

Returns:

Name Type Description
str str

The first alias, or None/IndexError if empty (though usually not empty).

Source code in source/qextrawidgets/gui/items/emoji_item.py
202
203
204
205
206
207
208
209
def firstAlias(self) -> str:
    """
    Returns the first alias/short name of the emoji.

    Returns:
        str: The first alias, or None/IndexError if empty (though usually not empty).
    """
    return self.shortNames()[0]

fromEmoji(emoji, skin_tone='') classmethod

Create a QEmojiItem from an emoji character string.

Parameters:

Name Type Description Default
emoji str

The emoji character.

required
skin_tone str

Skin tone modifier.

''

Returns:

Name Type Description
QEmojiItem QEmojiItem

The created item.

Raises:

Type Description
ValueError

If the emoji is not found in the database.

Source code in source/qextrawidgets/gui/items/emoji_item.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
@classmethod
def fromEmoji(cls, emoji: str, skin_tone: str = "") -> "QEmojiItem":
    """
    Create a QEmojiItem from an emoji character string.

    Args:
        emoji (str): The emoji character.
        skin_tone (str, optional): Skin tone modifier.

    Returns:
        QEmojiItem: The created item.

    Raises:
        ValueError: If the emoji is not found in the database.
    """
    emoji_char = _find_emoji_by_char(emoji)
    if not emoji_char:
        raise ValueError(f"Emoji '{emoji}' not found in emoji database.")
    return cls(emoji_char, skin_tone)

fromEmojiShortName(short_name, skin_tone='') classmethod

Create a QEmojiItem from a short name (e.g., 'smile' or ':smile:').

Parameters:

Name Type Description Default
short_name str

The short name of the emoji.

required
skin_tone str

Skin tone modifier.

''

Returns:

Name Type Description
QEmojiItem QEmojiItem

The created item.

Raises:

Type Description
ValueError

If the emoji is not found by short name.

Source code in source/qextrawidgets/gui/items/emoji_item.py
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
@classmethod
def fromEmojiShortName(cls, short_name: str, skin_tone: str = "") -> "QEmojiItem":
    """
    Create a QEmojiItem from a short name (e.g., 'smile' or ':smile:').

    Args:
        short_name (str): The short name of the emoji.
        skin_tone (str, optional): Skin tone modifier.

    Returns:
        QEmojiItem: The created item.

    Raises:
        ValueError: If the emoji is not found by short name.
    """
    emoji_char = _find_emoji_by_short_name(short_name)
    if not emoji_char:
        raise ValueError(f"Emoji with short name '{short_name}' not found.")
    return cls(emoji_char, skin_tone)

parent()

Returns the parent item of the emoji item.

Returns:

Name Type Description
QEmojiCategoryItem Optional[QEmojiCategoryItem]

The parent category item.

Source code in source/qextrawidgets/gui/items/emoji_item.py
250
251
252
253
254
255
256
257
258
259
260
def parent(self) -> typing.Optional[QEmojiCategoryItem]:  # type: ignore[override]
    """
    Returns the parent item of the emoji item.

    Returns:
        QEmojiCategoryItem: The parent category item.
    """
    item = super().parent()
    if isinstance(item, QEmojiCategoryItem):
        return item
    return None

shortNames()

Returns a list of short names (keywords) for the emoji.

Returns:

Type Description
List[str]

typing.List[str]: List of short names.

Source code in source/qextrawidgets/gui/items/emoji_item.py
184
185
186
187
188
189
190
191
def shortNames(self) -> typing.List[str]:
    """
    Returns a list of short names (keywords) for the emoji.

    Returns:
        typing.List[str]: List of short names.
    """
    return self.emojiChar().short_names or []

skinTone()

Returns the current skin tone hex string stored in data.

Returns:

Name Type Description
str str

The skin tone hex string (e.g., '1F3FB') or empty string.

Source code in source/qextrawidgets/gui/items/emoji_item.py
211
212
213
214
215
216
217
218
def skinTone(self) -> str:
    """
    Returns the current skin tone hex string stored in data.

    Returns:
        str: The skin tone hex string (e.g., '1F3FB') or empty string.
    """
    return self.data(self.QEmojiDataRole.SkinToneRole)

skinToneCompatible(emoji_char) staticmethod

Checks if the given emoji supports skin tone variations in the library.

Parameters:

Name Type Description Default
emoji_char EmojiChar

The emoji to check.

required

Returns:

Name Type Description
bool bool

True if it supports skin tone variations, False otherwise.

Source code in source/qextrawidgets/gui/items/emoji_item.py
161
162
163
164
165
166
167
168
169
170
171
172
173
174
@staticmethod
def skinToneCompatible(emoji_char: EmojiChar) -> bool:
    """
    Checks if the given emoji supports skin tone variations in the library.

    Args:
        emoji_char (EmojiChar): The emoji to check.

    Returns:
        bool: True if it supports skin tone variations, False otherwise.
    """
    return any(
        skin_tone in emoji_char.skin_variations for skin_tone in EmojiSkinTone
    )

Models

EmojiCategory

Bases: str, Enum

Standard emoji categories.

Source code in source/qextrawidgets/gui/models/emoji_picker_model.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class EmojiCategory(str, Enum):
    """Standard emoji categories."""

    Recents = QT_TRANSLATE_NOOP("EmojiCategory", "Recents")
    Favorites = QT_TRANSLATE_NOOP("EmojiCategory", "Favorites")
    SmileysAndEmotion = QT_TRANSLATE_NOOP("EmojiCategory", "Smileys & Emotion")
    PeopleAndBody = QT_TRANSLATE_NOOP("EmojiCategory", "People & Body")
    AnimalsAndNature = QT_TRANSLATE_NOOP("EmojiCategory", "Animals & Nature")
    FoodAndDrink = QT_TRANSLATE_NOOP("EmojiCategory", "Food & Drink")
    Symbols = QT_TRANSLATE_NOOP("EmojiCategory", "Symbols")
    Activities = QT_TRANSLATE_NOOP("EmojiCategory", "Activities")
    Objects = QT_TRANSLATE_NOOP("EmojiCategory", "Objects")
    TravelAndPlaces = QT_TRANSLATE_NOOP("EmojiCategory", "Travel & Places")
    Flags = QT_TRANSLATE_NOOP("EmojiCategory", "Flags")

QEmojiPickerModel

Bases: QStandardItemModel

Model for managing emoji categories and items using QStandardItemModel.

This model organizes emojis into categories (e.g., Smileys & Emotion, Animals & Nature) and supports optional 'Favorites' and 'Recents' categories. It also handles skin tone updates for compatible emojis.

Attributes:

Name Type Description
categoryInserted Signal(QEmojiCategoryItem

Emitted when a category is added.

categoryRemoved Signal(QEmojiCategoryItem

Emitted when a category is removed.

skinToneChanged Signal(QModelIndex

Emitted when a skin tone is applied to an emoji.

Source code in source/qextrawidgets/gui/models/emoji_picker_model.py
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
class QEmojiPickerModel(QStandardItemModel):
    """
    Model for managing emoji categories and items using QStandardItemModel.

    This model organizes emojis into categories (e.g., Smileys & Emotion, Animals & Nature)
    and supports optional 'Favorites' and 'Recents' categories. It also handles skin tone
    updates for compatible emojis.

    Attributes:
        categoryInserted (Signal(QEmojiCategoryItem)): Emitted when a category is added.
        categoryRemoved (Signal(QEmojiCategoryItem)): Emitted when a category is removed.
        skinToneChanged (Signal(QModelIndex)): Emitted when a skin tone is applied to an emoji.
    """

    categoryInserted = Signal(QEmojiCategoryItem)
    categoryRemoved = Signal(QEmojiCategoryItem)
    emojiInserted = Signal(QEmojiCategoryItem, QEmojiItem)
    emojiRemoved = Signal(QEmojiCategoryItem, QEmojiItem)
    skinToneChanged = Signal(QModelIndex)
    _emojis_skin_modifier_compatible = {}

    def __init__(self, favorite_category: bool = True, recent_category: bool = True):
        """
        Initialize the QEmojiPickerModel.

        Args:
            favorite_category (bool): Whether to include the Favorites category. Defaults to True.
            recent_category (bool): Whether to include the Recents category. Defaults to True.
        """
        super().__init__()
        self._favorite_category = favorite_category
        self._recent_category = recent_category

        self.rowsInserted.connect(self._on_rows_inserted)
        self.rowsAboutToBeRemoved.connect(self._on_rows_removed)

    def populate(self):
        """
        Populate the model with emoji categories and items.

        Iterates through the emoji database, groups emojis by category, and creates the hierarchical model structure.
        Compatible emojis are tracked for skin tone updates.
        """
        self._emojis_skin_modifier_compatible.clear()

        icons = {
            EmojiCategory.Recents: "fa6s.clock-rotate-left",
            EmojiCategory.Favorites: "fa6s.star",
            EmojiCategory.SmileysAndEmotion: "fa6s.face-smile",
            EmojiCategory.PeopleAndBody: "fa6s.user",
            EmojiCategory.AnimalsAndNature: "fa6s.leaf",
            EmojiCategory.FoodAndDrink: "fa6s.bowl-food",
            EmojiCategory.Symbols: "fa6s.heart",
            EmojiCategory.Activities: "fa6s.gamepad",
            EmojiCategory.Objects: "fa6s.lightbulb",
            EmojiCategory.TravelAndPlaces: "fa6s.bicycle",
            EmojiCategory.Flags: "fa6s.flag",
        }

        # 1. Add Categories in desired order (Standard Order + Specials)
        # Note: The order defined in EmojiCategory Enum or the loop below dictates display order
        # Adjust as needed. Here we follow a typical picker order.
        categories_order = [
            EmojiCategory.Recents,
            EmojiCategory.Favorites,
            EmojiCategory.SmileysAndEmotion,
            EmojiCategory.PeopleAndBody,
            EmojiCategory.AnimalsAndNature,
            EmojiCategory.FoodAndDrink,
            EmojiCategory.Activities,
            EmojiCategory.TravelAndPlaces,
            EmojiCategory.Objects,
            EmojiCategory.Symbols,
            EmojiCategory.Flags,
        ]

        for category in categories_order:
            if category == EmojiCategory.Recents and not self._recent_category:
                continue
            if category == EmojiCategory.Favorites and not self._favorite_category:
                continue

            icon = QThemeResponsiveIcon.fromAwesome(
                icons[category], options=[{"scale_factor": 0.9}]
            )
            self.addCategory(category, icon)

        # 2. Add Emojis
        for emoji_char in sorted(emoji_data, key=lambda e: e.sort_order):
            if emoji_char.category == "Component":
                continue

            self.addEmoji(emoji_char.category, QEmojiItem(emoji_char))

    def findEmojiInCategory(
        self, category_item: QEmojiCategoryItem, emoji: str
    ) -> typing.Optional[QEmojiItem]:
        """
        Find a specific emoji within a given category index.

        Args:
            category_item (QEmojiCategoryItem): The category to search in.
            emoji (str): The emoji character to find.

        Returns:
            Optional[QEmojiItem]: The found emoji item, or None if not found.
        """
        # match(start_index, role, value, hits, flags)
        # Search starting from the first child of the category
        start_index = self.index(0, 0, category_item.index())

        # We only want direct children, so we don't use Qt.MatchChange.MatchRecursive.
        matches = self.match(
            start_index,
            QEmojiItem.QEmojiDataRole.EmojiRole,
            emoji,
            1,  # Number of results (1 to stop at the first)
            Qt.MatchFlag.MatchExactly,
        )

        if matches:
            item = self.itemFromIndex(matches[0])
            if isinstance(item, QEmojiItem):
                return item
        return None

    def findEmojiInCategoryByName(
        self, category: typing.Union[str, EmojiCategory], emoji: str
    ) -> typing.Optional[QEmojiItem]:
        """
        Find a specific emoji within a given category by name.

        Args:
            category (Union[str, EmojiCategory]): The name or enum of the category to search in.
            emoji (str): The emoji character to find.

        Returns:
            Optional[QEmojiItem]: The found emoji item, or None if not found.
        """
        category_item = self.findCategory(category)
        if not category_item:
            return None
        return self.findEmojiInCategory(category_item, emoji)

    def findCategory(self, category_name: str) -> typing.Optional[QEmojiCategoryItem]:
        """
        Find a category by its name.

        Args:
            category_name (str): The name of the category to search for.

        Returns:
            Optional[QEmojiCategoryItem]: The category item, or None if not found.
        """
        start_index = self.index(0, 0)
        matches = self.match(
            start_index,
            QEmojiCategoryItem.QEmojiCategoryDataRole.CategoryRole,
            category_name,
            1,
            Qt.MatchFlag.MatchExactly,
        )
        if matches:
            item = self.itemFromIndex(matches[0])
            if isinstance(item, QEmojiCategoryItem):
                return item
        return None

    def setSkinTone(self, skin_tone: str):
        """
        Update the skin tone for all compatible emojis in the model.

        Iterates through tracked compatible emojis and updates their data with the new skin tone.

        Args:
            skin_tone (str): The new skin tone character/code.
        """
        for (
            category,
            emojis_with_skin_modifier,
        ) in self._emojis_skin_modifier_compatible.items():
            category_item = self.findCategory(category)
            if not category_item:
                return

            for emoji in emojis_with_skin_modifier:
                emoji_item = self.findEmojiInCategory(category_item, emoji)
                if not emoji_item:
                    continue

                emoji_item.setData(skin_tone, QEmojiItem.QEmojiDataRole.SkinToneRole)
                self.skinToneChanged.emit(emoji_item.index())

    def addCategory(self, name: str, icon: typing.Union[QIcon, QPixmap]) -> bool:
        """
        Add a new category to the model.

        Args:
            name (str): The name of the category.
            icon (Union[QIcon, QPixmap]): The icon for the category.

        Returns:
            bool: True if added, False if it already exists.
        """
        if self.findCategory(name):
            return False

        category_item = QEmojiCategoryItem(name, icon)
        self.appendRow(category_item)
        return True

    def categories(self) -> typing.List[QEmojiCategoryItem]:
        """
        Get all category items in the model.

        Returns:
            List[QEmojiCategoryItem]: A list of all emoji category items.
        """
        category_items = []
        for row in range(self.rowCount()):
            item = self.item(row)
            if isinstance(item, QEmojiCategoryItem):
                category_items.append(item)
        return category_items

    def removeCategory(self, name: str) -> bool:
        """
        Remove a category from the model.

        Args:
            name (str): The name of the category to remove.

        Returns:
            bool: True if removed, False if not found.
        """
        item = self.findCategory(name)
        if not item:
            return False

        self.removeRow(item.row())
        return True

    def addEmoji(self, category_name: str, item: QEmojiItem) -> bool:
        """
        Add an emoji to a specific category.

        Args:
            category_name (str): The name of the category.
            item (QEmojiItem): The emoji item to add.

        Returns:
            bool: True if added, False if category not found or emoji already exists.
        """
        category_item = self.findCategory(category_name)
        if not category_item:
            return False

        emoji_char = item.emojiChar()
        if self.findEmojiInCategory(category_item, emoji_char.char):
            return False

        category_item.appendRow(item)

        # Update skin tone compatibility index if needed
        if QEmojiItem.skinToneCompatible(emoji_char):
            if category_name not in self._emojis_skin_modifier_compatible:
                self._emojis_skin_modifier_compatible[category_name] = []
            if (
                emoji_char.char
                not in self._emojis_skin_modifier_compatible[category_name]
            ):
                self._emojis_skin_modifier_compatible[category_name].append(
                    emoji_char.char
                )

        return True

    def removeEmoji(self, category_name: str, emoji_char: str) -> bool:
        """
        Remove an emoji from a specific category.

        Args:
            category_name (str): The name of the category.
            emoji_char (str): The emoji character string.

        Returns:
            bool: True if removed, False if not found.
        """
        category_item = self.findCategory(category_name)
        if not category_item:
            return False

        emoji_item = self.findEmojiInCategory(category_item, emoji_char)
        if not emoji_item:
            return False

        category_item.removeRow(emoji_item.row())

        return True

    @Slot(QModelIndex, int, int)
    def _on_rows_removed(self, parent: QModelIndex, first: int, last: int):
        """
        Handle internal slot for rows removed signal.

        Emits categoryRemoved signal when top-level rows (categories) are removed.
        Emits emojiRemoved signal when child rows (emojis) are removed.

        Args:
            parent (QModelIndex): The parent index.
            first (int): The first removed row.
            last (int): The last removed row.
        """
        if parent.isValid():
            parent_item = self.itemFromIndex(parent)
            if isinstance(parent_item, QEmojiCategoryItem):
                for row in range(first, last + 1):
                    # Since this is connected to rowsAboutToBeRemoved, the items still exist
                    child_index = self.index(row, 0, parent)
                    child_item = self.itemFromIndex(child_index)
                    if isinstance(child_item, QEmojiItem):
                        self.emojiRemoved.emit(parent_item, child_item)
            return

        for row in range(first, last + 1):
            item = self.item(row)
            if isinstance(item, QEmojiCategoryItem):
                self.categoryRemoved.emit(item)

    @Slot(QModelIndex, int, int)
    def _on_rows_inserted(self, parent: QModelIndex, first: int, last: int):
        """
        Handle internal slot for rows inserted signal.

        Emits categoryInserted signal when top-level rows (categories) are added.
        Emits emojiInserted signal when child rows (emojis) are added.

        Args:
            parent (QModelIndex): The parent index.
            first (int): The first inserted row.
            last (int): The last inserted row.
        """
        if parent.isValid():
            parent_item = self.itemFromIndex(parent)
            if isinstance(parent_item, QEmojiCategoryItem):
                for row in range(first, last + 1):
                    child_index = self.index(row, 0, parent)
                    child_item = self.itemFromIndex(child_index)
                    if isinstance(child_item, QEmojiItem):
                        self.emojiInserted.emit(parent_item, child_item)
            return

        for row in range(first, last + 1):
            item = self.item(row)
            if isinstance(item, QEmojiCategoryItem):
                self.categoryInserted.emit(item)

__init__(favorite_category=True, recent_category=True)

Initialize the QEmojiPickerModel.

Parameters:

Name Type Description Default
favorite_category bool

Whether to include the Favorites category. Defaults to True.

True
recent_category bool

Whether to include the Recents category. Defaults to True.

True
Source code in source/qextrawidgets/gui/models/emoji_picker_model.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
def __init__(self, favorite_category: bool = True, recent_category: bool = True):
    """
    Initialize the QEmojiPickerModel.

    Args:
        favorite_category (bool): Whether to include the Favorites category. Defaults to True.
        recent_category (bool): Whether to include the Recents category. Defaults to True.
    """
    super().__init__()
    self._favorite_category = favorite_category
    self._recent_category = recent_category

    self.rowsInserted.connect(self._on_rows_inserted)
    self.rowsAboutToBeRemoved.connect(self._on_rows_removed)

addCategory(name, icon)

Add a new category to the model.

Parameters:

Name Type Description Default
name str

The name of the category.

required
icon Union[QIcon, QPixmap]

The icon for the category.

required

Returns:

Name Type Description
bool bool

True if added, False if it already exists.

Source code in source/qextrawidgets/gui/models/emoji_picker_model.py
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
def addCategory(self, name: str, icon: typing.Union[QIcon, QPixmap]) -> bool:
    """
    Add a new category to the model.

    Args:
        name (str): The name of the category.
        icon (Union[QIcon, QPixmap]): The icon for the category.

    Returns:
        bool: True if added, False if it already exists.
    """
    if self.findCategory(name):
        return False

    category_item = QEmojiCategoryItem(name, icon)
    self.appendRow(category_item)
    return True

addEmoji(category_name, item)

Add an emoji to a specific category.

Parameters:

Name Type Description Default
category_name str

The name of the category.

required
item QEmojiItem

The emoji item to add.

required

Returns:

Name Type Description
bool bool

True if added, False if category not found or emoji already exists.

Source code in source/qextrawidgets/gui/models/emoji_picker_model.py
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
def addEmoji(self, category_name: str, item: QEmojiItem) -> bool:
    """
    Add an emoji to a specific category.

    Args:
        category_name (str): The name of the category.
        item (QEmojiItem): The emoji item to add.

    Returns:
        bool: True if added, False if category not found or emoji already exists.
    """
    category_item = self.findCategory(category_name)
    if not category_item:
        return False

    emoji_char = item.emojiChar()
    if self.findEmojiInCategory(category_item, emoji_char.char):
        return False

    category_item.appendRow(item)

    # Update skin tone compatibility index if needed
    if QEmojiItem.skinToneCompatible(emoji_char):
        if category_name not in self._emojis_skin_modifier_compatible:
            self._emojis_skin_modifier_compatible[category_name] = []
        if (
            emoji_char.char
            not in self._emojis_skin_modifier_compatible[category_name]
        ):
            self._emojis_skin_modifier_compatible[category_name].append(
                emoji_char.char
            )

    return True

categories()

Get all category items in the model.

Returns:

Type Description
List[QEmojiCategoryItem]

List[QEmojiCategoryItem]: A list of all emoji category items.

Source code in source/qextrawidgets/gui/models/emoji_picker_model.py
240
241
242
243
244
245
246
247
248
249
250
251
252
def categories(self) -> typing.List[QEmojiCategoryItem]:
    """
    Get all category items in the model.

    Returns:
        List[QEmojiCategoryItem]: A list of all emoji category items.
    """
    category_items = []
    for row in range(self.rowCount()):
        item = self.item(row)
        if isinstance(item, QEmojiCategoryItem):
            category_items.append(item)
    return category_items

findCategory(category_name)

Find a category by its name.

Parameters:

Name Type Description Default
category_name str

The name of the category to search for.

required

Returns:

Type Description
Optional[QEmojiCategoryItem]

Optional[QEmojiCategoryItem]: The category item, or None if not found.

Source code in source/qextrawidgets/gui/models/emoji_picker_model.py
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
def findCategory(self, category_name: str) -> typing.Optional[QEmojiCategoryItem]:
    """
    Find a category by its name.

    Args:
        category_name (str): The name of the category to search for.

    Returns:
        Optional[QEmojiCategoryItem]: The category item, or None if not found.
    """
    start_index = self.index(0, 0)
    matches = self.match(
        start_index,
        QEmojiCategoryItem.QEmojiCategoryDataRole.CategoryRole,
        category_name,
        1,
        Qt.MatchFlag.MatchExactly,
    )
    if matches:
        item = self.itemFromIndex(matches[0])
        if isinstance(item, QEmojiCategoryItem):
            return item
    return None

findEmojiInCategory(category_item, emoji)

Find a specific emoji within a given category index.

Parameters:

Name Type Description Default
category_item QEmojiCategoryItem

The category to search in.

required
emoji str

The emoji character to find.

required

Returns:

Type Description
Optional[QEmojiItem]

Optional[QEmojiItem]: The found emoji item, or None if not found.

Source code in source/qextrawidgets/gui/models/emoji_picker_model.py
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
def findEmojiInCategory(
    self, category_item: QEmojiCategoryItem, emoji: str
) -> typing.Optional[QEmojiItem]:
    """
    Find a specific emoji within a given category index.

    Args:
        category_item (QEmojiCategoryItem): The category to search in.
        emoji (str): The emoji character to find.

    Returns:
        Optional[QEmojiItem]: The found emoji item, or None if not found.
    """
    # match(start_index, role, value, hits, flags)
    # Search starting from the first child of the category
    start_index = self.index(0, 0, category_item.index())

    # We only want direct children, so we don't use Qt.MatchChange.MatchRecursive.
    matches = self.match(
        start_index,
        QEmojiItem.QEmojiDataRole.EmojiRole,
        emoji,
        1,  # Number of results (1 to stop at the first)
        Qt.MatchFlag.MatchExactly,
    )

    if matches:
        item = self.itemFromIndex(matches[0])
        if isinstance(item, QEmojiItem):
            return item
    return None

findEmojiInCategoryByName(category, emoji)

Find a specific emoji within a given category by name.

Parameters:

Name Type Description Default
category Union[str, EmojiCategory]

The name or enum of the category to search in.

required
emoji str

The emoji character to find.

required

Returns:

Type Description
Optional[QEmojiItem]

Optional[QEmojiItem]: The found emoji item, or None if not found.

Source code in source/qextrawidgets/gui/models/emoji_picker_model.py
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
def findEmojiInCategoryByName(
    self, category: typing.Union[str, EmojiCategory], emoji: str
) -> typing.Optional[QEmojiItem]:
    """
    Find a specific emoji within a given category by name.

    Args:
        category (Union[str, EmojiCategory]): The name or enum of the category to search in.
        emoji (str): The emoji character to find.

    Returns:
        Optional[QEmojiItem]: The found emoji item, or None if not found.
    """
    category_item = self.findCategory(category)
    if not category_item:
        return None
    return self.findEmojiInCategory(category_item, emoji)

populate()

Populate the model with emoji categories and items.

Iterates through the emoji database, groups emojis by category, and creates the hierarchical model structure. Compatible emojis are tracked for skin tone updates.

Source code in source/qextrawidgets/gui/models/emoji_picker_model.py
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
def populate(self):
    """
    Populate the model with emoji categories and items.

    Iterates through the emoji database, groups emojis by category, and creates the hierarchical model structure.
    Compatible emojis are tracked for skin tone updates.
    """
    self._emojis_skin_modifier_compatible.clear()

    icons = {
        EmojiCategory.Recents: "fa6s.clock-rotate-left",
        EmojiCategory.Favorites: "fa6s.star",
        EmojiCategory.SmileysAndEmotion: "fa6s.face-smile",
        EmojiCategory.PeopleAndBody: "fa6s.user",
        EmojiCategory.AnimalsAndNature: "fa6s.leaf",
        EmojiCategory.FoodAndDrink: "fa6s.bowl-food",
        EmojiCategory.Symbols: "fa6s.heart",
        EmojiCategory.Activities: "fa6s.gamepad",
        EmojiCategory.Objects: "fa6s.lightbulb",
        EmojiCategory.TravelAndPlaces: "fa6s.bicycle",
        EmojiCategory.Flags: "fa6s.flag",
    }

    # 1. Add Categories in desired order (Standard Order + Specials)
    # Note: The order defined in EmojiCategory Enum or the loop below dictates display order
    # Adjust as needed. Here we follow a typical picker order.
    categories_order = [
        EmojiCategory.Recents,
        EmojiCategory.Favorites,
        EmojiCategory.SmileysAndEmotion,
        EmojiCategory.PeopleAndBody,
        EmojiCategory.AnimalsAndNature,
        EmojiCategory.FoodAndDrink,
        EmojiCategory.Activities,
        EmojiCategory.TravelAndPlaces,
        EmojiCategory.Objects,
        EmojiCategory.Symbols,
        EmojiCategory.Flags,
    ]

    for category in categories_order:
        if category == EmojiCategory.Recents and not self._recent_category:
            continue
        if category == EmojiCategory.Favorites and not self._favorite_category:
            continue

        icon = QThemeResponsiveIcon.fromAwesome(
            icons[category], options=[{"scale_factor": 0.9}]
        )
        self.addCategory(category, icon)

    # 2. Add Emojis
    for emoji_char in sorted(emoji_data, key=lambda e: e.sort_order):
        if emoji_char.category == "Component":
            continue

        self.addEmoji(emoji_char.category, QEmojiItem(emoji_char))

removeCategory(name)

Remove a category from the model.

Parameters:

Name Type Description Default
name str

The name of the category to remove.

required

Returns:

Name Type Description
bool bool

True if removed, False if not found.

Source code in source/qextrawidgets/gui/models/emoji_picker_model.py
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
def removeCategory(self, name: str) -> bool:
    """
    Remove a category from the model.

    Args:
        name (str): The name of the category to remove.

    Returns:
        bool: True if removed, False if not found.
    """
    item = self.findCategory(name)
    if not item:
        return False

    self.removeRow(item.row())
    return True

removeEmoji(category_name, emoji_char)

Remove an emoji from a specific category.

Parameters:

Name Type Description Default
category_name str

The name of the category.

required
emoji_char str

The emoji character string.

required

Returns:

Name Type Description
bool bool

True if removed, False if not found.

Source code in source/qextrawidgets/gui/models/emoji_picker_model.py
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
def removeEmoji(self, category_name: str, emoji_char: str) -> bool:
    """
    Remove an emoji from a specific category.

    Args:
        category_name (str): The name of the category.
        emoji_char (str): The emoji character string.

    Returns:
        bool: True if removed, False if not found.
    """
    category_item = self.findCategory(category_name)
    if not category_item:
        return False

    emoji_item = self.findEmojiInCategory(category_item, emoji_char)
    if not emoji_item:
        return False

    category_item.removeRow(emoji_item.row())

    return True

setSkinTone(skin_tone)

Update the skin tone for all compatible emojis in the model.

Iterates through tracked compatible emojis and updates their data with the new skin tone.

Parameters:

Name Type Description Default
skin_tone str

The new skin tone character/code.

required
Source code in source/qextrawidgets/gui/models/emoji_picker_model.py
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
def setSkinTone(self, skin_tone: str):
    """
    Update the skin tone for all compatible emojis in the model.

    Iterates through tracked compatible emojis and updates their data with the new skin tone.

    Args:
        skin_tone (str): The new skin tone character/code.
    """
    for (
        category,
        emojis_with_skin_modifier,
    ) in self._emojis_skin_modifier_compatible.items():
        category_item = self.findCategory(category)
        if not category_item:
            return

        for emoji in emojis_with_skin_modifier:
            emoji_item = self.findEmojiInCategory(category_item, emoji)
            if not emoji_item:
                continue

            emoji_item.setData(skin_tone, QEmojiItem.QEmojiDataRole.SkinToneRole)
            self.skinToneChanged.emit(emoji_item.index())

Proxies

QCheckStateProxyModel

Bases: QIdentityProxyModel

A proxy model that stores check states internally, without modifying the source model.

This is useful for views where the user needs to select items (e.g., for filtering) without affecting the selection state of the underlying data.

Source code in source/qextrawidgets/gui/proxys/check_state_proxy.py
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
class QCheckStateProxyModel(QIdentityProxyModel):
    """A proxy model that stores check states internally, without modifying the source model.

    This is useful for views where the user needs to select items (e.g., for filtering)
    without affecting the selection state of the underlying data.
    """

    def __init__(self, parent: typing.Optional[QObject] = None) -> None:
        super().__init__(parent)
        self._checks: typing.Dict[QPersistentModelIndex, Qt.CheckState] = {}
        self._default_check_state = Qt.CheckState.Unchecked

    def flags(
        self, index: typing.Union[QModelIndex, QPersistentModelIndex]
    ) -> Qt.ItemFlag:
        """Returns the item flags for the given index, ensuring it is checkable."""
        if not index.isValid():
            return Qt.ItemFlag.NoItemFlags

        flags = super().flags(index)
        return flags | Qt.ItemFlag.ItemIsUserCheckable

    def data(
        self,
        index: typing.Union[QModelIndex, QPersistentModelIndex],
        role: int = Qt.ItemDataRole.DisplayRole,
    ) -> typing.Any:
        """Returns the data for the given index and role."""
        if role == Qt.ItemDataRole.CheckStateRole:
            if index.isValid():
                persistent_index = QPersistentModelIndex(index)
                return self._checks.get(persistent_index, self._default_check_state)
            return None

        return super().data(index, role)

    def setData(
        self,
        index: typing.Union[QModelIndex, QPersistentModelIndex],
        value: typing.Any,
        role: int = Qt.ItemDataRole.EditRole,
    ) -> bool:
        """Sets the data for the given index and role."""
        if role == Qt.ItemDataRole.CheckStateRole:
            if not index.isValid():
                return False

            persistent_index = QPersistentModelIndex(index)
            self._checks[persistent_index] = value
            self.dataChanged.emit(index, index, [Qt.ItemDataRole.CheckStateRole])
            return True

        return super().setData(index, value, role)

    def setInitialCheckState(self, state: Qt.CheckState) -> None:
        """Sets the default check state for all items not explicitly set."""
        self._default_check_state = state
        # Invalidate all data to refresh the view
        if self.sourceModel():
            self.dataChanged.emit(
                self.index(0, 0),
                self.index(
                    self.sourceModel().rowCount() - 1,
                    self.sourceModel().columnCount() - 1,
                ),
                [Qt.ItemDataRole.CheckStateRole],
            )

    def setAllCheckState(self, state: Qt.CheckState) -> None:
        """Sets the check state for all items in the model."""
        self._checks.clear()
        self.setInitialCheckState(state)

    def getCheckedRows(self, column: int = 0) -> typing.Set[int]:
        """Returns a set of row numbers that are currently checked."""
        checked_rows = set()
        model = self.sourceModel()
        if not model:
            return checked_rows

        # Iterate over all rows to check their state
        # Note: This checks the effective state (explicit or default)
        for row in range(model.rowCount()):
            index = self.index(row, column)
            if (
                self.data(index, Qt.ItemDataRole.CheckStateRole)
                == Qt.CheckState.Checked
            ):
                checked_rows.add(row)

        return checked_rows

data(index, role=Qt.ItemDataRole.DisplayRole)

Returns the data for the given index and role.

Source code in source/qextrawidgets/gui/proxys/check_state_proxy.py
34
35
36
37
38
39
40
41
42
43
44
45
46
def data(
    self,
    index: typing.Union[QModelIndex, QPersistentModelIndex],
    role: int = Qt.ItemDataRole.DisplayRole,
) -> typing.Any:
    """Returns the data for the given index and role."""
    if role == Qt.ItemDataRole.CheckStateRole:
        if index.isValid():
            persistent_index = QPersistentModelIndex(index)
            return self._checks.get(persistent_index, self._default_check_state)
        return None

    return super().data(index, role)

flags(index)

Returns the item flags for the given index, ensuring it is checkable.

Source code in source/qextrawidgets/gui/proxys/check_state_proxy.py
24
25
26
27
28
29
30
31
32
def flags(
    self, index: typing.Union[QModelIndex, QPersistentModelIndex]
) -> Qt.ItemFlag:
    """Returns the item flags for the given index, ensuring it is checkable."""
    if not index.isValid():
        return Qt.ItemFlag.NoItemFlags

    flags = super().flags(index)
    return flags | Qt.ItemFlag.ItemIsUserCheckable

getCheckedRows(column=0)

Returns a set of row numbers that are currently checked.

Source code in source/qextrawidgets/gui/proxys/check_state_proxy.py
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def getCheckedRows(self, column: int = 0) -> typing.Set[int]:
    """Returns a set of row numbers that are currently checked."""
    checked_rows = set()
    model = self.sourceModel()
    if not model:
        return checked_rows

    # Iterate over all rows to check their state
    # Note: This checks the effective state (explicit or default)
    for row in range(model.rowCount()):
        index = self.index(row, column)
        if (
            self.data(index, Qt.ItemDataRole.CheckStateRole)
            == Qt.CheckState.Checked
        ):
            checked_rows.add(row)

    return checked_rows

setAllCheckState(state)

Sets the check state for all items in the model.

Source code in source/qextrawidgets/gui/proxys/check_state_proxy.py
80
81
82
83
def setAllCheckState(self, state: Qt.CheckState) -> None:
    """Sets the check state for all items in the model."""
    self._checks.clear()
    self.setInitialCheckState(state)

setData(index, value, role=Qt.ItemDataRole.EditRole)

Sets the data for the given index and role.

Source code in source/qextrawidgets/gui/proxys/check_state_proxy.py
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
def setData(
    self,
    index: typing.Union[QModelIndex, QPersistentModelIndex],
    value: typing.Any,
    role: int = Qt.ItemDataRole.EditRole,
) -> bool:
    """Sets the data for the given index and role."""
    if role == Qt.ItemDataRole.CheckStateRole:
        if not index.isValid():
            return False

        persistent_index = QPersistentModelIndex(index)
        self._checks[persistent_index] = value
        self.dataChanged.emit(index, index, [Qt.ItemDataRole.CheckStateRole])
        return True

    return super().setData(index, value, role)

setInitialCheckState(state)

Sets the default check state for all items not explicitly set.

Source code in source/qextrawidgets/gui/proxys/check_state_proxy.py
66
67
68
69
70
71
72
73
74
75
76
77
78
def setInitialCheckState(self, state: Qt.CheckState) -> None:
    """Sets the default check state for all items not explicitly set."""
    self._default_check_state = state
    # Invalidate all data to refresh the view
    if self.sourceModel():
        self.dataChanged.emit(
            self.index(0, 0),
            self.index(
                self.sourceModel().rowCount() - 1,
                self.sourceModel().columnCount() - 1,
            ),
            [Qt.ItemDataRole.CheckStateRole],
        )

QDecorationRoleProxyModel

Bases: QIdentityProxyModel

A proxy model that stores decoration data internally, without modifying the source model.

This is useful for views where you need to display icons or colors without affecting the underlying data.

Source code in source/qextrawidgets/gui/proxys/decoration_role_proxy.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
class QDecorationRoleProxyModel(QIdentityProxyModel):
    """A proxy model that stores decoration data internally, without modifying the source model.

    This is useful for views where you need to display icons or colors
    without affecting the underlying data.
    """

    def __init__(self, parent: typing.Optional[QObject] = None) -> None:
        super().__init__(parent)
        self._decorations: typing.Dict[QPersistentModelIndex, typing.Any] = {}
        self._default_decoration: typing.Any = None

    def data(
        self,
        index: typing.Union[QModelIndex, QPersistentModelIndex],
        role: int = Qt.ItemDataRole.DisplayRole,
    ) -> typing.Any:
        """Returns the data for the given index and role."""
        if role == Qt.ItemDataRole.DecorationRole:
            if index.isValid():
                persistent_index = QPersistentModelIndex(index)
                return self._decorations.get(persistent_index, self._default_decoration)
            return None

        return super().data(index, role)

    def setData(
        self,
        index: typing.Union[QModelIndex, QPersistentModelIndex],
        value: typing.Any,
        role: int = Qt.ItemDataRole.EditRole,
    ) -> bool:
        """Sets the data for the given index and role."""
        if role == Qt.ItemDataRole.DecorationRole:
            if not index.isValid():
                return False

            persistent_index = QPersistentModelIndex(index)
            self._decorations[persistent_index] = value
            self.dataChanged.emit(index, index, [Qt.ItemDataRole.DecorationRole])
            return True

        return super().setData(index, value, role)

    def setDefaultDecoration(self, decoration: typing.Any) -> None:
        """Sets the default decoration for all items not explicitly set."""
        self._default_decoration = decoration
        # Invalidate all data to refresh the view
        if self.sourceModel():
            self.dataChanged.emit(
                self.index(0, 0),
                self.index(
                    self.sourceModel().rowCount() - 1,
                    self.sourceModel().columnCount() - 1,
                ),
                [Qt.ItemDataRole.DecorationRole],
            )

    def clearDecorations(self) -> None:
        """Clears all explicit decorations, reverting to the default."""
        self._decorations.clear()
        if self.sourceModel():
            self.dataChanged.emit(
                self.index(0, 0),
                self.index(
                    self.sourceModel().rowCount() - 1,
                    self.sourceModel().columnCount() - 1,
                ),
                [Qt.ItemDataRole.DecorationRole],
            )

clearDecorations()

Clears all explicit decorations, reverting to the default.

Source code in source/qextrawidgets/gui/proxys/decoration_role_proxy.py
70
71
72
73
74
75
76
77
78
79
80
81
def clearDecorations(self) -> None:
    """Clears all explicit decorations, reverting to the default."""
    self._decorations.clear()
    if self.sourceModel():
        self.dataChanged.emit(
            self.index(0, 0),
            self.index(
                self.sourceModel().rowCount() - 1,
                self.sourceModel().columnCount() - 1,
            ),
            [Qt.ItemDataRole.DecorationRole],
        )

data(index, role=Qt.ItemDataRole.DisplayRole)

Returns the data for the given index and role.

Source code in source/qextrawidgets/gui/proxys/decoration_role_proxy.py
24
25
26
27
28
29
30
31
32
33
34
35
36
def data(
    self,
    index: typing.Union[QModelIndex, QPersistentModelIndex],
    role: int = Qt.ItemDataRole.DisplayRole,
) -> typing.Any:
    """Returns the data for the given index and role."""
    if role == Qt.ItemDataRole.DecorationRole:
        if index.isValid():
            persistent_index = QPersistentModelIndex(index)
            return self._decorations.get(persistent_index, self._default_decoration)
        return None

    return super().data(index, role)

setData(index, value, role=Qt.ItemDataRole.EditRole)

Sets the data for the given index and role.

Source code in source/qextrawidgets/gui/proxys/decoration_role_proxy.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
def setData(
    self,
    index: typing.Union[QModelIndex, QPersistentModelIndex],
    value: typing.Any,
    role: int = Qt.ItemDataRole.EditRole,
) -> bool:
    """Sets the data for the given index and role."""
    if role == Qt.ItemDataRole.DecorationRole:
        if not index.isValid():
            return False

        persistent_index = QPersistentModelIndex(index)
        self._decorations[persistent_index] = value
        self.dataChanged.emit(index, index, [Qt.ItemDataRole.DecorationRole])
        return True

    return super().setData(index, value, role)

setDefaultDecoration(decoration)

Sets the default decoration for all items not explicitly set.

Source code in source/qextrawidgets/gui/proxys/decoration_role_proxy.py
56
57
58
59
60
61
62
63
64
65
66
67
68
def setDefaultDecoration(self, decoration: typing.Any) -> None:
    """Sets the default decoration for all items not explicitly set."""
    self._default_decoration = decoration
    # Invalidate all data to refresh the view
    if self.sourceModel():
        self.dataChanged.emit(
            self.index(0, 0),
            self.index(
                self.sourceModel().rowCount() - 1,
                self.sourceModel().columnCount() - 1,
            ),
            [Qt.ItemDataRole.DecorationRole],
        )

QEmojiPickerProxyModel

Bases: QSortFilterProxyModel

A high-performance proxy model to filter emojis by their alias.

Optimizations: 1. Uses setRecursiveFilteringEnabled(True) to avoid manual O(N^2) child iteration. 2. Caches the search term to avoid repetitive string manipulations per row.

Source code in source/qextrawidgets/gui/proxys/emoji_picker_proxy.py
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
class QEmojiPickerProxyModel(QSortFilterProxyModel):
    """
    A high-performance proxy model to filter emojis by their alias.

    Optimizations:
    1. Uses setRecursiveFilteringEnabled(True) to avoid manual O(N^2) child iteration.
    2. Caches the search term to avoid repetitive string manipulations per row.
    """

    def __init__(self, parent: typing.Optional[QWidget] = None):
        """
        Initializes the QEmojiProxyModel.

        Args:
            parent (QWidget, optional): The parent widget. Defaults to None.
        """
        super().__init__(parent)
        self.setFilterCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
        self.setDynamicSortFilter(True)

        # [OPTIMIZATION]
        # Automatically shows the Category (Parent) if an Emoji (Child) matches.
        # This eliminates the need to manually iterate children in filterAcceptsRow.
        self.setRecursiveFilteringEnabled(True)

        # Cache for the prepared search term
        self._cached_pattern: str = ""

    def setFilterFixedString(self, pattern: str) -> None:
        """
        Overrides the base method to cache the lowercase pattern
        for faster comparison.
        """
        # Pre-calculate lower() once per keystroke, not once per row
        self._cached_pattern = pattern.lower() if pattern else ""
        super().setFilterFixedString(pattern)

    def filterAcceptsRow(self, source_row: int, source_parent: typing.Union[QModelIndex, QPersistentModelIndex]) -> bool:
        """
        Determines if a row should be included in the view.

        With recursive filtering enabled:
        - We only need to validate the leaf nodes (Emojis).
        - If we return True for an Emoji, its Category is auto-included.
        - If we return False for a Category, it is still shown if a child matches.
        """
        # If no filter, show everything
        if not self._cached_pattern:
            return True

        # [OPTIMIZATION]
        # If this is a Category (Root), we simply return False.
        # Why? Because setRecursiveFilteringEnabled(True) will force-show this
        # category later if any of its children return True.
        # This skips all logic for category rows.
        if not source_parent.isValid():
            return False

        # Get the index
        model = self.sourceModel()
        index = model.index(source_row, 0, source_parent)

        # Retrieve Aliases
        # Note: Ensure your QEmojiItem returns a list of strings for this role
        aliases = index.data(QEmojiItem.QEmojiDataRole.ShortNamesRole)

        if not aliases:
            return False

        # [OPTIMIZATION]
        # Fast string check using the cached pattern.
        # Python's 'in' operator is highly optimized for str.
        # We assume _cached_pattern is already lowercased in setFilterFixedString.

        # Check if case sensitivity is needed (rare for emoji pickers, but respecting the property)
        is_case_sensitive = self.filterCaseSensitivity() == Qt.CaseSensitivity.CaseSensitive
        search_term = self._cached_pattern if not is_case_sensitive else self.filterRegularExpression().pattern()

        for alias in aliases:
            # Clean alias (e.g., ":smile:" -> "smile") if needed, or check directly
            # Assuming alias data might have mixed case, we lower it only if insensitive
            alias_check = alias if is_case_sensitive else alias.lower()

            if search_term in alias_check:
                return True

        return False

__init__(parent=None)

Initializes the QEmojiProxyModel.

Parameters:

Name Type Description Default
parent QWidget

The parent widget. Defaults to None.

None
Source code in source/qextrawidgets/gui/proxys/emoji_picker_proxy.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def __init__(self, parent: typing.Optional[QWidget] = None):
    """
    Initializes the QEmojiProxyModel.

    Args:
        parent (QWidget, optional): The parent widget. Defaults to None.
    """
    super().__init__(parent)
    self.setFilterCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
    self.setDynamicSortFilter(True)

    # [OPTIMIZATION]
    # Automatically shows the Category (Parent) if an Emoji (Child) matches.
    # This eliminates the need to manually iterate children in filterAcceptsRow.
    self.setRecursiveFilteringEnabled(True)

    # Cache for the prepared search term
    self._cached_pattern: str = ""

filterAcceptsRow(source_row, source_parent)

Determines if a row should be included in the view.

With recursive filtering enabled: - We only need to validate the leaf nodes (Emojis). - If we return True for an Emoji, its Category is auto-included. - If we return False for a Category, it is still shown if a child matches.

Source code in source/qextrawidgets/gui/proxys/emoji_picker_proxy.py
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
def filterAcceptsRow(self, source_row: int, source_parent: typing.Union[QModelIndex, QPersistentModelIndex]) -> bool:
    """
    Determines if a row should be included in the view.

    With recursive filtering enabled:
    - We only need to validate the leaf nodes (Emojis).
    - If we return True for an Emoji, its Category is auto-included.
    - If we return False for a Category, it is still shown if a child matches.
    """
    # If no filter, show everything
    if not self._cached_pattern:
        return True

    # [OPTIMIZATION]
    # If this is a Category (Root), we simply return False.
    # Why? Because setRecursiveFilteringEnabled(True) will force-show this
    # category later if any of its children return True.
    # This skips all logic for category rows.
    if not source_parent.isValid():
        return False

    # Get the index
    model = self.sourceModel()
    index = model.index(source_row, 0, source_parent)

    # Retrieve Aliases
    # Note: Ensure your QEmojiItem returns a list of strings for this role
    aliases = index.data(QEmojiItem.QEmojiDataRole.ShortNamesRole)

    if not aliases:
        return False

    # [OPTIMIZATION]
    # Fast string check using the cached pattern.
    # Python's 'in' operator is highly optimized for str.
    # We assume _cached_pattern is already lowercased in setFilterFixedString.

    # Check if case sensitivity is needed (rare for emoji pickers, but respecting the property)
    is_case_sensitive = self.filterCaseSensitivity() == Qt.CaseSensitivity.CaseSensitive
    search_term = self._cached_pattern if not is_case_sensitive else self.filterRegularExpression().pattern()

    for alias in aliases:
        # Clean alias (e.g., ":smile:" -> "smile") if needed, or check directly
        # Assuming alias data might have mixed case, we lower it only if insensitive
        alias_check = alias if is_case_sensitive else alias.lower()

        if search_term in alias_check:
            return True

    return False

setFilterFixedString(pattern)

Overrides the base method to cache the lowercase pattern for faster comparison.

Source code in source/qextrawidgets/gui/proxys/emoji_picker_proxy.py
36
37
38
39
40
41
42
43
def setFilterFixedString(self, pattern: str) -> None:
    """
    Overrides the base method to cache the lowercase pattern
    for faster comparison.
    """
    # Pre-calculate lower() once per keystroke, not once per row
    self._cached_pattern = pattern.lower() if pattern else ""
    super().setFilterFixedString(pattern)

QFormatProxyModel

Bases: QIdentityProxyModel

Proxy model that acts as a visual translator applying formatting masks to specific columns.

The class keeps the original data intact for editing and calculations, applying formatting only when the View requests data for display (DisplayRole).

Example

proxy = FormatProxyModel() proxy.setSourceModel(source_model)

Formatter for currency values

def format_currency(value): return f"R$ {value:,.2f}"

Register formatter on column 2

proxy.setColumnFormatter(2, format_currency)

table_view.setModel(proxy)

Source code in source/qextrawidgets/gui/proxys/format_proxy.py
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
class QFormatProxyModel(QIdentityProxyModel):
    """Proxy model that acts as a visual translator applying formatting masks to specific columns.

    The class keeps the original data intact for editing and calculations,
    applying formatting only when the View requests data for display (DisplayRole).

    Example:
        proxy = FormatProxyModel()
        proxy.setSourceModel(source_model)

        # Formatter for currency values
        def format_currency(value):
            return f"R$ {value:,.2f}"

        # Register formatter on column 2
        proxy.setColumnFormatter(2, format_currency)

        table_view.setModel(proxy)
    """

    def __init__(self, parent: Optional[Any] = None) -> None:
        """Initializes the FormatProxyModel.

        Args:
            parent: Optional parent widget.
        """
        super().__init__(parent)

        # Dictionary mapping column index to formatting function
        self._column_formatters: Dict[int, Callable[[Any], str]] = {}

    def setColumnFormatter(
        self, column: int, formatter: Optional[Callable[[Any], str]]
    ) -> None:
        """Registers or updates the formatting function for a specific column.

        Args:
            column: Column index (0-based).
            formatter: Callable that receives the raw value and returns the formatted string.
                      If None, removes the formatter from the column.

        Example:
            # Add formatter
            proxy.setColumnFormatter(0, lambda x: f"{x:04d}")

            # Remove formatter
            proxy.setColumnFormatter(0, None)
        """
        # If formatter is None, remove existing formatter
        if formatter is None:
            if column in self._column_formatters:
                del self._column_formatters[column]
        else:
            # Register or update the formatter
            self._column_formatters[column] = formatter

        # Emit dataChanged to update the View
        # Notify that all items in the column need to be redrawn
        if self.sourceModel() is not None:
            row_count = self.rowCount()
            if row_count > 0:
                top_left = self.index(0, column)
                bottom_right = self.index(row_count - 1, column)
                self.dataChanged.emit(
                    top_left, bottom_right, [Qt.ItemDataRole.DisplayRole]
                )

    def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any:
        """Intercepts data requests to apply visual formatting.

        Formatting is applied only when:
        1. The requested role is DisplayRole
        2. The index column has a registered formatter

        For all other cases, delegates to the superclass implementation.

        Args:
            index: Index of the item in the model.
            role: Role defining the type of data requested.

        Returns:
            Formatted data (if DisplayRole and column has formatter),
            or original data from superclass.
        """
        # Check if interception should occur
        if not index.isValid():
            return super().data(index, role)

        column = index.column()

        # Intercept only DisplayRole for columns with registered formatter
        if role == Qt.ItemDataRole.DisplayRole and column in self._column_formatters:
            # Map proxy index to source model
            source_index = self.mapToSource(index)

            if not source_index.isValid():
                return super().data(index, role)

            # Extract raw data, prioritizing EditRole (unformatted data)
            # EditRole ensures we get the real value, not an already formatted version
            source_model = self.sourceModel()
            raw_data = source_model.data(source_index, Qt.ItemDataRole.EditRole)

            # Fallback to DisplayRole if EditRole returns None
            if raw_data is None:
                raw_data = source_model.data(source_index, Qt.ItemDataRole.DisplayRole)

            # If still no data, return None
            if raw_data is None:
                return None

            # Apply formatter with exception protection
            try:
                formatter = self._column_formatters[column]
                formatted_value = formatter(raw_data)
                return formatted_value
            except Exception:
                # On formatter error, return raw data as string
                # This prevents the application from breaking due to formatting function errors
                return str(raw_data)

        # For any other role or columns without formatter,
        # delegate to superclass implementation
        return super().data(index, role)

    def columnFormatter(self, column: int) -> Optional[Callable[[Any], str]]:
        """Returns the registered formatter for a specific column.

        Args:
            column: Column index.

        Returns:
            Formatter callable or None if no formatter is registered.
        """
        return self._column_formatters.get(column)

    def clearAllFormatters(self) -> None:
        """Removes all registered formatters and updates the View."""
        if not self._column_formatters:
            return

        # Store columns that had formatters
        columns = list(self._column_formatters.keys())

        # Clear dictionary
        self._column_formatters.clear()

        # Emit dataChanged for each affected column
        if self.sourceModel() is not None:
            row_count = self.rowCount()
            if row_count > 0:
                for column in columns:
                    top_left = self.index(0, column)
                    bottom_right = self.index(row_count - 1, column)
                    self.dataChanged.emit(
                        top_left, bottom_right, [Qt.ItemDataRole.DisplayRole]
                    )

__init__(parent=None)

Initializes the FormatProxyModel.

Parameters:

Name Type Description Default
parent Optional[Any]

Optional parent widget.

None
Source code in source/qextrawidgets/gui/proxys/format_proxy.py
25
26
27
28
29
30
31
32
33
34
def __init__(self, parent: Optional[Any] = None) -> None:
    """Initializes the FormatProxyModel.

    Args:
        parent: Optional parent widget.
    """
    super().__init__(parent)

    # Dictionary mapping column index to formatting function
    self._column_formatters: Dict[int, Callable[[Any], str]] = {}

clearAllFormatters()

Removes all registered formatters and updates the View.

Source code in source/qextrawidgets/gui/proxys/format_proxy.py
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
def clearAllFormatters(self) -> None:
    """Removes all registered formatters and updates the View."""
    if not self._column_formatters:
        return

    # Store columns that had formatters
    columns = list(self._column_formatters.keys())

    # Clear dictionary
    self._column_formatters.clear()

    # Emit dataChanged for each affected column
    if self.sourceModel() is not None:
        row_count = self.rowCount()
        if row_count > 0:
            for column in columns:
                top_left = self.index(0, column)
                bottom_right = self.index(row_count - 1, column)
                self.dataChanged.emit(
                    top_left, bottom_right, [Qt.ItemDataRole.DisplayRole]
                )

columnFormatter(column)

Returns the registered formatter for a specific column.

Parameters:

Name Type Description Default
column int

Column index.

required

Returns:

Type Description
Optional[Callable[[Any], str]]

Formatter callable or None if no formatter is registered.

Source code in source/qextrawidgets/gui/proxys/format_proxy.py
130
131
132
133
134
135
136
137
138
139
def columnFormatter(self, column: int) -> Optional[Callable[[Any], str]]:
    """Returns the registered formatter for a specific column.

    Args:
        column: Column index.

    Returns:
        Formatter callable or None if no formatter is registered.
    """
    return self._column_formatters.get(column)

data(index, role=Qt.ItemDataRole.DisplayRole)

Intercepts data requests to apply visual formatting.

Formatting is applied only when: 1. The requested role is DisplayRole 2. The index column has a registered formatter

For all other cases, delegates to the superclass implementation.

Parameters:

Name Type Description Default
index QModelIndex

Index of the item in the model.

required
role int

Role defining the type of data requested.

DisplayRole

Returns:

Type Description
Any

Formatted data (if DisplayRole and column has formatter),

Any

or original data from superclass.

Source code in source/qextrawidgets/gui/proxys/format_proxy.py
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any:
    """Intercepts data requests to apply visual formatting.

    Formatting is applied only when:
    1. The requested role is DisplayRole
    2. The index column has a registered formatter

    For all other cases, delegates to the superclass implementation.

    Args:
        index: Index of the item in the model.
        role: Role defining the type of data requested.

    Returns:
        Formatted data (if DisplayRole and column has formatter),
        or original data from superclass.
    """
    # Check if interception should occur
    if not index.isValid():
        return super().data(index, role)

    column = index.column()

    # Intercept only DisplayRole for columns with registered formatter
    if role == Qt.ItemDataRole.DisplayRole and column in self._column_formatters:
        # Map proxy index to source model
        source_index = self.mapToSource(index)

        if not source_index.isValid():
            return super().data(index, role)

        # Extract raw data, prioritizing EditRole (unformatted data)
        # EditRole ensures we get the real value, not an already formatted version
        source_model = self.sourceModel()
        raw_data = source_model.data(source_index, Qt.ItemDataRole.EditRole)

        # Fallback to DisplayRole if EditRole returns None
        if raw_data is None:
            raw_data = source_model.data(source_index, Qt.ItemDataRole.DisplayRole)

        # If still no data, return None
        if raw_data is None:
            return None

        # Apply formatter with exception protection
        try:
            formatter = self._column_formatters[column]
            formatted_value = formatter(raw_data)
            return formatted_value
        except Exception:
            # On formatter error, return raw data as string
            # This prevents the application from breaking due to formatting function errors
            return str(raw_data)

    # For any other role or columns without formatter,
    # delegate to superclass implementation
    return super().data(index, role)

setColumnFormatter(column, formatter)

Registers or updates the formatting function for a specific column.

Parameters:

Name Type Description Default
column int

Column index (0-based).

required
formatter Optional[Callable[[Any], str]]

Callable that receives the raw value and returns the formatted string. If None, removes the formatter from the column.

required
Example

Add formatter

proxy.setColumnFormatter(0, lambda x: f"{x:04d}")

Remove formatter

proxy.setColumnFormatter(0, None)

Source code in source/qextrawidgets/gui/proxys/format_proxy.py
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
def setColumnFormatter(
    self, column: int, formatter: Optional[Callable[[Any], str]]
) -> None:
    """Registers or updates the formatting function for a specific column.

    Args:
        column: Column index (0-based).
        formatter: Callable that receives the raw value and returns the formatted string.
                  If None, removes the formatter from the column.

    Example:
        # Add formatter
        proxy.setColumnFormatter(0, lambda x: f"{x:04d}")

        # Remove formatter
        proxy.setColumnFormatter(0, None)
    """
    # If formatter is None, remove existing formatter
    if formatter is None:
        if column in self._column_formatters:
            del self._column_formatters[column]
    else:
        # Register or update the formatter
        self._column_formatters[column] = formatter

    # Emit dataChanged to update the View
    # Notify that all items in the column need to be redrawn
    if self.sourceModel() is not None:
        row_count = self.rowCount()
        if row_count > 0:
            top_left = self.index(0, column)
            bottom_right = self.index(row_count - 1, column)
            self.dataChanged.emit(
                top_left, bottom_right, [Qt.ItemDataRole.DisplayRole]
            )

QHeaderProxyModel

Bases: QIdentityProxyModel

A proxy model that handles header data customizations, such as icons.

Source code in source/qextrawidgets/gui/proxys/header_proxy.py
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class QHeaderProxyModel(QIdentityProxyModel):
    """A proxy model that handles header data customizations, such as icons."""

    def __init__(self, parent: typing.Optional[QObject] = None) -> None:
        super().__init__(parent)
        self._header_icons = {}

    def setHeaderData(
        self,
        section: int,
        orientation: Qt.Orientation,
        value: typing.Any,
        role: int = Qt.ItemDataRole.EditRole,
    ) -> bool:
        """Sets the header data for the given section and orientation."""
        if (
            orientation == Qt.Orientation.Horizontal
            and role == Qt.ItemDataRole.DecorationRole
        ):
            self._header_icons[section] = value
            self.headerDataChanged.emit(orientation, section, section)
            return True
        return super().setHeaderData(section, orientation, value, role)

    def headerData(
        self,
        section: int,
        orientation: Qt.Orientation,
        role: int = Qt.ItemDataRole.DisplayRole,
    ) -> typing.Any:
        """Returns the header data for the given section and orientation."""
        if (
            orientation == Qt.Orientation.Horizontal
            and role == Qt.ItemDataRole.DecorationRole
        ):
            if section in self._header_icons:
                return self._header_icons[section]
        return super().headerData(section, orientation, role)

    def reset(self):
        """Resets the header icons."""
        self._header_icons = {}

headerData(section, orientation, role=Qt.ItemDataRole.DisplayRole)

Returns the header data for the given section and orientation.

Source code in source/qextrawidgets/gui/proxys/header_proxy.py
29
30
31
32
33
34
35
36
37
38
39
40
41
42
def headerData(
    self,
    section: int,
    orientation: Qt.Orientation,
    role: int = Qt.ItemDataRole.DisplayRole,
) -> typing.Any:
    """Returns the header data for the given section and orientation."""
    if (
        orientation == Qt.Orientation.Horizontal
        and role == Qt.ItemDataRole.DecorationRole
    ):
        if section in self._header_icons:
            return self._header_icons[section]
    return super().headerData(section, orientation, role)

reset()

Resets the header icons.

Source code in source/qextrawidgets/gui/proxys/header_proxy.py
44
45
46
def reset(self):
    """Resets the header icons."""
    self._header_icons = {}

setHeaderData(section, orientation, value, role=Qt.ItemDataRole.EditRole)

Sets the header data for the given section and orientation.

Source code in source/qextrawidgets/gui/proxys/header_proxy.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def setHeaderData(
    self,
    section: int,
    orientation: Qt.Orientation,
    value: typing.Any,
    role: int = Qt.ItemDataRole.EditRole,
) -> bool:
    """Sets the header data for the given section and orientation."""
    if (
        orientation == Qt.Orientation.Horizontal
        and role == Qt.ItemDataRole.DecorationRole
    ):
        self._header_icons[section] = value
        self.headerDataChanged.emit(orientation, section, section)
        return True
    return super().setHeaderData(section, orientation, value, role)

QMultiFilterProxyModel

Bases: QSortFilterProxyModel

A proxy model that supports multiple filters per column.

Source code in source/qextrawidgets/gui/proxys/multi_filter_proxy.py
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
class QMultiFilterProxyModel(QSortFilterProxyModel):
    """A proxy model that supports multiple filters per column."""

    def __init__(self, parent: typing.Optional[QObject] = None) -> None:
        """Initializes the multi-filter proxy model.

        Args:
            parent (QObject, optional): Parent object. Defaults to None.
        """
        super().__init__(parent)
        self._filters = {}

    def setFilter(
        self, col: int, text_list: typing.Optional[typing.Iterable[str]]
    ) -> None:
        """Sets the list of allowed values for a specific column.

        Args:
            col (int): Column index.
            text_list (Iterable[str], optional): List of allowed string values. If None or empty, the filter is removed.
        """
        if text_list:
            self._filters[col] = text_list
        else:
            self._filters.pop(col, None)
        self.invalidateFilter()

    def isFiltering(self) -> bool:
        """Returns True if any filter is active."""
        return bool(self._filters)

    def isColumnFiltered(self, col: int) -> bool:
        """Returns True if the given column is filtered."""
        return col in self._filters

    def filterAcceptsRow(
        self,
        source_row: int,
        source_parent: typing.Union[QModelIndex, QPersistentModelIndex],
    ) -> bool:
        """Determines if a row passes all column filters.

        Args:
            source_row (int): Row number.
            source_parent (QModelIndex): Parent index.

        Returns:
            bool: True if the row matches all filters, False otherwise.
        """
        model = self.sourceModel()
        if not model:
            return True

        for col, text_list in self._filters.items():
            index = model.index(source_row, col, source_parent)
            value = str(model.data(index))
            if not any(text == value for text in text_list):
                return False
        return True

    def reset(self):
        """Resets the filters."""
        self._filters = {}
        self.invalidateFilter()

__init__(parent=None)

Initializes the multi-filter proxy model.

Parameters:

Name Type Description Default
parent QObject

Parent object. Defaults to None.

None
Source code in source/qextrawidgets/gui/proxys/multi_filter_proxy.py
10
11
12
13
14
15
16
17
def __init__(self, parent: typing.Optional[QObject] = None) -> None:
    """Initializes the multi-filter proxy model.

    Args:
        parent (QObject, optional): Parent object. Defaults to None.
    """
    super().__init__(parent)
    self._filters = {}

filterAcceptsRow(source_row, source_parent)

Determines if a row passes all column filters.

Parameters:

Name Type Description Default
source_row int

Row number.

required
source_parent QModelIndex

Parent index.

required

Returns:

Name Type Description
bool bool

True if the row matches all filters, False otherwise.

Source code in source/qextrawidgets/gui/proxys/multi_filter_proxy.py
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
def filterAcceptsRow(
    self,
    source_row: int,
    source_parent: typing.Union[QModelIndex, QPersistentModelIndex],
) -> bool:
    """Determines if a row passes all column filters.

    Args:
        source_row (int): Row number.
        source_parent (QModelIndex): Parent index.

    Returns:
        bool: True if the row matches all filters, False otherwise.
    """
    model = self.sourceModel()
    if not model:
        return True

    for col, text_list in self._filters.items():
        index = model.index(source_row, col, source_parent)
        value = str(model.data(index))
        if not any(text == value for text in text_list):
            return False
    return True

isColumnFiltered(col)

Returns True if the given column is filtered.

Source code in source/qextrawidgets/gui/proxys/multi_filter_proxy.py
38
39
40
def isColumnFiltered(self, col: int) -> bool:
    """Returns True if the given column is filtered."""
    return col in self._filters

isFiltering()

Returns True if any filter is active.

Source code in source/qextrawidgets/gui/proxys/multi_filter_proxy.py
34
35
36
def isFiltering(self) -> bool:
    """Returns True if any filter is active."""
    return bool(self._filters)

reset()

Resets the filters.

Source code in source/qextrawidgets/gui/proxys/multi_filter_proxy.py
67
68
69
70
def reset(self):
    """Resets the filters."""
    self._filters = {}
    self.invalidateFilter()

setFilter(col, text_list)

Sets the list of allowed values for a specific column.

Parameters:

Name Type Description Default
col int

Column index.

required
text_list Iterable[str]

List of allowed string values. If None or empty, the filter is removed.

required
Source code in source/qextrawidgets/gui/proxys/multi_filter_proxy.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def setFilter(
    self, col: int, text_list: typing.Optional[typing.Iterable[str]]
) -> None:
    """Sets the list of allowed values for a specific column.

    Args:
        col (int): Column index.
        text_list (Iterable[str], optional): List of allowed string values. If None or empty, the filter is removed.
    """
    if text_list:
        self._filters[col] = text_list
    else:
        self._filters.pop(col, None)
    self.invalidateFilter()

QUniqueValuesProxyModel

Bases: QSortFilterProxyModel

A proxy model that filters rows to show only unique values from a specific column.

This is useful for creating filter lists where you want to show each available option exactly once, even if it appears multiple times in the source model.

Source code in source/qextrawidgets/gui/proxys/unique_values_proxy.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
class QUniqueValuesProxyModel(QSortFilterProxyModel):
    """A proxy model that filters rows to show only unique values from a specific column.

    This is useful for creating filter lists where you want to show each available option exactly once,
    even if it appears multiple times in the source model.
    """

    def __init__(self, parent: typing.Optional[QObject] = None) -> None:
        super().__init__(parent)
        self._target_column = 0
        self._unique_rows: typing.Set[int] = set()

    def setTargetColumn(self, column: int) -> None:
        """Sets the column to filter for unique values."""
        if self._target_column != column:
            self._target_column = column
            self.invalidateFilter()

    def targetColumn(self) -> int:
        return self._target_column

    def setSourceModel(self, source_model: QAbstractItemModel) -> None:
        super().setSourceModel(source_model)
        # Connect signals to invalidate cache on changes
        if source_model:
            # Connect using the correct signal signatures
            source_model.modelReset.connect(self.invalidateFilter)
            source_model.layoutChanged.connect(self.invalidateFilter)
            source_model.rowsInserted.connect(self.invalidateFilter)
            source_model.rowsRemoved.connect(self.invalidateFilter)
            source_model.dataChanged.connect(self.invalidateFilter)

        # Explicitly rebuild cache for the new model
        self.invalidateFilter()

    def filterAcceptsRow(
        self,
        source_row: int,
        source_parent: typing.Union[QModelIndex, QPersistentModelIndex],
    ) -> bool:
        """Accepts the row only if it's the first occurrence of its value."""
        # Note: QSortFilterProxyModel calls this method for every row.
        # We need to rely on the pre-calculated set of unique rows for performance.
        # If cache is empty and model is not, we might need to rebuild it?
        # Ideally, we rebuild cache in `invalidateFilter` or lazy load.
        # However, `filterAcceptsRow` is const and called during filtering.
        # We can't easily rebuild "once" inside here without flags.

        # Optimization: We check if this row is in our allowed set.
        return source_row in self._unique_rows

    def invalidateFilter(self) -> None:
        """Rebuilds the unique value cache and invalidates the filter."""
        self._rebuild_unique_cache()
        super().invalidate()

    def _rebuild_unique_cache(self) -> None:
        """Scans the source model and identifies the first row for each unique value."""
        self._unique_rows.clear()
        source = self.sourceModel()
        if not source:
            return

        seen_values = set()
        row_count = source.rowCount()

        for row in range(row_count):
            index = source.index(row, self._target_column)
            val = str(source.data(index, Qt.ItemDataRole.DisplayRole))

            if val not in seen_values:
                seen_values.add(val)
                self._unique_rows.add(row)

    def data(
        self,
        index: typing.Union[QModelIndex, QPersistentModelIndex],
        role: int = Qt.ItemDataRole.DisplayRole,
    ) -> typing.Any:
        # We might want to override data to potentially format stuff, but default proxy behavior is fine.
        return super().data(index, role)

filterAcceptsRow(source_row, source_parent)

Accepts the row only if it's the first occurrence of its value.

Source code in source/qextrawidgets/gui/proxys/unique_values_proxy.py
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def filterAcceptsRow(
    self,
    source_row: int,
    source_parent: typing.Union[QModelIndex, QPersistentModelIndex],
) -> bool:
    """Accepts the row only if it's the first occurrence of its value."""
    # Note: QSortFilterProxyModel calls this method for every row.
    # We need to rely on the pre-calculated set of unique rows for performance.
    # If cache is empty and model is not, we might need to rebuild it?
    # Ideally, we rebuild cache in `invalidateFilter` or lazy load.
    # However, `filterAcceptsRow` is const and called during filtering.
    # We can't easily rebuild "once" inside here without flags.

    # Optimization: We check if this row is in our allowed set.
    return source_row in self._unique_rows

invalidateFilter()

Rebuilds the unique value cache and invalidates the filter.

Source code in source/qextrawidgets/gui/proxys/unique_values_proxy.py
63
64
65
66
def invalidateFilter(self) -> None:
    """Rebuilds the unique value cache and invalidates the filter."""
    self._rebuild_unique_cache()
    super().invalidate()

setTargetColumn(column)

Sets the column to filter for unique values.

Source code in source/qextrawidgets/gui/proxys/unique_values_proxy.py
24
25
26
27
28
def setTargetColumn(self, column: int) -> None:
    """Sets the column to filter for unique values."""
    if self._target_column != column:
        self._target_column = column
        self.invalidateFilter()

Validators

QEmojiValidator

Bases: QRegularExpressionValidator

A validator that only accepts text consisting exclusively of emojis.

Source code in source/qextrawidgets/gui/validators/emoji_validator.py
 8
 9
10
11
12
13
14
15
16
17
class QEmojiValidator(QRegularExpressionValidator):
    """A validator that only accepts text consisting exclusively of emojis."""

    def __init__(self, parent: typing.Optional[QObject] = None) -> None:
        """Initializes the emoji validator.

        Args:
            parent (QObject, optional): Parent object. Defaults to None.
        """
        super().__init__(QEmojiRegex(), parent)

__init__(parent=None)

Initializes the emoji validator.

Parameters:

Name Type Description Default
parent QObject

Parent object. Defaults to None.

None
Source code in source/qextrawidgets/gui/validators/emoji_validator.py
11
12
13
14
15
16
17
def __init__(self, parent: typing.Optional[QObject] = None) -> None:
    """Initializes the emoji validator.

    Args:
        parent (QObject, optional): Parent object. Defaults to None.
    """
    super().__init__(QEmojiRegex(), parent)

Widgets

Buttons

QColorButton

Bases: QPushButton

A button that displays a specific color and automatically adjusts its text color for contrast.

Source code in source/qextrawidgets/widgets/buttons/color_button.py
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
class QColorButton(QPushButton):
    """A button that displays a specific color and automatically adjusts its text color for contrast."""

    def __init__(self,
                color: typing.Union[Qt.GlobalColor, QColor, str],
                text: str = "",
                text_color: typing.Union[Qt.GlobalColor, QColor, str, None] = None,
                checked_color: typing.Union[Qt.GlobalColor, QColor, str, None] = None,
                parent: typing.Optional[QWidget] = None) -> None:
        """Initializes the color button.

        Args:
            color (Qt.GlobalColor, QColor, str): Background color of the button.
            text (str, optional): Button text. Defaults to "".
            text_color (Qt.GlobalColor, QColor, str, optional): Text color. If None, it's calculated for contrast. Defaults to None.
            checked_color (Qt.GlobalColor, QColor, str, optional): Color when the button is in checked state. Defaults to None.
            parent (QWidget, optional): Parent widget. Defaults to None.
        """
        super().__init__(text, parent)

        # We store colors as class attributes
        self._color = QColor(color)
        self._text_color: typing.Optional[QColor]
        self._checked_color: typing.Optional[QColor]
        self.setTextColor(text_color)
        self.setCheckedColor(checked_color)

        # Initial visual configuration
        # Removed setAutoFillBackground(True) to avoid square background artifacts on rounded buttons

    def initStyleOption(self, option: QStyleOptionButton) -> None:
        """Method called automatically by Qt before drawing the button.

        Here we intercept the style option and change the palette color
        based on the current state (Hover, Pressed, etc).

        Args:
            option (QStyleOptionButton): The style option to initialize.
        """
        # 1. Let QPushButton fill the option with the default state
        super().initStyleOption(option)

        state: QStyle.StateFlag = getattr(option, 'state')
        palette: QPalette = getattr(option, 'palette')

        # Determine the base color to use (Normal or Checked)
        base_color = self._color
        if (state & QStyle.StateFlag.State_On) and self._checked_color is not None:
            base_color = self._checked_color

        # 2. Check the state in the 'option' object and change the palette color locally
        if state & QStyle.StateFlag.State_Sunken:  # Pressed
            pressed_color = base_color.darker(115)  # 15% darker
            palette.setColor(QPalette.ColorRole.Button, pressed_color)
            # Removed Window role setting as it's not needed for button face and causes artifacts

        elif state & QStyle.StateFlag.State_MouseOver:  # Mouse over
            hover_color = base_color.lighter(115)  # 15% lighter
            palette.setColor(QPalette.ColorRole.Button, hover_color)

        else:  # Normal State (or Checked State if not interacting)
            palette.setColor(QPalette.ColorRole.Button, base_color)

        if self._text_color is None:
            palette.setColor(QPalette.ColorRole.ButtonText, QColorUtils.getContrastingTextColor(base_color))

        else:
            palette.setColor(QPalette.ColorRole.ButtonText, self._text_color)

    def color(self) -> QColor:
        """Returns the button background color.

        Returns:
            QColor: Background color.
        """
        return self._color

    def setColor(self, color: typing.Union[Qt.GlobalColor, QColor, str]) -> None:
        """Sets the button background color.

        Args:
            color (Qt.GlobalColor, QColor, str): New background color.
        """
        self._color = QColor(color)

    def checkedColor(self) -> typing.Optional[QColor]:
        """Returns the color used when the button is checked.

        Returns:
            QColor, optional: Checked color or None.
        """
        return self._checked_color

    def setCheckedColor(self, color: typing.Union[Qt.GlobalColor, QColor, str, None]) -> None:
        """Sets the color to use when the button is checked.

        Args:
            color (Qt.GlobalColor, QColor, str, None): New checked color.
        """
        if color is None:
            self._checked_color = None
        else:
            self._checked_color = QColor(color)

    def textColor(self) -> typing.Optional[QColor]:
        """Returns the text color.

        Returns:
            QColor, optional: Text color or None if automatically calculated.
        """
        return self._text_color

    def setTextColor(self, text_color: typing.Union[Qt.GlobalColor, QColor, str, None]) -> None:
        """Sets the text color.

        Args:
            text_color (Qt.GlobalColor, QColor, str, None): New text color.
        """
        if text_color is None:
            self._text_color = text_color
        else:
            self._text_color = QColor(text_color)

__init__(color, text='', text_color=None, checked_color=None, parent=None)

Initializes the color button.

Parameters:

Name Type Description Default
color (GlobalColor, QColor, str)

Background color of the button.

required
text str

Button text. Defaults to "".

''
text_color (GlobalColor, QColor, str)

Text color. If None, it's calculated for contrast. Defaults to None.

None
checked_color (GlobalColor, QColor, str)

Color when the button is in checked state. Defaults to None.

None
parent QWidget

Parent widget. Defaults to None.

None
Source code in source/qextrawidgets/widgets/buttons/color_button.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def __init__(self,
            color: typing.Union[Qt.GlobalColor, QColor, str],
            text: str = "",
            text_color: typing.Union[Qt.GlobalColor, QColor, str, None] = None,
            checked_color: typing.Union[Qt.GlobalColor, QColor, str, None] = None,
            parent: typing.Optional[QWidget] = None) -> None:
    """Initializes the color button.

    Args:
        color (Qt.GlobalColor, QColor, str): Background color of the button.
        text (str, optional): Button text. Defaults to "".
        text_color (Qt.GlobalColor, QColor, str, optional): Text color. If None, it's calculated for contrast. Defaults to None.
        checked_color (Qt.GlobalColor, QColor, str, optional): Color when the button is in checked state. Defaults to None.
        parent (QWidget, optional): Parent widget. Defaults to None.
    """
    super().__init__(text, parent)

    # We store colors as class attributes
    self._color = QColor(color)
    self._text_color: typing.Optional[QColor]
    self._checked_color: typing.Optional[QColor]
    self.setTextColor(text_color)
    self.setCheckedColor(checked_color)

checkedColor()

Returns the color used when the button is checked.

Returns:

Type Description
Optional[QColor]

QColor, optional: Checked color or None.

Source code in source/qextrawidgets/widgets/buttons/color_button.py
 95
 96
 97
 98
 99
100
101
def checkedColor(self) -> typing.Optional[QColor]:
    """Returns the color used when the button is checked.

    Returns:
        QColor, optional: Checked color or None.
    """
    return self._checked_color

color()

Returns the button background color.

Returns:

Name Type Description
QColor QColor

Background color.

Source code in source/qextrawidgets/widgets/buttons/color_button.py
79
80
81
82
83
84
85
def color(self) -> QColor:
    """Returns the button background color.

    Returns:
        QColor: Background color.
    """
    return self._color

initStyleOption(option)

Method called automatically by Qt before drawing the button.

Here we intercept the style option and change the palette color based on the current state (Hover, Pressed, etc).

Parameters:

Name Type Description Default
option QStyleOptionButton

The style option to initialize.

required
Source code in source/qextrawidgets/widgets/buttons/color_button.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
def initStyleOption(self, option: QStyleOptionButton) -> None:
    """Method called automatically by Qt before drawing the button.

    Here we intercept the style option and change the palette color
    based on the current state (Hover, Pressed, etc).

    Args:
        option (QStyleOptionButton): The style option to initialize.
    """
    # 1. Let QPushButton fill the option with the default state
    super().initStyleOption(option)

    state: QStyle.StateFlag = getattr(option, 'state')
    palette: QPalette = getattr(option, 'palette')

    # Determine the base color to use (Normal or Checked)
    base_color = self._color
    if (state & QStyle.StateFlag.State_On) and self._checked_color is not None:
        base_color = self._checked_color

    # 2. Check the state in the 'option' object and change the palette color locally
    if state & QStyle.StateFlag.State_Sunken:  # Pressed
        pressed_color = base_color.darker(115)  # 15% darker
        palette.setColor(QPalette.ColorRole.Button, pressed_color)
        # Removed Window role setting as it's not needed for button face and causes artifacts

    elif state & QStyle.StateFlag.State_MouseOver:  # Mouse over
        hover_color = base_color.lighter(115)  # 15% lighter
        palette.setColor(QPalette.ColorRole.Button, hover_color)

    else:  # Normal State (or Checked State if not interacting)
        palette.setColor(QPalette.ColorRole.Button, base_color)

    if self._text_color is None:
        palette.setColor(QPalette.ColorRole.ButtonText, QColorUtils.getContrastingTextColor(base_color))

    else:
        palette.setColor(QPalette.ColorRole.ButtonText, self._text_color)

setCheckedColor(color)

Sets the color to use when the button is checked.

Parameters:

Name Type Description Default
color (GlobalColor, QColor, str, None)

New checked color.

required
Source code in source/qextrawidgets/widgets/buttons/color_button.py
103
104
105
106
107
108
109
110
111
112
def setCheckedColor(self, color: typing.Union[Qt.GlobalColor, QColor, str, None]) -> None:
    """Sets the color to use when the button is checked.

    Args:
        color (Qt.GlobalColor, QColor, str, None): New checked color.
    """
    if color is None:
        self._checked_color = None
    else:
        self._checked_color = QColor(color)

setColor(color)

Sets the button background color.

Parameters:

Name Type Description Default
color (GlobalColor, QColor, str)

New background color.

required
Source code in source/qextrawidgets/widgets/buttons/color_button.py
87
88
89
90
91
92
93
def setColor(self, color: typing.Union[Qt.GlobalColor, QColor, str]) -> None:
    """Sets the button background color.

    Args:
        color (Qt.GlobalColor, QColor, str): New background color.
    """
    self._color = QColor(color)

setTextColor(text_color)

Sets the text color.

Parameters:

Name Type Description Default
text_color (GlobalColor, QColor, str, None)

New text color.

required
Source code in source/qextrawidgets/widgets/buttons/color_button.py
122
123
124
125
126
127
128
129
130
131
def setTextColor(self, text_color: typing.Union[Qt.GlobalColor, QColor, str, None]) -> None:
    """Sets the text color.

    Args:
        text_color (Qt.GlobalColor, QColor, str, None): New text color.
    """
    if text_color is None:
        self._text_color = text_color
    else:
        self._text_color = QColor(text_color)

textColor()

Returns the text color.

Returns:

Type Description
Optional[QColor]

QColor, optional: Text color or None if automatically calculated.

Source code in source/qextrawidgets/widgets/buttons/color_button.py
114
115
116
117
118
119
120
def textColor(self) -> typing.Optional[QColor]:
    """Returns the text color.

    Returns:
        QColor, optional: Text color or None if automatically calculated.
    """
    return self._text_color

QColorToolButton

Bases: QToolButton

A tool button that displays a specific color and automatically adjusts its text color for contrast.

Source code in source/qextrawidgets/widgets/buttons/color_tool_button.py
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
class QColorToolButton(QToolButton):
    """A tool button that displays a specific color and automatically adjusts its text color for contrast."""

    def __init__(self,
                 color: typing.Union[Qt.GlobalColor, QColor, str],
                 text: str = "",
                 text_color: typing.Union[Qt.GlobalColor, QColor, str, None] = None,
                 checked_color: typing.Union[Qt.GlobalColor, QColor, str, None] = None,
                 parent: typing.Optional[QWidget] = None) -> None:
        """Initializes the color tool button.

        Args:
            color (Qt.GlobalColor, QColor, str): Background color of the button.
            text (str, optional): Button text. Defaults to "".
            text_color (Qt.GlobalColor, QColor, str, optional): Text color. If None, it's calculated for contrast. Defaults to None.
            checked_color (Qt.GlobalColor, QColor, str, optional): Color when the button is in checked state. Defaults to None.
            parent (QWidget, optional): Parent widget. Defaults to None.
        """
        super().__init__(parent)
        self.setText(text)

        # We store colors as class attributes
        self._color: QColor
        self._text_color: typing.Optional[QColor]
        self._checked_color: typing.Optional[QColor]

        self.setColor(color)
        self.setTextColor(text_color)
        self.setCheckedColor(checked_color)

    def initStyleOption(self, option: QStyleOptionToolButton) -> None:
        """Method called automatically by Qt before drawing the button.

        Here we intercept the style option and change the palette color
        based on the current state (Hover, Pressed, etc).

        Args:
            option (QStyleOptionToolButton): The style option to initialize.
        """
        # 1. Let QToolButton fill the option with the default state
        super().initStyleOption(option)

        state: QStyle.StateFlag = getattr(option, 'state')
        palette: QPalette = getattr(option, 'palette')

        # Determine the base color to use (Normal or Checked)
        base_color = self._color
        if (state & QStyle.StateFlag.State_On) and self._checked_color is not None:
            base_color = self._checked_color

        # 2. Check the state in the 'option' object and change the palette color locally
        if state & QStyle.StateFlag.State_Sunken:  # Pressed
            pressed_color = base_color.darker(115)  # 15% darker
            palette.setColor(QPalette.ColorRole.Button, pressed_color)

        elif state & QStyle.StateFlag.State_MouseOver:  # Mouse over
            hover_color = base_color.lighter(115)  # 15% lighter
            palette.setColor(QPalette.ColorRole.Button, hover_color)

        else:  # Normal State (or Checked State if not interacting)
            palette.setColor(QPalette.ColorRole.Button, base_color)

        if self._text_color is None:
            palette.setColor(QPalette.ColorRole.ButtonText, QColorUtils.getContrastingTextColor(base_color))

        else:
            palette.setColor(QPalette.ColorRole.ButtonText, self._text_color)

    def color(self) -> QColor:
        """Returns the button background color.

        Returns:
            QColor: Background color.
        """
        return self._color

    def setColor(self, color: typing.Union[Qt.GlobalColor, QColor, str]) -> None:
        """Sets the button background color.

        Args:
            color (Qt.GlobalColor, QColor, str): New background color.
        """
        self._color = QColor(color)

    def checkedColor(self) -> typing.Optional[QColor]:
        """Returns the color used when the button is checked.

        Returns:
            QColor, optional: Checked color or None.
        """
        return self._checked_color

    def setCheckedColor(self, color: typing.Union[Qt.GlobalColor, QColor, str, None]) -> None:
        """Sets the color to use when the button is checked.

        Args:
            color (Qt.GlobalColor, QColor, str, None): New checked color.
        """
        if color is None:
            self._checked_color = None
        else:
            self._checked_color = QColor(color)

    def textColor(self) -> typing.Optional[QColor]:
        """Returns the text color.

        Returns:
            QColor, optional: Text color or None if automatically calculated.
        """
        return self._text_color

    def setTextColor(self, text_color: typing.Union[Qt.GlobalColor, QColor, str, None]) -> None:
        """Sets the text color.

        Args:
            text_color (Qt.GlobalColor, QColor, str, None): New text color.
        """
        if text_color is None:
            self._text_color = text_color
        else:
            self._text_color = QColor(text_color)

__init__(color, text='', text_color=None, checked_color=None, parent=None)

Initializes the color tool button.

Parameters:

Name Type Description Default
color (GlobalColor, QColor, str)

Background color of the button.

required
text str

Button text. Defaults to "".

''
text_color (GlobalColor, QColor, str)

Text color. If None, it's calculated for contrast. Defaults to None.

None
checked_color (GlobalColor, QColor, str)

Color when the button is in checked state. Defaults to None.

None
parent QWidget

Parent widget. Defaults to None.

None
Source code in source/qextrawidgets/widgets/buttons/color_tool_button.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
def __init__(self,
             color: typing.Union[Qt.GlobalColor, QColor, str],
             text: str = "",
             text_color: typing.Union[Qt.GlobalColor, QColor, str, None] = None,
             checked_color: typing.Union[Qt.GlobalColor, QColor, str, None] = None,
             parent: typing.Optional[QWidget] = None) -> None:
    """Initializes the color tool button.

    Args:
        color (Qt.GlobalColor, QColor, str): Background color of the button.
        text (str, optional): Button text. Defaults to "".
        text_color (Qt.GlobalColor, QColor, str, optional): Text color. If None, it's calculated for contrast. Defaults to None.
        checked_color (Qt.GlobalColor, QColor, str, optional): Color when the button is in checked state. Defaults to None.
        parent (QWidget, optional): Parent widget. Defaults to None.
    """
    super().__init__(parent)
    self.setText(text)

    # We store colors as class attributes
    self._color: QColor
    self._text_color: typing.Optional[QColor]
    self._checked_color: typing.Optional[QColor]

    self.setColor(color)
    self.setTextColor(text_color)
    self.setCheckedColor(checked_color)

checkedColor()

Returns the color used when the button is checked.

Returns:

Type Description
Optional[QColor]

QColor, optional: Checked color or None.

Source code in source/qextrawidgets/widgets/buttons/color_tool_button.py
 94
 95
 96
 97
 98
 99
100
def checkedColor(self) -> typing.Optional[QColor]:
    """Returns the color used when the button is checked.

    Returns:
        QColor, optional: Checked color or None.
    """
    return self._checked_color

color()

Returns the button background color.

Returns:

Name Type Description
QColor QColor

Background color.

Source code in source/qextrawidgets/widgets/buttons/color_tool_button.py
78
79
80
81
82
83
84
def color(self) -> QColor:
    """Returns the button background color.

    Returns:
        QColor: Background color.
    """
    return self._color

initStyleOption(option)

Method called automatically by Qt before drawing the button.

Here we intercept the style option and change the palette color based on the current state (Hover, Pressed, etc).

Parameters:

Name Type Description Default
option QStyleOptionToolButton

The style option to initialize.

required
Source code in source/qextrawidgets/widgets/buttons/color_tool_button.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
def initStyleOption(self, option: QStyleOptionToolButton) -> None:
    """Method called automatically by Qt before drawing the button.

    Here we intercept the style option and change the palette color
    based on the current state (Hover, Pressed, etc).

    Args:
        option (QStyleOptionToolButton): The style option to initialize.
    """
    # 1. Let QToolButton fill the option with the default state
    super().initStyleOption(option)

    state: QStyle.StateFlag = getattr(option, 'state')
    palette: QPalette = getattr(option, 'palette')

    # Determine the base color to use (Normal or Checked)
    base_color = self._color
    if (state & QStyle.StateFlag.State_On) and self._checked_color is not None:
        base_color = self._checked_color

    # 2. Check the state in the 'option' object and change the palette color locally
    if state & QStyle.StateFlag.State_Sunken:  # Pressed
        pressed_color = base_color.darker(115)  # 15% darker
        palette.setColor(QPalette.ColorRole.Button, pressed_color)

    elif state & QStyle.StateFlag.State_MouseOver:  # Mouse over
        hover_color = base_color.lighter(115)  # 15% lighter
        palette.setColor(QPalette.ColorRole.Button, hover_color)

    else:  # Normal State (or Checked State if not interacting)
        palette.setColor(QPalette.ColorRole.Button, base_color)

    if self._text_color is None:
        palette.setColor(QPalette.ColorRole.ButtonText, QColorUtils.getContrastingTextColor(base_color))

    else:
        palette.setColor(QPalette.ColorRole.ButtonText, self._text_color)

setCheckedColor(color)

Sets the color to use when the button is checked.

Parameters:

Name Type Description Default
color (GlobalColor, QColor, str, None)

New checked color.

required
Source code in source/qextrawidgets/widgets/buttons/color_tool_button.py
102
103
104
105
106
107
108
109
110
111
def setCheckedColor(self, color: typing.Union[Qt.GlobalColor, QColor, str, None]) -> None:
    """Sets the color to use when the button is checked.

    Args:
        color (Qt.GlobalColor, QColor, str, None): New checked color.
    """
    if color is None:
        self._checked_color = None
    else:
        self._checked_color = QColor(color)

setColor(color)

Sets the button background color.

Parameters:

Name Type Description Default
color (GlobalColor, QColor, str)

New background color.

required
Source code in source/qextrawidgets/widgets/buttons/color_tool_button.py
86
87
88
89
90
91
92
def setColor(self, color: typing.Union[Qt.GlobalColor, QColor, str]) -> None:
    """Sets the button background color.

    Args:
        color (Qt.GlobalColor, QColor, str): New background color.
    """
    self._color = QColor(color)

setTextColor(text_color)

Sets the text color.

Parameters:

Name Type Description Default
text_color (GlobalColor, QColor, str, None)

New text color.

required
Source code in source/qextrawidgets/widgets/buttons/color_tool_button.py
121
122
123
124
125
126
127
128
129
130
def setTextColor(self, text_color: typing.Union[Qt.GlobalColor, QColor, str, None]) -> None:
    """Sets the text color.

    Args:
        text_color (Qt.GlobalColor, QColor, str, None): New text color.
    """
    if text_color is None:
        self._text_color = text_color
    else:
        self._text_color = QColor(text_color)

textColor()

Returns the text color.

Returns:

Type Description
Optional[QColor]

QColor, optional: Text color or None if automatically calculated.

Source code in source/qextrawidgets/widgets/buttons/color_tool_button.py
113
114
115
116
117
118
119
def textColor(self) -> typing.Optional[QColor]:
    """Returns the text color.

    Returns:
        QColor, optional: Text color or None if automatically calculated.
    """
    return self._text_color

Delegates

QGridIconDelegate

Bases: QStyledItemDelegate

Delegate for a grid view. Renders items as rounded grid cells containing ONLY icons or pixmaps.

Implements lazy loading signals for missing images.

Attributes:

Name Type Description
requestImage Signal

Emitted when an item needs an image loaded. Sends QPersistentModelIndex.

_requested_indices Set[QPersistentModelIndex]

Cache of indices that already requested an image.

Source code in source/qextrawidgets/widgets/delegates/grid_icon_delegate.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
class QGridIconDelegate(QStyledItemDelegate):
    """
    Delegate for a grid view.
    Renders items as rounded grid cells containing ONLY icons or pixmaps.

    Implements lazy loading signals for missing images.

    Attributes:
        requestImage (Signal): Emitted when an item needs an image loaded.
                               Sends QPersistentModelIndex.
        _requested_indices (Set[QPersistentModelIndex]): Cache of indices that already requested an image.
    """

    # Signal emitted when an item has no DecorationRole data
    requestImage = Signal(QPersistentModelIndex)

    def __init__(
        self,
        parent: typing.Optional[QObject] = None,
        item_internal_margin_ratio: float = 0.1,
    ):
        """
        Initialize the delegate.

        Args:
            parent (Optional[Any]): The parent object.
            item_internal_margin_ratio (float): Internal margin ratio (0.0 to 0.5).
        """
        super().__init__(parent)
        self._requested_indices: typing.Set[QPersistentModelIndex] = set()
        self.setItemInternalMargin(item_internal_margin_ratio)

    def setItemInternalMargin(self, ratio: float) -> None:
        """
        Set the internal margin ratio for the item content.

        Args:
            ratio (float): A value between 0.0 (0%) and 0.5 (50%).
        """
        self._item_internal_margin_ratio = max(0.0, min(0.5, ratio))

    def itemInternalMargin(self) -> float:
        """
        Get the internal margin ratio for the item content.

        Returns:
            float: A value between 0.0 (0%) and 0.5 (50%).
        """
        return self._item_internal_margin_ratio

    def forceReloadAll(self) -> None:
        """
        Clear the cache of ALL requested images.

        The next time the view paints a missing image item (e.g. on scroll or hover),
        it will emit requestImage again.
        """
        self._requested_indices.clear()

    def forceReload(self, index: QModelIndex) -> None:
        """
        Clear the cache for a specific index to force re-requesting the image.

        Args:
            index (QModelIndex): The index to clear from the cache.
        """
        persistent_index = QPersistentModelIndex(index)
        if persistent_index in self._requested_indices:
            self._requested_indices.remove(persistent_index)

    def paint(
        self,
        painter: QPainter,
        option: QStyleOptionViewItem,
        index: typing.Union[QModelIndex, QPersistentModelIndex],
    ) -> None:
        """
        Paint the item.

        Args:
            painter (QPainter): The painter object.
            option (QStyleOptionViewItem): Style options for rendering.
            index (QModelIndex): The index of the item being painted.
        """
        painter.save()
        self._draw_grid_item(painter, option, index)
        painter.restore()

    def _draw_grid_item(
        self,
        painter: QPainter,
        option: QStyleOptionViewItem,
        index: typing.Union[QModelIndex, QPersistentModelIndex],
    ) -> None:
        """
        Draw a child item in the grid used for lazy loading check.

        Checks for DecorationRole; if missing, triggers requestImage signal.
        Renders the icon or pixmap centered in the item rect.

        Args:
            painter (QPainter): The painter object.
            option (QStyleOptionViewItem): The style options.
            index (QModelIndex): The model index of the item.
        """

        painter.setRenderHint(QPainter.RenderHint.Antialiasing)
        painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)

        palette = typing.cast(QPalette, option.palette)
        current_state = typing.cast(QStyle.StateFlag, option.state)
        bg_color = None
        base_bg_color = palette.color(QPalette.ColorRole.Base)

        # Determine Background Color for Selection/Hover
        if current_state & QStyle.StateFlag.State_Selected:
            bg_color = palette.color(QPalette.ColorRole.Highlight)
        elif current_state & QStyle.StateFlag.State_MouseOver:
            bg_color = base_bg_color.lighter(120)

        # Draw Background (Rounded Rect)
        rect = typing.cast(QRect, option.rect).adjusted(2, 2, -2, -2)

        if bg_color is not None:
            painter.setPen(Qt.PenStyle.NoPen)
            painter.setBrush(bg_color)
            painter.drawRoundedRect(rect, 8.0, 8.0)

        # Retrieve Data
        item_data = index.data(Qt.ItemDataRole.DecorationRole)

        margin = int(
            min(rect.width(), rect.height()) * self._item_internal_margin_ratio
        )
        target_rect = rect.adjusted(margin, margin, -margin, -margin)

        # --- Lazy Loading Logic ---
        # If no valid data is found, trigger the signal
        is_data_valid = False
        if item_data is not None:
            if isinstance(item_data, QIcon) and not item_data.isNull():
                is_data_valid = True
            elif isinstance(item_data, (QPixmap, QImage)) and not item_data.isNull():
                is_data_valid = True

        # Check if we already requested this index to avoid spamming the signal in the paint loop
        p_index = QPersistentModelIndex(index)
        if p_index not in self._requested_indices:
            self._requested_indices.add(p_index)
            # Emit asynchronously to not block painting
            QTimer.singleShot(0, lambda: self.requestImage.emit(p_index))

        if not is_data_valid:
            # Optional: Draw a placeholder (e.g., a simple loading circle or gray box)
            painter.setPen(Qt.PenStyle.DotLine)
            painter.setPen(palette.color(QPalette.ColorRole.Mid))
            painter.setBrush(Qt.BrushStyle.NoBrush)
            painter.drawRoundedRect(target_rect, 4, 4)

        else:
            # --- Drawing Logic ---
            if isinstance(item_data, QIcon):
                mode = QIcon.Mode.Normal
                if not (current_state & QStyle.StateFlag.State_Enabled):
                    mode = QIcon.Mode.Disabled
                elif current_state & QStyle.StateFlag.State_Selected:
                    mode = QIcon.Mode.Selected

                item_data.paint(
                    painter,
                    target_rect,
                    Qt.AlignmentFlag.AlignCenter,
                    mode,
                    QIcon.State.Off,
                )

            elif isinstance(item_data, (QPixmap, QImage)):
                if isinstance(item_data, QImage):
                    pixmap = QPixmap.fromImage(item_data)
                else:
                    pixmap = item_data

                scaled_pixmap = pixmap.scaled(
                    target_rect.size(),
                    Qt.AspectRatioMode.KeepAspectRatio,
                    Qt.TransformationMode.SmoothTransformation,
                )

                x = target_rect.x() + (target_rect.width() - scaled_pixmap.width()) // 2
                y = (
                    target_rect.y()
                    + (target_rect.height() - scaled_pixmap.height()) // 2
                )

                if not (current_state & QStyle.StateFlag.State_Enabled):
                    painter.setOpacity(0.5)

                painter.drawPixmap(x, y, scaled_pixmap)

__init__(parent=None, item_internal_margin_ratio=0.1)

Initialize the delegate.

Parameters:

Name Type Description Default
parent Optional[Any]

The parent object.

None
item_internal_margin_ratio float

Internal margin ratio (0.0 to 0.5).

0.1
Source code in source/qextrawidgets/widgets/delegates/grid_icon_delegate.py
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
def __init__(
    self,
    parent: typing.Optional[QObject] = None,
    item_internal_margin_ratio: float = 0.1,
):
    """
    Initialize the delegate.

    Args:
        parent (Optional[Any]): The parent object.
        item_internal_margin_ratio (float): Internal margin ratio (0.0 to 0.5).
    """
    super().__init__(parent)
    self._requested_indices: typing.Set[QPersistentModelIndex] = set()
    self.setItemInternalMargin(item_internal_margin_ratio)

forceReload(index)

Clear the cache for a specific index to force re-requesting the image.

Parameters:

Name Type Description Default
index QModelIndex

The index to clear from the cache.

required
Source code in source/qextrawidgets/widgets/delegates/grid_icon_delegate.py
78
79
80
81
82
83
84
85
86
87
def forceReload(self, index: QModelIndex) -> None:
    """
    Clear the cache for a specific index to force re-requesting the image.

    Args:
        index (QModelIndex): The index to clear from the cache.
    """
    persistent_index = QPersistentModelIndex(index)
    if persistent_index in self._requested_indices:
        self._requested_indices.remove(persistent_index)

forceReloadAll()

Clear the cache of ALL requested images.

The next time the view paints a missing image item (e.g. on scroll or hover), it will emit requestImage again.

Source code in source/qextrawidgets/widgets/delegates/grid_icon_delegate.py
69
70
71
72
73
74
75
76
def forceReloadAll(self) -> None:
    """
    Clear the cache of ALL requested images.

    The next time the view paints a missing image item (e.g. on scroll or hover),
    it will emit requestImage again.
    """
    self._requested_indices.clear()

itemInternalMargin()

Get the internal margin ratio for the item content.

Returns:

Name Type Description
float float

A value between 0.0 (0%) and 0.5 (50%).

Source code in source/qextrawidgets/widgets/delegates/grid_icon_delegate.py
60
61
62
63
64
65
66
67
def itemInternalMargin(self) -> float:
    """
    Get the internal margin ratio for the item content.

    Returns:
        float: A value between 0.0 (0%) and 0.5 (50%).
    """
    return self._item_internal_margin_ratio

paint(painter, option, index)

Paint the item.

Parameters:

Name Type Description Default
painter QPainter

The painter object.

required
option QStyleOptionViewItem

Style options for rendering.

required
index QModelIndex

The index of the item being painted.

required
Source code in source/qextrawidgets/widgets/delegates/grid_icon_delegate.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
def paint(
    self,
    painter: QPainter,
    option: QStyleOptionViewItem,
    index: typing.Union[QModelIndex, QPersistentModelIndex],
) -> None:
    """
    Paint the item.

    Args:
        painter (QPainter): The painter object.
        option (QStyleOptionViewItem): Style options for rendering.
        index (QModelIndex): The index of the item being painted.
    """
    painter.save()
    self._draw_grid_item(painter, option, index)
    painter.restore()

setItemInternalMargin(ratio)

Set the internal margin ratio for the item content.

Parameters:

Name Type Description Default
ratio float

A value between 0.0 (0%) and 0.5 (50%).

required
Source code in source/qextrawidgets/widgets/delegates/grid_icon_delegate.py
51
52
53
54
55
56
57
58
def setItemInternalMargin(self, ratio: float) -> None:
    """
    Set the internal margin ratio for the item content.

    Args:
        ratio (float): A value between 0.0 (0%) and 0.5 (50%).
    """
    self._item_internal_margin_ratio = max(0.0, min(0.5, ratio))

QGroupedIconDelegate

Bases: QGridIconDelegate

Delegate for the QGroupedIconView. Renders categories as horizontal bars with expansion arrows and child items as rounded grid cells containing ONLY icons or pixmaps.

Implements lazy loading signals for missing images via QGridIconDelegate.

Source code in source/qextrawidgets/widgets/delegates/grouped_icon_delegate.py
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
class QGroupedIconDelegate(QGridIconDelegate):
    """
    Delegate for the QGroupedIconView.
    Renders categories as horizontal bars with expansion arrows and child items
    as rounded grid cells containing ONLY icons or pixmaps.

    Implements lazy loading signals for missing images via QGridIconDelegate.
    """

    def __init__(
        self,
        parent: typing.Optional[QWidget] = None,
        arrow_icon: typing.Optional[QIcon] = None,
        item_internal_margin_ratio: float = 0.1,
    ):
        """
        Initialize the delegate.

        Args:
            parent (Optional[Any]): The parent object.
            arrow_icon (Optional[QIcon]): Custom icon for the expansion arrow. If None, uses default primitive.
            item_internal_margin_ratio (float): Internal margin ratio (0.0 to 0.5).
        """
        super().__init__(parent, item_internal_margin_ratio=item_internal_margin_ratio)
        self._arrow_icon: QIcon = arrow_icon if arrow_icon else QIcon()

    def setArrowIcon(self, icon: QIcon) -> None:
        """
        Set the icon used for the expansion indicator.

        Args:
            icon (QIcon): The new arrow icon.
        """
        self._arrow_icon = icon

    def arrowIcon(self) -> QIcon:
        """
        Get the current arrow icon.

        Returns:
            QIcon: The current arrow icon.
        """
        return self._arrow_icon

    def paint(
        self,
        painter: QPainter,
        option: QStyleOptionViewItem,
        index: typing.Union[QModelIndex, QPersistentModelIndex],
    ) -> None:
        """
        Paint the item.

        Delegates to _draw_category for category items and _draw_grid_item (from base) for child items.

        Args:
            painter (QPainter): The painter object.
            option (QStyleOptionViewItem): Style options for rendering.
            index (QModelIndex): The index of the item being painted.
        """
        painter.save()

        is_category = not index.parent().isValid()

        if is_category:
            self._draw_category(painter, option, index)
        else:
            self._draw_grid_item(painter, option, index)

        painter.restore()

    def _draw_category(
        self,
        painter: QPainter,
        option: QStyleOptionViewItem,
        index: typing.Union[QModelIndex, QPersistentModelIndex],
    ) -> None:
        """
        Draw a category header item.

        Renders the background, expansion arrow, icon, and text.

        Args:
            painter (QPainter): The painter object.
            option (QStyleOptionViewItem): The style options.
            index (QModelIndex): The model index of the category.
        """
        widget = typing.cast(QWidget, option.widget)
        style = widget.style()
        palette = typing.cast(QPalette, option.palette)
        current_state = typing.cast(QStyle.StateFlag, option.state)
        option_rect = typing.cast(QRect, option.rect)

        if current_state & QStyle.StateFlag.State_MouseOver:
            bg_color = palette.color(QPalette.ColorRole.Button).lighter(110)
        else:
            bg_color = palette.color(QPalette.ColorRole.Button)

        painter.fillRect(option_rect, bg_color)
        painter.setPen(palette.color(QPalette.ColorRole.Mid))
        painter.drawLine(option_rect.bottomLeft(), option_rect.bottomRight())

        is_expanded = bool(current_state & QStyle.StateFlag.State_Open)

        left_padding = 5
        element_spacing = 5
        arrow_size = 20
        user_icon_size = 20
        current_x = option_rect.left() + left_padding
        center_y = option_rect.top() + (option_rect.height() - arrow_size) // 2

        # Draw Arrow
        arrow_rect = QRect(current_x, center_y, arrow_size, arrow_size)
        if not self._arrow_icon.isNull():
            state = QIcon.State.On if is_expanded else QIcon.State.Off
            mode = (
                QIcon.Mode.Disabled
                if not (current_state & QStyle.StateFlag.State_Enabled)
                else QIcon.Mode.Normal
            )
            self._arrow_icon.paint(
                painter, arrow_rect, Qt.AlignmentFlag.AlignCenter, mode, state
            )
        else:
            arrow_opt = QStyleOptionViewItem(option)
            setattr(arrow_opt, "rect", arrow_rect)
            primitive = (
                QStyle.PrimitiveElement.PE_IndicatorArrowDown
                if is_expanded
                else QStyle.PrimitiveElement.PE_IndicatorArrowRight
            )
            style.drawPrimitive(primitive, arrow_opt, painter, widget)

        current_x += arrow_size + element_spacing

        # Draw Category Icon
        user_icon = index.data(Qt.ItemDataRole.DecorationRole)
        if isinstance(user_icon, QIcon) and not user_icon.isNull():
            icon_rect = QRect(current_x, center_y, user_icon_size, user_icon_size)
            mode = (
                QIcon.Mode.Disabled
                if not (current_state & QStyle.StateFlag.State_Enabled)
                else QIcon.Mode.Normal
            )
            user_icon.paint(
                painter, icon_rect, Qt.AlignmentFlag.AlignCenter, mode, QIcon.State.Off
            )
            current_x += user_icon_size + element_spacing

        # Draw Text
        text_rect = QRect(
            current_x,
            option_rect.top(),
            option_rect.right() - current_x - 5,
            option_rect.height(),
        )
        painter.setPen(palette.color(QPalette.ColorRole.ButtonText))
        font = typing.cast(QFont, option.font)
        font.setBold(True)
        painter.setFont(font)
        text = str(index.data(Qt.ItemDataRole.DisplayRole))
        style.drawItemText(
            painter,
            text_rect,
            Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft,
            palette,
            True,
            text,
        )

__init__(parent=None, arrow_icon=None, item_internal_margin_ratio=0.1)

Initialize the delegate.

Parameters:

Name Type Description Default
parent Optional[Any]

The parent object.

None
arrow_icon Optional[QIcon]

Custom icon for the expansion arrow. If None, uses default primitive.

None
item_internal_margin_ratio float

Internal margin ratio (0.0 to 0.5).

0.1
Source code in source/qextrawidgets/widgets/delegates/grouped_icon_delegate.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def __init__(
    self,
    parent: typing.Optional[QWidget] = None,
    arrow_icon: typing.Optional[QIcon] = None,
    item_internal_margin_ratio: float = 0.1,
):
    """
    Initialize the delegate.

    Args:
        parent (Optional[Any]): The parent object.
        arrow_icon (Optional[QIcon]): Custom icon for the expansion arrow. If None, uses default primitive.
        item_internal_margin_ratio (float): Internal margin ratio (0.0 to 0.5).
    """
    super().__init__(parent, item_internal_margin_ratio=item_internal_margin_ratio)
    self._arrow_icon: QIcon = arrow_icon if arrow_icon else QIcon()

arrowIcon()

Get the current arrow icon.

Returns:

Name Type Description
QIcon QIcon

The current arrow icon.

Source code in source/qextrawidgets/widgets/delegates/grouped_icon_delegate.py
44
45
46
47
48
49
50
51
def arrowIcon(self) -> QIcon:
    """
    Get the current arrow icon.

    Returns:
        QIcon: The current arrow icon.
    """
    return self._arrow_icon

paint(painter, option, index)

Paint the item.

Delegates to _draw_category for category items and _draw_grid_item (from base) for child items.

Parameters:

Name Type Description Default
painter QPainter

The painter object.

required
option QStyleOptionViewItem

Style options for rendering.

required
index QModelIndex

The index of the item being painted.

required
Source code in source/qextrawidgets/widgets/delegates/grouped_icon_delegate.py
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
def paint(
    self,
    painter: QPainter,
    option: QStyleOptionViewItem,
    index: typing.Union[QModelIndex, QPersistentModelIndex],
) -> None:
    """
    Paint the item.

    Delegates to _draw_category for category items and _draw_grid_item (from base) for child items.

    Args:
        painter (QPainter): The painter object.
        option (QStyleOptionViewItem): Style options for rendering.
        index (QModelIndex): The index of the item being painted.
    """
    painter.save()

    is_category = not index.parent().isValid()

    if is_category:
        self._draw_category(painter, option, index)
    else:
        self._draw_grid_item(painter, option, index)

    painter.restore()

setArrowIcon(icon)

Set the icon used for the expansion indicator.

Parameters:

Name Type Description Default
icon QIcon

The new arrow icon.

required
Source code in source/qextrawidgets/widgets/delegates/grouped_icon_delegate.py
35
36
37
38
39
40
41
42
def setArrowIcon(self, icon: QIcon) -> None:
    """
    Set the icon used for the expansion indicator.

    Args:
        icon (QIcon): The new arrow icon.
    """
    self._arrow_icon = icon

Dialogs

QFilterPopup

Bases: QDialog

A popup dialog used for filtering and sorting columns in a QFilterableTable.

Provides options to sort data, search for specific values, and select/deselect items to be displayed in the table.

Source code in source/qextrawidgets/widgets/dialogs/filter_popup.py
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
class QFilterPopup(QDialog):
    """A popup dialog used for filtering and sorting columns in a QFilterableTable.

    Provides options to sort data, search for specific values, and select/deselect
    items to be displayed in the table.
    """

    orderChanged = Signal(int, Qt.SortOrder)
    clearRequested = Signal()

    def __init__(
        self,
        model: QAbstractItemModel,
        column: int,
        parent: typing.Optional[QWidget] = None,
    ) -> None:
        """Initializes the filter popup.

        Args:
            model (QAbstractItemModel): The source data model.
            column (int): The column to filter.
            parent (QWidget, optional): Parent widget. Defaults to None.
        """
        super().__init__(parent)
        self.setWindowFlags(Qt.WindowType.Popup)
        self.setSizeGripEnabled(True)

        self._order_button = self._create_filter_button(self.tr("Order A to Z"))
        self._order_button.setIcon(
            QThemeResponsiveIcon.fromAwesome("fa6s.arrow-down-a-z")
        )

        self._reverse_orden_button = self._create_filter_button(self.tr("Order Z to A"))
        self._reverse_orden_button.setIcon(
            QThemeResponsiveIcon.fromAwesome("fa6s.arrow-down-z-a")
        )

        self._clear_filter_button = self._create_filter_button(self.tr("Clear Filter"))
        self._clear_filter_button.setIcon(
            QThemeResponsiveIcon.fromAwesome("fa6s.filter-circle-xmark")
        )

        self._line = QFrame()
        self._line.setFrameShape(QFrame.Shape.HLine)
        self._line.setFrameShadow(QFrame.Shadow.Sunken)

        self._search_field = QLineEdit()
        self._search_field.setPlaceholderText(self.tr("Search..."))
        self._search_field.setClearButtonEnabled(True)

        self._check_all_box = QCheckBox(self.tr("(Select All)"))
        self._check_all_box.setTristate(True)
        self._check_all_box.setCheckState(Qt.CheckState.Checked)

        self._list_view = QListView()
        self._list_view.setUniformItemSizes(True)

        self._apply_button = QPushButton(self.tr("Apply"))
        self._cancel_button = QPushButton(self.tr("Cancel"))

        self._setup_layout()
        self._setup_model(model, column)
        self._setup_connections()

    def _setup_layout(self) -> None:
        layout = QVBoxLayout(self)
        layout.setContentsMargins(8, 8, 8, 8)
        layout.setSpacing(6)

        layout.addWidget(self._order_button)
        layout.addWidget(self._reverse_orden_button)
        layout.addWidget(self._clear_filter_button)
        layout.addWidget(self._line)

        layout.addWidget(self._search_field)
        layout.addWidget(self._check_all_box)
        layout.addWidget(self._list_view)

        btn_layout = QHBoxLayout()
        btn_layout.addStretch()
        btn_layout.addWidget(self._apply_button)
        btn_layout.addWidget(self._cancel_button)
        layout.addLayout(btn_layout)

    @staticmethod
    def _create_filter_button(text: str) -> QToolButton:
        """Creates a tool button for filter actions.

        Args:
            text (str): Button text.

        Returns:
            QToolButton: The created tool button.
        """
        tool_button = QToolButton()
        tool_button.setText(text)
        tool_button.setAutoRaise(True)
        tool_button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
        tool_button.setSizePolicy(
            QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed
        )
        tool_button.setCursor(Qt.CursorShape.PointingHandCursor)
        return tool_button

    def _setup_model(self, model: QAbstractItemModel, column: int) -> None:
        """Sets up the data model and proxy chain for the list view."""
        # 1. Unique Values Proxy (Filters source to show only unique rows for the column)
        self._unique_proxy = QUniqueValuesProxyModel(self)
        self._unique_proxy.setSourceModel(model)
        self._unique_proxy.setTargetColumn(column)

        # 2. Check State Proxy (Adds checkbox state to the unique rows)
        self._check_proxy = QCheckStateProxyModel(self)
        self._check_proxy.setSourceModel(self._unique_proxy)
        self._check_proxy.setInitialCheckState(Qt.CheckState.Checked)

        # 3. Sort Proxy (Allows searching/sorting within the popup)
        self._proxy_model = QSortFilterProxyModel(self)
        self._proxy_model.setSourceModel(self._check_proxy)
        self._proxy_model.setFilterKeyColumn(column)

        # Filters and Sorting settings
        self._proxy_model.setFilterCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
        self._proxy_model.setFilterRole(Qt.ItemDataRole.DisplayRole)
        self._proxy_model.setSortCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
        self._proxy_model.setDynamicSortFilter(True)
        self._proxy_model.sort(column, Qt.SortOrder.AscendingOrder)

        self._list_view.setModel(self._proxy_model)
        # Ensure we show the correct column in the list view (as it's a table model potentially)
        self._list_view.setModelColumn(column)

    def _setup_connections(self) -> None:
        """Sets up signals and slots connections."""
        self._search_field.textChanged.connect(self._proxy_model.setFilterFixedString)
        self._cancel_button.clicked.connect(self.reject)
        self._order_button.clicked.connect(self.reject)
        self._reverse_orden_button.clicked.connect(self.reject)
        self._clear_filter_button.clicked.connect(self._on_clear_clicked)
        self._apply_button.clicked.connect(self.accept)

        self._check_all_box.clicked.connect(self._on_check_all_clicked)
        self._proxy_model.dataChanged.connect(self._update_select_all_state)

        self._order_button.clicked.connect(
            lambda: self.orderChanged.emit(
                self._proxy_model.filterKeyColumn(), Qt.SortOrder.AscendingOrder
            )
        )
        self._reverse_orden_button.clicked.connect(
            lambda: self.orderChanged.emit(
                self._proxy_model.filterKeyColumn(), Qt.SortOrder.DescendingOrder
            )
        )

    def _on_check_all_clicked(self) -> None:
        """Handles clicking the 'Select All' checkbox."""
        state = self._check_all_box.checkState()

        # When clicking "Select All", we only affect what is VISIBLE in the search
        # We need to iterate over visible rows in the SortProxy
        row_count = self._proxy_model.rowCount()
        column = self._proxy_model.filterKeyColumn()  # Should be our target column

        for row in range(row_count):
            # Map from sort proxy to check proxy
            sort_index = self._proxy_model.index(row, column)
            check_index = self._proxy_model.mapToSource(sort_index)

            if check_index.isValid():
                self._check_proxy.setData(
                    check_index, state, Qt.ItemDataRole.CheckStateRole
                )

        self._check_all_box.setCheckState(state)

    @Slot()
    def _update_select_all_state(self) -> None:
        """Updates the state of the 'Select All' checkbox based on items.

        """
        checked_count = 0
        total_count = self._proxy_model.rowCount()

        if total_count == 0:
            return

        for row in range(total_count):
            column = self._proxy_model.filterKeyColumn()
            proxy_idx = self._proxy_model.index(row, column)
            check_state = self._proxy_model.data(
                proxy_idx, Qt.ItemDataRole.CheckStateRole
            )

            if not isinstance(check_state, Qt.CheckState):
                check_state = Qt.CheckState(check_state)

            if check_state == Qt.CheckState.Checked:
                checked_count += 1

        self._check_all_box.blockSignals(True)
        if checked_count == 0:
            self._check_all_box.setCheckState(Qt.CheckState.Unchecked)
        elif checked_count == total_count:
            self._check_all_box.setCheckState(Qt.CheckState.Checked)
        else:
            self._check_all_box.setCheckState(Qt.CheckState.PartiallyChecked)
        self._check_all_box.blockSignals(False)

    def _on_clear_clicked(self) -> None:
        """Handles the clear filter button click."""
        self._search_field.clear()
        self.clearRequested.emit()
        self.reject()

    def setClearEnabled(self, enabled: bool) -> None:
        """Sets the enabled state of the clear filter button.

        Args:
            enabled (bool): True to enable, False to disable.
        """
        self._clear_filter_button.setEnabled(enabled)

    # --- Data API ---

    def accept(self) -> None:
        super().accept()
        self._update_select_all_state()

    def getSelectedData(self) -> typing.Set[str]:
        """Returns all checked items in the unique check proxy that are visible in the proxy model.

        Returns:
            Set[str]: Set of checked item texts.
        """
        data = set()
        # Iterate over the proxy model (which contains visible values)
        row_count = self._proxy_model.rowCount()
        column = self._proxy_model.filterKeyColumn()

        for row in range(row_count):
            index = self._proxy_model.index(row, column)

            check_state = self._proxy_model.data(index, Qt.ItemDataRole.CheckStateRole)
            if not isinstance(data, Qt.CheckState):
                check_state = Qt.CheckState(check_state)

            if check_state == Qt.CheckState.Checked:
                val = self._proxy_model.data(index, Qt.ItemDataRole.DisplayRole)
                data.add(str(val))

        return data

    def isFiltering(self) -> bool:
        """Checks if there is any unchecked item, indicating an active filter.

        Returns:
            bool: True if any item is unchecked, False otherwise.
        """
        return bool(self._unique_proxy.rowCount() - len(self.getSelectedData()))

__init__(model, column, parent=None)

Initializes the filter popup.

Parameters:

Name Type Description Default
model QAbstractItemModel

The source data model.

required
column int

The column to filter.

required
parent QWidget

Parent widget. Defaults to None.

None
Source code in source/qextrawidgets/widgets/dialogs/filter_popup.py
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
def __init__(
    self,
    model: QAbstractItemModel,
    column: int,
    parent: typing.Optional[QWidget] = None,
) -> None:
    """Initializes the filter popup.

    Args:
        model (QAbstractItemModel): The source data model.
        column (int): The column to filter.
        parent (QWidget, optional): Parent widget. Defaults to None.
    """
    super().__init__(parent)
    self.setWindowFlags(Qt.WindowType.Popup)
    self.setSizeGripEnabled(True)

    self._order_button = self._create_filter_button(self.tr("Order A to Z"))
    self._order_button.setIcon(
        QThemeResponsiveIcon.fromAwesome("fa6s.arrow-down-a-z")
    )

    self._reverse_orden_button = self._create_filter_button(self.tr("Order Z to A"))
    self._reverse_orden_button.setIcon(
        QThemeResponsiveIcon.fromAwesome("fa6s.arrow-down-z-a")
    )

    self._clear_filter_button = self._create_filter_button(self.tr("Clear Filter"))
    self._clear_filter_button.setIcon(
        QThemeResponsiveIcon.fromAwesome("fa6s.filter-circle-xmark")
    )

    self._line = QFrame()
    self._line.setFrameShape(QFrame.Shape.HLine)
    self._line.setFrameShadow(QFrame.Shadow.Sunken)

    self._search_field = QLineEdit()
    self._search_field.setPlaceholderText(self.tr("Search..."))
    self._search_field.setClearButtonEnabled(True)

    self._check_all_box = QCheckBox(self.tr("(Select All)"))
    self._check_all_box.setTristate(True)
    self._check_all_box.setCheckState(Qt.CheckState.Checked)

    self._list_view = QListView()
    self._list_view.setUniformItemSizes(True)

    self._apply_button = QPushButton(self.tr("Apply"))
    self._cancel_button = QPushButton(self.tr("Cancel"))

    self._setup_layout()
    self._setup_model(model, column)
    self._setup_connections()

getSelectedData()

Returns all checked items in the unique check proxy that are visible in the proxy model.

Returns:

Type Description
Set[str]

Set[str]: Set of checked item texts.

Source code in source/qextrawidgets/widgets/dialogs/filter_popup.py
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
def getSelectedData(self) -> typing.Set[str]:
    """Returns all checked items in the unique check proxy that are visible in the proxy model.

    Returns:
        Set[str]: Set of checked item texts.
    """
    data = set()
    # Iterate over the proxy model (which contains visible values)
    row_count = self._proxy_model.rowCount()
    column = self._proxy_model.filterKeyColumn()

    for row in range(row_count):
        index = self._proxy_model.index(row, column)

        check_state = self._proxy_model.data(index, Qt.ItemDataRole.CheckStateRole)
        if not isinstance(data, Qt.CheckState):
            check_state = Qt.CheckState(check_state)

        if check_state == Qt.CheckState.Checked:
            val = self._proxy_model.data(index, Qt.ItemDataRole.DisplayRole)
            data.add(str(val))

    return data

isFiltering()

Checks if there is any unchecked item, indicating an active filter.

Returns:

Name Type Description
bool bool

True if any item is unchecked, False otherwise.

Source code in source/qextrawidgets/widgets/dialogs/filter_popup.py
279
280
281
282
283
284
285
def isFiltering(self) -> bool:
    """Checks if there is any unchecked item, indicating an active filter.

    Returns:
        bool: True if any item is unchecked, False otherwise.
    """
    return bool(self._unique_proxy.rowCount() - len(self.getSelectedData()))

setClearEnabled(enabled)

Sets the enabled state of the clear filter button.

Parameters:

Name Type Description Default
enabled bool

True to enable, False to disable.

required
Source code in source/qextrawidgets/widgets/dialogs/filter_popup.py
241
242
243
244
245
246
247
def setClearEnabled(self, enabled: bool) -> None:
    """Sets the enabled state of the clear filter button.

    Args:
        enabled (bool): True to enable, False to disable.
    """
    self._clear_filter_button.setEnabled(enabled)

Displays

QThemeResponsiveLabel

Bases: QLabel

A QLabel that displays a QThemeResponsiveIcon and updates it automatically when the system theme or the widget size changes.

Source code in source/qextrawidgets/widgets/displays/theme_responsive_label.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
class QThemeResponsiveLabel(QLabel):
    """
    A QLabel that displays a QThemeResponsiveIcon and updates it automatically
    when the system theme or the widget size changes.
    """

    def __init__(self, parent: typing.Optional[QWidget] = None) -> None:
        """
        Initializes the label.

        Args:
            parent: The parent widget.
        """
        super().__init__(parent)
        self._icon: typing.Optional[QThemeResponsiveIcon] = None
        style_hints = QApplication.styleHints()
        style_hints.colorSchemeChanged.connect(self._on_theme_change)

    def _on_theme_change(self, scheme: Qt.ColorScheme) -> None:
        """
        Handles theme change events.
        """
        self._update_pixmap(scheme)

    def _update_pixmap(self, scheme: Qt.ColorScheme) -> None:
        """
        Updates the label's pixmap based on the current icon and size.
        """
        if self._icon:
            size = self.size()
            if not size.isEmpty():
                pixmap = self._icon.themePixmap(size, QIcon.Mode.Normal, QIcon.State.Off, scheme)
                self.setPixmap(pixmap)

    def resizeEvent(self, event: QResizeEvent) -> None:
        """
        Updates the pixmap when the widget is resized.

        Args:
            event: The resize event.
        """
        super().resizeEvent(event)
        self._update_pixmap(QApplication.styleHints().colorScheme())

    def setIcon(self, icon: QThemeResponsiveIcon) -> None:
        """
        Sets the icon to be displayed.

        Args:
            icon: The theme-responsive icon.
        """
        self._icon = icon
        self._update_pixmap(QApplication.styleHints().colorScheme())

    def icon(self) -> typing.Optional[QThemeResponsiveIcon]:
        """
        Returns the current icon.

        Returns:
            The current icon or None.
        """
        return self._icon

__init__(parent=None)

Initializes the label.

Parameters:

Name Type Description Default
parent Optional[QWidget]

The parent widget.

None
Source code in source/qextrawidgets/widgets/displays/theme_responsive_label.py
16
17
18
19
20
21
22
23
24
25
26
def __init__(self, parent: typing.Optional[QWidget] = None) -> None:
    """
    Initializes the label.

    Args:
        parent: The parent widget.
    """
    super().__init__(parent)
    self._icon: typing.Optional[QThemeResponsiveIcon] = None
    style_hints = QApplication.styleHints()
    style_hints.colorSchemeChanged.connect(self._on_theme_change)

icon()

Returns the current icon.

Returns:

Type Description
Optional[QThemeResponsiveIcon]

The current icon or None.

Source code in source/qextrawidgets/widgets/displays/theme_responsive_label.py
64
65
66
67
68
69
70
71
def icon(self) -> typing.Optional[QThemeResponsiveIcon]:
    """
    Returns the current icon.

    Returns:
        The current icon or None.
    """
    return self._icon

resizeEvent(event)

Updates the pixmap when the widget is resized.

Parameters:

Name Type Description Default
event QResizeEvent

The resize event.

required
Source code in source/qextrawidgets/widgets/displays/theme_responsive_label.py
44
45
46
47
48
49
50
51
52
def resizeEvent(self, event: QResizeEvent) -> None:
    """
    Updates the pixmap when the widget is resized.

    Args:
        event: The resize event.
    """
    super().resizeEvent(event)
    self._update_pixmap(QApplication.styleHints().colorScheme())

setIcon(icon)

Sets the icon to be displayed.

Parameters:

Name Type Description Default
icon QThemeResponsiveIcon

The theme-responsive icon.

required
Source code in source/qextrawidgets/widgets/displays/theme_responsive_label.py
54
55
56
57
58
59
60
61
62
def setIcon(self, icon: QThemeResponsiveIcon) -> None:
    """
    Sets the icon to be displayed.

    Args:
        icon: The theme-responsive icon.
    """
    self._icon = icon
    self._update_pixmap(QApplication.styleHints().colorScheme())

Inputs

QExtraTextEdit

Bases: QTextEdit

A QTextEdit extension that supports auto-resizing based on content and input validation.

Source code in source/qextrawidgets/widgets/inputs/extra_text_edit.py
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
class QExtraTextEdit(QTextEdit):
    """A QTextEdit extension that supports auto-resizing based on content and input validation."""

    def __init__(self, parent: typing.Optional[QWidget] = None) -> None:
        """Initializes the text edit widget.

        Args:
            parent (QWidget, optional): Parent widget. Defaults to None.
        """
        super().__init__(parent)

        # Private Variables
        self._validator: typing.Optional[QValidator] = None
        self._max_height = 16777215  # QWIDGETSIZE_MAX (Qt Default)
        self._responsive = False

        # Initialization
        self.setResponsive(True)

        # Size Policy Adjustment
        # For a growing widget, 'Minimum' or 'Preferred' vertically is better than 'Expanding'
        size_policy = self.sizePolicy()
        size_policy.setVerticalPolicy(QSizePolicy.Policy.Minimum)
        self.setSizePolicy(size_policy)

    # --- Qt System Overrides ---

    def sizeHint(self) -> QSize:
        """Informs the layout of the ideal size of the widget at this moment.

        Returns:
            QSize: The suggested size for the widget.
        """
        if self._responsive and self.document():
            # 1. Calculates the height of the actual content
            document_height = self.document().size().height()

            # 2. Adds internal margins and frame borders
            # frameWidth() covers borders drawn by the style
            margins = self.contentsMargins()
            frame_borders = self.frameWidth() * 2

            total_height = (
                document_height + margins.top() + margins.bottom() + frame_borders
            )

            # 3. Limits to the defined maximum height
            final_height = min(total_height, self._max_height)

            return QSize(super().sizeHint().width(), int(final_height))

        return super().sizeHint()

    # --- Getters and Setters ---

    def isResponsive(self) -> bool:
        """Returns whether the widget automatically resizes based on content.

        Returns:
            bool: True if responsive, False otherwise.
        """
        return self._responsive

    def setResponsive(self, responsive: bool = True) -> None:
        """Sets whether the widget should automatically resize based on content.

        Args:
            responsive (bool, optional): True to enable auto-resize. Defaults to True.
        """
        if self._responsive == responsive:
            return

        self._responsive = responsive

        if responsive:
            self.textChanged.connect(self._on_text_changed)
            # Removes default automatic scroll policy to manage manually
            self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
            self._on_text_changed()  # Forces initial adjustment
        else:
            try:
                self.textChanged.disconnect(self._on_text_changed)
            except RuntimeError:
                pass

            # Restores default behavior
            self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
            self.updateGeometry()

    def maximumHeight(self) -> int:
        """Returns the maximum height the widget can grow to.

        Returns:
            int: Maximum height in pixels.
        """
        return self._max_height

    def setMaximumHeight(self, height: int) -> None:
        """Sets the maximum height the widget can grow to.

        Args:
            height (int): Maximum height in pixels.
        """
        self._max_height = height
        # We don't call super().setMaximumHeight here to not lock the widget visually
        # The constraint is applied logically in sizeHint
        self.updateGeometry()

    def setValidator(self, validator: typing.Optional[QValidator]) -> None:
        """Sets a validator for the input text.

        Args:
            validator (QValidator, None): The validator to use.
        """
        self._validator = validator

    def validator(self) -> typing.Optional[QValidator]:
        """Returns the current input validator.

        Returns:
            QValidator, None: The current validator.
        """
        return self._validator

    # --- Internal Logic ---

    def _on_text_changed(self) -> None:
        """Called when text changes to recalculate geometry."""
        if not self._responsive:
            return

        # 1. Notifies layout that ideal size changed
        self.updateGeometry()

        # 2. Manages ScrollBar visibility
        # If content is larger than max limit, we need scrollbar
        document_height = self.document().size().height()
        content_margins = (
            self.contentsMargins().top()
            + self.contentsMargins().bottom()
            + (self.frameWidth() * 2)
        )

        if (document_height + content_margins) > self._max_height:
            self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
        else:
            self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)

    def keyPressEvent(self, event: QKeyEvent) -> None:
        """Handles key press events and applies validation.

        Args:
            event (QKeyEvent): Key event.
        """
        if self._validator is None:
            return super().keyPressEvent(event)

        # Step A: Allow control keys (Backspace, Delete, Enter, Arrows, Tab, Ctrl+C, etc.)
        # If not done, the editor becomes unusable (cannot delete or navigate).
        is_control = (
            event.key()
            in (
                Qt.Key.Key_Backspace,
                Qt.Key.Key_Delete,
                Qt.Key.Key_Return,
                Qt.Key.Key_Enter,
                Qt.Key.Key_Tab,
                Qt.Key.Key_Left,
                Qt.Key.Key_Right,
                Qt.Key.Key_Up,
                Qt.Key.Key_Down,
            )
            or event.modifiers()
            & Qt.KeyboardModifier.ControlModifier  # Allows shortcuts like Ctrl+C
        )

        if is_control:
            return super().keyPressEvent(event)

        text = event.text()

        validation_result = self._validator.validate(text, 0)
        state = (
            validation_result[0]
            if isinstance(validation_result, tuple)
            else validation_result
        )

        if state == QValidator.State.Acceptable:
            super().keyPressEvent(event)
        return None

    def insertFromMimeData(self, source: QMimeData) -> None:
        """Handles insertion from MIME data (pasting) and applies validation.

        Args:
            source (QMimeData): MIME data to insert.
        """
        if source.hasText() and self._validator is not None:
            validation_result = self._validator.validate(source.text(), 0)
            state = (
                validation_result[0]
                if isinstance(validation_result, tuple)
                else validation_result
            )

            if state == QValidator.State.Acceptable:
                super().insertFromMimeData(source)
        else:
            super().insertFromMimeData(source)

__init__(parent=None)

Initializes the text edit widget.

Parameters:

Name Type Description Default
parent QWidget

Parent widget. Defaults to None.

None
Source code in source/qextrawidgets/widgets/inputs/extra_text_edit.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def __init__(self, parent: typing.Optional[QWidget] = None) -> None:
    """Initializes the text edit widget.

    Args:
        parent (QWidget, optional): Parent widget. Defaults to None.
    """
    super().__init__(parent)

    # Private Variables
    self._validator: typing.Optional[QValidator] = None
    self._max_height = 16777215  # QWIDGETSIZE_MAX (Qt Default)
    self._responsive = False

    # Initialization
    self.setResponsive(True)

    # Size Policy Adjustment
    # For a growing widget, 'Minimum' or 'Preferred' vertically is better than 'Expanding'
    size_policy = self.sizePolicy()
    size_policy.setVerticalPolicy(QSizePolicy.Policy.Minimum)
    self.setSizePolicy(size_policy)

insertFromMimeData(source)

Handles insertion from MIME data (pasting) and applies validation.

Parameters:

Name Type Description Default
source QMimeData

MIME data to insert.

required
Source code in source/qextrawidgets/widgets/inputs/extra_text_edit.py
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
def insertFromMimeData(self, source: QMimeData) -> None:
    """Handles insertion from MIME data (pasting) and applies validation.

    Args:
        source (QMimeData): MIME data to insert.
    """
    if source.hasText() and self._validator is not None:
        validation_result = self._validator.validate(source.text(), 0)
        state = (
            validation_result[0]
            if isinstance(validation_result, tuple)
            else validation_result
        )

        if state == QValidator.State.Acceptable:
            super().insertFromMimeData(source)
    else:
        super().insertFromMimeData(source)

isResponsive()

Returns whether the widget automatically resizes based on content.

Returns:

Name Type Description
bool bool

True if responsive, False otherwise.

Source code in source/qextrawidgets/widgets/inputs/extra_text_edit.py
62
63
64
65
66
67
68
def isResponsive(self) -> bool:
    """Returns whether the widget automatically resizes based on content.

    Returns:
        bool: True if responsive, False otherwise.
    """
    return self._responsive

keyPressEvent(event)

Handles key press events and applies validation.

Parameters:

Name Type Description Default
event QKeyEvent

Key event.

required
Source code in source/qextrawidgets/widgets/inputs/extra_text_edit.py
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
def keyPressEvent(self, event: QKeyEvent) -> None:
    """Handles key press events and applies validation.

    Args:
        event (QKeyEvent): Key event.
    """
    if self._validator is None:
        return super().keyPressEvent(event)

    # Step A: Allow control keys (Backspace, Delete, Enter, Arrows, Tab, Ctrl+C, etc.)
    # If not done, the editor becomes unusable (cannot delete or navigate).
    is_control = (
        event.key()
        in (
            Qt.Key.Key_Backspace,
            Qt.Key.Key_Delete,
            Qt.Key.Key_Return,
            Qt.Key.Key_Enter,
            Qt.Key.Key_Tab,
            Qt.Key.Key_Left,
            Qt.Key.Key_Right,
            Qt.Key.Key_Up,
            Qt.Key.Key_Down,
        )
        or event.modifiers()
        & Qt.KeyboardModifier.ControlModifier  # Allows shortcuts like Ctrl+C
    )

    if is_control:
        return super().keyPressEvent(event)

    text = event.text()

    validation_result = self._validator.validate(text, 0)
    state = (
        validation_result[0]
        if isinstance(validation_result, tuple)
        else validation_result
    )

    if state == QValidator.State.Acceptable:
        super().keyPressEvent(event)
    return None

maximumHeight()

Returns the maximum height the widget can grow to.

Returns:

Name Type Description
int int

Maximum height in pixels.

Source code in source/qextrawidgets/widgets/inputs/extra_text_edit.py
 96
 97
 98
 99
100
101
102
def maximumHeight(self) -> int:
    """Returns the maximum height the widget can grow to.

    Returns:
        int: Maximum height in pixels.
    """
    return self._max_height

setMaximumHeight(height)

Sets the maximum height the widget can grow to.

Parameters:

Name Type Description Default
height int

Maximum height in pixels.

required
Source code in source/qextrawidgets/widgets/inputs/extra_text_edit.py
104
105
106
107
108
109
110
111
112
113
def setMaximumHeight(self, height: int) -> None:
    """Sets the maximum height the widget can grow to.

    Args:
        height (int): Maximum height in pixels.
    """
    self._max_height = height
    # We don't call super().setMaximumHeight here to not lock the widget visually
    # The constraint is applied logically in sizeHint
    self.updateGeometry()

setResponsive(responsive=True)

Sets whether the widget should automatically resize based on content.

Parameters:

Name Type Description Default
responsive bool

True to enable auto-resize. Defaults to True.

True
Source code in source/qextrawidgets/widgets/inputs/extra_text_edit.py
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
def setResponsive(self, responsive: bool = True) -> None:
    """Sets whether the widget should automatically resize based on content.

    Args:
        responsive (bool, optional): True to enable auto-resize. Defaults to True.
    """
    if self._responsive == responsive:
        return

    self._responsive = responsive

    if responsive:
        self.textChanged.connect(self._on_text_changed)
        # Removes default automatic scroll policy to manage manually
        self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
        self._on_text_changed()  # Forces initial adjustment
    else:
        try:
            self.textChanged.disconnect(self._on_text_changed)
        except RuntimeError:
            pass

        # Restores default behavior
        self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
        self.updateGeometry()

setValidator(validator)

Sets a validator for the input text.

Parameters:

Name Type Description Default
validator (QValidator, None)

The validator to use.

required
Source code in source/qextrawidgets/widgets/inputs/extra_text_edit.py
115
116
117
118
119
120
121
def setValidator(self, validator: typing.Optional[QValidator]) -> None:
    """Sets a validator for the input text.

    Args:
        validator (QValidator, None): The validator to use.
    """
    self._validator = validator

sizeHint()

Informs the layout of the ideal size of the widget at this moment.

Returns:

Name Type Description
QSize QSize

The suggested size for the widget.

Source code in source/qextrawidgets/widgets/inputs/extra_text_edit.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
def sizeHint(self) -> QSize:
    """Informs the layout of the ideal size of the widget at this moment.

    Returns:
        QSize: The suggested size for the widget.
    """
    if self._responsive and self.document():
        # 1. Calculates the height of the actual content
        document_height = self.document().size().height()

        # 2. Adds internal margins and frame borders
        # frameWidth() covers borders drawn by the style
        margins = self.contentsMargins()
        frame_borders = self.frameWidth() * 2

        total_height = (
            document_height + margins.top() + margins.bottom() + frame_borders
        )

        # 3. Limits to the defined maximum height
        final_height = min(total_height, self._max_height)

        return QSize(super().sizeHint().width(), int(final_height))

    return super().sizeHint()

validator()

Returns the current input validator.

Returns:

Type Description
Optional[QValidator]

QValidator, None: The current validator.

Source code in source/qextrawidgets/widgets/inputs/extra_text_edit.py
123
124
125
126
127
128
129
def validator(self) -> typing.Optional[QValidator]:
    """Returns the current input validator.

    Returns:
        QValidator, None: The current validator.
    """
    return self._validator

QIconComboBox

Bases: QToolButton

A widget similar to QComboBox but optimized for icons or short text.

It maintains a 1:1 aspect ratio and the style of a QToolButton.

Signals

currentIndexChanged (int): Emitted when the current index changes. currentDataChanged (object): Emitted when the data of the current item changes.

Source code in source/qextrawidgets/widgets/inputs/icon_combo_box.py
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
class QIconComboBox(QToolButton):
    """A widget similar to QComboBox but optimized for icons or short text.

    It maintains a 1:1 aspect ratio and the style of a QToolButton.

    Signals:
        currentIndexChanged (int): Emitted when the current index changes.
        currentDataChanged (object): Emitted when the data of the current item changes.
    """

    currentIndexChanged = Signal(int)
    currentDataChanged = Signal(object)

    def __init__(self, parent: typing.Optional[QWidget] = None, size: int = 40) -> None:
        """Initializes the icon combo box.

        Args:
            parent (QWidget, optional): Parent widget. Defaults to None.
            size (int, optional): Size of the button (width and height). Defaults to 40.
        """
        super().__init__(parent)

        self._size = size
        self._items = []  # List of dictionaries {'icon': QIcon, 'text': str, 'data': object, 'button': QToolButton}
        self._current_index = -1

        # Main Button Configuration
        self.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
        self.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
        self.setFixedSize(self._size, self._size)
        self.setStyleSheet("QToolButton::menu-indicator { image: none; }")

        # Create Menu
        self._menu = QMenu(self)
        self.setMenu(self._menu)

        # Internal menu panel
        self._container_action = QWidgetAction(self._menu)
        self._panel = QWidget()
        self._layout = QVBoxLayout(self._panel)
        self._layout.setContentsMargins(0, 0, 0, 0)
        self._layout.setSpacing(0)

        self._container_action.setDefaultWidget(self._panel)
        self._menu.addAction(self._container_action)

    def addItem(self, icon: typing.Optional[typing.Union[QIcon, str, QPixmap]] = None, text: typing.Optional[str] = None, data: typing.Any = None, font: typing.Optional[QFont] = None) -> int:
        """Adds an item to the menu.

        Args:
            icon (Union[QIcon, str, QPixmap], optional): Item icon, theme icon name, or QPixmap. Defaults to None.
            text (str, optional): Item text. Defaults to None.
            data (Any, optional): Custom data associated with the item. Defaults to None.
            font (QFont, optional): Custom font for the item. Defaults to None.

        Returns:
            int: The index of the added item.
        """
        index = len(self._items)

        btn_item = QToolButton()
        btn_item.setAutoRaise(True)
        btn_item.setFixedSize(self._size, self._size)

        if font:
            btn_item.setFont(font)
        else:
            btn_item.setFont(self.font())

        if icon:
            if isinstance(icon, QPixmap):
                icon = QIcon(icon)
            elif isinstance(icon, str):
                icon = QIcon.fromTheme(icon)
            btn_item.setIcon(icon)
            btn_item.setIconSize(QSize(int(self._size * 0.6), int(self._size * 0.6)))
        elif text:
            btn_item.setText(text)
            btn_item.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextOnly)

        btn_item.clicked.connect(lambda: self.setCurrentIndex(index))
        btn_item.clicked.connect(self._menu.close)

        self._layout.addWidget(btn_item)

        item_info = {
            'icon': icon,
            'text': text,
            'data': data,
            'font': font,
            'button': btn_item
        }
        self._items.append(item_info)

        if self._current_index == -1:
            self.setCurrentIndex(0)

        return index

    def setCurrentIndex(self, index: int) -> None:
        """Sets the current index and updates the main button.

        Args:
            index (int): Index to set as current.
        """
        if 0 <= index < len(self._items):
            self._current_index = index
            item = self._items[index]

            if item['font']:
                self.setFont(item['font'])
            else:
                self.setFont(self._panel.font())  # Reset to default font if none specified

            if item['icon']:
                self.setIcon(item['icon'])
                self.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
            elif item['text']:
                self.setText(item['text'])
                self.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextOnly)

            self.currentIndexChanged.emit(index)
            self.currentDataChanged.emit(item['data'])
        elif index == -1:
            self._current_index = -1
            self.setIcon(QIcon())
            self.setText("")

    def currentIndex(self) -> int:
        """Returns the current index.

        Returns:
            int: Current index.
        """
        return self._current_index

    def currentData(self) -> typing.Any:
        """Returns the data associated with the current item.

        Returns:
            Any: Current item data or None.
        """
        if 0 <= self._current_index < len(self._items):
            return self._items[self._current_index]['data']
        return None

    def itemData(self, index: int) -> typing.Any:
        """Returns the data associated with the item at the given index.

        Args:
            index (int): Item index.

        Returns:
            Any: Item data or None.
        """
        if 0 <= index < len(self._items):
            return self._items[index]['data']
        return None

    def setItemFont(self, index: int, font: QFont) -> None:
        """Sets the font for the item at the given index.

        Args:
            index (int): Item index.
            font (QFont): New font.
        """
        if 0 <= index < len(self._items):
            self._items[index]['font'] = font
            self._items[index]['button'].setFont(font)
            if index == self._current_index:
                self.setFont(font)

    def itemFont(self, index: int) -> typing.Optional[QFont]:
        """Returns the font of the item at the given index.

        Args:
            index (int): Item index.

        Returns:
            QFont, optional: Item font or None.
        """
        if 0 <= index < len(self._items):
            return self._items[index]['font']
        return None

    def setItemText(self, index: int, text: str) -> None:
        """Sets the text for the item at the given index.

        Args:
            index (int): Item index.
            text (str): New text.
        """
        if 0 <= index < len(self._items):
            self._items[index]['text'] = text
            btn = self._items[index]['button']
            btn.setText(text)
            if not self._items[index]['icon']:
                btn.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextOnly)

            if index == self._current_index:
                self.setCurrentIndex(index)

    def itemText(self, index: int) -> str:
        """Returns the text of the item at the given index.

        Args:
            index (int): Item index.

        Returns:
            str: Item text.
        """
        if 0 <= index < len(self._items):
            return self._items[index]['text']
        return ""

    def setItemIcon(self, index: int, icon: typing.Optional[typing.Union[QIcon, str, QPixmap]]) -> None:
        """Sets the icon for the item at the given index.

        Args:
            index (int): Item index.
            icon (Union[QIcon, str, QPixmap], optional): New icon, theme icon name, or QPixmap.
        """
        if 0 <= index < len(self._items):
            if isinstance(icon, QPixmap):
                icon = QIcon(icon)
            elif isinstance(icon, str):
                icon = QIcon.fromTheme(icon)

            self._items[index]['icon'] = icon
            btn = self._items[index]['button']

            if icon:
                btn.setIcon(icon)
                btn.setIconSize(QSize(int(self._size * 0.6), int(self._size * 0.6)))
                btn.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
            else:
                btn.setIcon(QIcon())
                if self._items[index]['text']:
                    btn.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextOnly)

            if index == self._current_index:
                self.setCurrentIndex(index)

    def itemIcon(self, index: int) -> QIcon:
        """Returns the icon of the item at the given index.

        Args:
            index (int): Item index.

        Returns:
            QIcon: Item icon.
        """
        if 0 <= index < len(self._items):
            return self._items[index]['icon']
        return QIcon()

    def setItemData(self, index: int, data: typing.Any) -> None:
        """Sets the data associated with the item at the given index.

        Args:
            index (int): Item index.
            data (Any): New data.
        """
        if 0 <= index < len(self._items):
            self._items[index]['data'] = data
            if index == self._current_index:
                self.currentDataChanged.emit(data)

    def count(self) -> int:
        """Returns the number of items in the combo box.

        Returns:
            int: Number of items.
        """
        return len(self._items)

    def clear(self) -> None:
        """Removes all items from the combo box."""
        self._items = []
        self._current_index = -1
        # Clear layout
        while self._layout.count():
            child = self._layout.takeAt(0)
            if child:
                widget = child.widget()
                if widget:
                    widget.deleteLater()
            else:
                break
        self.setIcon(QIcon())
        self.setText("")

__init__(parent=None, size=40)

Initializes the icon combo box.

Parameters:

Name Type Description Default
parent QWidget

Parent widget. Defaults to None.

None
size int

Size of the button (width and height). Defaults to 40.

40
Source code in source/qextrawidgets/widgets/inputs/icon_combo_box.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
def __init__(self, parent: typing.Optional[QWidget] = None, size: int = 40) -> None:
    """Initializes the icon combo box.

    Args:
        parent (QWidget, optional): Parent widget. Defaults to None.
        size (int, optional): Size of the button (width and height). Defaults to 40.
    """
    super().__init__(parent)

    self._size = size
    self._items = []  # List of dictionaries {'icon': QIcon, 'text': str, 'data': object, 'button': QToolButton}
    self._current_index = -1

    # Main Button Configuration
    self.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
    self.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
    self.setFixedSize(self._size, self._size)
    self.setStyleSheet("QToolButton::menu-indicator { image: none; }")

    # Create Menu
    self._menu = QMenu(self)
    self.setMenu(self._menu)

    # Internal menu panel
    self._container_action = QWidgetAction(self._menu)
    self._panel = QWidget()
    self._layout = QVBoxLayout(self._panel)
    self._layout.setContentsMargins(0, 0, 0, 0)
    self._layout.setSpacing(0)

    self._container_action.setDefaultWidget(self._panel)
    self._menu.addAction(self._container_action)

addItem(icon=None, text=None, data=None, font=None)

Adds an item to the menu.

Parameters:

Name Type Description Default
icon Union[QIcon, str, QPixmap]

Item icon, theme icon name, or QPixmap. Defaults to None.

None
text str

Item text. Defaults to None.

None
data Any

Custom data associated with the item. Defaults to None.

None
font QFont

Custom font for the item. Defaults to None.

None

Returns:

Name Type Description
int int

The index of the added item.

Source code in source/qextrawidgets/widgets/inputs/icon_combo_box.py
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
def addItem(self, icon: typing.Optional[typing.Union[QIcon, str, QPixmap]] = None, text: typing.Optional[str] = None, data: typing.Any = None, font: typing.Optional[QFont] = None) -> int:
    """Adds an item to the menu.

    Args:
        icon (Union[QIcon, str, QPixmap], optional): Item icon, theme icon name, or QPixmap. Defaults to None.
        text (str, optional): Item text. Defaults to None.
        data (Any, optional): Custom data associated with the item. Defaults to None.
        font (QFont, optional): Custom font for the item. Defaults to None.

    Returns:
        int: The index of the added item.
    """
    index = len(self._items)

    btn_item = QToolButton()
    btn_item.setAutoRaise(True)
    btn_item.setFixedSize(self._size, self._size)

    if font:
        btn_item.setFont(font)
    else:
        btn_item.setFont(self.font())

    if icon:
        if isinstance(icon, QPixmap):
            icon = QIcon(icon)
        elif isinstance(icon, str):
            icon = QIcon.fromTheme(icon)
        btn_item.setIcon(icon)
        btn_item.setIconSize(QSize(int(self._size * 0.6), int(self._size * 0.6)))
    elif text:
        btn_item.setText(text)
        btn_item.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextOnly)

    btn_item.clicked.connect(lambda: self.setCurrentIndex(index))
    btn_item.clicked.connect(self._menu.close)

    self._layout.addWidget(btn_item)

    item_info = {
        'icon': icon,
        'text': text,
        'data': data,
        'font': font,
        'button': btn_item
    }
    self._items.append(item_info)

    if self._current_index == -1:
        self.setCurrentIndex(0)

    return index

clear()

Removes all items from the combo box.

Source code in source/qextrawidgets/widgets/inputs/icon_combo_box.py
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
def clear(self) -> None:
    """Removes all items from the combo box."""
    self._items = []
    self._current_index = -1
    # Clear layout
    while self._layout.count():
        child = self._layout.takeAt(0)
        if child:
            widget = child.widget()
            if widget:
                widget.deleteLater()
        else:
            break
    self.setIcon(QIcon())
    self.setText("")

count()

Returns the number of items in the combo box.

Returns:

Name Type Description
int int

Number of items.

Source code in source/qextrawidgets/widgets/inputs/icon_combo_box.py
275
276
277
278
279
280
281
def count(self) -> int:
    """Returns the number of items in the combo box.

    Returns:
        int: Number of items.
    """
    return len(self._items)

currentData()

Returns the data associated with the current item.

Returns:

Name Type Description
Any Any

Current item data or None.

Source code in source/qextrawidgets/widgets/inputs/icon_combo_box.py
143
144
145
146
147
148
149
150
151
def currentData(self) -> typing.Any:
    """Returns the data associated with the current item.

    Returns:
        Any: Current item data or None.
    """
    if 0 <= self._current_index < len(self._items):
        return self._items[self._current_index]['data']
    return None

currentIndex()

Returns the current index.

Returns:

Name Type Description
int int

Current index.

Source code in source/qextrawidgets/widgets/inputs/icon_combo_box.py
135
136
137
138
139
140
141
def currentIndex(self) -> int:
    """Returns the current index.

    Returns:
        int: Current index.
    """
    return self._current_index

itemData(index)

Returns the data associated with the item at the given index.

Parameters:

Name Type Description Default
index int

Item index.

required

Returns:

Name Type Description
Any Any

Item data or None.

Source code in source/qextrawidgets/widgets/inputs/icon_combo_box.py
153
154
155
156
157
158
159
160
161
162
163
164
def itemData(self, index: int) -> typing.Any:
    """Returns the data associated with the item at the given index.

    Args:
        index (int): Item index.

    Returns:
        Any: Item data or None.
    """
    if 0 <= index < len(self._items):
        return self._items[index]['data']
    return None

itemFont(index)

Returns the font of the item at the given index.

Parameters:

Name Type Description Default
index int

Item index.

required

Returns:

Type Description
Optional[QFont]

QFont, optional: Item font or None.

Source code in source/qextrawidgets/widgets/inputs/icon_combo_box.py
179
180
181
182
183
184
185
186
187
188
189
190
def itemFont(self, index: int) -> typing.Optional[QFont]:
    """Returns the font of the item at the given index.

    Args:
        index (int): Item index.

    Returns:
        QFont, optional: Item font or None.
    """
    if 0 <= index < len(self._items):
        return self._items[index]['font']
    return None

itemIcon(index)

Returns the icon of the item at the given index.

Parameters:

Name Type Description Default
index int

Item index.

required

Returns:

Name Type Description
QIcon QIcon

Item icon.

Source code in source/qextrawidgets/widgets/inputs/icon_combo_box.py
250
251
252
253
254
255
256
257
258
259
260
261
def itemIcon(self, index: int) -> QIcon:
    """Returns the icon of the item at the given index.

    Args:
        index (int): Item index.

    Returns:
        QIcon: Item icon.
    """
    if 0 <= index < len(self._items):
        return self._items[index]['icon']
    return QIcon()

itemText(index)

Returns the text of the item at the given index.

Parameters:

Name Type Description Default
index int

Item index.

required

Returns:

Name Type Description
str str

Item text.

Source code in source/qextrawidgets/widgets/inputs/icon_combo_box.py
209
210
211
212
213
214
215
216
217
218
219
220
def itemText(self, index: int) -> str:
    """Returns the text of the item at the given index.

    Args:
        index (int): Item index.

    Returns:
        str: Item text.
    """
    if 0 <= index < len(self._items):
        return self._items[index]['text']
    return ""

setCurrentIndex(index)

Sets the current index and updates the main button.

Parameters:

Name Type Description Default
index int

Index to set as current.

required
Source code in source/qextrawidgets/widgets/inputs/icon_combo_box.py
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
def setCurrentIndex(self, index: int) -> None:
    """Sets the current index and updates the main button.

    Args:
        index (int): Index to set as current.
    """
    if 0 <= index < len(self._items):
        self._current_index = index
        item = self._items[index]

        if item['font']:
            self.setFont(item['font'])
        else:
            self.setFont(self._panel.font())  # Reset to default font if none specified

        if item['icon']:
            self.setIcon(item['icon'])
            self.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
        elif item['text']:
            self.setText(item['text'])
            self.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextOnly)

        self.currentIndexChanged.emit(index)
        self.currentDataChanged.emit(item['data'])
    elif index == -1:
        self._current_index = -1
        self.setIcon(QIcon())
        self.setText("")

setItemData(index, data)

Sets the data associated with the item at the given index.

Parameters:

Name Type Description Default
index int

Item index.

required
data Any

New data.

required
Source code in source/qextrawidgets/widgets/inputs/icon_combo_box.py
263
264
265
266
267
268
269
270
271
272
273
def setItemData(self, index: int, data: typing.Any) -> None:
    """Sets the data associated with the item at the given index.

    Args:
        index (int): Item index.
        data (Any): New data.
    """
    if 0 <= index < len(self._items):
        self._items[index]['data'] = data
        if index == self._current_index:
            self.currentDataChanged.emit(data)

setItemFont(index, font)

Sets the font for the item at the given index.

Parameters:

Name Type Description Default
index int

Item index.

required
font QFont

New font.

required
Source code in source/qextrawidgets/widgets/inputs/icon_combo_box.py
166
167
168
169
170
171
172
173
174
175
176
177
def setItemFont(self, index: int, font: QFont) -> None:
    """Sets the font for the item at the given index.

    Args:
        index (int): Item index.
        font (QFont): New font.
    """
    if 0 <= index < len(self._items):
        self._items[index]['font'] = font
        self._items[index]['button'].setFont(font)
        if index == self._current_index:
            self.setFont(font)

setItemIcon(index, icon)

Sets the icon for the item at the given index.

Parameters:

Name Type Description Default
index int

Item index.

required
icon Union[QIcon, str, QPixmap]

New icon, theme icon name, or QPixmap.

required
Source code in source/qextrawidgets/widgets/inputs/icon_combo_box.py
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
def setItemIcon(self, index: int, icon: typing.Optional[typing.Union[QIcon, str, QPixmap]]) -> None:
    """Sets the icon for the item at the given index.

    Args:
        index (int): Item index.
        icon (Union[QIcon, str, QPixmap], optional): New icon, theme icon name, or QPixmap.
    """
    if 0 <= index < len(self._items):
        if isinstance(icon, QPixmap):
            icon = QIcon(icon)
        elif isinstance(icon, str):
            icon = QIcon.fromTheme(icon)

        self._items[index]['icon'] = icon
        btn = self._items[index]['button']

        if icon:
            btn.setIcon(icon)
            btn.setIconSize(QSize(int(self._size * 0.6), int(self._size * 0.6)))
            btn.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
        else:
            btn.setIcon(QIcon())
            if self._items[index]['text']:
                btn.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextOnly)

        if index == self._current_index:
            self.setCurrentIndex(index)

setItemText(index, text)

Sets the text for the item at the given index.

Parameters:

Name Type Description Default
index int

Item index.

required
text str

New text.

required
Source code in source/qextrawidgets/widgets/inputs/icon_combo_box.py
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
def setItemText(self, index: int, text: str) -> None:
    """Sets the text for the item at the given index.

    Args:
        index (int): Item index.
        text (str): New text.
    """
    if 0 <= index < len(self._items):
        self._items[index]['text'] = text
        btn = self._items[index]['button']
        btn.setText(text)
        if not self._items[index]['icon']:
            btn.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextOnly)

        if index == self._current_index:
            self.setCurrentIndex(index)

QPasswordLineEdit

Bases: QLineEdit

A line edit widget for passwords with a built-in toggle button to show/hide the text.

Source code in source/qextrawidgets/widgets/inputs/password_line_edit.py
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class QPasswordLineEdit(QLineEdit):
    """A line edit widget for passwords with a built-in toggle button to show/hide the text."""

    def __init__(self, parent: typing.Optional[QWidget] = None) -> None:
        """Initializes the password line edit.

        Args:
            parent (QWidget, optional): Parent widget. Defaults to None.
        """
        super().__init__(parent)
        self._action = QAction("&Hide/show", self)
        self._action.setCheckable(True)
        self._action.toggled.connect(self.setPasswordHidden)
        self.addAction(self._action, QLineEdit.ActionPosition.TrailingPosition)
        self.setPasswordHidden(True)

    def isPasswordHidden(self) -> bool:
        """Checks if the password is currently hidden (EchoMode.Password).

        Returns:
            bool: True if hidden, False otherwise.
        """
        return self.echoMode() == QLineEdit.EchoMode.Password

    def setPasswordHidden(self, hide: bool) -> None:
        """Sets whether the password should be hidden or visible.

        Args:
            hide (bool): True to hide the password, False to show it.
        """
        if hide:
            self.setEchoMode(QLineEdit.EchoMode.Password)
            self._action.setIcon(QThemeResponsiveIcon.fromAwesome("fa6s.eye"))
        else:
            self.setEchoMode(QLineEdit.EchoMode.Normal)
            self._action.setIcon(QThemeResponsiveIcon.fromAwesome("fa6s.eye-slash"))

__init__(parent=None)

Initializes the password line edit.

Parameters:

Name Type Description Default
parent QWidget

Parent widget. Defaults to None.

None
Source code in source/qextrawidgets/widgets/inputs/password_line_edit.py
11
12
13
14
15
16
17
18
19
20
21
22
def __init__(self, parent: typing.Optional[QWidget] = None) -> None:
    """Initializes the password line edit.

    Args:
        parent (QWidget, optional): Parent widget. Defaults to None.
    """
    super().__init__(parent)
    self._action = QAction("&Hide/show", self)
    self._action.setCheckable(True)
    self._action.toggled.connect(self.setPasswordHidden)
    self.addAction(self._action, QLineEdit.ActionPosition.TrailingPosition)
    self.setPasswordHidden(True)

isPasswordHidden()

Checks if the password is currently hidden (EchoMode.Password).

Returns:

Name Type Description
bool bool

True if hidden, False otherwise.

Source code in source/qextrawidgets/widgets/inputs/password_line_edit.py
24
25
26
27
28
29
30
def isPasswordHidden(self) -> bool:
    """Checks if the password is currently hidden (EchoMode.Password).

    Returns:
        bool: True if hidden, False otherwise.
    """
    return self.echoMode() == QLineEdit.EchoMode.Password

setPasswordHidden(hide)

Sets whether the password should be hidden or visible.

Parameters:

Name Type Description Default
hide bool

True to hide the password, False to show it.

required
Source code in source/qextrawidgets/widgets/inputs/password_line_edit.py
32
33
34
35
36
37
38
39
40
41
42
43
def setPasswordHidden(self, hide: bool) -> None:
    """Sets whether the password should be hidden or visible.

    Args:
        hide (bool): True to hide the password, False to show it.
    """
    if hide:
        self.setEchoMode(QLineEdit.EchoMode.Password)
        self._action.setIcon(QThemeResponsiveIcon.fromAwesome("fa6s.eye"))
    else:
        self.setEchoMode(QLineEdit.EchoMode.Normal)
        self._action.setIcon(QThemeResponsiveIcon.fromAwesome("fa6s.eye-slash"))

QSearchLineEdit

Bases: QLineEdit

A search line edit with a magnifying glass icon and a clear button.

Source code in source/qextrawidgets/widgets/inputs/search_line_edit.py
 7
 8
 9
10
11
12
13
14
15
16
17
18
class QSearchLineEdit(QLineEdit):
    """A search line edit with a magnifying glass icon and a clear button."""

    def __init__(self, parent: typing.Optional[QWidget] = None) -> None:
        """Initializes the search line edit.

        Args:
            parent (QWidget, optional): Parent widget. Defaults to None.
        """
        super().__init__(parent)
        self.setClearButtonEnabled(True)
        self.addAction(QThemeResponsiveIcon.fromAwesome("fa6s.magnifying-glass"), QLineEdit.ActionPosition.LeadingPosition)

__init__(parent=None)

Initializes the search line edit.

Parameters:

Name Type Description Default
parent QWidget

Parent widget. Defaults to None.

None
Source code in source/qextrawidgets/widgets/inputs/search_line_edit.py
10
11
12
13
14
15
16
17
18
def __init__(self, parent: typing.Optional[QWidget] = None) -> None:
    """Initializes the search line edit.

    Args:
        parent (QWidget, optional): Parent widget. Defaults to None.
    """
    super().__init__(parent)
    self.setClearButtonEnabled(True)
    self.addAction(QThemeResponsiveIcon.fromAwesome("fa6s.magnifying-glass"), QLineEdit.ActionPosition.LeadingPosition)

QEmojiPickerMenu

Bases: QMenu

A menu that displays a QEmojiPicker.

Signals

picked (str): Emitted when an emoji is selected.

Source code in source/qextrawidgets/widgets/menus/emoji_picker_menu.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class QEmojiPickerMenu(QMenu):
    """A menu that displays a QEmojiPicker.

    Signals:
        picked (str): Emitted when an emoji is selected.
    """

    picked = Signal(QEmojiItem)

    def __init__(
            self,
            parent: typing.Optional[QWidget] = None,
            model: typing.Optional[QEmojiPickerModel] = None,
            emoji_pixmap_getter: typing.Union[str, QFont, typing.Callable[[str], QPixmap]] = partial(
                QTwemojiImageProvider.getPixmap, margin=0, size=128),
            emoji_label_size: QSize = QSize(32, 32)) -> None:
        """Initialize the emoji picker menu.

        Args:
            parent (QWidget, optional): The parent widget.
            model (QEmojiPickerModel, optional): Custom emoji model. Defaults to None.
            emoji_pixmap_getter (Union[str, QFont, Callable[[str], QPixmap]], optional):
                Method or font to generate emoji pixmaps. Defaults to EmojiImageProvider.getPixmap.
            emoji_label_size (QSize, optional): Size of the preview emoji label. Defaults to QSize(32, 32).
        """
        super().__init__(parent)
        self._picker = QEmojiPicker(model, emoji_pixmap_getter, emoji_label_size)
        self._picker.picked.connect(self._on_picked)

        action = QWidgetAction(self)
        action.setDefaultWidget(self._picker)
        self.addAction(action)

    def picker(self) -> QEmojiPicker:
        """Returns the internal emoji picker widget.

        Returns:
            QEmojiPicker: The emoji picker widget.
        """
        return self._picker

    def _on_picked(self, item: QEmojiItem) -> None:
        """Handles the emoji picked signal.

        Args:
            item (QEmojiItem): The picked emoji item.
        """
        self.picked.emit(item)
        self.close()

__init__(parent=None, model=None, emoji_pixmap_getter=partial(QTwemojiImageProvider.getPixmap, margin=0, size=128), emoji_label_size=QSize(32, 32))

Initialize the emoji picker menu.

Parameters:

Name Type Description Default
parent QWidget

The parent widget.

None
model QEmojiPickerModel

Custom emoji model. Defaults to None.

None
emoji_pixmap_getter Union[str, QFont, Callable[[str], QPixmap]]

Method or font to generate emoji pixmaps. Defaults to EmojiImageProvider.getPixmap.

partial(getPixmap, margin=0, size=128)
emoji_label_size QSize

Size of the preview emoji label. Defaults to QSize(32, 32).

QSize(32, 32)
Source code in source/qextrawidgets/widgets/menus/emoji_picker_menu.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
def __init__(
        self,
        parent: typing.Optional[QWidget] = None,
        model: typing.Optional[QEmojiPickerModel] = None,
        emoji_pixmap_getter: typing.Union[str, QFont, typing.Callable[[str], QPixmap]] = partial(
            QTwemojiImageProvider.getPixmap, margin=0, size=128),
        emoji_label_size: QSize = QSize(32, 32)) -> None:
    """Initialize the emoji picker menu.

    Args:
        parent (QWidget, optional): The parent widget.
        model (QEmojiPickerModel, optional): Custom emoji model. Defaults to None.
        emoji_pixmap_getter (Union[str, QFont, Callable[[str], QPixmap]], optional):
            Method or font to generate emoji pixmaps. Defaults to EmojiImageProvider.getPixmap.
        emoji_label_size (QSize, optional): Size of the preview emoji label. Defaults to QSize(32, 32).
    """
    super().__init__(parent)
    self._picker = QEmojiPicker(model, emoji_pixmap_getter, emoji_label_size)
    self._picker.picked.connect(self._on_picked)

    action = QWidgetAction(self)
    action.setDefaultWidget(self._picker)
    self.addAction(action)

picker()

Returns the internal emoji picker widget.

Returns:

Name Type Description
QEmojiPicker QEmojiPicker

The emoji picker widget.

Source code in source/qextrawidgets/widgets/menus/emoji_picker_menu.py
47
48
49
50
51
52
53
def picker(self) -> QEmojiPicker:
    """Returns the internal emoji picker widget.

    Returns:
        QEmojiPicker: The emoji picker widget.
    """
    return self._picker

Miscellaneous

QAccordionHeader

Bases: QFrame

Header widget for an accordion item.

Signals

clicked: Emitted when the header is clicked.

Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion_header.py
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
class QAccordionHeader(QFrame):
    """Header widget for an accordion item.

    Signals:
        clicked: Emitted when the header is clicked.
    """

    clicked = Signal()

    IconPosition = QLineEdit.ActionPosition

    class IndicatorStyle(IntEnum):
        """Style of the expansion indicator icon."""
        Arrow = auto()  # Arrow (> v)
        PlusMinus = auto()  # Plus/Minus (+ -)

    def __init__(
            self,
            title: str = "",
            parent: typing.Optional[QWidget] = None,
            flat: bool = False,
            icon_style: IndicatorStyle = IndicatorStyle.Arrow,
            icon_position: IconPosition = IconPosition.LeadingPosition
    ) -> None:
        """Initializes the accordion header.

        Args:
            title (str, optional): Header title. Defaults to "".
            parent (QWidget, optional): Parent widget. Defaults to None.
            flat (bool, optional): Whether the header is flat. Defaults to False.
            icon_style (IndicatorStyle, optional): Icon style. Defaults to Arrow.
            icon_position (IconPosition, optional): Icon position. Defaults to LeadingPosition.
        """
        super().__init__(parent)

        # Native visual style
        self.setCursor(Qt.CursorShape.PointingHandCursor)
        self.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)

        # States
        self._is_expanded = False
        self._icon_position = icon_position
        self._icon_style = icon_style

        # Widgets
        self._label_title = QLabel(title)
        self._label_title.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)

        # --- CHANGE: We use QToolButton instead of QLabel ---
        # This allows QAutoIcon to manage dynamic painting (colors)
        self._label = QThemeResponsiveLabel()
        self._label.setFixedSize(24, 24)

        # Layout
        self._layout_header = QHBoxLayout(self)
        self._layout_header.setContentsMargins(10, 5, 10, 5)

        # Initialization
        self.updateIcon()
        self.refreshLayout()
        self.setFlat(flat)

    def closeEvent(self, event) -> None:
        """Disconnects signals to prevent crashes on destruction."""
        # QAccordionHeader doesn't have _on_theme_change, it uses QThemeResponsiveLabel
        # So we don't need to disconnect anything here that doesn't exist.
        super().closeEvent(event)

    def setFlat(self, flat: bool) -> None:
        """Defines whether the header looks like a raised button or plain text.

        Args:
            flat (bool): True for flat (plain text), False for raised button.
        """
        if flat:
            self.setFrameStyle(QFrame.Shape.NoFrame)
            self.setAutoFillBackground(False)
        else:
            self.setFrameStyle(QFrame.Shape.StyledPanel | QFrame.Shadow.Raised)
            self.setAutoFillBackground(True)

    def isFlat(self) -> bool:
        """Returns whether the header is flat.

        Returns:
            bool: True if flat, False otherwise.
        """
        return self.frameStyle() == QFrame.Shape.NoFrame and not self.autoFillBackground()

    def mousePressEvent(self, event: QMouseEvent) -> None:
        """Handles mouse press events.

        Args:
            event (QMouseEvent): Mouse event.
        """
        if event.button() == Qt.MouseButton.LeftButton:
            self.clicked.emit()
        super().mousePressEvent(event)

    def setExpanded(self, expanded: bool) -> None:
        """Sets the expanded state and updates the icon.

        Args:
            expanded (bool): True to show expanded state, False for collapsed.
        """
        self._is_expanded = expanded
        self.updateIcon()

    def isExpanded(self) -> bool:
        """Returns whether the header is in expanded state.

        Returns:
            bool: True if expanded, False otherwise.
        """
        return self._is_expanded

    def setIconStyle(self, style: IndicatorStyle) -> None:
        """Sets the expansion indicator icon style.

        Args:
            style (IndicatorStyle): Icon style (Arrow or PlusMinus).
        """
        if style in [QAccordionHeader.IndicatorStyle.Arrow, QAccordionHeader.IndicatorStyle.PlusMinus]:
            self._icon_style = style
            self.updateIcon()

    def updateIcon(self) -> None:
        """Updates the icon using QThemeResponsiveIcon to ensure dynamic colors."""
        icon_name = ""

        if self._icon_style == QAccordionHeader.IndicatorStyle.Arrow:
            icon_name = "fa6s.angle-down" if self._is_expanded else "fa6s.angle-right"

        elif self._icon_style == QAccordionHeader.IndicatorStyle.PlusMinus:
            icon_name = "fa6s.minus" if self._is_expanded else "fa6s.plus"

        if icon_name:
            self._label.setIcon(QThemeResponsiveIcon.fromAwesome(icon_name))

    def setIconPosition(self, position: IconPosition) -> None:
        """Sets the position of the expansion icon.

        Args:
            position (IconPosition): Position (Leading or Trailing).
        """
        if position in [QAccordionHeader.IconPosition.TrailingPosition, QAccordionHeader.IconPosition.LeadingPosition]:
            self._icon_position = position
            self.refreshLayout()

    def refreshLayout(self) -> None:
        """Refreshes the layout based on icon position."""
        while self._layout_header.count():
            self._layout_header.takeAt(0)

        if self._icon_position == QAccordionHeader.IconPosition.LeadingPosition:
            self._layout_header.addWidget(self._label)
            self._layout_header.addWidget(self._label_title)
            self._label_title.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)

        elif self._icon_position == QAccordionHeader.IconPosition.TrailingPosition:
            self._label_title.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
            self._layout_header.addWidget(self._label_title)
            self._layout_header.addWidget(self._label)

    def setTitle(self, title: str) -> None:
        """Sets the header title.

        Args:
            title (str): New title text.
        """
        self._label_title.setText(title)

    def titleLabel(self) -> QLabel:
        """Returns the title label widget.

        Returns:
            QLabel: Title label.
        """
        return self._label_title

    def iconWidget(self) -> QWidget:
        """Returns the icon widget.

        Returns:
            QWidget: Icon widget.
        """
        # Renamed from iconLabel because it is now a button
        return self._label

IndicatorStyle

Bases: IntEnum

Style of the expansion indicator icon.

Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion_header.py
23
24
25
26
class IndicatorStyle(IntEnum):
    """Style of the expansion indicator icon."""
    Arrow = auto()  # Arrow (> v)
    PlusMinus = auto()  # Plus/Minus (+ -)

__init__(title='', parent=None, flat=False, icon_style=IndicatorStyle.Arrow, icon_position=IconPosition.LeadingPosition)

Initializes the accordion header.

Parameters:

Name Type Description Default
title str

Header title. Defaults to "".

''
parent QWidget

Parent widget. Defaults to None.

None
flat bool

Whether the header is flat. Defaults to False.

False
icon_style IndicatorStyle

Icon style. Defaults to Arrow.

Arrow
icon_position IconPosition

Icon position. Defaults to LeadingPosition.

LeadingPosition
Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion_header.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
def __init__(
        self,
        title: str = "",
        parent: typing.Optional[QWidget] = None,
        flat: bool = False,
        icon_style: IndicatorStyle = IndicatorStyle.Arrow,
        icon_position: IconPosition = IconPosition.LeadingPosition
) -> None:
    """Initializes the accordion header.

    Args:
        title (str, optional): Header title. Defaults to "".
        parent (QWidget, optional): Parent widget. Defaults to None.
        flat (bool, optional): Whether the header is flat. Defaults to False.
        icon_style (IndicatorStyle, optional): Icon style. Defaults to Arrow.
        icon_position (IconPosition, optional): Icon position. Defaults to LeadingPosition.
    """
    super().__init__(parent)

    # Native visual style
    self.setCursor(Qt.CursorShape.PointingHandCursor)
    self.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)

    # States
    self._is_expanded = False
    self._icon_position = icon_position
    self._icon_style = icon_style

    # Widgets
    self._label_title = QLabel(title)
    self._label_title.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)

    # --- CHANGE: We use QToolButton instead of QLabel ---
    # This allows QAutoIcon to manage dynamic painting (colors)
    self._label = QThemeResponsiveLabel()
    self._label.setFixedSize(24, 24)

    # Layout
    self._layout_header = QHBoxLayout(self)
    self._layout_header.setContentsMargins(10, 5, 10, 5)

    # Initialization
    self.updateIcon()
    self.refreshLayout()
    self.setFlat(flat)

closeEvent(event)

Disconnects signals to prevent crashes on destruction.

Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion_header.py
74
75
76
77
78
def closeEvent(self, event) -> None:
    """Disconnects signals to prevent crashes on destruction."""
    # QAccordionHeader doesn't have _on_theme_change, it uses QThemeResponsiveLabel
    # So we don't need to disconnect anything here that doesn't exist.
    super().closeEvent(event)

iconWidget()

Returns the icon widget.

Returns:

Name Type Description
QWidget QWidget

Icon widget.

Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion_header.py
192
193
194
195
196
197
198
199
def iconWidget(self) -> QWidget:
    """Returns the icon widget.

    Returns:
        QWidget: Icon widget.
    """
    # Renamed from iconLabel because it is now a button
    return self._label

isExpanded()

Returns whether the header is in expanded state.

Returns:

Name Type Description
bool bool

True if expanded, False otherwise.

Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion_header.py
120
121
122
123
124
125
126
def isExpanded(self) -> bool:
    """Returns whether the header is in expanded state.

    Returns:
        bool: True if expanded, False otherwise.
    """
    return self._is_expanded

isFlat()

Returns whether the header is flat.

Returns:

Name Type Description
bool bool

True if flat, False otherwise.

Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion_header.py
93
94
95
96
97
98
99
def isFlat(self) -> bool:
    """Returns whether the header is flat.

    Returns:
        bool: True if flat, False otherwise.
    """
    return self.frameStyle() == QFrame.Shape.NoFrame and not self.autoFillBackground()

mousePressEvent(event)

Handles mouse press events.

Parameters:

Name Type Description Default
event QMouseEvent

Mouse event.

required
Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion_header.py
101
102
103
104
105
106
107
108
109
def mousePressEvent(self, event: QMouseEvent) -> None:
    """Handles mouse press events.

    Args:
        event (QMouseEvent): Mouse event.
    """
    if event.button() == Qt.MouseButton.LeftButton:
        self.clicked.emit()
    super().mousePressEvent(event)

refreshLayout()

Refreshes the layout based on icon position.

Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion_header.py
161
162
163
164
165
166
167
168
169
170
171
172
173
174
def refreshLayout(self) -> None:
    """Refreshes the layout based on icon position."""
    while self._layout_header.count():
        self._layout_header.takeAt(0)

    if self._icon_position == QAccordionHeader.IconPosition.LeadingPosition:
        self._layout_header.addWidget(self._label)
        self._layout_header.addWidget(self._label_title)
        self._label_title.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)

    elif self._icon_position == QAccordionHeader.IconPosition.TrailingPosition:
        self._label_title.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
        self._layout_header.addWidget(self._label_title)
        self._layout_header.addWidget(self._label)

setExpanded(expanded)

Sets the expanded state and updates the icon.

Parameters:

Name Type Description Default
expanded bool

True to show expanded state, False for collapsed.

required
Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion_header.py
111
112
113
114
115
116
117
118
def setExpanded(self, expanded: bool) -> None:
    """Sets the expanded state and updates the icon.

    Args:
        expanded (bool): True to show expanded state, False for collapsed.
    """
    self._is_expanded = expanded
    self.updateIcon()

setFlat(flat)

Defines whether the header looks like a raised button or plain text.

Parameters:

Name Type Description Default
flat bool

True for flat (plain text), False for raised button.

required
Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion_header.py
80
81
82
83
84
85
86
87
88
89
90
91
def setFlat(self, flat: bool) -> None:
    """Defines whether the header looks like a raised button or plain text.

    Args:
        flat (bool): True for flat (plain text), False for raised button.
    """
    if flat:
        self.setFrameStyle(QFrame.Shape.NoFrame)
        self.setAutoFillBackground(False)
    else:
        self.setFrameStyle(QFrame.Shape.StyledPanel | QFrame.Shadow.Raised)
        self.setAutoFillBackground(True)

setIconPosition(position)

Sets the position of the expansion icon.

Parameters:

Name Type Description Default
position IconPosition

Position (Leading or Trailing).

required
Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion_header.py
151
152
153
154
155
156
157
158
159
def setIconPosition(self, position: IconPosition) -> None:
    """Sets the position of the expansion icon.

    Args:
        position (IconPosition): Position (Leading or Trailing).
    """
    if position in [QAccordionHeader.IconPosition.TrailingPosition, QAccordionHeader.IconPosition.LeadingPosition]:
        self._icon_position = position
        self.refreshLayout()

setIconStyle(style)

Sets the expansion indicator icon style.

Parameters:

Name Type Description Default
style IndicatorStyle

Icon style (Arrow or PlusMinus).

required
Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion_header.py
128
129
130
131
132
133
134
135
136
def setIconStyle(self, style: IndicatorStyle) -> None:
    """Sets the expansion indicator icon style.

    Args:
        style (IndicatorStyle): Icon style (Arrow or PlusMinus).
    """
    if style in [QAccordionHeader.IndicatorStyle.Arrow, QAccordionHeader.IndicatorStyle.PlusMinus]:
        self._icon_style = style
        self.updateIcon()

setTitle(title)

Sets the header title.

Parameters:

Name Type Description Default
title str

New title text.

required
Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion_header.py
176
177
178
179
180
181
182
def setTitle(self, title: str) -> None:
    """Sets the header title.

    Args:
        title (str): New title text.
    """
    self._label_title.setText(title)

titleLabel()

Returns the title label widget.

Returns:

Name Type Description
QLabel QLabel

Title label.

Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion_header.py
184
185
186
187
188
189
190
def titleLabel(self) -> QLabel:
    """Returns the title label widget.

    Returns:
        QLabel: Title label.
    """
    return self._label_title

updateIcon()

Updates the icon using QThemeResponsiveIcon to ensure dynamic colors.

Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion_header.py
138
139
140
141
142
143
144
145
146
147
148
149
def updateIcon(self) -> None:
    """Updates the icon using QThemeResponsiveIcon to ensure dynamic colors."""
    icon_name = ""

    if self._icon_style == QAccordionHeader.IndicatorStyle.Arrow:
        icon_name = "fa6s.angle-down" if self._is_expanded else "fa6s.angle-right"

    elif self._icon_style == QAccordionHeader.IndicatorStyle.PlusMinus:
        icon_name = "fa6s.minus" if self._is_expanded else "fa6s.plus"

    if icon_name:
        self._label.setIcon(QThemeResponsiveIcon.fromAwesome(icon_name))

QAccordionItem

Bases: QWidget

Accordion item with optional smooth expand/collapse animation.

Signals

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

Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion_item.py
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
class QAccordionItem(QWidget):
    """Accordion item with optional smooth expand/collapse animation.

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

    expandedChanged = Signal(bool)

    def __init__(
        self,
        title: str,
        content_widget: QWidget,
        parent: typing.Optional[QWidget] = None,
        expanded: bool = False,
        flat: bool = False,
        icon_style: QAccordionHeader.IndicatorStyle = QAccordionHeader.IndicatorStyle.Arrow,
        icon_position: QAccordionHeader.IconPosition = QAccordionHeader.IconPosition.LeadingPosition,
        animation_enabled: bool = False,
        animation_duration: int = 200,
        animation_easing: QEasingCurve.Type = QEasingCurve.Type.InOutQuart,
    ) -> None:
        """Initializes the accordion item.

        Args:
            title (str): Section title.
            content_widget (QWidget): Content widget to be shown/hidden.
            parent (QWidget, optional): Parent widget. Defaults to None.
            expanded (bool, optional): Initial expansion state. Defaults to False.
            flat (bool, optional): Whether the header is flat. Defaults to False.
            icon_style (QAccordionHeader.IndicatorStyle, optional): Icon style. Defaults to Arrow.
            icon_position (QAccordionHeader.IconPosition, optional): Icon position. Defaults to LeadingPosition.
            animation_enabled (bool, optional): Whether animations are enabled. Defaults to True.
            animation_duration (int, optional): Animation duration in ms. Defaults to 200.
            animation_easing (QEasingCurve.Type, optional): Animation easing curve. Defaults to InOutQuart.
        """
        super().__init__(parent)
        self._layout = QVBoxLayout(self)
        self._layout.setContentsMargins(0, 0, 0, 0)
        self._layout.setSpacing(0)

        self._header = QAccordionHeader(title, self, flat, icon_style, icon_position)
        self._content = content_widget

        # Animation setup
        self._animation_enabled = animation_enabled

        # Animation object
        self._animation = QPropertyAnimation(self._content, b"maximumHeight")
        self._animation.setDuration(animation_duration)
        self._animation.setEasingCurve(animation_easing)
        self._animation.finished.connect(self._on_animation_finished)

        # Initial state
        self._content.setVisible(False)

        self._layout.addWidget(self._header)
        self._layout.addWidget(self._content, stretch=1)

        self._header.clicked.connect(self.toggle)

        # Apply initial settings
        if expanded:
            self.setExpanded(True, animated=False)

    def toggle(self) -> None:
        """Toggles the expanded state."""
        self.setExpanded(not self.isExpanded(), animated=True)

    def setTitle(self, text: str) -> None:
        """Sets the item title.

        Args:
            text (str): New title text.
        """
        self.header().setTitle(text)

    def setExpanded(self, expanded: bool, animated: bool = False) -> None:
        """Sets the expanded state.

        Args:
            expanded (bool): True to expand, False to collapse.
            animated (bool, optional): Override animation setting for this call. If None, uses the widget's setting. Defaults to None.
        """
        # Determine if we should animate
        use_animation = self._animation_enabled and animated

        # Stop any running animation
        if self._animation.state() == QAbstractAnimation.State.Running:
            self._animation.stop()

        # Update header state
        self._header.setExpanded(expanded)

        if expanded:
            # Expanding
            self._content.setVisible(True)

            if use_animation:
                target_height = self._content.sizeHint().height()

                # Animate from 0 to target height
                self._animation.setStartValue(0)
                self._animation.setEndValue(target_height)
                self._animation.start()
            else:
                # Instant expand - ensure no height limit
                self._content.setMaximumHeight(16777215)
        else:
            # Collapsing
            if use_animation:
                # Get current height
                current_height = self._content.height()

                # Animate from current height to 0
                self._animation.setStartValue(current_height)
                self._animation.setEndValue(0)
                self._animation.start()
            else:
                # Instant collapse
                self._content.setVisible(False)
        self.expandedChanged.emit(expanded)

    @Slot()
    def _on_animation_finished(self) -> None:
        """Called when any animation finishes."""
        if self.isExpanded():
            # After expand animation: remove height constraint to allow stretching
            self._content.setMaximumHeight(16777215)
        else:
            # After collapse animation: hide the content
            self._content.setVisible(False)

    def isExpanded(self) -> bool:
        """Returns whether the item is expanded.

        Returns:
            bool: True if expanded, False otherwise.
        """
        return self._header.isExpanded()

    # --- Animation Settings ---

    def setAnimationEnabled(self, enabled: bool) -> None:
        """Enable or disable animations.

        Args:
            enabled (bool): True to enable animations, False to disable.
        """
        self._animation_enabled = enabled

    def isAnimationEnabled(self) -> bool:
        """Returns whether animations are enabled.

        Returns:
            bool: True if animations are enabled, False otherwise.
        """
        return self._animation_enabled

    def setAnimationDuration(self, duration: int) -> None:
        """Sets the animation duration in milliseconds.

        Args:
            duration (int): Duration in milliseconds (typical range: 100-500).
        """
        self._animation.setDuration(duration)

    def animationDuration(self) -> int:
        """Returns the animation duration in milliseconds.

        Returns:
            int: Animation duration.
        """
        return self._animation.duration()

    def setAnimationEasing(self, easing: QEasingCurve.Type) -> None:
        """Sets the animation easing curve.

        Args:
            easing (QEasingCurve.Type): QEasingCurve.Type (e.g., InOutQuart, OutCubic, Linear).
        """
        self._animation.setEasingCurve(easing)

    def animationEasing(self) -> QEasingCurve.Type:
        """Returns the animation easing curve.

        Returns:
            QEasingCurve.Type: The easing curve.
        """
        return self._animation.easingCurve().type()

    # --- Style Settings ---

    def setIconPosition(self, position: QAccordionHeader.IconPosition) -> None:
        """Sets the icon position.

        Args:
            position (QAccordionHeader.IconPosition): Position (Leading or Trailing).
        """
        self._header.setIconPosition(position)

    def setIconStyle(self, style: QAccordionHeader.IndicatorStyle) -> None:
        """Sets the icon style.

        Args:
            style (QAccordionHeader.IndicatorStyle): Icon style (Arrow or PlusMinus).
        """
        self._header.setIconStyle(style)

    def setFlat(self, flat: bool) -> None:
        """Sets whether the header is flat or raised.

        Args:
            flat (bool): True for flat, False for raised.
        """
        self._header.setFlat(flat)

    # --- Accessors ---

    def content(self) -> QWidget:
        """Returns the content widget.

        Returns:
            QWidget: Content widget.
        """
        return self._content

    def header(self) -> QAccordionHeader:
        """Returns the header widget.

        Returns:
            QAccordionHeader: Header widget.
        """
        return self._header

__init__(title, content_widget, parent=None, expanded=False, flat=False, icon_style=QAccordionHeader.IndicatorStyle.Arrow, icon_position=QAccordionHeader.IconPosition.LeadingPosition, animation_enabled=False, animation_duration=200, animation_easing=QEasingCurve.Type.InOutQuart)

Initializes the accordion item.

Parameters:

Name Type Description Default
title str

Section title.

required
content_widget QWidget

Content widget to be shown/hidden.

required
parent QWidget

Parent widget. Defaults to None.

None
expanded bool

Initial expansion state. Defaults to False.

False
flat bool

Whether the header is flat. Defaults to False.

False
icon_style IndicatorStyle

Icon style. Defaults to Arrow.

Arrow
icon_position IconPosition

Icon position. Defaults to LeadingPosition.

LeadingPosition
animation_enabled bool

Whether animations are enabled. Defaults to True.

False
animation_duration int

Animation duration in ms. Defaults to 200.

200
animation_easing Type

Animation easing curve. Defaults to InOutQuart.

InOutQuart
Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion_item.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
def __init__(
    self,
    title: str,
    content_widget: QWidget,
    parent: typing.Optional[QWidget] = None,
    expanded: bool = False,
    flat: bool = False,
    icon_style: QAccordionHeader.IndicatorStyle = QAccordionHeader.IndicatorStyle.Arrow,
    icon_position: QAccordionHeader.IconPosition = QAccordionHeader.IconPosition.LeadingPosition,
    animation_enabled: bool = False,
    animation_duration: int = 200,
    animation_easing: QEasingCurve.Type = QEasingCurve.Type.InOutQuart,
) -> None:
    """Initializes the accordion item.

    Args:
        title (str): Section title.
        content_widget (QWidget): Content widget to be shown/hidden.
        parent (QWidget, optional): Parent widget. Defaults to None.
        expanded (bool, optional): Initial expansion state. Defaults to False.
        flat (bool, optional): Whether the header is flat. Defaults to False.
        icon_style (QAccordionHeader.IndicatorStyle, optional): Icon style. Defaults to Arrow.
        icon_position (QAccordionHeader.IconPosition, optional): Icon position. Defaults to LeadingPosition.
        animation_enabled (bool, optional): Whether animations are enabled. Defaults to True.
        animation_duration (int, optional): Animation duration in ms. Defaults to 200.
        animation_easing (QEasingCurve.Type, optional): Animation easing curve. Defaults to InOutQuart.
    """
    super().__init__(parent)
    self._layout = QVBoxLayout(self)
    self._layout.setContentsMargins(0, 0, 0, 0)
    self._layout.setSpacing(0)

    self._header = QAccordionHeader(title, self, flat, icon_style, icon_position)
    self._content = content_widget

    # Animation setup
    self._animation_enabled = animation_enabled

    # Animation object
    self._animation = QPropertyAnimation(self._content, b"maximumHeight")
    self._animation.setDuration(animation_duration)
    self._animation.setEasingCurve(animation_easing)
    self._animation.finished.connect(self._on_animation_finished)

    # Initial state
    self._content.setVisible(False)

    self._layout.addWidget(self._header)
    self._layout.addWidget(self._content, stretch=1)

    self._header.clicked.connect(self.toggle)

    # Apply initial settings
    if expanded:
        self.setExpanded(True, animated=False)

animationDuration()

Returns the animation duration in milliseconds.

Returns:

Name Type Description
int int

Animation duration.

Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion_item.py
184
185
186
187
188
189
190
def animationDuration(self) -> int:
    """Returns the animation duration in milliseconds.

    Returns:
        int: Animation duration.
    """
    return self._animation.duration()

animationEasing()

Returns the animation easing curve.

Returns:

Type Description
Type

QEasingCurve.Type: The easing curve.

Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion_item.py
200
201
202
203
204
205
206
def animationEasing(self) -> QEasingCurve.Type:
    """Returns the animation easing curve.

    Returns:
        QEasingCurve.Type: The easing curve.
    """
    return self._animation.easingCurve().type()

content()

Returns the content widget.

Returns:

Name Type Description
QWidget QWidget

Content widget.

Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion_item.py
236
237
238
239
240
241
242
def content(self) -> QWidget:
    """Returns the content widget.

    Returns:
        QWidget: Content widget.
    """
    return self._content

header()

Returns the header widget.

Returns:

Name Type Description
QAccordionHeader QAccordionHeader

Header widget.

Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion_item.py
244
245
246
247
248
249
250
def header(self) -> QAccordionHeader:
    """Returns the header widget.

    Returns:
        QAccordionHeader: Header widget.
    """
    return self._header

isAnimationEnabled()

Returns whether animations are enabled.

Returns:

Name Type Description
bool bool

True if animations are enabled, False otherwise.

Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion_item.py
168
169
170
171
172
173
174
def isAnimationEnabled(self) -> bool:
    """Returns whether animations are enabled.

    Returns:
        bool: True if animations are enabled, False otherwise.
    """
    return self._animation_enabled

isExpanded()

Returns whether the item is expanded.

Returns:

Name Type Description
bool bool

True if expanded, False otherwise.

Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion_item.py
150
151
152
153
154
155
156
def isExpanded(self) -> bool:
    """Returns whether the item is expanded.

    Returns:
        bool: True if expanded, False otherwise.
    """
    return self._header.isExpanded()

setAnimationDuration(duration)

Sets the animation duration in milliseconds.

Parameters:

Name Type Description Default
duration int

Duration in milliseconds (typical range: 100-500).

required
Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion_item.py
176
177
178
179
180
181
182
def setAnimationDuration(self, duration: int) -> None:
    """Sets the animation duration in milliseconds.

    Args:
        duration (int): Duration in milliseconds (typical range: 100-500).
    """
    self._animation.setDuration(duration)

setAnimationEasing(easing)

Sets the animation easing curve.

Parameters:

Name Type Description Default
easing Type

QEasingCurve.Type (e.g., InOutQuart, OutCubic, Linear).

required
Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion_item.py
192
193
194
195
196
197
198
def setAnimationEasing(self, easing: QEasingCurve.Type) -> None:
    """Sets the animation easing curve.

    Args:
        easing (QEasingCurve.Type): QEasingCurve.Type (e.g., InOutQuart, OutCubic, Linear).
    """
    self._animation.setEasingCurve(easing)

setAnimationEnabled(enabled)

Enable or disable animations.

Parameters:

Name Type Description Default
enabled bool

True to enable animations, False to disable.

required
Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion_item.py
160
161
162
163
164
165
166
def setAnimationEnabled(self, enabled: bool) -> None:
    """Enable or disable animations.

    Args:
        enabled (bool): True to enable animations, False to disable.
    """
    self._animation_enabled = enabled

setExpanded(expanded, animated=False)

Sets the expanded state.

Parameters:

Name Type Description Default
expanded bool

True to expand, False to collapse.

required
animated bool

Override animation setting for this call. If None, uses the widget's setting. Defaults to None.

False
Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion_item.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
def setExpanded(self, expanded: bool, animated: bool = False) -> None:
    """Sets the expanded state.

    Args:
        expanded (bool): True to expand, False to collapse.
        animated (bool, optional): Override animation setting for this call. If None, uses the widget's setting. Defaults to None.
    """
    # Determine if we should animate
    use_animation = self._animation_enabled and animated

    # Stop any running animation
    if self._animation.state() == QAbstractAnimation.State.Running:
        self._animation.stop()

    # Update header state
    self._header.setExpanded(expanded)

    if expanded:
        # Expanding
        self._content.setVisible(True)

        if use_animation:
            target_height = self._content.sizeHint().height()

            # Animate from 0 to target height
            self._animation.setStartValue(0)
            self._animation.setEndValue(target_height)
            self._animation.start()
        else:
            # Instant expand - ensure no height limit
            self._content.setMaximumHeight(16777215)
    else:
        # Collapsing
        if use_animation:
            # Get current height
            current_height = self._content.height()

            # Animate from current height to 0
            self._animation.setStartValue(current_height)
            self._animation.setEndValue(0)
            self._animation.start()
        else:
            # Instant collapse
            self._content.setVisible(False)
    self.expandedChanged.emit(expanded)

setFlat(flat)

Sets whether the header is flat or raised.

Parameters:

Name Type Description Default
flat bool

True for flat, False for raised.

required
Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion_item.py
226
227
228
229
230
231
232
def setFlat(self, flat: bool) -> None:
    """Sets whether the header is flat or raised.

    Args:
        flat (bool): True for flat, False for raised.
    """
    self._header.setFlat(flat)

setIconPosition(position)

Sets the icon position.

Parameters:

Name Type Description Default
position IconPosition

Position (Leading or Trailing).

required
Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion_item.py
210
211
212
213
214
215
216
def setIconPosition(self, position: QAccordionHeader.IconPosition) -> None:
    """Sets the icon position.

    Args:
        position (QAccordionHeader.IconPosition): Position (Leading or Trailing).
    """
    self._header.setIconPosition(position)

setIconStyle(style)

Sets the icon style.

Parameters:

Name Type Description Default
style IndicatorStyle

Icon style (Arrow or PlusMinus).

required
Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion_item.py
218
219
220
221
222
223
224
def setIconStyle(self, style: QAccordionHeader.IndicatorStyle) -> None:
    """Sets the icon style.

    Args:
        style (QAccordionHeader.IndicatorStyle): Icon style (Arrow or PlusMinus).
    """
    self._header.setIconStyle(style)

setTitle(text)

Sets the item title.

Parameters:

Name Type Description Default
text str

New title text.

required
Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion_item.py
86
87
88
89
90
91
92
def setTitle(self, text: str) -> None:
    """Sets the item title.

    Args:
        text (str): New title text.
    """
    self.header().setTitle(text)

toggle()

Toggles the expanded state.

Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion_item.py
82
83
84
def toggle(self) -> None:
    """Toggles the expanded state."""
    self.setExpanded(not self.isExpanded(), animated=True)

QAccordion

Bases: QWidget

Accordion widget with optional smooth animations.

A container that organizes content into collapsible sections. Supports multiple accordion items with expand/collapse animations, customizable styling (flat/raised, icon style, icon position), and vertical alignment control.

Signals

enteredSection (QAccordionItem): Emitted when a section is scrolled into view. leftSection (QAccordionItem): Emitted when a section is scrolled out of view.

Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion.py
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
class QAccordion(QWidget):
    """Accordion widget with optional smooth animations.

    A container that organizes content into collapsible sections.
    Supports multiple accordion items with expand/collapse animations,
    customizable styling (flat/raised, icon style, icon position),
    and vertical alignment control.

    Signals:
        enteredSection (QAccordionItem): Emitted when a section is scrolled into view.
        leftSection (QAccordionItem): Emitted when a section is scrolled out of view.
    """

    enteredSection = Signal(QAccordionItem)
    leftSection = Signal(QAccordionItem)

    def __init__(
        self,
        parent: typing.Optional[QWidget] = None,
        items_alignment: Qt.AlignmentFlag = Qt.AlignmentFlag.AlignTop,
        items_flat: bool = False,
        items_icon_style: QAccordionHeader.IndicatorStyle = QAccordionHeader.IndicatorStyle.Arrow,
        items_icon_position: QAccordionHeader.IconPosition = QAccordionHeader.IconPosition.LeadingPosition,
        animation_enabled: bool = False,
        animation_duration: int = 200,
        animation_easing: QEasingCurve.Type = QEasingCurve.Type.InOutQuart,
    ) -> None:
        """Initializes the accordion widget.

        Args:
            parent (QWidget, optional): Parent widget. Defaults to None.
            items_alignment (Qt.AlignmentFlag, optional): Vertical alignment of items. Defaults to AlignTop.
            items_flat (bool, optional): Whether items are flat. Defaults to False.
            items_icon_style (QAccordionHeader.IndicatorStyle, optional): Icon style. Defaults to Arrow.
            items_icon_position (QAccordionHeader.IconPosition, optional): Icon position. Defaults to LeadingPosition.
            animation_enabled (bool, optional): Whether animations are enabled. Defaults to False.
            animation_duration (int, optional): Animation duration in ms. Defaults to 200.
            animation_easing (QEasingCurve.Type, optional): Animation easing curve. Defaults to InOutQuart.
        """
        super().__init__(parent)
        self._main_layout = QVBoxLayout(self)
        self._main_layout.setContentsMargins(0, 0, 0, 0)

        self._scroll = QScrollArea()
        self._scroll.setWidgetResizable(True)
        self._scroll.setFrameShape(QFrame.Shape.NoFrame)

        self._scroll_content = QWidget()
        self._scroll_layout = QVBoxLayout(self._scroll_content)
        self._scroll_layout.setAlignment(items_alignment)

        self._scroll.setWidget(self._scroll_content)
        self._main_layout.addWidget(self._scroll)

        self._active_section = None
        self._items = []

        # Animation settings (applied to new items)
        self._animation_enabled = animation_enabled
        self._animation_duration = animation_duration
        self._animation_easing = animation_easing

        self._items_alignment = items_alignment
        self._items_flat = items_flat
        self._items_icon_style = items_icon_style
        self._items_icon_position = items_icon_position
        self._auto_stretch = True

        self._setup_connections()

    def setAutoStretch(self, enabled: bool) -> None:
        """Sets whether expanded items should automatically stretch to fill available space.

        Args:
            enabled (bool): True to enable auto-stretch, False to disable.
        """
        self._auto_stretch = enabled
        for item in self._items:
            # Re-apply stretch logic based on current state
            self._scroll_layout.setStretchFactor(
                item, 1 if (self._auto_stretch and item.isExpanded()) else 0
            )
            self._update_item_alignment(item)

    def isAutoStretch(self) -> bool:
        """Returns whether auto-stretch is enabled.

        Returns:
            bool: True if enabled, False otherwise.
        """
        return self._auto_stretch

    def _setup_connections(self) -> None:
        """Sets up signals and slots connections."""
        self._scroll.verticalScrollBar().valueChanged.connect(self._on_scroll)

    @Slot(int)
    def _on_scroll(self, value: int) -> None:
        """Handles scroll value changes.

        Args:
            value (int): Current scroll value.
        """
        for item in self._items:
            if (
                item.y() <= value <= item.y() + item.height()
                and item != self._active_section
            ):
                if self._active_section:
                    self.leftSection.emit(item)
                self._active_section = item
                self.enteredSection.emit(item)
                break

    # --- Item Management ---

    def setSectionTitle(self, index: int, title: str) -> None:
        """Sets the title of the section at the given index.

        Args:
            index (int): Index of the section.
            title (str): New title for the section.
        """
        self._items[index].setTitle(title)

    def addSection(
        self, title: str, widget: QWidget, name: typing.Optional[str] = None
    ) -> QAccordionItem:
        """Creates and adds a new accordion section at the end.

        Args:
            title (str): Section title.
            widget (QWidget): Content widget.
            name (str, optional): Unique name for the section. Defaults to None.

        Returns:
            QAccordionItem: The created accordion item.
        """
        return self.insertSection(title, widget, name=name)

    def addAccordionItem(self, item: QAccordionItem) -> None:
        """Adds an existing accordion item at the end.

        Args:
            item (QAccordionItem): Accordion item to add.
        """
        self.insertAccordionItem(item)

    def insertSection(
        self,
        title: str,
        widget: QWidget,
        position: int = -1,
        expanded: bool = False,
        name: typing.Optional[str] = None,
    ) -> QAccordionItem:
        """Creates and inserts a new accordion section.

        Args:
            title (str): Section title.
            widget (QWidget): Content widget.
            position (int, optional): Insert position (-1 for end). Defaults to -1.
            expanded (bool, optional): Whether the section is expanded. Defaults to False.
            name (str, optional): Unique name for the section. Defaults to None.

        Returns:
            QAccordionItem: The created accordion item.
        """
        item = QAccordionItem(
            title,
            widget,
            self._scroll_content,
            expanded,
            self._items_flat,
            self._items_icon_style,
            self._items_icon_position,
            self._animation_enabled,
            self._animation_duration,
            self._animation_easing,
        )

        if name:
            item.setObjectName(name)

        self.insertAccordionItem(item, position)
        return item

    def insertAccordionItem(self, item: QAccordionItem, position: int = -1) -> None:
        """Inserts an existing accordion item.

        Args:
            item (QAccordionItem): Accordion item to insert.
            position (int, optional): Insert position (-1 for end). Defaults to -1.
        """
        self._scroll_layout.insertWidget(position, item)
        if position == -1:
            self._items.append(item)
        else:
            self._items.insert(position, item)

        self._update_item_alignment(item)

        item.expandedChanged.connect(
            lambda expanded: self._on_item_toggled(item, expanded)
        )

    def _on_item_toggled(self, item: QAccordionItem, expanded: bool) -> None:
        """Handles item toggle events.

        Args:
            item (QAccordionItem): The item that was toggled.
            expanded (bool): Whether the item is checked (expanded).
        """
        if self._auto_stretch:
            self._scroll_layout.setStretchFactor(item, 1 if expanded else 0)
            item.layout().update()
        self._update_item_alignment(item)

    def _update_item_alignment(self, item: QAccordionItem) -> None:
        """Updates the alignment for a single item based on its state."""
        if self._auto_stretch and item.isExpanded():
            # If expanded and stretching, remove alignment so it fills the space
            self._scroll_layout.setAlignment(item, Qt.Alignment())
        else:
            # Otherwise, respect the global alignment setting
            self._scroll_layout.setAlignment(item, self._items_alignment)

    def removeAccordionItem(self, item: QAccordionItem) -> None:
        """Removes an accordion item.

        Args:
            item (QAccordionItem): Accordion item to remove.
        """
        self._scroll_layout.removeWidget(item)
        self._items.remove(item)

    def item(self, name: str) -> Optional[QAccordionItem]:
        """Retrieves an accordion item by its name.

        Args:
            name (str): The name of the item to retrieve.

        Returns:
            Optional[QAccordionItem]: The item with the matching name, or None if not found.
        """
        for item in self._items:
            if item.objectName() == name:
                return item
        return None

    def items(self) -> typing.List[QAccordionItem]:
        """"""
        return self._items

    # --- Style Settings (Applied to ALL items) ---

    def setIconPosition(self, position: QAccordionHeader.IconPosition) -> None:
        """Changes the icon position of all items.

        Args:
            position (QAccordionHeader.IconPosition): New icon position.
        """
        self._items_icon_position = position
        for item in self._items:
            item.setIconPosition(position)

    def setIconStyle(self, style: QAccordionHeader.IndicatorStyle) -> None:
        """Changes the icon style of all items.

        Args:
            style (QAccordionHeader.IndicatorStyle): New icon style.
        """
        self._items_icon_style = style
        for item in self._items:
            item.setIconStyle(style)

    def setFlat(self, flat: bool) -> None:
        """Sets whether headers are flat or raised for all items.

        Args:
            flat (bool): True for flat headers, False for raised.
        """
        self._items_flat = flat
        for item in self._items:
            item.setFlat(flat)

    def setItemsAlignment(self, alignment: Qt.AlignmentFlag) -> None:
        """Sets the vertical alignment of the accordion items.

        Args:
            alignment (Qt.AlignmentFlag): The alignment (AlignTop, AlignVCenter, AlignBottom).
        """
        self._items_alignment = alignment
        self._scroll_layout.setAlignment(alignment)
        for item in self._items:
            self._update_item_alignment(item)
        self._scroll_layout.update()

    def itemsAlignment(self) -> Qt.AlignmentFlag:
        """Returns the current vertical alignment of the accordion items.

        Returns:
            Qt.AlignmentFlag: The current alignment.
        """
        return self._items_alignment

    # --- Animation Settings (Applied to ALL items) ---

    def setAnimationEnabled(self, enabled: bool) -> None:
        """Enables or disables animations for all items.

        Args:
            enabled (bool): True to enable animations, False to disable.
        """
        self._animation_enabled = enabled
        for item in self._items:
            item.setAnimationEnabled(enabled)

    def isAnimationEnabled(self) -> bool:
        """Checks if animations are enabled by default.

        Returns:
            bool: True if animations are enabled, False otherwise.
        """
        return self._animation_enabled

    def setAnimationDuration(self, duration: int) -> None:
        """Sets the animation duration in milliseconds for all items.

        Args:
            duration (int): Duration in milliseconds (typical: 100-500).
        """
        self._animation_duration = duration
        for item in self._items:
            item.setAnimationDuration(duration)

    def animationDuration(self) -> int:
        """Returns the default animation duration.

        Returns:
            int: Animation duration in milliseconds.
        """
        return self._animation_duration

    def setAnimationEasing(self, easing: QEasingCurve.Type) -> None:
        """Sets the animation easing curve for all items.

        Args:
            easing (QEasingCurve.Type): The easing curve type.
        """
        self._animation_easing = easing
        for item in self._items:
            item.setAnimationEasing(easing)

    def animationEasing(self) -> QEasingCurve.Type:
        """Returns the default animation easing curve.

        Returns:
            QEasingCurve.Type: The easing curve type.
        """
        return self._animation_easing

    # --- Expand/Collapse Operations ---

    def expandAll(self, animated: bool = False) -> None:
        """Expands all accordion items.

        Args:
            animated (bool, optional): Override animation setting. If None, uses each item's setting. Defaults to None.
        """
        for item in self._items:
            item.setExpanded(True, animated=animated)

    def collapseAll(self, animated: bool = False) -> None:
        """Collapses all accordion items.

        Args:
            animated (bool, optional): Override animation setting. If None, uses each item's setting. Defaults to None.
        """
        for item in self._items:
            item.setExpanded(False, animated=animated)

    # --- Scroll Operations ---

    def scrollToItem(self, target_item: QAccordionItem) -> None:
        """Scrolls to make the target item visible.

        Args:
            target_item (QAccordionItem): The item to scroll to.
        """
        # Gets the Y coordinate of the target widget relative to the ScrollArea content
        y_pos = target_item.y()
        # Sets the vertical scroll bar value
        self._scroll.verticalScrollBar().setValue(y_pos)

    def resetScroll(self) -> None:
        """Scrolls to the top of the accordion."""
        self._scroll.verticalScrollBar().setValue(0)

__init__(parent=None, items_alignment=Qt.AlignmentFlag.AlignTop, items_flat=False, items_icon_style=QAccordionHeader.IndicatorStyle.Arrow, items_icon_position=QAccordionHeader.IconPosition.LeadingPosition, animation_enabled=False, animation_duration=200, animation_easing=QEasingCurve.Type.InOutQuart)

Initializes the accordion widget.

Parameters:

Name Type Description Default
parent QWidget

Parent widget. Defaults to None.

None
items_alignment AlignmentFlag

Vertical alignment of items. Defaults to AlignTop.

AlignTop
items_flat bool

Whether items are flat. Defaults to False.

False
items_icon_style IndicatorStyle

Icon style. Defaults to Arrow.

Arrow
items_icon_position IconPosition

Icon position. Defaults to LeadingPosition.

LeadingPosition
animation_enabled bool

Whether animations are enabled. Defaults to False.

False
animation_duration int

Animation duration in ms. Defaults to 200.

200
animation_easing Type

Animation easing curve. Defaults to InOutQuart.

InOutQuart
Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion.py
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
def __init__(
    self,
    parent: typing.Optional[QWidget] = None,
    items_alignment: Qt.AlignmentFlag = Qt.AlignmentFlag.AlignTop,
    items_flat: bool = False,
    items_icon_style: QAccordionHeader.IndicatorStyle = QAccordionHeader.IndicatorStyle.Arrow,
    items_icon_position: QAccordionHeader.IconPosition = QAccordionHeader.IconPosition.LeadingPosition,
    animation_enabled: bool = False,
    animation_duration: int = 200,
    animation_easing: QEasingCurve.Type = QEasingCurve.Type.InOutQuart,
) -> None:
    """Initializes the accordion widget.

    Args:
        parent (QWidget, optional): Parent widget. Defaults to None.
        items_alignment (Qt.AlignmentFlag, optional): Vertical alignment of items. Defaults to AlignTop.
        items_flat (bool, optional): Whether items are flat. Defaults to False.
        items_icon_style (QAccordionHeader.IndicatorStyle, optional): Icon style. Defaults to Arrow.
        items_icon_position (QAccordionHeader.IconPosition, optional): Icon position. Defaults to LeadingPosition.
        animation_enabled (bool, optional): Whether animations are enabled. Defaults to False.
        animation_duration (int, optional): Animation duration in ms. Defaults to 200.
        animation_easing (QEasingCurve.Type, optional): Animation easing curve. Defaults to InOutQuart.
    """
    super().__init__(parent)
    self._main_layout = QVBoxLayout(self)
    self._main_layout.setContentsMargins(0, 0, 0, 0)

    self._scroll = QScrollArea()
    self._scroll.setWidgetResizable(True)
    self._scroll.setFrameShape(QFrame.Shape.NoFrame)

    self._scroll_content = QWidget()
    self._scroll_layout = QVBoxLayout(self._scroll_content)
    self._scroll_layout.setAlignment(items_alignment)

    self._scroll.setWidget(self._scroll_content)
    self._main_layout.addWidget(self._scroll)

    self._active_section = None
    self._items = []

    # Animation settings (applied to new items)
    self._animation_enabled = animation_enabled
    self._animation_duration = animation_duration
    self._animation_easing = animation_easing

    self._items_alignment = items_alignment
    self._items_flat = items_flat
    self._items_icon_style = items_icon_style
    self._items_icon_position = items_icon_position
    self._auto_stretch = True

    self._setup_connections()

addAccordionItem(item)

Adds an existing accordion item at the end.

Parameters:

Name Type Description Default
item QAccordionItem

Accordion item to add.

required
Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion.py
153
154
155
156
157
158
159
def addAccordionItem(self, item: QAccordionItem) -> None:
    """Adds an existing accordion item at the end.

    Args:
        item (QAccordionItem): Accordion item to add.
    """
    self.insertAccordionItem(item)

addSection(title, widget, name=None)

Creates and adds a new accordion section at the end.

Parameters:

Name Type Description Default
title str

Section title.

required
widget QWidget

Content widget.

required
name str

Unique name for the section. Defaults to None.

None

Returns:

Name Type Description
QAccordionItem QAccordionItem

The created accordion item.

Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion.py
138
139
140
141
142
143
144
145
146
147
148
149
150
151
def addSection(
    self, title: str, widget: QWidget, name: typing.Optional[str] = None
) -> QAccordionItem:
    """Creates and adds a new accordion section at the end.

    Args:
        title (str): Section title.
        widget (QWidget): Content widget.
        name (str, optional): Unique name for the section. Defaults to None.

    Returns:
        QAccordionItem: The created accordion item.
    """
    return self.insertSection(title, widget, name=name)

animationDuration()

Returns the default animation duration.

Returns:

Name Type Description
int int

Animation duration in milliseconds.

Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion.py
349
350
351
352
353
354
355
def animationDuration(self) -> int:
    """Returns the default animation duration.

    Returns:
        int: Animation duration in milliseconds.
    """
    return self._animation_duration

animationEasing()

Returns the default animation easing curve.

Returns:

Type Description
Type

QEasingCurve.Type: The easing curve type.

Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion.py
367
368
369
370
371
372
373
def animationEasing(self) -> QEasingCurve.Type:
    """Returns the default animation easing curve.

    Returns:
        QEasingCurve.Type: The easing curve type.
    """
    return self._animation_easing

collapseAll(animated=False)

Collapses all accordion items.

Parameters:

Name Type Description Default
animated bool

Override animation setting. If None, uses each item's setting. Defaults to None.

False
Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion.py
386
387
388
389
390
391
392
393
def collapseAll(self, animated: bool = False) -> None:
    """Collapses all accordion items.

    Args:
        animated (bool, optional): Override animation setting. If None, uses each item's setting. Defaults to None.
    """
    for item in self._items:
        item.setExpanded(False, animated=animated)

expandAll(animated=False)

Expands all accordion items.

Parameters:

Name Type Description Default
animated bool

Override animation setting. If None, uses each item's setting. Defaults to None.

False
Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion.py
377
378
379
380
381
382
383
384
def expandAll(self, animated: bool = False) -> None:
    """Expands all accordion items.

    Args:
        animated (bool, optional): Override animation setting. If None, uses each item's setting. Defaults to None.
    """
    for item in self._items:
        item.setExpanded(True, animated=animated)

insertAccordionItem(item, position=-1)

Inserts an existing accordion item.

Parameters:

Name Type Description Default
item QAccordionItem

Accordion item to insert.

required
position int

Insert position (-1 for end). Defaults to -1.

-1
Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion.py
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
def insertAccordionItem(self, item: QAccordionItem, position: int = -1) -> None:
    """Inserts an existing accordion item.

    Args:
        item (QAccordionItem): Accordion item to insert.
        position (int, optional): Insert position (-1 for end). Defaults to -1.
    """
    self._scroll_layout.insertWidget(position, item)
    if position == -1:
        self._items.append(item)
    else:
        self._items.insert(position, item)

    self._update_item_alignment(item)

    item.expandedChanged.connect(
        lambda expanded: self._on_item_toggled(item, expanded)
    )

insertSection(title, widget, position=-1, expanded=False, name=None)

Creates and inserts a new accordion section.

Parameters:

Name Type Description Default
title str

Section title.

required
widget QWidget

Content widget.

required
position int

Insert position (-1 for end). Defaults to -1.

-1
expanded bool

Whether the section is expanded. Defaults to False.

False
name str

Unique name for the section. Defaults to None.

None

Returns:

Name Type Description
QAccordionItem QAccordionItem

The created accordion item.

Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion.py
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
def insertSection(
    self,
    title: str,
    widget: QWidget,
    position: int = -1,
    expanded: bool = False,
    name: typing.Optional[str] = None,
) -> QAccordionItem:
    """Creates and inserts a new accordion section.

    Args:
        title (str): Section title.
        widget (QWidget): Content widget.
        position (int, optional): Insert position (-1 for end). Defaults to -1.
        expanded (bool, optional): Whether the section is expanded. Defaults to False.
        name (str, optional): Unique name for the section. Defaults to None.

    Returns:
        QAccordionItem: The created accordion item.
    """
    item = QAccordionItem(
        title,
        widget,
        self._scroll_content,
        expanded,
        self._items_flat,
        self._items_icon_style,
        self._items_icon_position,
        self._animation_enabled,
        self._animation_duration,
        self._animation_easing,
    )

    if name:
        item.setObjectName(name)

    self.insertAccordionItem(item, position)
    return item

isAnimationEnabled()

Checks if animations are enabled by default.

Returns:

Name Type Description
bool bool

True if animations are enabled, False otherwise.

Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion.py
331
332
333
334
335
336
337
def isAnimationEnabled(self) -> bool:
    """Checks if animations are enabled by default.

    Returns:
        bool: True if animations are enabled, False otherwise.
    """
    return self._animation_enabled

isAutoStretch()

Returns whether auto-stretch is enabled.

Returns:

Name Type Description
bool bool

True if enabled, False otherwise.

Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion.py
 97
 98
 99
100
101
102
103
def isAutoStretch(self) -> bool:
    """Returns whether auto-stretch is enabled.

    Returns:
        bool: True if enabled, False otherwise.
    """
    return self._auto_stretch

item(name)

Retrieves an accordion item by its name.

Parameters:

Name Type Description Default
name str

The name of the item to retrieve.

required

Returns:

Type Description
Optional[QAccordionItem]

Optional[QAccordionItem]: The item with the matching name, or None if not found.

Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion.py
249
250
251
252
253
254
255
256
257
258
259
260
261
def item(self, name: str) -> Optional[QAccordionItem]:
    """Retrieves an accordion item by its name.

    Args:
        name (str): The name of the item to retrieve.

    Returns:
        Optional[QAccordionItem]: The item with the matching name, or None if not found.
    """
    for item in self._items:
        if item.objectName() == name:
            return item
    return None

items()

Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion.py
263
264
265
def items(self) -> typing.List[QAccordionItem]:
    """"""
    return self._items

itemsAlignment()

Returns the current vertical alignment of the accordion items.

Returns:

Type Description
AlignmentFlag

Qt.AlignmentFlag: The current alignment.

Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion.py
311
312
313
314
315
316
317
def itemsAlignment(self) -> Qt.AlignmentFlag:
    """Returns the current vertical alignment of the accordion items.

    Returns:
        Qt.AlignmentFlag: The current alignment.
    """
    return self._items_alignment

removeAccordionItem(item)

Removes an accordion item.

Parameters:

Name Type Description Default
item QAccordionItem

Accordion item to remove.

required
Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion.py
240
241
242
243
244
245
246
247
def removeAccordionItem(self, item: QAccordionItem) -> None:
    """Removes an accordion item.

    Args:
        item (QAccordionItem): Accordion item to remove.
    """
    self._scroll_layout.removeWidget(item)
    self._items.remove(item)

resetScroll()

Scrolls to the top of the accordion.

Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion.py
408
409
410
def resetScroll(self) -> None:
    """Scrolls to the top of the accordion."""
    self._scroll.verticalScrollBar().setValue(0)

scrollToItem(target_item)

Scrolls to make the target item visible.

Parameters:

Name Type Description Default
target_item QAccordionItem

The item to scroll to.

required
Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion.py
397
398
399
400
401
402
403
404
405
406
def scrollToItem(self, target_item: QAccordionItem) -> None:
    """Scrolls to make the target item visible.

    Args:
        target_item (QAccordionItem): The item to scroll to.
    """
    # Gets the Y coordinate of the target widget relative to the ScrollArea content
    y_pos = target_item.y()
    # Sets the vertical scroll bar value
    self._scroll.verticalScrollBar().setValue(y_pos)

setAnimationDuration(duration)

Sets the animation duration in milliseconds for all items.

Parameters:

Name Type Description Default
duration int

Duration in milliseconds (typical: 100-500).

required
Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion.py
339
340
341
342
343
344
345
346
347
def setAnimationDuration(self, duration: int) -> None:
    """Sets the animation duration in milliseconds for all items.

    Args:
        duration (int): Duration in milliseconds (typical: 100-500).
    """
    self._animation_duration = duration
    for item in self._items:
        item.setAnimationDuration(duration)

setAnimationEasing(easing)

Sets the animation easing curve for all items.

Parameters:

Name Type Description Default
easing Type

The easing curve type.

required
Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion.py
357
358
359
360
361
362
363
364
365
def setAnimationEasing(self, easing: QEasingCurve.Type) -> None:
    """Sets the animation easing curve for all items.

    Args:
        easing (QEasingCurve.Type): The easing curve type.
    """
    self._animation_easing = easing
    for item in self._items:
        item.setAnimationEasing(easing)

setAnimationEnabled(enabled)

Enables or disables animations for all items.

Parameters:

Name Type Description Default
enabled bool

True to enable animations, False to disable.

required
Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion.py
321
322
323
324
325
326
327
328
329
def setAnimationEnabled(self, enabled: bool) -> None:
    """Enables or disables animations for all items.

    Args:
        enabled (bool): True to enable animations, False to disable.
    """
    self._animation_enabled = enabled
    for item in self._items:
        item.setAnimationEnabled(enabled)

setAutoStretch(enabled)

Sets whether expanded items should automatically stretch to fill available space.

Parameters:

Name Type Description Default
enabled bool

True to enable auto-stretch, False to disable.

required
Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion.py
83
84
85
86
87
88
89
90
91
92
93
94
95
def setAutoStretch(self, enabled: bool) -> None:
    """Sets whether expanded items should automatically stretch to fill available space.

    Args:
        enabled (bool): True to enable auto-stretch, False to disable.
    """
    self._auto_stretch = enabled
    for item in self._items:
        # Re-apply stretch logic based on current state
        self._scroll_layout.setStretchFactor(
            item, 1 if (self._auto_stretch and item.isExpanded()) else 0
        )
        self._update_item_alignment(item)

setFlat(flat)

Sets whether headers are flat or raised for all items.

Parameters:

Name Type Description Default
flat bool

True for flat headers, False for raised.

required
Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion.py
289
290
291
292
293
294
295
296
297
def setFlat(self, flat: bool) -> None:
    """Sets whether headers are flat or raised for all items.

    Args:
        flat (bool): True for flat headers, False for raised.
    """
    self._items_flat = flat
    for item in self._items:
        item.setFlat(flat)

setIconPosition(position)

Changes the icon position of all items.

Parameters:

Name Type Description Default
position IconPosition

New icon position.

required
Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion.py
269
270
271
272
273
274
275
276
277
def setIconPosition(self, position: QAccordionHeader.IconPosition) -> None:
    """Changes the icon position of all items.

    Args:
        position (QAccordionHeader.IconPosition): New icon position.
    """
    self._items_icon_position = position
    for item in self._items:
        item.setIconPosition(position)

setIconStyle(style)

Changes the icon style of all items.

Parameters:

Name Type Description Default
style IndicatorStyle

New icon style.

required
Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion.py
279
280
281
282
283
284
285
286
287
def setIconStyle(self, style: QAccordionHeader.IndicatorStyle) -> None:
    """Changes the icon style of all items.

    Args:
        style (QAccordionHeader.IndicatorStyle): New icon style.
    """
    self._items_icon_style = style
    for item in self._items:
        item.setIconStyle(style)

setItemsAlignment(alignment)

Sets the vertical alignment of the accordion items.

Parameters:

Name Type Description Default
alignment AlignmentFlag

The alignment (AlignTop, AlignVCenter, AlignBottom).

required
Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion.py
299
300
301
302
303
304
305
306
307
308
309
def setItemsAlignment(self, alignment: Qt.AlignmentFlag) -> None:
    """Sets the vertical alignment of the accordion items.

    Args:
        alignment (Qt.AlignmentFlag): The alignment (AlignTop, AlignVCenter, AlignBottom).
    """
    self._items_alignment = alignment
    self._scroll_layout.setAlignment(alignment)
    for item in self._items:
        self._update_item_alignment(item)
    self._scroll_layout.update()

setSectionTitle(index, title)

Sets the title of the section at the given index.

Parameters:

Name Type Description Default
index int

Index of the section.

required
title str

New title for the section.

required
Source code in source/qextrawidgets/widgets/miscellaneous/accordion/accordion.py
129
130
131
132
133
134
135
136
def setSectionTitle(self, index: int, title: str) -> None:
    """Sets the title of the section at the given index.

    Args:
        index (int): Index of the section.
        title (str): New title for the section.
    """
    self._items[index].setTitle(title)

QDualList

Bases: QWidget

Base class containing layout structure and business logic for a dual list selection widget.

Instantiates widgets via factory methods (create*) to allow visual customization in child classes.

Signals

selectionChanged (list): Emitted when the selected items change.

Source code in source/qextrawidgets/widgets/miscellaneous/dual_list.py
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
class QDualList(QWidget):
    """Base class containing layout structure and business logic for a dual list selection widget.

    Instantiates widgets via factory methods (_create_*) to allow visual customization in child classes.

    Signals:
        selectionChanged (list): Emitted when the selected items change.
    """

    # Public signal
    selectionChanged = Signal(list)

    def __init__(self, parent: typing.Optional[QWidget] = None) -> None:
        """Initializes the dual list widget.

        Args:
            parent (QWidget, optional): Parent widget. Defaults to None.
        """
        super().__init__(parent)

        # --- 1. Interface Construction (Directly in __init__) ---

        main_layout = QHBoxLayout(self)

        # A. Left Side (Available)
        # Creates the container using the factory method
        self._available_container = self._create_container(self.tr("Available"))
        # The container layout depends on the returned container type
        container_layout_l = self._available_container.layout()
        if not container_layout_l:
            container_layout_l = QVBoxLayout(self._available_container)

        self._search_input = self._create_search_input()
        self._list_available = self._create_list_widget()

        container_layout_l.addWidget(self._search_input)
        container_layout_l.addWidget(self._list_available)

        # B. Center (Buttons)
        buttons_layout = QVBoxLayout()
        buttons_layout.addStretch()

        self._btn_move_all_right = self._create_button(QThemeResponsiveIcon.fromAwesome("fa6s.angles-right"))
        self._btn_move_right = self._create_button(QThemeResponsiveIcon.fromAwesome("fa6s.angle-right"))
        self._btn_move_left = self._create_button(QThemeResponsiveIcon.fromAwesome("fa6s.angle-left"))
        self._btn_move_all_left = self._create_button(QThemeResponsiveIcon.fromAwesome("fa6s.angles-left"))

        buttons_layout.addWidget(self._btn_move_all_right)
        buttons_layout.addWidget(self._btn_move_right)
        buttons_layout.addWidget(self._btn_move_left)
        buttons_layout.addWidget(self._btn_move_all_left)
        buttons_layout.addStretch()

        # C. Right Side (Selected)
        self._selected_container = self._create_container(self.tr("Selected"))
        container_layout_r = self._selected_container.layout()
        if not container_layout_r:
            container_layout_r = QVBoxLayout(self._selected_container)

        self._list_selected = self._create_list_widget()
        self._lbl_count = self._create_label(self.tr("0 items"))

        container_layout_r.addWidget(self._list_selected)
        container_layout_r.addWidget(self._lbl_count)

        # D. Final Composition
        main_layout.addWidget(self._available_container)
        main_layout.addLayout(buttons_layout)
        main_layout.addWidget(self._selected_container)

        # --- 2. Connections Setup ---
        self._setup_connections()

    # --- Factory Methods (Extension points for child class) ---

    @staticmethod
    def _create_container(title: str) -> QWidget:
        """Creates the default container (QGroupBox).

        Args:
            title (str): Container title.

        Returns:
            QWidget: The created container widget.
        """
        box = QGroupBox(title)
        return box

    @staticmethod
    def _create_list_widget() -> QListWidget:
        """Creates the default list widget (QListWidget).

        Returns:
            QListWidget: The created list widget.
        """
        list_widget = QListWidget()
        list_widget.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
        list_widget.setDragEnabled(True)
        list_widget.setAcceptDrops(True)
        list_widget.setDragDropMode(QAbstractItemView.DragDropMode.DragDrop)
        list_widget.setDefaultDropAction(Qt.DropAction.MoveAction)
        list_widget.setAlternatingRowColors(True)
        return list_widget

    @staticmethod
    def _create_button(icon: QIcon) -> QPushButton:
        """Creates a default action button.

        Args:
            icon (QIcon): Button icon.

        Returns:
            QPushButton: The created button.
        """
        btn = QPushButton()
        btn.setIcon(icon)
        return btn

    def _create_search_input(self) -> QLineEdit:
        """Creates a default search input.

        Returns:
            QLineEdit: The created search input.
        """
        line = QLineEdit()
        line.setPlaceholderText(self.tr("Filter..."))
        return line

    @staticmethod
    def _create_label(text: str) -> QLabel:
        """Creates a default label.

        Args:
            text (str): Label text.

        Returns:
            QLabel: The created label.
        """
        lbl = QLabel(text)
        lbl.setAlignment(Qt.AlignmentFlag.AlignRight)
        return lbl

    # --- Internal Logic (snake_case) ---

    def _setup_connections(self) -> None:
        """Sets up signals and slots connections."""
        # Buttons
        self._btn_move_right.clicked.connect(lambda: self._move_items(self._list_available, self._list_selected))
        self._btn_move_left.clicked.connect(lambda: self._move_items(self._list_selected, self._list_available))
        self._btn_move_all_right.clicked.connect(
            lambda: self._move_all_items(self._list_available, self._list_selected))
        self._btn_move_all_left.clicked.connect(lambda: self._move_all_items(self._list_selected, self._list_available))

        # Double Click
        self._list_available.itemDoubleClicked.connect(
            lambda: self._move_items(self._list_available, self._list_selected))
        self._list_selected.itemDoubleClicked.connect(
            lambda: self._move_items(self._list_selected, self._list_available))

        # Filter
        self._search_input.textChanged.connect(self._filter_available_items)

        # Monitoring
        self._list_selected.model().rowsInserted.connect(self._update_internal_count)
        self._list_selected.model().rowsRemoved.connect(self._update_internal_count)

    def _move_items(self, source_list: QListWidget, dest_list: QListWidget) -> None:
        """Moves selected items from source list to destination list.

        Args:
            source_list (QListWidget): List to move items from.
            dest_list (QListWidget): List to move items to.
        """
        items = source_list.selectedItems()
        for item in items:
            source_list.takeItem(source_list.row(item))
            dest_list.addItem(item)
        dest_list.sortItems()
        self._update_internal_count()

    def _move_all_items(self, source_list: QListWidget, dest_list: QListWidget) -> None:
        """Moves all non-hidden items from source list to destination list.

        Args:
            source_list (QListWidget): List to move items from.
            dest_list (QListWidget): List to move items to.
        """
        for i in range(source_list.count() - 1, -1, -1):
            item = source_list.item(i)
            if not item.isHidden():
                source_list.takeItem(i)
                dest_list.addItem(item)
        dest_list.sortItems()
        self._update_internal_count()

    def _filter_available_items(self, text: str) -> None:
        """Filters items in the available list based on text.

        Args:
            text (str): Filter text.
        """
        count = self._list_available.count()
        for i in range(count):
            item = self._list_available.item(i)
            item.setHidden(text.lower() not in item.text().lower())

    @Slot()
    def _update_internal_count(self) -> None:
        """Updates the selected items count and emits selectionChanged signal."""
        count = self._list_selected.count()
        self._lbl_count.setText(self.tr("{} items").format(count))
        current_data = [self._list_selected.item(i).text() for i in range(count)]
        self.selectionChanged.emit(current_data)

    # --- Public API (camelCase) ---

    def setAvailableItems(self, items: typing.List[str]) -> None:
        """Sets the list of available items.

        Args:
            items (List[str]): List of strings to display in available list.
        """
        self._list_available.clear()
        self._list_selected.clear()
        self._list_available.addItems(items)
        self._list_available.sortItems()
        self._update_internal_count()

    def getSelectedItems(self) -> typing.List[str]:
        """Returns the list of currently selected items.

        Returns:
            List[str]: List of selected strings.
        """
        return [self._list_selected.item(i).text() for i in range(self._list_selected.count())]

    def setSelectedItems(self, items: typing.List[str]) -> None:
        """Sets the list of selected items.

        Args:
            items (List[str]): List of strings to display in selected list.
        """
        self._list_selected.clear()
        self._list_selected.addItems(items)
        self._update_internal_count()

__init__(parent=None)

Initializes the dual list widget.

Parameters:

Name Type Description Default
parent QWidget

Parent widget. Defaults to None.

None
Source code in source/qextrawidgets/widgets/miscellaneous/dual_list.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
def __init__(self, parent: typing.Optional[QWidget] = None) -> None:
    """Initializes the dual list widget.

    Args:
        parent (QWidget, optional): Parent widget. Defaults to None.
    """
    super().__init__(parent)

    # --- 1. Interface Construction (Directly in __init__) ---

    main_layout = QHBoxLayout(self)

    # A. Left Side (Available)
    # Creates the container using the factory method
    self._available_container = self._create_container(self.tr("Available"))
    # The container layout depends on the returned container type
    container_layout_l = self._available_container.layout()
    if not container_layout_l:
        container_layout_l = QVBoxLayout(self._available_container)

    self._search_input = self._create_search_input()
    self._list_available = self._create_list_widget()

    container_layout_l.addWidget(self._search_input)
    container_layout_l.addWidget(self._list_available)

    # B. Center (Buttons)
    buttons_layout = QVBoxLayout()
    buttons_layout.addStretch()

    self._btn_move_all_right = self._create_button(QThemeResponsiveIcon.fromAwesome("fa6s.angles-right"))
    self._btn_move_right = self._create_button(QThemeResponsiveIcon.fromAwesome("fa6s.angle-right"))
    self._btn_move_left = self._create_button(QThemeResponsiveIcon.fromAwesome("fa6s.angle-left"))
    self._btn_move_all_left = self._create_button(QThemeResponsiveIcon.fromAwesome("fa6s.angles-left"))

    buttons_layout.addWidget(self._btn_move_all_right)
    buttons_layout.addWidget(self._btn_move_right)
    buttons_layout.addWidget(self._btn_move_left)
    buttons_layout.addWidget(self._btn_move_all_left)
    buttons_layout.addStretch()

    # C. Right Side (Selected)
    self._selected_container = self._create_container(self.tr("Selected"))
    container_layout_r = self._selected_container.layout()
    if not container_layout_r:
        container_layout_r = QVBoxLayout(self._selected_container)

    self._list_selected = self._create_list_widget()
    self._lbl_count = self._create_label(self.tr("0 items"))

    container_layout_r.addWidget(self._list_selected)
    container_layout_r.addWidget(self._lbl_count)

    # D. Final Composition
    main_layout.addWidget(self._available_container)
    main_layout.addLayout(buttons_layout)
    main_layout.addWidget(self._selected_container)

    # --- 2. Connections Setup ---
    self._setup_connections()

getSelectedItems()

Returns the list of currently selected items.

Returns:

Type Description
List[str]

List[str]: List of selected strings.

Source code in source/qextrawidgets/widgets/miscellaneous/dual_list.py
239
240
241
242
243
244
245
def getSelectedItems(self) -> typing.List[str]:
    """Returns the list of currently selected items.

    Returns:
        List[str]: List of selected strings.
    """
    return [self._list_selected.item(i).text() for i in range(self._list_selected.count())]

setAvailableItems(items)

Sets the list of available items.

Parameters:

Name Type Description Default
items List[str]

List of strings to display in available list.

required
Source code in source/qextrawidgets/widgets/miscellaneous/dual_list.py
227
228
229
230
231
232
233
234
235
236
237
def setAvailableItems(self, items: typing.List[str]) -> None:
    """Sets the list of available items.

    Args:
        items (List[str]): List of strings to display in available list.
    """
    self._list_available.clear()
    self._list_selected.clear()
    self._list_available.addItems(items)
    self._list_available.sortItems()
    self._update_internal_count()

setSelectedItems(items)

Sets the list of selected items.

Parameters:

Name Type Description Default
items List[str]

List of strings to display in selected list.

required
Source code in source/qextrawidgets/widgets/miscellaneous/dual_list.py
247
248
249
250
251
252
253
254
255
def setSelectedItems(self, items: typing.List[str]) -> None:
    """Sets the list of selected items.

    Args:
        items (List[str]): List of strings to display in selected list.
    """
    self._list_selected.clear()
    self._list_selected.addItems(items)
    self._update_internal_count()

QEmojiPicker

Bases: QWidget

A comprehensive emoji picker widget.

Features categories, search, skin tone selection, and recent/favorite emojis.

Signals

picked (str): Emitted when an emoji is selected.

Source code in source/qextrawidgets/widgets/miscellaneous/emoji_picker.py
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
class QEmojiPicker(QWidget):
    """A comprehensive emoji picker widget.

    Features categories, search, skin tone selection, and recent/favorite emojis.

    Signals:
        picked (str): Emitted when an emoji is selected.
    """

    picked = Signal(QEmojiItem)

    def __init__(
        self,
        model: typing.Optional[QEmojiPickerModel] = None,
        emoji_pixmap_getter: typing.Union[
            str, QFont, typing.Callable[[str], QPixmap]
        ] = partial(QTwemojiImageProvider.getPixmap, margin=0, size=128),
        emoji_label_size: QSize = QSize(32, 32),
    ) -> None:
        """Initializes the emoji picker.

        Args:
            model (QEmojiPickerModel, optional): Custom emoji model. Defaults to None.
            emoji_pixmap_getter (Union[str, QFont, Callable[[str], QPixmap]], optional):
                Method or font to generate emoji pixmaps. Defaults to EmojiImageProvider.getPixmap.
            emoji_label_size (QSize, optional): Size of the preview emoji label. Defaults to QSize(32, 32).
        """
        super().__init__()

        self._skin_tone_selector_emojis = {
            EmojiSkinTone.Default: "👏",
            EmojiSkinTone.Light: "👏🏻",
            EmojiSkinTone.MediumLight: "👏🏼",
            EmojiSkinTone.Medium: "👏🏽",
            EmojiSkinTone.MediumDark: "👏🏾",
            EmojiSkinTone.Dark: "👏🏿",
        }

        if model:
            self._model = model
        else:
            self._model = QEmojiPickerModel()

        self._proxy = QEmojiPickerProxyModel()
        self._proxy.setSourceModel(self._model)

        self._search_line_edit = self._create_search_line_edit()

        self._grouped_icon_view = QGroupedIconView(self, QSize(40, 40), 5)
        self._grouped_icon_view.setContextMenuPolicy(
            Qt.ContextMenuPolicy.CustomContextMenu
        )
        self._grouped_icon_view.setSelectionMode(
            QAbstractItemView.SelectionMode.NoSelection
        )
        self._grouped_icon_view.setModel(self._proxy)

        self._search_timer = QTimer(self)
        self._search_timer.setSingleShot(True)
        self._search_timer.setInterval(200)

        self._skin_tone_selector = QIconComboBox()

        for skin_tone, emoji in self._skin_tone_selector_emojis.items():
            self._skin_tone_selector.addItem(text=emoji, data=skin_tone)

        self._shortcuts_container = QWidget()
        self._shortcuts_container.setFixedHeight(40)  # Fixed height for the bar

        self._shortcuts_group = QButtonGroup(self)
        self._shortcuts_group.setExclusive(True)

        self._emoji_on_label = None

        self._emoji_label = QLabel()
        self._emoji_label.setFixedSize(emoji_label_size)
        self._emoji_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self._emoji_label.setScaledContents(True)

        self._aliases_emoji_label = self._create_emoji_label()

        self._setup_layout()
        self._setup_connections()

        if model is None:
            self._model.populate()
        else:
            for item in self._model.categories():
                self._on_categories_inserted(item)

        self._emoji_pixmap_getter: typing.Callable[[str], QPixmap]
        self.setEmojiPixmapGetter(emoji_pixmap_getter)
        self.setContentsMargins(5, 5, 5, 5)

        self.translateUI()

    def _setup_layout(self) -> None:
        """Sets up the initial layout of the widget."""
        self._shortcuts_layout = QHBoxLayout(self._shortcuts_container)
        self._shortcuts_layout.setContentsMargins(5, 0, 5, 0)
        self._shortcuts_layout.setSpacing(2)

        header_layout = QHBoxLayout()
        header_layout.addWidget(self._search_line_edit, True)
        header_layout.addWidget(self._skin_tone_selector)

        content_layout = QHBoxLayout()
        content_layout.addWidget(self._emoji_label)
        content_layout.addWidget(self._aliases_emoji_label, True)

        main_layout = QVBoxLayout(self)
        main_layout.setContentsMargins(0, 0, 0, 0)
        main_layout.addLayout(header_layout)
        main_layout.addWidget(self._shortcuts_container)
        main_layout.addWidget(self._grouped_icon_view)
        main_layout.addLayout(content_layout)

    def _setup_connections(self) -> None:
        """Sets up signals and slots connections."""
        self._search_timer.timeout.connect(self._on_filter_emojis)
        self._search_line_edit.textChanged.connect(lambda: self._search_timer.start())

        self._model.categoryInserted.connect(self._on_categories_inserted)
        self._model.categoryRemoved.connect(self._on_categories_removed)

        self._grouped_icon_view.itemEntered.connect(self._on_mouse_entered_emoji)
        self._grouped_icon_view.itemExited.connect(self._on_mouse_exited_emoji)
        self._grouped_icon_view.itemClicked.connect(self._on_item_clicked)
        self._grouped_icon_view.customContextMenuRequested.connect(
            self._on_context_menu
        )

        self._skin_tone_selector.currentDataChanged.connect(self._on_set_skin_tone)

        delegate: QGroupedIconDelegate = self._grouped_icon_view.itemDelegate()
        delegate.requestImage.connect(self._on_request_image)

        self._model.skinToneChanged.connect(self._on_skin_tone_changed)

    @Slot(QModelIndex)
    def _on_skin_tone_changed(self, source_index: QModelIndex) -> None:
        """Handles skin tone changes from the model.

        Args:
            source_index (QModelIndex): The index in the source model that changed.
        """
        proxy_index = self._proxy.mapFromSource(source_index)
        delegate: QGroupedIconDelegate = self._grouped_icon_view.itemDelegate()
        delegate.forceReload(proxy_index)

    @Slot(str)
    def _on_set_skin_tone(self, skin_tone: str) -> None:
        """Updates the skin tone of the emojis.

        Args:
            skin_tone (str): Skin tone modifier.
        """
        self._model.setSkinTone(skin_tone)

    @Slot(QModelIndex)
    def _on_item_clicked(self, proxy_index: QModelIndex) -> None:
        """Handles clicks on emoji items.

        Args:
            proxy_index (QModelIndex): The index in the proxy model that was clicked.
        """
        source_index = self._proxy.mapToSource(proxy_index)
        item = self._model.itemFromIndex(source_index)

        if not isinstance(item, QEmojiItem):
            return

        self.picked.emit(item)

        recent_category_item = self._model.findCategory(EmojiCategory.Recents)

        if recent_category_item:
            self._model.addEmoji(EmojiCategory.Recents, item.clone())

    @Slot(QPoint)
    def _on_context_menu(self, position: QPoint) -> None:
        """Handles the context menu for an emoji.

        Args:
            position (QPoint): Pixel position where the context menu was requested.
        """
        proxy_index = self._grouped_icon_view.indexAt(position)
        source_index = self._proxy.mapToSource(proxy_index)
        item = self._model.itemFromIndex(source_index)

        menu = QMenu(self._grouped_icon_view)

        if isinstance(item, QEmojiCategoryItem):
            collapse_all_action = menu.addAction(self.tr("Collapse all"))
            collapse_all_action.triggered.connect(self._grouped_icon_view.collapseAll)
            expand_all_action = menu.addAction(self.tr("Expand all"))
            expand_all_action.triggered.connect(self._grouped_icon_view.expandAll)

        elif isinstance(item, QEmojiItem):
            emoji_char = item.data(QEmojiItem.QEmojiDataRole.EmojiRole)

            # Check if emoji exists in favorites using helper method
            favorite_item = self._model.findEmojiInCategoryByName(
                EmojiCategory.Favorites, emoji_char
            )

            if favorite_item:
                action = menu.addAction(self.tr("Unfavorite"))
                action.triggered.connect(
                    lambda: self._model.removeEmoji(EmojiCategory.Favorites, emoji_char)
                )
            else:
                action = menu.addAction(self.tr("Favorite"))
                # We use item.emojiChar() here because addEmoji expects an EmojiChar object
                action.triggered.connect(
                    lambda: self._model.addEmoji(EmojiCategory.Favorites, item.clone())
                )

            copy_alias_action = menu.addAction(self.tr("Copy alias"))
            clipboard = QApplication.clipboard()
            alias = item.firstAlias()
            copy_alias_action.triggered.connect(lambda: clipboard.setText(alias))
        else:
            return

        menu.exec(self._grouped_icon_view.mapToGlobal(position))

    @Slot(QPersistentModelIndex)
    def _on_request_image(self, persistent_index: QPersistentModelIndex) -> None:
        """Loads the emoji image when requested by the delegate.

        Args:
            persistent_index (QPersistentModelIndex): The persistent index of the item needing an image.
        """
        if not persistent_index.isValid():
            return

        # 1. Explicitly convert to QModelIndex
        # Note: QModelIndex constructor does not accept QPersistentModelIndex directly in PySide6
        proxy_index = persistent_index.model().index(
            persistent_index.row(), persistent_index.column(), persistent_index.parent()
        )

        if not proxy_index.isValid():
            return

        # 2. Map from Proxy to Source Model
        source_index = self._proxy.mapToSource(proxy_index)

        if not source_index.isValid():
            return

        # 3. Fetch the item and set the image
        item = self._model.itemFromIndex(source_index)
        if isinstance(item, QEmojiItem):
            # Generate the pixmap
            pixmap = self.emojiPixmapGetter()(item.emoji())

            # Debug: Ensure the pixmap was generated
            if pixmap.isNull():
                print(f"ALERT: Null pixmap generated for {item.emoji()}")

            # Set the icon (This triggers dataChanged in model -> proxy -> view)
            item.setIcon(pixmap)

    @Slot(QEmojiCategoryItem)
    def _on_categories_inserted(self, category_item: QEmojiCategoryItem) -> None:
        """Handles the insertion of categories into the model.

        Args:
            category_item (QEmojiCategoryItem): The inserted category item.
        """
        category = category_item.text()
        icon = category_item.icon()

        shortcut = self._create_shortcut_button(category, icon)
        shortcut.setObjectName(category)
        shortcut.clicked.connect(
            lambda: self._on_shortcut_clicked(category_item.index())
        )

        self._shortcuts_layout.addWidget(shortcut)
        self._shortcuts_group.addButton(shortcut)

    @Slot(QEmojiCategoryItem)
    def _on_categories_removed(self, category_item: QEmojiCategoryItem) -> None:
        category = category_item.category()
        button = self._shortcuts_container.findChild(QToolButton, category)

        if button:
            self._shortcuts_layout.removeWidget(button)
            self._shortcuts_group.removeButton(button)
            button.deleteLater()

    @Slot(QModelIndex)
    def _on_mouse_entered_emoji(self, index: QModelIndex) -> None:
        """Handles mouse entry events on emoji items to show preview.

        Args:
            index (QModelIndex): The index of the item under the mouse.
        """
        source_index = self._proxy.mapToSource(index)
        item = self._model.itemFromIndex(source_index)
        if isinstance(item, QEmojiItem):
            self._emoji_on_label = item.emoji()
            self._paint_emoji_on_label()
            metrics = QFontMetrics(self._aliases_emoji_label.font())
            aliases_text = item.aliasesText()
            elided_alias = metrics.elidedText(
                aliases_text,
                Qt.TextElideMode.ElideRight,
                self._aliases_emoji_label.width(),
            )
            self._aliases_emoji_label.setText(elided_alias)

    @Slot(QModelIndex)
    def _on_shortcut_clicked(self, source_index: QModelIndex) -> None:
        """Scrolls the view to the selected category section.

        Args:
            source_index (QModelIndex): The index of the category in the source model.
        """
        proxy_index = self._proxy.mapFromSource(source_index)
        self._grouped_icon_view.scrollTo(proxy_index)
        self._grouped_icon_view.setExpanded(proxy_index, True)

    @Slot()
    def _on_filter_emojis(self) -> None:
        """Filters the emojis across all categories based on the search text."""
        text = self._search_line_edit.text()
        self._proxy.setFilterFixedString(text)

    @Slot()
    def _on_mouse_exited_emoji(self) -> None:
        """Clears the emoji preview area."""
        self._emoji_label.clear()
        self._aliases_emoji_label.clear()
        self._emoji_on_label = None

    @staticmethod
    def _create_search_line_edit() -> QLineEdit:
        """Creates and configures a search line edit.

        Returns:
            QLineEdit: The configured search line edit.
        """
        font = QFont()
        font.setPointSize(12)
        line_edit = QSearchLineEdit()
        line_edit.setFont(font)
        return line_edit

    @staticmethod
    def _create_emoji_label() -> QLabel:
        """Creates and configures the emoji alias label.

        Returns:
            QLabel: The configured alias label.
        """
        font = QFont()
        font.setBold(True)
        font.setPointSize(13)
        label = QLabel()
        label.setFont(font)
        return label

    @staticmethod
    def _create_shortcut_button(text: str, icon: QIcon) -> QToolButton:
        """Creates a shortcut button for the category bar.

        Args:
            text (str): Tooltip text.
            icon (QIcon): Button icon.

        Returns:
            QToolButton: The configured shortcut button.
        """
        btn = QToolButton()
        btn.setCheckable(True)
        btn.setAutoRaise(True)
        btn.setFixedSize(32, 32)
        btn.setIconSize(QSize(22, 22))
        btn.setToolTip(text)
        btn.setText(text)
        btn.setIcon(icon)
        return btn

    def _paint_emoji_on_label(self) -> None:
        """Updates the preview label with the current emoji pixmap."""
        if self._emoji_on_label:
            pixmap = self.emojiPixmapGetter()(self._emoji_on_label)
            self._emoji_label.setPixmap(pixmap)

    def _paint_skintones(self) -> None:
        """Updates the skin tone selector icons."""
        emoji_pixmap_getter = self.emojiPixmapGetter()
        for index, emoji in enumerate(self._skin_tone_selector_emojis.values()):
            self._skin_tone_selector.setItemIcon(index, emoji_pixmap_getter(emoji))

    # --- Public API (camelCase) ---

    def translateUI(self) -> None:
        """Translates the UI components."""
        self._search_line_edit.setPlaceholderText(self.tr("Search emoji..."))

    def resetPicker(self) -> None:
        """Resets the picker state."""
        self._search_line_edit.clear()

    def setEmojiPixmapGetter(
        self,
        emoji_pixmap_getter: typing.Union[str, QFont, typing.Callable[[str], QPixmap]],
    ) -> None:
        """Sets the strategy for retrieving emoji pixmaps.

        Args:
            emoji_pixmap_getter (Union[str, QFont, Callable[[str], QPixmap]]):
                Can be a font family name (str), a QFont object, or a callable that takes an emoji string
                and returns a QPixmap.
        """
        if isinstance(emoji_pixmap_getter, str):
            font_family = emoji_pixmap_getter
        elif isinstance(emoji_pixmap_getter, QFont):
            font_family = emoji_pixmap_getter.family()
        else:
            font_family = None

        if font_family:
            emoji_font = QFont()
            emoji_font.setFamily(font_family)
            self._emoji_pixmap_getter = partial(
                QIconGenerator.charToPixmap,
                font=emoji_font,
                target_size=QSize(100, 100),
            )
        else:
            self._emoji_pixmap_getter = typing.cast(
                typing.Callable[[str], QPixmap], emoji_pixmap_getter
            )

        self._paint_emoji_on_label()
        self._paint_skintones()

        delegate = self.delegate()
        delegate.forceReloadAll()

    def emojiPixmapGetter(self) -> typing.Callable[[str], QPixmap]:
        """Returns the current emoji pixmap getter function.

        Returns:
            Callable[[str], QPixmap]: A function that takes an emoji string and returns a QPixmap.
        """
        return self._emoji_pixmap_getter

    def delegate(self) -> QGroupedIconDelegate:
        """Returns the item delegate used by the view."""
        return self._grouped_icon_view.itemDelegate()

    def view(self) -> QGroupedIconView:
        """Returns the internal grouped icon view."""
        return self._grouped_icon_view

    def model(self) -> QEmojiPickerModel:
        """Returns the emoji picker model."""
        return self._model

__init__(model=None, emoji_pixmap_getter=partial(QTwemojiImageProvider.getPixmap, margin=0, size=128), emoji_label_size=QSize(32, 32))

Initializes the emoji picker.

Parameters:

Name Type Description Default
model QEmojiPickerModel

Custom emoji model. Defaults to None.

None
emoji_pixmap_getter Union[str, QFont, Callable[[str], QPixmap]]

Method or font to generate emoji pixmaps. Defaults to EmojiImageProvider.getPixmap.

partial(getPixmap, margin=0, size=128)
emoji_label_size QSize

Size of the preview emoji label. Defaults to QSize(32, 32).

QSize(32, 32)
Source code in source/qextrawidgets/widgets/miscellaneous/emoji_picker.py
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
def __init__(
    self,
    model: typing.Optional[QEmojiPickerModel] = None,
    emoji_pixmap_getter: typing.Union[
        str, QFont, typing.Callable[[str], QPixmap]
    ] = partial(QTwemojiImageProvider.getPixmap, margin=0, size=128),
    emoji_label_size: QSize = QSize(32, 32),
) -> None:
    """Initializes the emoji picker.

    Args:
        model (QEmojiPickerModel, optional): Custom emoji model. Defaults to None.
        emoji_pixmap_getter (Union[str, QFont, Callable[[str], QPixmap]], optional):
            Method or font to generate emoji pixmaps. Defaults to EmojiImageProvider.getPixmap.
        emoji_label_size (QSize, optional): Size of the preview emoji label. Defaults to QSize(32, 32).
    """
    super().__init__()

    self._skin_tone_selector_emojis = {
        EmojiSkinTone.Default: "👏",
        EmojiSkinTone.Light: "👏🏻",
        EmojiSkinTone.MediumLight: "👏🏼",
        EmojiSkinTone.Medium: "👏🏽",
        EmojiSkinTone.MediumDark: "👏🏾",
        EmojiSkinTone.Dark: "👏🏿",
    }

    if model:
        self._model = model
    else:
        self._model = QEmojiPickerModel()

    self._proxy = QEmojiPickerProxyModel()
    self._proxy.setSourceModel(self._model)

    self._search_line_edit = self._create_search_line_edit()

    self._grouped_icon_view = QGroupedIconView(self, QSize(40, 40), 5)
    self._grouped_icon_view.setContextMenuPolicy(
        Qt.ContextMenuPolicy.CustomContextMenu
    )
    self._grouped_icon_view.setSelectionMode(
        QAbstractItemView.SelectionMode.NoSelection
    )
    self._grouped_icon_view.setModel(self._proxy)

    self._search_timer = QTimer(self)
    self._search_timer.setSingleShot(True)
    self._search_timer.setInterval(200)

    self._skin_tone_selector = QIconComboBox()

    for skin_tone, emoji in self._skin_tone_selector_emojis.items():
        self._skin_tone_selector.addItem(text=emoji, data=skin_tone)

    self._shortcuts_container = QWidget()
    self._shortcuts_container.setFixedHeight(40)  # Fixed height for the bar

    self._shortcuts_group = QButtonGroup(self)
    self._shortcuts_group.setExclusive(True)

    self._emoji_on_label = None

    self._emoji_label = QLabel()
    self._emoji_label.setFixedSize(emoji_label_size)
    self._emoji_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
    self._emoji_label.setScaledContents(True)

    self._aliases_emoji_label = self._create_emoji_label()

    self._setup_layout()
    self._setup_connections()

    if model is None:
        self._model.populate()
    else:
        for item in self._model.categories():
            self._on_categories_inserted(item)

    self._emoji_pixmap_getter: typing.Callable[[str], QPixmap]
    self.setEmojiPixmapGetter(emoji_pixmap_getter)
    self.setContentsMargins(5, 5, 5, 5)

    self.translateUI()

delegate()

Returns the item delegate used by the view.

Source code in source/qextrawidgets/widgets/miscellaneous/emoji_picker.py
495
496
497
def delegate(self) -> QGroupedIconDelegate:
    """Returns the item delegate used by the view."""
    return self._grouped_icon_view.itemDelegate()

emojiPixmapGetter()

Returns the current emoji pixmap getter function.

Returns:

Type Description
Callable[[str], QPixmap]

Callable[[str], QPixmap]: A function that takes an emoji string and returns a QPixmap.

Source code in source/qextrawidgets/widgets/miscellaneous/emoji_picker.py
487
488
489
490
491
492
493
def emojiPixmapGetter(self) -> typing.Callable[[str], QPixmap]:
    """Returns the current emoji pixmap getter function.

    Returns:
        Callable[[str], QPixmap]: A function that takes an emoji string and returns a QPixmap.
    """
    return self._emoji_pixmap_getter

model()

Returns the emoji picker model.

Source code in source/qextrawidgets/widgets/miscellaneous/emoji_picker.py
503
504
505
def model(self) -> QEmojiPickerModel:
    """Returns the emoji picker model."""
    return self._model

resetPicker()

Resets the picker state.

Source code in source/qextrawidgets/widgets/miscellaneous/emoji_picker.py
446
447
448
def resetPicker(self) -> None:
    """Resets the picker state."""
    self._search_line_edit.clear()

setEmojiPixmapGetter(emoji_pixmap_getter)

Sets the strategy for retrieving emoji pixmaps.

Parameters:

Name Type Description Default
emoji_pixmap_getter Union[str, QFont, Callable[[str], QPixmap]]

Can be a font family name (str), a QFont object, or a callable that takes an emoji string and returns a QPixmap.

required
Source code in source/qextrawidgets/widgets/miscellaneous/emoji_picker.py
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
def setEmojiPixmapGetter(
    self,
    emoji_pixmap_getter: typing.Union[str, QFont, typing.Callable[[str], QPixmap]],
) -> None:
    """Sets the strategy for retrieving emoji pixmaps.

    Args:
        emoji_pixmap_getter (Union[str, QFont, Callable[[str], QPixmap]]):
            Can be a font family name (str), a QFont object, or a callable that takes an emoji string
            and returns a QPixmap.
    """
    if isinstance(emoji_pixmap_getter, str):
        font_family = emoji_pixmap_getter
    elif isinstance(emoji_pixmap_getter, QFont):
        font_family = emoji_pixmap_getter.family()
    else:
        font_family = None

    if font_family:
        emoji_font = QFont()
        emoji_font.setFamily(font_family)
        self._emoji_pixmap_getter = partial(
            QIconGenerator.charToPixmap,
            font=emoji_font,
            target_size=QSize(100, 100),
        )
    else:
        self._emoji_pixmap_getter = typing.cast(
            typing.Callable[[str], QPixmap], emoji_pixmap_getter
        )

    self._paint_emoji_on_label()
    self._paint_skintones()

    delegate = self.delegate()
    delegate.forceReloadAll()

translateUI()

Translates the UI components.

Source code in source/qextrawidgets/widgets/miscellaneous/emoji_picker.py
442
443
444
def translateUI(self) -> None:
    """Translates the UI components."""
    self._search_line_edit.setPlaceholderText(self.tr("Search emoji..."))

view()

Returns the internal grouped icon view.

Source code in source/qextrawidgets/widgets/miscellaneous/emoji_picker.py
499
500
501
def view(self) -> QGroupedIconView:
    """Returns the internal grouped icon view."""
    return self._grouped_icon_view

QPager

Bases: QWidget

Pagination component with a sliding window of buttons and in-place editing.

Signals

currentPageChanged (int): Emitted when the current page changes.

Source code in source/qextrawidgets/widgets/miscellaneous/pager.py
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
class QPager(QWidget):
    """Pagination component with a sliding window of buttons and in-place editing.

    Signals:
        currentPageChanged (int): Emitted when the current page changes.
    """

    # Public signals
    currentPageChanged = Signal(int)

    def __init__(self, parent: typing.Optional[QWidget] = None) -> None:
        """Initializes the pager widget.

        Args:
            parent (QWidget, optional): Parent widget. Defaults to None.
        """
        super().__init__(parent)

        # --- Data Variables ---
        self._total_pages = 1
        self._current_page = 1
        self._max_visible_buttons = 5

        # --- UI and Layout Configuration ---
        main_layout = QHBoxLayout(self)
        main_layout.setContentsMargins(0, 0, 0, 0)
        main_layout.setSpacing(4)
        main_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)

        # Group for visual exclusivity
        self._button_group = QButtonGroup(self)
        self._button_group.setExclusive(True)

        # List to track dynamic widgets
        self._page_widgets = []

        # 1. Navigation Buttons
        self._btn_first = self._create_nav_button(QThemeResponsiveIcon.fromAwesome("fa6s.backward-step"))
        self._btn_prev = self._create_nav_button(QThemeResponsiveIcon.fromAwesome("fa6s.angle-left"))
        self._btn_next = self._create_nav_button(QThemeResponsiveIcon.fromAwesome("fa6s.angle-right"))
        self._btn_last = self._create_nav_button(QThemeResponsiveIcon.fromAwesome("fa6s.forward-step"))

        # 2. Layout for numbers (where the magic happens)
        self._numbers_layout = QHBoxLayout()
        self._numbers_layout.setContentsMargins(0, 0, 0, 0)
        self._numbers_layout.setSpacing(2)

        # 3. Add to main layout
        main_layout.addWidget(self._btn_first)
        main_layout.addWidget(self._btn_prev)
        main_layout.addLayout(self._numbers_layout)
        main_layout.addWidget(self._btn_next)
        main_layout.addWidget(self._btn_last)

        # --- Connections ---
        self._setup_connections()

        # Initialization
        self._update_view()

    # --- Internal Creation Methods ---

    @staticmethod
    def _create_nav_button(icon: QIcon) -> QPushButton:
        """Creates a navigation button (first, prev, next, last).

        Args:
            icon (QIcon): Button icon.

        Returns:
            QPushButton: The created button.
        """
        btn = QPushButton()
        btn.setIcon(icon)
        btn.setFixedSize(30, 30)
        btn.setCursor(Qt.CursorShape.PointingHandCursor)
        return btn

    @staticmethod
    def _create_page_button(text: str) -> QPushButton:
        """Creates a button representing a page number.

        Args:
            text (str): Button text (page number).

        Returns:
            QPushButton: The created page button.
        """
        btn = QPushButton(text)
        btn.setCheckable(True)
        btn.setFixedSize(30, 30)
        btn.setCursor(Qt.CursorShape.PointingHandCursor)
        return btn

    def _create_editor(self) -> QSpinBox:
        """Creates the numeric input that replaces the button for in-place editing.

        Returns:
            QSpinBox: The created spin box editor.
        """
        spin = QSpinBox()
        spin.setFixedSize(60, 30)  # Slightly wider to fit large numbers
        spin.setFrame(False)  # No border to look integrated
        spin.setAlignment(Qt.AlignmentFlag.AlignCenter)
        spin.setButtonSymbols(QSpinBox.ButtonSymbols.NoButtons)  # Remove up/down arrows
        spin.setRange(1, self._total_pages)
        spin.setValue(self._current_page)
        return spin

    def _setup_connections(self) -> None:
        """Sets up signals and slots connections."""
        self._btn_first.clicked.connect(lambda: self.setCurrentPage(1))
        self._btn_prev.clicked.connect(lambda: self.setCurrentPage(self._current_page - 1))
        self._btn_next.clicked.connect(lambda: self.setCurrentPage(self._current_page + 1))
        self._btn_last.clicked.connect(lambda: self.setCurrentPage(self._total_pages))

    # --- Visualization and Editing Logic ---

    def _update_view(self) -> None:
        """Rebuilds the number bar based on current state."""

        # 1. Calculate Sliding Window
        half = self._max_visible_buttons // 2
        start_page = max(1, self._current_page - half)
        end_page = min(self._total_pages, start_page + self._max_visible_buttons - 1)

        if end_page - start_page + 1 < self._max_visible_buttons:
            start_page = max(1, end_page - self._max_visible_buttons + 1)

        # 2. Total Cleanup of numeric area
        while self._numbers_layout.count():
            item = self._numbers_layout.takeAt(0)
            if item is None:
                break
            widget = item.widget()
            if widget:
                if isinstance(widget, QPushButton):
                    self._button_group.removeButton(widget)
                widget.deleteLater()
        self._page_widgets.clear()

        # 3. Button Construction
        for page_num in range(start_page, end_page + 1):
            btn = self._create_page_button(str(page_num))
            self._button_group.addButton(btn)

            if page_num == self._current_page:
                btn.setChecked(True)
                # The current button not only navigates, it opens editing
                btn.setToolTip(self.tr("Click to type page"))
                btn.clicked.connect(partial(self.__on_edit_requested, btn))
            else:
                # Normal buttons just navigate
                btn.clicked.connect(partial(self.setCurrentPage, page_num))

            self._numbers_layout.addWidget(btn)
            self._page_widgets.append(btn)

        # 4. Navigation States
        self._btn_first.setEnabled(self._current_page > 1)
        self._btn_prev.setEnabled(self._current_page > 1)
        self._btn_next.setEnabled(self._current_page < self._total_pages)
        self._btn_last.setEnabled(self._current_page < self._total_pages)

    def __on_edit_requested(self, button_sender: QPushButton) -> None:
        """Slot called when the user clicks on the current page to start editing.

        Replaces the button with a SpinBox.

        Args:
            button_sender (QPushButton): The button that was clicked.
        """
        # 1. Identify position in layout
        index = self._numbers_layout.indexOf(button_sender)
        if index == -1:
            return

        # 2. Create and configure editor
        spin = self._create_editor()

        # 3. Replace in layout (Swap)
        # We remove the button from the layout and hide it (don't delete yet to avoid crash in active slots)
        self._numbers_layout.takeAt(index)
        button_sender.hide()
        self._button_group.removeButton(button_sender)  # Important not to bug the group

        self._numbers_layout.insertWidget(index, spin)
        spin.setFocus()
        spin.selectAll()

        # 4. Editor Connections
        # If Enter is pressed or focus is lost, confirms editing
        spin.editingFinished.connect(lambda: self.setCurrentPage(spin.value()))

        # If focus is lost without pressing enter, we force update to restore the button
        # (This is done implicitly because setCurrentPage calls _update_view)

    # --- Public API ---

    def setTotalPages(self, total: int) -> None:
        """Sets the total number of pages.

        Args:
            total (int): Total page count.
        """
        if total < 1:
            total = 1
        self._total_pages = total
        if self._current_page > total:
            self.setCurrentPage(total)
        else:
            self._update_view()

    def totalPages(self) -> int:
        """Returns the total number of pages.

        Returns:
            int: Total page count.
        """
        return self._total_pages

    def setVisibleButtonCount(self, count: int) -> None:
        """Sets how many page buttons are visible at once.

        Args:
            count (int): Maximum number of visible page buttons.
        """
        if count < 1:
            count = 1
        self._max_visible_buttons = count
        self._update_view()

    def visibleButtonCount(self) -> int:
        """Returns the maximum number of visible page buttons.

        Returns:
            int: Visible button count.
        """
        return self._max_visible_buttons

    def setCurrentPage(self, page: int) -> None:
        """Sets the current page index.

        Args:
            page (int): Page index to set.
        """
        if page < 1:
            page = 1
        if page > self._total_pages:
            page = self._total_pages

        # Updates state
        self._current_page = page

        # Emits signal only if changed
        # But ALWAYS calls _update_view to ensure SpinBox
        # (if it exists) is destroyed and the button returns.
        self._update_view()
        self.currentPageChanged.emit(page)

    def currentPage(self) -> int:
        """Returns the current page index.

        Returns:
            int: Current page.
        """
        return self._current_page

__init__(parent=None)

Initializes the pager widget.

Parameters:

Name Type Description Default
parent QWidget

Parent widget. Defaults to None.

None
Source code in source/qextrawidgets/widgets/miscellaneous/pager.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
def __init__(self, parent: typing.Optional[QWidget] = None) -> None:
    """Initializes the pager widget.

    Args:
        parent (QWidget, optional): Parent widget. Defaults to None.
    """
    super().__init__(parent)

    # --- Data Variables ---
    self._total_pages = 1
    self._current_page = 1
    self._max_visible_buttons = 5

    # --- UI and Layout Configuration ---
    main_layout = QHBoxLayout(self)
    main_layout.setContentsMargins(0, 0, 0, 0)
    main_layout.setSpacing(4)
    main_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)

    # Group for visual exclusivity
    self._button_group = QButtonGroup(self)
    self._button_group.setExclusive(True)

    # List to track dynamic widgets
    self._page_widgets = []

    # 1. Navigation Buttons
    self._btn_first = self._create_nav_button(QThemeResponsiveIcon.fromAwesome("fa6s.backward-step"))
    self._btn_prev = self._create_nav_button(QThemeResponsiveIcon.fromAwesome("fa6s.angle-left"))
    self._btn_next = self._create_nav_button(QThemeResponsiveIcon.fromAwesome("fa6s.angle-right"))
    self._btn_last = self._create_nav_button(QThemeResponsiveIcon.fromAwesome("fa6s.forward-step"))

    # 2. Layout for numbers (where the magic happens)
    self._numbers_layout = QHBoxLayout()
    self._numbers_layout.setContentsMargins(0, 0, 0, 0)
    self._numbers_layout.setSpacing(2)

    # 3. Add to main layout
    main_layout.addWidget(self._btn_first)
    main_layout.addWidget(self._btn_prev)
    main_layout.addLayout(self._numbers_layout)
    main_layout.addWidget(self._btn_next)
    main_layout.addWidget(self._btn_last)

    # --- Connections ---
    self._setup_connections()

    # Initialization
    self._update_view()

__on_edit_requested(button_sender)

Slot called when the user clicks on the current page to start editing.

Replaces the button with a SpinBox.

Parameters:

Name Type Description Default
button_sender QPushButton

The button that was clicked.

required
Source code in source/qextrawidgets/widgets/miscellaneous/pager.py
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
def __on_edit_requested(self, button_sender: QPushButton) -> None:
    """Slot called when the user clicks on the current page to start editing.

    Replaces the button with a SpinBox.

    Args:
        button_sender (QPushButton): The button that was clicked.
    """
    # 1. Identify position in layout
    index = self._numbers_layout.indexOf(button_sender)
    if index == -1:
        return

    # 2. Create and configure editor
    spin = self._create_editor()

    # 3. Replace in layout (Swap)
    # We remove the button from the layout and hide it (don't delete yet to avoid crash in active slots)
    self._numbers_layout.takeAt(index)
    button_sender.hide()
    self._button_group.removeButton(button_sender)  # Important not to bug the group

    self._numbers_layout.insertWidget(index, spin)
    spin.setFocus()
    spin.selectAll()

    # 4. Editor Connections
    # If Enter is pressed or focus is lost, confirms editing
    spin.editingFinished.connect(lambda: self.setCurrentPage(spin.value()))

currentPage()

Returns the current page index.

Returns:

Name Type Description
int int

Current page.

Source code in source/qextrawidgets/widgets/miscellaneous/pager.py
272
273
274
275
276
277
278
def currentPage(self) -> int:
    """Returns the current page index.

    Returns:
        int: Current page.
    """
    return self._current_page

setCurrentPage(page)

Sets the current page index.

Parameters:

Name Type Description Default
page int

Page index to set.

required
Source code in source/qextrawidgets/widgets/miscellaneous/pager.py
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
def setCurrentPage(self, page: int) -> None:
    """Sets the current page index.

    Args:
        page (int): Page index to set.
    """
    if page < 1:
        page = 1
    if page > self._total_pages:
        page = self._total_pages

    # Updates state
    self._current_page = page

    # Emits signal only if changed
    # But ALWAYS calls _update_view to ensure SpinBox
    # (if it exists) is destroyed and the button returns.
    self._update_view()
    self.currentPageChanged.emit(page)

setTotalPages(total)

Sets the total number of pages.

Parameters:

Name Type Description Default
total int

Total page count.

required
Source code in source/qextrawidgets/widgets/miscellaneous/pager.py
211
212
213
214
215
216
217
218
219
220
221
222
223
def setTotalPages(self, total: int) -> None:
    """Sets the total number of pages.

    Args:
        total (int): Total page count.
    """
    if total < 1:
        total = 1
    self._total_pages = total
    if self._current_page > total:
        self.setCurrentPage(total)
    else:
        self._update_view()

setVisibleButtonCount(count)

Sets how many page buttons are visible at once.

Parameters:

Name Type Description Default
count int

Maximum number of visible page buttons.

required
Source code in source/qextrawidgets/widgets/miscellaneous/pager.py
233
234
235
236
237
238
239
240
241
242
def setVisibleButtonCount(self, count: int) -> None:
    """Sets how many page buttons are visible at once.

    Args:
        count (int): Maximum number of visible page buttons.
    """
    if count < 1:
        count = 1
    self._max_visible_buttons = count
    self._update_view()

totalPages()

Returns the total number of pages.

Returns:

Name Type Description
int int

Total page count.

Source code in source/qextrawidgets/widgets/miscellaneous/pager.py
225
226
227
228
229
230
231
def totalPages(self) -> int:
    """Returns the total number of pages.

    Returns:
        int: Total page count.
    """
    return self._total_pages

visibleButtonCount()

Returns the maximum number of visible page buttons.

Returns:

Name Type Description
int int

Visible button count.

Source code in source/qextrawidgets/widgets/miscellaneous/pager.py
244
245
246
247
248
249
250
def visibleButtonCount(self) -> int:
    """Returns the maximum number of visible page buttons.

    Returns:
        int: Visible button count.
    """
    return self._max_visible_buttons

Views

QFilterHeaderView

Bases: QHeaderView

A customized horizontal header for QFilterableTable that renders filter icons.

This header overrides the default painting to draw a filter icon on the right side of the section if the model provides one via the DecorationRole.

Source code in source/qextrawidgets/widgets/views/filter_header_view.py
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
class QFilterHeaderView(QHeaderView):
    """A customized horizontal header for QFilterableTable that renders filter icons.

    This header overrides the default painting to draw a filter icon on the right
    side of the section if the model provides one via the DecorationRole.
    """

    filterClicked = Signal(int)

    def __init__(
        self, orientation: Qt.Orientation, parent: typing.Optional[QWidget] = None
    ) -> None:
        """Initializes the filter header.

        Args:
            orientation (Qt.Orientation): Orientation of the header (Horizontal).
            parent (QHeaderView, optional): Parent widget. Defaults to None.
        """
        super().__init__(orientation, parent)
        self.setSectionsClickable(False)
        self.setMouseTracking(True)
        self.setTextElideMode(Qt.TextElideMode.ElideRight)
        self._press_pos: typing.Optional[QPoint] = None
        self._current_hover_pos: typing.Optional[QPoint] = None

    @staticmethod
    def _get_icon_rect(section_rect: QRect) -> QRect:
        """Calculates the filter icon rectangle within the section."""
        icon_size = 16  # Comfortable default size
        padding = 4

        return QRect(
            section_rect.right() - icon_size - padding,
            section_rect.top() + (section_rect.height() - icon_size) // 2,
            icon_size,
            icon_size,
        )

    def mouseMoveEvent(self, e: QMouseEvent) -> None:
        """Updates hover position and triggers repaint."""
        self._current_hover_pos = e.pos()
        # We could optimize by only updating the affected section, but updating viewport is safer/easier
        self.viewport().update()
        super().mouseMoveEvent(e)

    def leaveEvent(self, e: QEvent) -> None:
        """Resets hover position when mouse leaves the header."""
        self._current_hover_pos = None
        self.viewport().update()
        super().leaveEvent(e)

    def mousePressEvent(self, e: QMouseEvent) -> None:
        """Stores the position of the click to detect drags."""
        self._press_pos = e.pos()
        self.viewport().update()
        super().mousePressEvent(e)

    def mouseReleaseEvent(self, e: QMouseEvent) -> None:
        """Handles mouse release to manually trigger click signals."""
        super().mouseReleaseEvent(e)

        if self._press_pos is None:
            return

        press_pos = self._press_pos
        self._press_pos = None
        self.viewport().update()

        # Check if it was a click (not a drag)
        moved = (
            e.pos() - press_pos
        ).manhattanLength() > QApplication.startDragDistance()
        if moved:
            return

        index = self.logicalIndexAt(e.pos())
        if index == -1:
            return

        # Only handle left clicks
        if e.button() == Qt.MouseButton.LeftButton:
            # 1. Reconstruct section geometry to check hit on icon
            # Logic borrowed from how paintSection gets the rect, but here we need valid geometry
            # sectionViewportPosition gives the start X relative to viewport
            x = self.sectionViewportPosition(index)
            w = self.sectionSize(index)
            h = self.height()
            section_rect = QRect(x, 0, w, h)

            icon_rect = self._get_icon_rect(section_rect)

            # 2. Check Hit
            if icon_rect.contains(e.pos()):
                # Clicked on filter icon -> Show Popup
                self.filterClicked.emit(index)
            else:
                # Clicked elsewhere -> Select Column
                # (Standard Behavior simulation, no modifiers needed anymore)
                self.sectionClicked.emit(index)

    def paintSection(self, painter: QPainter, rect: QRect, logical_index: int) -> None:
        """Paints the header section with an optional filter icon.

        Args:
            painter (QPainter): The painter to use.
            rect (QRect): The rectangle to paint in.
            logical_index (int): The logical index of the section.
        """
        painter.save()

        # 1. Configure native style options
        opt = QStyleOptionHeader()
        self.initStyleOption(opt)

        setattr(opt, "rect", rect)
        setattr(opt, "section", logical_index)
        setattr(opt, "textAlignment", Qt.AlignmentFlag.AlignCenter)

        # Get data from model
        model = self.model()
        if model:
            # Text
            text = model.headerData(
                logical_index, Qt.Orientation.Horizontal, Qt.ItemDataRole.DisplayRole
            )
            if text is None:
                text = ""
            setattr(opt, "text", text)

            # Alignment
            alignment = model.headerData(
                logical_index,
                Qt.Orientation.Horizontal,
                Qt.ItemDataRole.TextAlignmentRole,
            )
            if alignment:
                setattr(opt, "textAlignment", alignment)

            # Icon (Filter)
            icon = model.headerData(
                logical_index, Qt.Orientation.Horizontal, Qt.ItemDataRole.DecorationRole
            )

            # If there is an icon, reserve space on the right for it
            if isinstance(icon, QIcon) and not icon.isNull():
                # Draw the native control (Background + Text)
                # We'll trick the style saying there is no icon, as we'll draw it manually on the right
                setattr(opt, "icon", QIcon())
                self.style().drawControl(
                    QStyle.ControlElement.CE_Header, opt, painter, self
                )

                # Draw the icon aligned to the right manually
                icon_rect = self._get_icon_rect(rect)

                # Icon state (active/disabled) based on header
                mode = QIcon.Mode.Normal
                if not self.isEnabled():
                    mode = QIcon.Mode.Disabled
                else:
                    # Check if mouse is hovering THIS icon
                    if self._current_hover_pos and icon_rect.contains(
                        self._current_hover_pos
                    ):
                        # Draw hover background
                        palette = typing.cast(QPalette, opt.palette)
                        hover_color = palette.text().color()
                        if self._press_pos:
                            hover_color.setAlphaF(0.3)
                        else:
                            hover_color.setAlphaF(0.1)
                        painter.setPen(Qt.PenStyle.NoPen)
                        painter.setBrush(hover_color)
                        # Adjust rect slightly to give some padding
                        painter.drawRoundedRect(icon_rect.adjusted(-2, -2, 2, 2), 2, 2)

                        # mode remains Normal
                    elif (
                        typing.cast(QStyle.StateFlag, opt.state)
                        & QStyle.StateFlag.State_MouseOver
                    ):
                        # Fallback: if section is hovered but not icon, maybe different state?
                        # For now, keep Normal unless specifically hovering icon, OR use Active if just section is hovered?
                        # User requested "hover effect on the icon", usually implies specific icon hover.
                        pass

                icon.paint(
                    painter,
                    icon_rect,
                    alignment=Qt.AlignmentFlag.AlignCenter,
                    mode=mode,
                )

            else:
                # Full standard native drawing
                self.style().drawControl(
                    QStyle.ControlElement.CE_Header, opt, painter, self
                )

        painter.restore()

__init__(orientation, parent=None)

Initializes the filter header.

Parameters:

Name Type Description Default
orientation Orientation

Orientation of the header (Horizontal).

required
parent QHeaderView

Parent widget. Defaults to None.

None
Source code in source/qextrawidgets/widgets/views/filter_header_view.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def __init__(
    self, orientation: Qt.Orientation, parent: typing.Optional[QWidget] = None
) -> None:
    """Initializes the filter header.

    Args:
        orientation (Qt.Orientation): Orientation of the header (Horizontal).
        parent (QHeaderView, optional): Parent widget. Defaults to None.
    """
    super().__init__(orientation, parent)
    self.setSectionsClickable(False)
    self.setMouseTracking(True)
    self.setTextElideMode(Qt.TextElideMode.ElideRight)
    self._press_pos: typing.Optional[QPoint] = None
    self._current_hover_pos: typing.Optional[QPoint] = None

leaveEvent(e)

Resets hover position when mouse leaves the header.

Source code in source/qextrawidgets/widgets/views/filter_header_view.py
59
60
61
62
63
def leaveEvent(self, e: QEvent) -> None:
    """Resets hover position when mouse leaves the header."""
    self._current_hover_pos = None
    self.viewport().update()
    super().leaveEvent(e)

mouseMoveEvent(e)

Updates hover position and triggers repaint.

Source code in source/qextrawidgets/widgets/views/filter_header_view.py
52
53
54
55
56
57
def mouseMoveEvent(self, e: QMouseEvent) -> None:
    """Updates hover position and triggers repaint."""
    self._current_hover_pos = e.pos()
    # We could optimize by only updating the affected section, but updating viewport is safer/easier
    self.viewport().update()
    super().mouseMoveEvent(e)

mousePressEvent(e)

Stores the position of the click to detect drags.

Source code in source/qextrawidgets/widgets/views/filter_header_view.py
65
66
67
68
69
def mousePressEvent(self, e: QMouseEvent) -> None:
    """Stores the position of the click to detect drags."""
    self._press_pos = e.pos()
    self.viewport().update()
    super().mousePressEvent(e)

mouseReleaseEvent(e)

Handles mouse release to manually trigger click signals.

Source code in source/qextrawidgets/widgets/views/filter_header_view.py
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
def mouseReleaseEvent(self, e: QMouseEvent) -> None:
    """Handles mouse release to manually trigger click signals."""
    super().mouseReleaseEvent(e)

    if self._press_pos is None:
        return

    press_pos = self._press_pos
    self._press_pos = None
    self.viewport().update()

    # Check if it was a click (not a drag)
    moved = (
        e.pos() - press_pos
    ).manhattanLength() > QApplication.startDragDistance()
    if moved:
        return

    index = self.logicalIndexAt(e.pos())
    if index == -1:
        return

    # Only handle left clicks
    if e.button() == Qt.MouseButton.LeftButton:
        # 1. Reconstruct section geometry to check hit on icon
        # Logic borrowed from how paintSection gets the rect, but here we need valid geometry
        # sectionViewportPosition gives the start X relative to viewport
        x = self.sectionViewportPosition(index)
        w = self.sectionSize(index)
        h = self.height()
        section_rect = QRect(x, 0, w, h)

        icon_rect = self._get_icon_rect(section_rect)

        # 2. Check Hit
        if icon_rect.contains(e.pos()):
            # Clicked on filter icon -> Show Popup
            self.filterClicked.emit(index)
        else:
            # Clicked elsewhere -> Select Column
            # (Standard Behavior simulation, no modifiers needed anymore)
            self.sectionClicked.emit(index)

paintSection(painter, rect, logical_index)

Paints the header section with an optional filter icon.

Parameters:

Name Type Description Default
painter QPainter

The painter to use.

required
rect QRect

The rectangle to paint in.

required
logical_index int

The logical index of the section.

required
Source code in source/qextrawidgets/widgets/views/filter_header_view.py
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
def paintSection(self, painter: QPainter, rect: QRect, logical_index: int) -> None:
    """Paints the header section with an optional filter icon.

    Args:
        painter (QPainter): The painter to use.
        rect (QRect): The rectangle to paint in.
        logical_index (int): The logical index of the section.
    """
    painter.save()

    # 1. Configure native style options
    opt = QStyleOptionHeader()
    self.initStyleOption(opt)

    setattr(opt, "rect", rect)
    setattr(opt, "section", logical_index)
    setattr(opt, "textAlignment", Qt.AlignmentFlag.AlignCenter)

    # Get data from model
    model = self.model()
    if model:
        # Text
        text = model.headerData(
            logical_index, Qt.Orientation.Horizontal, Qt.ItemDataRole.DisplayRole
        )
        if text is None:
            text = ""
        setattr(opt, "text", text)

        # Alignment
        alignment = model.headerData(
            logical_index,
            Qt.Orientation.Horizontal,
            Qt.ItemDataRole.TextAlignmentRole,
        )
        if alignment:
            setattr(opt, "textAlignment", alignment)

        # Icon (Filter)
        icon = model.headerData(
            logical_index, Qt.Orientation.Horizontal, Qt.ItemDataRole.DecorationRole
        )

        # If there is an icon, reserve space on the right for it
        if isinstance(icon, QIcon) and not icon.isNull():
            # Draw the native control (Background + Text)
            # We'll trick the style saying there is no icon, as we'll draw it manually on the right
            setattr(opt, "icon", QIcon())
            self.style().drawControl(
                QStyle.ControlElement.CE_Header, opt, painter, self
            )

            # Draw the icon aligned to the right manually
            icon_rect = self._get_icon_rect(rect)

            # Icon state (active/disabled) based on header
            mode = QIcon.Mode.Normal
            if not self.isEnabled():
                mode = QIcon.Mode.Disabled
            else:
                # Check if mouse is hovering THIS icon
                if self._current_hover_pos and icon_rect.contains(
                    self._current_hover_pos
                ):
                    # Draw hover background
                    palette = typing.cast(QPalette, opt.palette)
                    hover_color = palette.text().color()
                    if self._press_pos:
                        hover_color.setAlphaF(0.3)
                    else:
                        hover_color.setAlphaF(0.1)
                    painter.setPen(Qt.PenStyle.NoPen)
                    painter.setBrush(hover_color)
                    # Adjust rect slightly to give some padding
                    painter.drawRoundedRect(icon_rect.adjusted(-2, -2, 2, 2), 2, 2)

                    # mode remains Normal
                elif (
                    typing.cast(QStyle.StateFlag, opt.state)
                    & QStyle.StateFlag.State_MouseOver
                ):
                    # Fallback: if section is hovered but not icon, maybe different state?
                    # For now, keep Normal unless specifically hovering icon, OR use Active if just section is hovered?
                    # User requested "hover effect on the icon", usually implies specific icon hover.
                    pass

            icon.paint(
                painter,
                icon_rect,
                alignment=Qt.AlignmentFlag.AlignCenter,
                mode=mode,
            )

        else:
            # Full standard native drawing
            self.style().drawControl(
                QStyle.ControlElement.CE_Header, opt, painter, self
            )

    painter.restore()

QFilterableTableView

Bases: QTableView

A QTableView extension that provides Excel-style filtering and sorting on headers.

Source code in source/qextrawidgets/widgets/views/filterable_table_view.py
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
class QFilterableTableView(QTableView):
    """A QTableView extension that provides Excel-style filtering and sorting on headers."""

    def __init__(self, parent: typing.Optional[QWidget] = None) -> None:
        """Initializes the filterable table.

        Args:
            parent (QWidget, optional): Parent widget. Defaults to None.
        """
        super().__init__(parent)

        self._filter_proxy = QMultiFilterProxyModel()
        self._header_proxy = QHeaderProxyModel()
        self._header_proxy.setSourceModel(self._filter_proxy)

        super().setModel(self._header_proxy)
        self._popups: typing.Dict[int, QFilterPopup] = {}

        header = QFilterHeaderView(Qt.Orientation.Horizontal, self)
        # header.setSectionsClickable(False) is set in header __init__
        header.filterClicked.connect(self._on_header_clicked)
        header.sectionClicked.connect(self._on_section_clicked)
        self.setHorizontalHeader(header)

        self.setModel(QStandardItemModel(self))

    # --- Public API ---

    def setModel(self, model: typing.Optional[QAbstractItemModel]) -> None:
        """Sets the source model for the table and initializes filters.

        Args:
            model (Optional[QAbstractItemModel]): The data model to display.
        """
        if model is None:
            return

        if self._filter_proxy.sourceModel():
            self._disconnect_model_signals(self._filter_proxy.sourceModel())

        self._filter_proxy.setSourceModel(model)

        if model:
            self._connect_model_signals(model)

        self._refresh_popups()

    def model(self) -> QAbstractItemModel:
        """Returns the source model (not the proxy).

        Returns:
            QAbstractItemModel: The source model.
        """
        return self._filter_proxy.sourceModel()

    # --- Popup Logic ---

    def _refresh_popups(self) -> None:
        """Clears and recreates filter popups for all columns."""
        self._filter_proxy.reset()
        self._header_proxy.reset()

        for popup in self._popups.values():
            popup.deleteLater()
        self._popups.clear()

        model = self.model()
        if not model:
            return

        for col in range(model.columnCount()):
            self._create_popup(col)

    def _create_popup(self, logical_index: int) -> None:
        """Creates a filter popup for a specific column.

        Args:
            logical_index (int): Column index.
        """
        if logical_index in self._popups:
            return

        popup = QFilterPopup(self._filter_proxy, logical_index, self)

        popup.accepted.connect(lambda: self._apply_filter(logical_index))

        popup.orderChanged.connect(lambda col, order: self.sortByColumn(col, order))

        popup.clearRequested.connect(lambda: self._clear_filter(logical_index))

        self._popups[logical_index] = popup
        self._update_header_icon(logical_index)

    def _on_header_clicked(self, logical_index: int) -> None:
        """Handles header clicks to show the filter popup.

        Args:
            logical_index (int): Column index clicked.
        """
        if logical_index not in self._popups:
            return

        popup = self._popups[logical_index]

        # QFilterPopup now handles unique values internally via proxy.
        # We just need to show it.

        header = self.horizontalHeader()
        viewport_pos = header.sectionViewportPosition(logical_index)
        global_pos = self.mapToGlobal(QRect(viewport_pos, 0, 0, 0).topLeft())

        popup.move(global_pos.x(), global_pos.y() + header.height())
        popup.setClearEnabled(self._filter_proxy.isColumnFiltered(logical_index))
        popup.exec()

    @Slot(int)
    def _on_section_clicked(self, logical_index: int) -> None:
        """Handles header clicks to show the filter popup.

        Args:
            logical_index (int): Column index clicked.
        """
        self.selectColumn(logical_index)

    @Slot(int)
    def _apply_filter(self, logical_index: int) -> None:
        """Applies the selected filter from the popup to the proxy model.

        Args:
            logical_index (int): Column index.
        """
        popup = self._popups.get(logical_index)
        if not popup:
            return

        if popup.isFiltering():
            filter_data = popup.getSelectedData()
            self._filter_proxy.setFilter(logical_index, filter_data)

        self._update_header_icon(logical_index)

    @Slot(int)
    def _clear_filter(self, logical_index: int) -> None:
        """Clears the filter for the specified column.

        Args:
            logical_index (int): Column index.
        """
        self._filter_proxy.setFilter(logical_index, None)
        self._update_header_icon(logical_index)

    def _update_header_icon(self, logical_index: int) -> None:
        """Updates the header icon to reflect if a filter is active.

        Args:
            logical_index (int): Column index.
        """
        if logical_index not in self._popups:
            return

        # The icon reflects if there is an ACTIVE filter in the popup
        icon_name = (
            "fa6s.filter"
            if self._filter_proxy.isColumnFiltered(logical_index)
            else "fa6s.angle-down"
        )
        icon = QThemeResponsiveIcon.fromAwesome(icon_name)

        # Use the proxy to set the header data. This works for QSqlTableModel and others
        # that might not support setting header icons directly or easily.
        self._header_proxy.setHeaderData(
            logical_index,
            Qt.Orientation.Horizontal,
            icon,
            Qt.ItemDataRole.DecorationRole,
        )

    # --- Smart Data Logic ---

    # --- Model Signals ---

    def _connect_model_signals(self, model: QAbstractItemModel) -> None:
        """Connects signals to react to model changes.

        Args:
            model (QAbstractItemModel): The model to connect to.
        """
        model.columnsInserted.connect(self._on_columns_inserted)
        model.columnsRemoved.connect(self._on_columns_removed)
        model.modelReset.connect(self._refresh_popups)

    def _disconnect_model_signals(self, model: QAbstractItemModel) -> None:
        """Disconnects signals from the model.

        Args:
            model (QAbstractItemModel): The model to disconnect from.
        """
        try:
            model.columnsInserted.disconnect(self._on_columns_inserted)
            model.columnsRemoved.disconnect(self._on_columns_removed)
            model.modelReset.disconnect(self._refresh_popups)
        except RuntimeError:
            pass

    def _on_columns_inserted(self, _: QModelIndex, start: int, end: int) -> None:
        """Handles columns inserted in the model.

        Args:
            start (int): Start index.
            end (int): End index.
        """
        for i in range(start, end + 1):
            self._create_popup(i)

    @Slot()
    def _on_columns_removed(self) -> None:
        """Handles columns removed from the model.

        """
        self._refresh_popups()

__init__(parent=None)

Initializes the filterable table.

Parameters:

Name Type Description Default
parent QWidget

Parent widget. Defaults to None.

None
Source code in source/qextrawidgets/widgets/views/filterable_table_view.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
def __init__(self, parent: typing.Optional[QWidget] = None) -> None:
    """Initializes the filterable table.

    Args:
        parent (QWidget, optional): Parent widget. Defaults to None.
    """
    super().__init__(parent)

    self._filter_proxy = QMultiFilterProxyModel()
    self._header_proxy = QHeaderProxyModel()
    self._header_proxy.setSourceModel(self._filter_proxy)

    super().setModel(self._header_proxy)
    self._popups: typing.Dict[int, QFilterPopup] = {}

    header = QFilterHeaderView(Qt.Orientation.Horizontal, self)
    # header.setSectionsClickable(False) is set in header __init__
    header.filterClicked.connect(self._on_header_clicked)
    header.sectionClicked.connect(self._on_section_clicked)
    self.setHorizontalHeader(header)

    self.setModel(QStandardItemModel(self))

model()

Returns the source model (not the proxy).

Returns:

Name Type Description
QAbstractItemModel QAbstractItemModel

The source model.

Source code in source/qextrawidgets/widgets/views/filterable_table_view.py
61
62
63
64
65
66
67
def model(self) -> QAbstractItemModel:
    """Returns the source model (not the proxy).

    Returns:
        QAbstractItemModel: The source model.
    """
    return self._filter_proxy.sourceModel()

setModel(model)

Sets the source model for the table and initializes filters.

Parameters:

Name Type Description Default
model Optional[QAbstractItemModel]

The data model to display.

required
Source code in source/qextrawidgets/widgets/views/filterable_table_view.py
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
def setModel(self, model: typing.Optional[QAbstractItemModel]) -> None:
    """Sets the source model for the table and initializes filters.

    Args:
        model (Optional[QAbstractItemModel]): The data model to display.
    """
    if model is None:
        return

    if self._filter_proxy.sourceModel():
        self._disconnect_model_signals(self._filter_proxy.sourceModel())

    self._filter_proxy.setSourceModel(model)

    if model:
        self._connect_model_signals(model)

    self._refresh_popups()

QGridIconView

Bases: QAbstractItemView

A custom item view that displays items in a grid layout.

Uses QPersistentModelIndex for internal caching and QTimer for layout debouncing.

Source code in source/qextrawidgets/widgets/views/grid_icon_view.py
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
class QGridIconView(QAbstractItemView):
    """
    A custom item view that displays items in a grid layout.

    Uses QPersistentModelIndex for internal caching and QTimer for layout debouncing.
    """

    itemEntered = Signal(QModelIndex)
    itemExited = Signal(QModelIndex)
    itemClicked = Signal(QModelIndex)

    def __init__(
        self,
        parent: typing.Optional[QWidget] = None,
        icon_size: QSize = QSize(100, 100),
        margin: int = 8,
    ):
        """
        Initialize the QGridIconView.

        Args:
            parent (Optional[QWidget]): The parent widget.
            icon_size (QSize): The size of the icons in the grid. Defaults to 100x100.
            margin (int): The margin between items. Defaults to 8.
        """
        super().__init__(parent)

        # Cache using Persistent Indices
        self._item_rects: dict[QPersistentModelIndex, QRect] = {}

        self._hidden_rows: typing.List[int] = []

        # Debounce Timer for Layout Updates
        self._layout_timer = QTimer(self)
        self._layout_timer.setSingleShot(True)
        self._layout_timer.setInterval(0)
        self._layout_timer.timeout.connect(self._execute_delayed_layout)

        # View State
        self._hover_index: QPersistentModelIndex = QPersistentModelIndex()

        # Layout Configuration
        self._margin: int = margin

        self.setIconSize(icon_size)

        # Mouse Tracking
        self.setMouseTracking(True)
        self.viewport().setMouseTracking(True)
        self.viewport().setAttribute(Qt.WidgetAttribute.WA_Hover)
        self.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)

        # Disable default AutoScroll to prevent unintentional scrolling on click-drag
        self.setAutoScroll(False)

        # Connect Scroll Signals
        self.verticalScrollBar().valueChanged.connect(self._on_scroll_value_changed)

        # Set Delegate (can be overridden)
        self.setItemDelegate(QGridIconDelegate(self))

    # -------------------------------------------------------------------------
    # Public API
    # -------------------------------------------------------------------------

    def itemDelegate(
        self, _: typing.Union[QModelIndex, QPersistentModelIndex, None] = None
    ) -> QGridIconDelegate:
        """Returns the item delegate used by the view."""
        return typing.cast(QGridIconDelegate, super().itemDelegate())

    def setIconSize(self, size: QSize) -> None:
        """
        Set the size of the icons in the grid view.

        Args:
            size (QSize): The new size for the icons.
        """
        super().setIconSize(size)
        self._schedule_layout()

    def setMargin(self, margin: int) -> None:
        """
        Set the margin between items.

        Args:
            margin (int): The new margin value in pixels.
        """
        if self._margin == margin:
            return
        self._margin = margin
        self._schedule_layout()

    def margin(self) -> int:
        """
        Get the current margin between items.

        Returns:
            int: The current margin in pixels.
        """
        return self._margin

    def setRowHidden(self, row: int, hidden: bool) -> None:
        """
        Hide/show the row from the user view.

        Args:
            row (int): The row to hide/show.
            hidden (bool): Whether the row should be hidden.
        """
        if hidden:
            if not self.isRowHidden(row):
                self._hidden_rows.append(row)
        else:
            if self.isRowHidden(row):
                self._hidden_rows.remove(row)

        self._schedule_layout()

    def isRowHidden(self, row: int) -> bool:
        return row in self._hidden_rows

    # -------------------------------------------------------------------------
    # Internal Logic Helpers
    # -------------------------------------------------------------------------

    @staticmethod
    def _persistent_to_index(persistent: QPersistentModelIndex) -> QModelIndex:
        """Helper to convert QPersistentModelIndex to QModelIndex (workaround for PySide6)."""
        if not persistent.isValid():
            return QModelIndex()
        model = persistent.model()
        if not model:
            return QModelIndex()
        return model.index(persistent.row(), persistent.column(), persistent.parent())

    @Slot()
    def _on_scroll_value_changed(self) -> None:
        self._recalculate_hover()
        self.viewport().update()

    def _recalculate_hover(self) -> None:
        if not self._item_rects:
            return

        pos_global = QCursor.pos()
        pos_local = self.viewport().mapFromGlobal(pos_global)

        if self.viewport().rect().contains(pos_local):
            new_index_temp = self.indexAt(pos_local)
        else:
            new_index_temp = QModelIndex()

        new_persistent = QPersistentModelIndex(new_index_temp)

        if new_persistent != self._hover_index:
            if self._hover_index.isValid():
                self.itemExited.emit(self._persistent_to_index(self._hover_index))

            self._hover_index = new_persistent

            if self._hover_index.isValid():
                self.itemEntered.emit(self._persistent_to_index(self._hover_index))

            if not self.verticalScrollBar().isSliderDown():
                self.viewport().update()

    def _init_option(self, option: QStyleOptionViewItem, index: QModelIndex) -> None:
        """
        Initialize the style option for the given index.

        Args:
            option (QStyleOptionViewItem): The option to initialize.
            index (QModelIndex): The index of the item.
        """
        p_index = QPersistentModelIndex(index)
        rect = self._item_rects.get(p_index)
        if not rect:
            return

        scroll_y = self.verticalScrollBar().value()
        visual_rect = rect.translated(0, -scroll_y)

        # Optimization: We check intersections in paintEvent loop usually,
        # but here we just set the rect. The caller (paintEvent) already checks visibility.
        setattr(option, "rect", visual_rect)

        state = QStyle.StateFlag.State_None

        if self.isEnabled():
            state |= QStyle.StateFlag.State_Enabled

        if self.selectionModel().isSelected(index):
            state |= QStyle.StateFlag.State_Selected

        if p_index == self._hover_index:
            state |= QStyle.StateFlag.State_MouseOver

        setattr(option, "state", state)

    # -------------------------------------------------------------------------
    # Layout Scheduling & Cache Management
    # -------------------------------------------------------------------------

    def _schedule_layout(self) -> None:
        if not self._layout_timer.isActive():
            self._layout_timer.start()

    def _execute_delayed_layout(self) -> None:
        self.updateGeometries()
        self.viewport().update()

    def _clear_cache(self, *args) -> None:
        self._item_rects.clear()
        self._hover_index = QPersistentModelIndex()
        self.viewport().update()

    def setModel(self, model: typing.Optional[QAbstractItemModel]) -> None:
        """
        Set the model for the view.

        Connects to necessary signals for handling layout updates and structural changes.

        Args:
            model (Optional[QAbstractItemModel]): The model to be set.
        """
        current_model = self.model()
        if current_model == model:
            return

        if current_model:
            try:
                current_model.layoutChanged.disconnect(self._on_layout_changed)
                current_model.modelReset.disconnect(self._on_model_reset)
                current_model.rowsInserted.disconnect(self._on_rows_inserted)
                current_model.rowsRemoved.disconnect(self._on_rows_removed)
                current_model.dataChanged.disconnect(self._on_data_changed)

                current_model.layoutAboutToBeChanged.disconnect(self._clear_cache)
                current_model.rowsAboutToBeRemoved.disconnect(self._clear_cache)
            except Exception:
                pass

        # Disconnect from old selection model
        old_selection_model = self.selectionModel()
        if old_selection_model:
            try:
                old_selection_model.selectionChanged.disconnect(
                    self._on_selection_changed
                )
                old_selection_model.currentChanged.disconnect(self._on_current_changed)
            except Exception:
                pass

        super().setModel(model)

        if model:
            model.layoutAboutToBeChanged.connect(self._clear_cache)
            model.rowsAboutToBeRemoved.connect(self._clear_cache)

            model.layoutChanged.connect(self._on_layout_changed)
            model.modelReset.connect(self._on_model_reset)
            model.rowsInserted.connect(self._on_rows_inserted)
            model.rowsRemoved.connect(self._on_rows_removed)
            model.dataChanged.connect(self._on_data_changed)

        # Connect to new selection model
        new_selection_model = self.selectionModel()
        if new_selection_model:
            new_selection_model.selectionChanged.connect(self._on_selection_changed)
            new_selection_model.currentChanged.connect(self._on_current_changed)

    @Slot()
    def _on_layout_changed(self) -> None:
        self._schedule_layout()

    @Slot()
    def _on_model_reset(self) -> None:
        self._schedule_layout()

    @Slot()
    def _on_rows_inserted(self):
        self._schedule_layout()

    @Slot()
    def _on_rows_removed(self):
        self._schedule_layout()

    def _on_data_changed(
        self,
        top_left: QModelIndex,
        _: QModelIndex,
        roles: typing.Optional[list[int]] = None,
    ) -> None:
        if roles is None:
            roles = []

        # [CRUCIAL] Se for uma mudança de dados (como o ícone chegando), força a repintura!
        if not roles or Qt.ItemDataRole.DecorationRole in roles:
            self.update(top_left)

    @Slot()
    def _on_selection_changed(self) -> None:
        """Handle selection changes to update visual feedback."""
        self.viewport().update()

    @Slot()
    def _on_current_changed(self) -> None:
        """Handle current index changes (e.g., from setCurrentIndex) to update visual feedback."""
        self.viewport().update()

    # -------------------------------------------------------------------------
    # Event Handlers
    # -------------------------------------------------------------------------

    def mousePressEvent(self, event: QMouseEvent) -> None:
        """
        Handle mouse press events.

        Args:
            event (QMouseEvent): The mouse event.
        """
        if not self._item_rects:
            return

        index = self.indexAt(event.position().toPoint())

        if index.isValid() and event.button() == Qt.MouseButton.LeftButton:
            self.itemClicked.emit(index)

        super().mousePressEvent(event)

    def mouseMoveEvent(self, event: QMouseEvent) -> None:
        """
        Handle mouse move events to track hover state.

        Args:
            event (QMouseEvent): The mouse event.
        """
        self._recalculate_hover()
        super().mouseMoveEvent(event)

    def leaveEvent(self, event: QEvent) -> None:
        """
        Handle mouse leave events to reset hover state.

        Args:
            event (QEvent): The leave event.
        """
        if self._hover_index.isValid():
            self.itemExited.emit(self._persistent_to_index(self._hover_index))
        self._hover_index = QPersistentModelIndex()
        self.viewport().update()
        super().leaveEvent(event)

    # noinspection PyUnresolvedReferences
    def paintEvent(self, event: QPaintEvent) -> None:
        """
        Paint the items in the view.

        Args:
            event (QPaintEvent): The paint event.
        """
        if not self._item_rects:
            return

        painter = QPainter(self.viewport())
        option = QStyleOptionViewItem()
        option.initFrom(self)
        setattr(option, "widget", self)

        viewport_rect = self.viewport().rect()
        # Use singleStep * 2 as a reasonable buffer based on scroll speed/granularity
        preload_margin = self.verticalScrollBar().singleStep() * 2
        visible_rect = viewport_rect.adjusted(0, -preload_margin, 0, preload_margin)

        for p_index, rect in self._item_rects.items():
            if not p_index.isValid():
                continue

            index = self._persistent_to_index(p_index)
            if not index.isValid():
                continue

            self._init_option(option, index)

            # Optimization: Check if item is visible in viewport before painting
            # option.rect is already translated by scroll position in _init_option
            visual_rect = typing.cast(QRect, option.rect)
            if not visual_rect.intersects(visible_rect):
                continue

            self.itemDelegate(index).paint(painter, option, index)

    # -------------------------------------------------------------------------
    # QAbstractItemView Implementation
    # -------------------------------------------------------------------------

    def updateGeometries(self) -> None:
        """
        Recalculate the layout of item rectangles and update scrollbars.
        Assumes a flat model structure.
        """
        if not self.model():
            return

        self._item_rects.clear()
        width = self.viewport().width()

        item_w = self.iconSize().width()
        item_h = self.iconSize().height()

        effective_width = width - (2 * self._margin)
        cols = max(1, effective_width // (item_w + self._margin))
        root = self.rootIndex()

        row_count = self.model().rowCount(root)

        col_current = 0
        y = self._margin

        for r in range(row_count):
            index = self.model().index(r, 0, root)
            if not index.isValid():
                continue

            if self.isRowHidden(r):
                continue

            px = self._margin + (col_current * (item_w + self._margin))
            self._item_rects[QPersistentModelIndex(index)] = QRect(
                px, y, item_w, item_h
            )

            col_current += 1
            if col_current >= cols:
                col_current = 0
                y += item_h + self._margin

        if col_current != 0:
            y += item_h + self._margin

        content_height = y
        scroll_range = max(0, content_height - self.viewport().height())

        self.verticalScrollBar().setRange(0, scroll_range)
        self.verticalScrollBar().setPageStep(self.viewport().height())
        # Set a reasonable single step, maybe icon height or a fraction of it
        self.verticalScrollBar().setSingleStep(item_h // 2)

        super().updateGeometries()

    def visualRect(
        self, index: typing.Union[QModelIndex, QPersistentModelIndex]
    ) -> QRect:
        """
        Return the rectangle on the viewport occupied by the item at index.

        Args:
            index (QModelIndex | QPersistentModelIndex): The index of the item.

        Returns:
            QRect: The visual rectangle.
        """
        p_index = QPersistentModelIndex(index)
        rect = self._item_rects.get(p_index)
        if rect:
            return rect.translated(0, -self.verticalScrollBar().value())
        return QRect()

    def indexAt(self, point: QPoint) -> QModelIndex:
        """
        Return the model index of the item at the viewport coordinates point.

        Args:
            point (QPoint): The coordinates in the viewport.

        Returns:
            QModelIndex: The index at the given point, or valid if not found.
        """
        if not self._item_rects:
            return QModelIndex()

        real_point = point + QPoint(0, self.verticalScrollBar().value())
        for p_index, rect in self._item_rects.items():
            if rect.contains(real_point):
                return self._persistent_to_index(p_index)
        return QModelIndex()

    def scrollTo(
        self,
        index: typing.Union[QModelIndex, QPersistentModelIndex],
        hint: QAbstractItemView.ScrollHint = QAbstractItemView.ScrollHint.EnsureVisible,
    ) -> None:
        """
        Scroll the view to ensure the item at index is visible.

        Args:
            index (QModelIndex | QPersistentModelIndex): The index to scroll to.
            hint (QAbstractItemView.ScrollHint): The scroll hint.
        """
        p_index = QPersistentModelIndex(index)
        rect = self._item_rects.get(p_index)
        if not rect:
            return

        scroll_val = self.verticalScrollBar().value()
        viewport_height = self.viewport().height()

        item_top = rect.y()
        item_bottom = rect.bottom()

        if hint == QAbstractItemView.ScrollHint.EnsureVisible:
            if item_top < scroll_val:
                self.verticalScrollBar().setValue(item_top)
            elif item_bottom > scroll_val + viewport_height:
                self.verticalScrollBar().setValue(item_bottom - viewport_height)

        elif hint == QAbstractItemView.ScrollHint.PositionAtTop:
            self.verticalScrollBar().setValue(item_top)

        elif hint == QAbstractItemView.ScrollHint.PositionAtBottom:
            self.verticalScrollBar().setValue(item_bottom - viewport_height)

        elif hint == QAbstractItemView.ScrollHint.PositionAtCenter:
            center_target = int(item_top - (viewport_height / 2) + (rect.height() / 2))
            self.verticalScrollBar().setValue(center_target)

    # -------------------------------------------------------------------------
    # Abstract Stubs
    # -------------------------------------------------------------------------

    def horizontalOffset(self) -> int:
        """Return the horizontal offset of the view (always 0 for this view)."""
        return 0

    def verticalOffset(self) -> int:
        """Return the vertical offset of the view."""
        return self.verticalScrollBar().value()

    def moveCursor(self, cursor_action, modifiers) -> QModelIndex:
        """
        Move the cursor in response to key navigation (Not implemented).

        Returns:
            QModelIndex: An invalid index.
        """
        return QModelIndex()

    def setSelection(
        self, rect: QRect, command: QItemSelectionModel.SelectionFlag
    ) -> None:
        """
        Apply selection to items within the rectangle.

        Args:
            rect (QRect): The rectangle in viewport coordinates.
            command (QItemSelectionModel.SelectionFlag): The selection command.
        """
        if not self.model():
            return

        selection = QItemSelection()

        # Transform viewport rect to logical coordinates
        scroll_y = self.verticalScrollBar().value()
        logical_rect = rect.translated(0, scroll_y)

        for p_index, item_rect in self._item_rects.items():
            if not p_index.isValid():
                continue

            if item_rect.intersects(logical_rect):
                index = self._persistent_to_index(p_index)
                if index.isValid():
                    selection.select(index, index)

        self.selectionModel().select(selection, command)

        # Force update to show selection changes
        self.viewport().update()

    def visualRegionForSelection(self, selection: QItemSelection) -> QRegion:
        """
        Return the region covered by the selection.

        Args:
            selection (QItemSelection): The selection to get the region for.

        Returns:
            QRegion: The region covered by the selection in viewport coordinates.
        """
        region = QRegion()

        if not self._item_rects:
            return region

        scroll_y = self.verticalScrollBar().value()

        for index in selection.indexes():
            p_index = QPersistentModelIndex(index)
            item_rect = self._item_rects.get(p_index)

            if item_rect:
                visual_rect = item_rect.translated(0, -scroll_y)
                region = region.united(visual_rect)

        return region

    def isIndexHidden(
        self, index: typing.Union[QModelIndex, QPersistentModelIndex]
    ) -> bool:
        """
        Return True if the item referred to by index is hidden; otherwise returns False.
        """
        # In the simple grid view, usually nothing is hidden unless filtered by model
        # or if we implement filtering here.
        if not index.isValid():
            return True
        return False

__init__(parent=None, icon_size=QSize(100, 100), margin=8)

Initialize the QGridIconView.

Parameters:

Name Type Description Default
parent Optional[QWidget]

The parent widget.

None
icon_size QSize

The size of the icons in the grid. Defaults to 100x100.

QSize(100, 100)
margin int

The margin between items. Defaults to 8.

8
Source code in source/qextrawidgets/widgets/views/grid_icon_view.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
def __init__(
    self,
    parent: typing.Optional[QWidget] = None,
    icon_size: QSize = QSize(100, 100),
    margin: int = 8,
):
    """
    Initialize the QGridIconView.

    Args:
        parent (Optional[QWidget]): The parent widget.
        icon_size (QSize): The size of the icons in the grid. Defaults to 100x100.
        margin (int): The margin between items. Defaults to 8.
    """
    super().__init__(parent)

    # Cache using Persistent Indices
    self._item_rects: dict[QPersistentModelIndex, QRect] = {}

    self._hidden_rows: typing.List[int] = []

    # Debounce Timer for Layout Updates
    self._layout_timer = QTimer(self)
    self._layout_timer.setSingleShot(True)
    self._layout_timer.setInterval(0)
    self._layout_timer.timeout.connect(self._execute_delayed_layout)

    # View State
    self._hover_index: QPersistentModelIndex = QPersistentModelIndex()

    # Layout Configuration
    self._margin: int = margin

    self.setIconSize(icon_size)

    # Mouse Tracking
    self.setMouseTracking(True)
    self.viewport().setMouseTracking(True)
    self.viewport().setAttribute(Qt.WidgetAttribute.WA_Hover)
    self.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)

    # Disable default AutoScroll to prevent unintentional scrolling on click-drag
    self.setAutoScroll(False)

    # Connect Scroll Signals
    self.verticalScrollBar().valueChanged.connect(self._on_scroll_value_changed)

    # Set Delegate (can be overridden)
    self.setItemDelegate(QGridIconDelegate(self))

horizontalOffset()

Return the horizontal offset of the view (always 0 for this view).

Source code in source/qextrawidgets/widgets/views/grid_icon_view.py
555
556
557
def horizontalOffset(self) -> int:
    """Return the horizontal offset of the view (always 0 for this view)."""
    return 0

indexAt(point)

Return the model index of the item at the viewport coordinates point.

Parameters:

Name Type Description Default
point QPoint

The coordinates in the viewport.

required

Returns:

Name Type Description
QModelIndex QModelIndex

The index at the given point, or valid if not found.

Source code in source/qextrawidgets/widgets/views/grid_icon_view.py
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
def indexAt(self, point: QPoint) -> QModelIndex:
    """
    Return the model index of the item at the viewport coordinates point.

    Args:
        point (QPoint): The coordinates in the viewport.

    Returns:
        QModelIndex: The index at the given point, or valid if not found.
    """
    if not self._item_rects:
        return QModelIndex()

    real_point = point + QPoint(0, self.verticalScrollBar().value())
    for p_index, rect in self._item_rects.items():
        if rect.contains(real_point):
            return self._persistent_to_index(p_index)
    return QModelIndex()

isIndexHidden(index)

Return True if the item referred to by index is hidden; otherwise returns False.

Source code in source/qextrawidgets/widgets/views/grid_icon_view.py
632
633
634
635
636
637
638
639
640
641
642
def isIndexHidden(
    self, index: typing.Union[QModelIndex, QPersistentModelIndex]
) -> bool:
    """
    Return True if the item referred to by index is hidden; otherwise returns False.
    """
    # In the simple grid view, usually nothing is hidden unless filtered by model
    # or if we implement filtering here.
    if not index.isValid():
        return True
    return False

itemDelegate(_=None)

Returns the item delegate used by the view.

Source code in source/qextrawidgets/widgets/views/grid_icon_view.py
88
89
90
91
92
def itemDelegate(
    self, _: typing.Union[QModelIndex, QPersistentModelIndex, None] = None
) -> QGridIconDelegate:
    """Returns the item delegate used by the view."""
    return typing.cast(QGridIconDelegate, super().itemDelegate())

leaveEvent(event)

Handle mouse leave events to reset hover state.

Parameters:

Name Type Description Default
event QEvent

The leave event.

required
Source code in source/qextrawidgets/widgets/views/grid_icon_view.py
365
366
367
368
369
370
371
372
373
374
375
376
def leaveEvent(self, event: QEvent) -> None:
    """
    Handle mouse leave events to reset hover state.

    Args:
        event (QEvent): The leave event.
    """
    if self._hover_index.isValid():
        self.itemExited.emit(self._persistent_to_index(self._hover_index))
    self._hover_index = QPersistentModelIndex()
    self.viewport().update()
    super().leaveEvent(event)

margin()

Get the current margin between items.

Returns:

Name Type Description
int int

The current margin in pixels.

Source code in source/qextrawidgets/widgets/views/grid_icon_view.py
116
117
118
119
120
121
122
123
def margin(self) -> int:
    """
    Get the current margin between items.

    Returns:
        int: The current margin in pixels.
    """
    return self._margin

mouseMoveEvent(event)

Handle mouse move events to track hover state.

Parameters:

Name Type Description Default
event QMouseEvent

The mouse event.

required
Source code in source/qextrawidgets/widgets/views/grid_icon_view.py
355
356
357
358
359
360
361
362
363
def mouseMoveEvent(self, event: QMouseEvent) -> None:
    """
    Handle mouse move events to track hover state.

    Args:
        event (QMouseEvent): The mouse event.
    """
    self._recalculate_hover()
    super().mouseMoveEvent(event)

mousePressEvent(event)

Handle mouse press events.

Parameters:

Name Type Description Default
event QMouseEvent

The mouse event.

required
Source code in source/qextrawidgets/widgets/views/grid_icon_view.py
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
def mousePressEvent(self, event: QMouseEvent) -> None:
    """
    Handle mouse press events.

    Args:
        event (QMouseEvent): The mouse event.
    """
    if not self._item_rects:
        return

    index = self.indexAt(event.position().toPoint())

    if index.isValid() and event.button() == Qt.MouseButton.LeftButton:
        self.itemClicked.emit(index)

    super().mousePressEvent(event)

moveCursor(cursor_action, modifiers)

Move the cursor in response to key navigation (Not implemented).

Returns:

Name Type Description
QModelIndex QModelIndex

An invalid index.

Source code in source/qextrawidgets/widgets/views/grid_icon_view.py
563
564
565
566
567
568
569
570
def moveCursor(self, cursor_action, modifiers) -> QModelIndex:
    """
    Move the cursor in response to key navigation (Not implemented).

    Returns:
        QModelIndex: An invalid index.
    """
    return QModelIndex()

paintEvent(event)

Paint the items in the view.

Parameters:

Name Type Description Default
event QPaintEvent

The paint event.

required
Source code in source/qextrawidgets/widgets/views/grid_icon_view.py
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
def paintEvent(self, event: QPaintEvent) -> None:
    """
    Paint the items in the view.

    Args:
        event (QPaintEvent): The paint event.
    """
    if not self._item_rects:
        return

    painter = QPainter(self.viewport())
    option = QStyleOptionViewItem()
    option.initFrom(self)
    setattr(option, "widget", self)

    viewport_rect = self.viewport().rect()
    # Use singleStep * 2 as a reasonable buffer based on scroll speed/granularity
    preload_margin = self.verticalScrollBar().singleStep() * 2
    visible_rect = viewport_rect.adjusted(0, -preload_margin, 0, preload_margin)

    for p_index, rect in self._item_rects.items():
        if not p_index.isValid():
            continue

        index = self._persistent_to_index(p_index)
        if not index.isValid():
            continue

        self._init_option(option, index)

        # Optimization: Check if item is visible in viewport before painting
        # option.rect is already translated by scroll position in _init_option
        visual_rect = typing.cast(QRect, option.rect)
        if not visual_rect.intersects(visible_rect):
            continue

        self.itemDelegate(index).paint(painter, option, index)

scrollTo(index, hint=QAbstractItemView.ScrollHint.EnsureVisible)

Scroll the view to ensure the item at index is visible.

Parameters:

Name Type Description Default
index QModelIndex | QPersistentModelIndex

The index to scroll to.

required
hint ScrollHint

The scroll hint.

EnsureVisible
Source code in source/qextrawidgets/widgets/views/grid_icon_view.py
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
def scrollTo(
    self,
    index: typing.Union[QModelIndex, QPersistentModelIndex],
    hint: QAbstractItemView.ScrollHint = QAbstractItemView.ScrollHint.EnsureVisible,
) -> None:
    """
    Scroll the view to ensure the item at index is visible.

    Args:
        index (QModelIndex | QPersistentModelIndex): The index to scroll to.
        hint (QAbstractItemView.ScrollHint): The scroll hint.
    """
    p_index = QPersistentModelIndex(index)
    rect = self._item_rects.get(p_index)
    if not rect:
        return

    scroll_val = self.verticalScrollBar().value()
    viewport_height = self.viewport().height()

    item_top = rect.y()
    item_bottom = rect.bottom()

    if hint == QAbstractItemView.ScrollHint.EnsureVisible:
        if item_top < scroll_val:
            self.verticalScrollBar().setValue(item_top)
        elif item_bottom > scroll_val + viewport_height:
            self.verticalScrollBar().setValue(item_bottom - viewport_height)

    elif hint == QAbstractItemView.ScrollHint.PositionAtTop:
        self.verticalScrollBar().setValue(item_top)

    elif hint == QAbstractItemView.ScrollHint.PositionAtBottom:
        self.verticalScrollBar().setValue(item_bottom - viewport_height)

    elif hint == QAbstractItemView.ScrollHint.PositionAtCenter:
        center_target = int(item_top - (viewport_height / 2) + (rect.height() / 2))
        self.verticalScrollBar().setValue(center_target)

setIconSize(size)

Set the size of the icons in the grid view.

Parameters:

Name Type Description Default
size QSize

The new size for the icons.

required
Source code in source/qextrawidgets/widgets/views/grid_icon_view.py
 94
 95
 96
 97
 98
 99
100
101
102
def setIconSize(self, size: QSize) -> None:
    """
    Set the size of the icons in the grid view.

    Args:
        size (QSize): The new size for the icons.
    """
    super().setIconSize(size)
    self._schedule_layout()

setMargin(margin)

Set the margin between items.

Parameters:

Name Type Description Default
margin int

The new margin value in pixels.

required
Source code in source/qextrawidgets/widgets/views/grid_icon_view.py
104
105
106
107
108
109
110
111
112
113
114
def setMargin(self, margin: int) -> None:
    """
    Set the margin between items.

    Args:
        margin (int): The new margin value in pixels.
    """
    if self._margin == margin:
        return
    self._margin = margin
    self._schedule_layout()

setModel(model)

Set the model for the view.

Connects to necessary signals for handling layout updates and structural changes.

Parameters:

Name Type Description Default
model Optional[QAbstractItemModel]

The model to be set.

required
Source code in source/qextrawidgets/widgets/views/grid_icon_view.py
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
def setModel(self, model: typing.Optional[QAbstractItemModel]) -> None:
    """
    Set the model for the view.

    Connects to necessary signals for handling layout updates and structural changes.

    Args:
        model (Optional[QAbstractItemModel]): The model to be set.
    """
    current_model = self.model()
    if current_model == model:
        return

    if current_model:
        try:
            current_model.layoutChanged.disconnect(self._on_layout_changed)
            current_model.modelReset.disconnect(self._on_model_reset)
            current_model.rowsInserted.disconnect(self._on_rows_inserted)
            current_model.rowsRemoved.disconnect(self._on_rows_removed)
            current_model.dataChanged.disconnect(self._on_data_changed)

            current_model.layoutAboutToBeChanged.disconnect(self._clear_cache)
            current_model.rowsAboutToBeRemoved.disconnect(self._clear_cache)
        except Exception:
            pass

    # Disconnect from old selection model
    old_selection_model = self.selectionModel()
    if old_selection_model:
        try:
            old_selection_model.selectionChanged.disconnect(
                self._on_selection_changed
            )
            old_selection_model.currentChanged.disconnect(self._on_current_changed)
        except Exception:
            pass

    super().setModel(model)

    if model:
        model.layoutAboutToBeChanged.connect(self._clear_cache)
        model.rowsAboutToBeRemoved.connect(self._clear_cache)

        model.layoutChanged.connect(self._on_layout_changed)
        model.modelReset.connect(self._on_model_reset)
        model.rowsInserted.connect(self._on_rows_inserted)
        model.rowsRemoved.connect(self._on_rows_removed)
        model.dataChanged.connect(self._on_data_changed)

    # Connect to new selection model
    new_selection_model = self.selectionModel()
    if new_selection_model:
        new_selection_model.selectionChanged.connect(self._on_selection_changed)
        new_selection_model.currentChanged.connect(self._on_current_changed)

setRowHidden(row, hidden)

Hide/show the row from the user view.

Parameters:

Name Type Description Default
row int

The row to hide/show.

required
hidden bool

Whether the row should be hidden.

required
Source code in source/qextrawidgets/widgets/views/grid_icon_view.py
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
def setRowHidden(self, row: int, hidden: bool) -> None:
    """
    Hide/show the row from the user view.

    Args:
        row (int): The row to hide/show.
        hidden (bool): Whether the row should be hidden.
    """
    if hidden:
        if not self.isRowHidden(row):
            self._hidden_rows.append(row)
    else:
        if self.isRowHidden(row):
            self._hidden_rows.remove(row)

    self._schedule_layout()

setSelection(rect, command)

Apply selection to items within the rectangle.

Parameters:

Name Type Description Default
rect QRect

The rectangle in viewport coordinates.

required
command SelectionFlag

The selection command.

required
Source code in source/qextrawidgets/widgets/views/grid_icon_view.py
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
def setSelection(
    self, rect: QRect, command: QItemSelectionModel.SelectionFlag
) -> None:
    """
    Apply selection to items within the rectangle.

    Args:
        rect (QRect): The rectangle in viewport coordinates.
        command (QItemSelectionModel.SelectionFlag): The selection command.
    """
    if not self.model():
        return

    selection = QItemSelection()

    # Transform viewport rect to logical coordinates
    scroll_y = self.verticalScrollBar().value()
    logical_rect = rect.translated(0, scroll_y)

    for p_index, item_rect in self._item_rects.items():
        if not p_index.isValid():
            continue

        if item_rect.intersects(logical_rect):
            index = self._persistent_to_index(p_index)
            if index.isValid():
                selection.select(index, index)

    self.selectionModel().select(selection, command)

    # Force update to show selection changes
    self.viewport().update()

updateGeometries()

Recalculate the layout of item rectangles and update scrollbars. Assumes a flat model structure.

Source code in source/qextrawidgets/widgets/views/grid_icon_view.py
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
def updateGeometries(self) -> None:
    """
    Recalculate the layout of item rectangles and update scrollbars.
    Assumes a flat model structure.
    """
    if not self.model():
        return

    self._item_rects.clear()
    width = self.viewport().width()

    item_w = self.iconSize().width()
    item_h = self.iconSize().height()

    effective_width = width - (2 * self._margin)
    cols = max(1, effective_width // (item_w + self._margin))
    root = self.rootIndex()

    row_count = self.model().rowCount(root)

    col_current = 0
    y = self._margin

    for r in range(row_count):
        index = self.model().index(r, 0, root)
        if not index.isValid():
            continue

        if self.isRowHidden(r):
            continue

        px = self._margin + (col_current * (item_w + self._margin))
        self._item_rects[QPersistentModelIndex(index)] = QRect(
            px, y, item_w, item_h
        )

        col_current += 1
        if col_current >= cols:
            col_current = 0
            y += item_h + self._margin

    if col_current != 0:
        y += item_h + self._margin

    content_height = y
    scroll_range = max(0, content_height - self.viewport().height())

    self.verticalScrollBar().setRange(0, scroll_range)
    self.verticalScrollBar().setPageStep(self.viewport().height())
    # Set a reasonable single step, maybe icon height or a fraction of it
    self.verticalScrollBar().setSingleStep(item_h // 2)

    super().updateGeometries()

verticalOffset()

Return the vertical offset of the view.

Source code in source/qextrawidgets/widgets/views/grid_icon_view.py
559
560
561
def verticalOffset(self) -> int:
    """Return the vertical offset of the view."""
    return self.verticalScrollBar().value()

visualRect(index)

Return the rectangle on the viewport occupied by the item at index.

Parameters:

Name Type Description Default
index QModelIndex | QPersistentModelIndex

The index of the item.

required

Returns:

Name Type Description
QRect QRect

The visual rectangle.

Source code in source/qextrawidgets/widgets/views/grid_icon_view.py
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
def visualRect(
    self, index: typing.Union[QModelIndex, QPersistentModelIndex]
) -> QRect:
    """
    Return the rectangle on the viewport occupied by the item at index.

    Args:
        index (QModelIndex | QPersistentModelIndex): The index of the item.

    Returns:
        QRect: The visual rectangle.
    """
    p_index = QPersistentModelIndex(index)
    rect = self._item_rects.get(p_index)
    if rect:
        return rect.translated(0, -self.verticalScrollBar().value())
    return QRect()

visualRegionForSelection(selection)

Return the region covered by the selection.

Parameters:

Name Type Description Default
selection QItemSelection

The selection to get the region for.

required

Returns:

Name Type Description
QRegion QRegion

The region covered by the selection in viewport coordinates.

Source code in source/qextrawidgets/widgets/views/grid_icon_view.py
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
def visualRegionForSelection(self, selection: QItemSelection) -> QRegion:
    """
    Return the region covered by the selection.

    Args:
        selection (QItemSelection): The selection to get the region for.

    Returns:
        QRegion: The region covered by the selection in viewport coordinates.
    """
    region = QRegion()

    if not self._item_rects:
        return region

    scroll_y = self.verticalScrollBar().value()

    for index in selection.indexes():
        p_index = QPersistentModelIndex(index)
        item_rect = self._item_rects.get(p_index)

        if item_rect:
            visual_rect = item_rect.translated(0, -scroll_y)
            region = region.united(visual_rect)

    return region

QGroupedIconView

Bases: QGridIconView

A custom item view that displays categories as headers (accordion style) and children items in a grid layout using icons.

Uses QPersistentModelIndex for internal caching and QTimer for layout debouncing. The expansion state is stored in the model using ExpansionStateRole.

Source code in source/qextrawidgets/widgets/views/grouped_icon_view.py
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
class QGroupedIconView(QGridIconView):
    """
    A custom item view that displays categories as headers (accordion style)
    and children items in a grid layout using icons.

    Uses QPersistentModelIndex for internal caching and QTimer for layout debouncing.
    The expansion state is stored in the model using ExpansionStateRole.
    """

    def __init__(
        self,
        parent: typing.Optional[QWidget] = None,
        icon_size: QSize = QSize(100, 100),
        margin: int = 8,
        header_height: int = 36,
    ):
        """
        Initialize the QGroupedIconView.

        Args:
            parent (Optional[QWidget]): The parent widget.
            icon_size (QSize): The size of the icons in the grid. Defaults to 100x100.
            margin (int): The margin between items and headers. Defaults to 8.
            header_height (int): The height of the category headers. Defaults to 36.
        """
        super().__init__(parent, icon_size=icon_size, margin=margin)

        # View State
        self._expanded_items: set[QPersistentModelIndex] = set()

        # Layout Configuration
        self._header_height: int = header_height

        # Set Delegate
        self.setItemDelegate(QGroupedIconDelegate(self, arrow_icon=None))

    # -------------------------------------------------------------------------
    # Public API
    # -------------------------------------------------------------------------

    def itemDelegate(
        self, _: typing.Union[QModelIndex, QPersistentModelIndex, None] = None
    ) -> QGroupedIconDelegate:
        """Returns the item delegate used by the view."""
        return typing.cast(QGroupedIconDelegate, super().itemDelegate())

    def setHeaderHeight(self, height: int) -> None:
        """
        Set the height of the category headers.

        Args:
            height (int): The new header height in pixels.
        """
        if self._header_height == height:
            return
        self._header_height = height
        self._schedule_layout()

    def headerHeight(self) -> int:
        """
        Get the current height of the category headers.

        Returns:
            int: The header height in pixels.
        """
        return self._header_height

    def isExpanded(self, index: QModelIndex) -> bool:
        """Return True if the category at index is expanded."""
        if not index.isValid():
            return False
        return QPersistentModelIndex(index) in self._expanded_items

    def setExpanded(self, index: QModelIndex, expanded: bool) -> None:
        """Set the expansion state of the category at index."""
        if not index.isValid():
            return

        p_index = QPersistentModelIndex(index)
        if expanded:
            self._expanded_items.add(p_index)
        else:
            self._expanded_items.discard(p_index)

        self._schedule_layout()

    def expandAll(self) -> None:
        """Expand all categories."""
        if not self.model():
            return

        count = self.model().rowCount()
        for i in range(count):
            index = self.model().index(i, 0)
            self._expanded_items.add(QPersistentModelIndex(index))
        self._schedule_layout()

    def collapseAll(self) -> None:
        """Collapse all categories."""
        self._expanded_items.clear()
        self._schedule_layout()

    # -------------------------------------------------------------------------
    # Internal Logic Helpers
    # -------------------------------------------------------------------------

    @staticmethod
    def _is_category(
        index: typing.Union[QModelIndex, QPersistentModelIndex]
    ) -> bool:
        """Check if the given index represents a category (header)."""
        return index.isValid() and not index.parent().isValid()

    @staticmethod
    def _is_item(index: typing.Union[QModelIndex, QPersistentModelIndex]) -> bool:
        """Check if the given index represents a child item."""
        return index.isValid() and index.parent().isValid()

    def _init_option(self, option: QStyleOptionViewItem, index: QModelIndex) -> None:
        """
        Initialize the style option with expansion state.
        """
        super()._init_option(option, index)
        if self._is_category(index) and self.isExpanded(index):
            state = typing.cast(QStyle.StateFlag, option.state)
            state |= QStyle.StateFlag.State_Open
            setattr(option, "state", state)

    def _clear_cache(self, *args) -> None:
        super()._clear_cache(*args)
        # Clean up invalid persistent indices from expansion set
        self._expanded_items = {pi for pi in self._expanded_items if pi.isValid()}

    def setModel(self, model: typing.Optional[QAbstractItemModel]) -> None:
        """
        Set the model for the view.

        Connects to necessary signals for handling layout updates and structural changes.

        Args:
            model (Optional[QAbstractItemModel]): The model to be set.
        """
        super().setModel(model)
        self._expanded_items.clear()

    def _on_model_reset(self) -> None:
        self._expanded_items.clear()
        super()._on_model_reset()

    # -------------------------------------------------------------------------
    # Event Handlers
    # -------------------------------------------------------------------------

    def mousePressEvent(self, event: QMouseEvent) -> None:
        """
        Handle mouse press events.

        Toggles category expansion or emits itemClicked signal.

        Args:
            event (QMouseEvent): The mouse event.
        """
        if not self._item_rects:
            return

        index = self.indexAt(event.position().toPoint())

        if index.isValid() and event.button() == Qt.MouseButton.LeftButton:
            if self._is_category(index):
                self.setExpanded(index, not self.isExpanded(index))
                event.accept()
                return
            elif self._is_item(index):
                # The base class emits itemClicked, but we also want to handle it here explicitly if needed.
                # Actually base class handles itemClicked emission.
                # But we just return if handled category.
                pass

        super().mousePressEvent(event)

    # -------------------------------------------------------------------------
    # QAbstractItemView Implementation
    # -------------------------------------------------------------------------

    def updateGeometries(self) -> None:
        """
        Recalculate the layout of item rectangles and update scrollbars.
        """
        if not self.model():
            return

        self._item_rects.clear()
        width = self.viewport().width()
        y = 0

        item_w = self.iconSize().width()
        item_h = self.iconSize().height()

        effective_width = width - (2 * self._margin)
        cols = max(1, effective_width // (item_w + self._margin))
        root = self.rootIndex()

        row_count = self.model().rowCount(root)

        for r in range(row_count):
            cat_index = self.model().index(r, 0, root)
            if not cat_index.isValid():
                continue

            is_expanded = self.isExpanded(cat_index)

            self._item_rects[QPersistentModelIndex(cat_index)] = QRect(
                0, y, width, self._header_height
            )
            y += self._header_height

            if is_expanded:
                child_count = self.model().rowCount(cat_index)
                if child_count > 0:
                    y += self._margin
                    col_current = 0

                    for c_row in range(child_count):
                        child = self.model().index(c_row, 0, cat_index)
                        if not child.isValid():
                            continue

                        px = self._margin + (col_current * (item_w + self._margin))
                        self._item_rects[QPersistentModelIndex(child)] = QRect(
                            px, y, item_w, item_h
                        )

                        col_current += 1
                        if col_current >= cols:
                            col_current = 0
                            y += item_h + self._margin

                    if col_current != 0:
                        y += item_h + self._margin

        content_height = y
        scroll_range = max(0, content_height - self.viewport().height())

        self.verticalScrollBar().setRange(0, scroll_range)
        self.verticalScrollBar().setPageStep(self.viewport().height())
        self.verticalScrollBar().setSingleStep(self._header_height)

        # We don't call super().updateGeometries() because we fully implemented it here for the grouped view

    def scrollTo(
        self,
        index: typing.Union[QModelIndex, QPersistentModelIndex],
        hint: QAbstractItemView.ScrollHint = QAbstractItemView.ScrollHint.EnsureVisible,
    ) -> None:
        """
        Scroll the view to ensure the item at index is visible.

        Args:
            index (QModelIndex | QPersistentModelIndex): The index to scroll to.
            hint (QAbstractItemView.ScrollHint): The scroll hint.
        """
        p_index = QPersistentModelIndex(index)
        rect = self._item_rects.get(p_index)
        if not rect:
            return

        # [CHANGED] Hybrid Behavior:
        # 1. Categories: Always force scroll to top (Classic Behavior).
        if self._is_category(index):
            self.verticalScrollBar().setValue(rect.y())
            return

        # 2. Items: Smart scroll (New Behavior) - Only scroll if needed.
        # Use base class implementation for items
        super().scrollTo(index, hint)

    def isIndexHidden(
        self, index: typing.Union[QModelIndex, QPersistentModelIndex]
    ) -> bool:
        """
        Return True if the item referred to by index is hidden; otherwise returns False.
        """
        if not index.isValid():
            return True

        if self._is_category(index):
            return False

        # Check if parent category is expanded
        return not self.isExpanded(index.parent())

__init__(parent=None, icon_size=QSize(100, 100), margin=8, header_height=36)

Initialize the QGroupedIconView.

Parameters:

Name Type Description Default
parent Optional[QWidget]

The parent widget.

None
icon_size QSize

The size of the icons in the grid. Defaults to 100x100.

QSize(100, 100)
margin int

The margin between items and headers. Defaults to 8.

8
header_height int

The height of the category headers. Defaults to 36.

36
Source code in source/qextrawidgets/widgets/views/grouped_icon_view.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
def __init__(
    self,
    parent: typing.Optional[QWidget] = None,
    icon_size: QSize = QSize(100, 100),
    margin: int = 8,
    header_height: int = 36,
):
    """
    Initialize the QGroupedIconView.

    Args:
        parent (Optional[QWidget]): The parent widget.
        icon_size (QSize): The size of the icons in the grid. Defaults to 100x100.
        margin (int): The margin between items and headers. Defaults to 8.
        header_height (int): The height of the category headers. Defaults to 36.
    """
    super().__init__(parent, icon_size=icon_size, margin=margin)

    # View State
    self._expanded_items: set[QPersistentModelIndex] = set()

    # Layout Configuration
    self._header_height: int = header_height

    # Set Delegate
    self.setItemDelegate(QGroupedIconDelegate(self, arrow_icon=None))

collapseAll()

Collapse all categories.

Source code in source/qextrawidgets/widgets/views/grouped_icon_view.py
115
116
117
118
def collapseAll(self) -> None:
    """Collapse all categories."""
    self._expanded_items.clear()
    self._schedule_layout()

expandAll()

Expand all categories.

Source code in source/qextrawidgets/widgets/views/grouped_icon_view.py
104
105
106
107
108
109
110
111
112
113
def expandAll(self) -> None:
    """Expand all categories."""
    if not self.model():
        return

    count = self.model().rowCount()
    for i in range(count):
        index = self.model().index(i, 0)
        self._expanded_items.add(QPersistentModelIndex(index))
    self._schedule_layout()

headerHeight()

Get the current height of the category headers.

Returns:

Name Type Description
int int

The header height in pixels.

Source code in source/qextrawidgets/widgets/views/grouped_icon_view.py
76
77
78
79
80
81
82
83
def headerHeight(self) -> int:
    """
    Get the current height of the category headers.

    Returns:
        int: The header height in pixels.
    """
    return self._header_height

isExpanded(index)

Return True if the category at index is expanded.

Source code in source/qextrawidgets/widgets/views/grouped_icon_view.py
85
86
87
88
89
def isExpanded(self, index: QModelIndex) -> bool:
    """Return True if the category at index is expanded."""
    if not index.isValid():
        return False
    return QPersistentModelIndex(index) in self._expanded_items

isIndexHidden(index)

Return True if the item referred to by index is hidden; otherwise returns False.

Source code in source/qextrawidgets/widgets/views/grouped_icon_view.py
294
295
296
297
298
299
300
301
302
303
304
305
306
307
def isIndexHidden(
    self, index: typing.Union[QModelIndex, QPersistentModelIndex]
) -> bool:
    """
    Return True if the item referred to by index is hidden; otherwise returns False.
    """
    if not index.isValid():
        return True

    if self._is_category(index):
        return False

    # Check if parent category is expanded
    return not self.isExpanded(index.parent())

itemDelegate(_=None)

Returns the item delegate used by the view.

Source code in source/qextrawidgets/widgets/views/grouped_icon_view.py
58
59
60
61
62
def itemDelegate(
    self, _: typing.Union[QModelIndex, QPersistentModelIndex, None] = None
) -> QGroupedIconDelegate:
    """Returns the item delegate used by the view."""
    return typing.cast(QGroupedIconDelegate, super().itemDelegate())

mousePressEvent(event)

Handle mouse press events.

Toggles category expansion or emits itemClicked signal.

Parameters:

Name Type Description Default
event QMouseEvent

The mouse event.

required
Source code in source/qextrawidgets/widgets/views/grouped_icon_view.py
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
def mousePressEvent(self, event: QMouseEvent) -> None:
    """
    Handle mouse press events.

    Toggles category expansion or emits itemClicked signal.

    Args:
        event (QMouseEvent): The mouse event.
    """
    if not self._item_rects:
        return

    index = self.indexAt(event.position().toPoint())

    if index.isValid() and event.button() == Qt.MouseButton.LeftButton:
        if self._is_category(index):
            self.setExpanded(index, not self.isExpanded(index))
            event.accept()
            return
        elif self._is_item(index):
            # The base class emits itemClicked, but we also want to handle it here explicitly if needed.
            # Actually base class handles itemClicked emission.
            # But we just return if handled category.
            pass

    super().mousePressEvent(event)

scrollTo(index, hint=QAbstractItemView.ScrollHint.EnsureVisible)

Scroll the view to ensure the item at index is visible.

Parameters:

Name Type Description Default
index QModelIndex | QPersistentModelIndex

The index to scroll to.

required
hint ScrollHint

The scroll hint.

EnsureVisible
Source code in source/qextrawidgets/widgets/views/grouped_icon_view.py
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
def scrollTo(
    self,
    index: typing.Union[QModelIndex, QPersistentModelIndex],
    hint: QAbstractItemView.ScrollHint = QAbstractItemView.ScrollHint.EnsureVisible,
) -> None:
    """
    Scroll the view to ensure the item at index is visible.

    Args:
        index (QModelIndex | QPersistentModelIndex): The index to scroll to.
        hint (QAbstractItemView.ScrollHint): The scroll hint.
    """
    p_index = QPersistentModelIndex(index)
    rect = self._item_rects.get(p_index)
    if not rect:
        return

    # [CHANGED] Hybrid Behavior:
    # 1. Categories: Always force scroll to top (Classic Behavior).
    if self._is_category(index):
        self.verticalScrollBar().setValue(rect.y())
        return

    # 2. Items: Smart scroll (New Behavior) - Only scroll if needed.
    # Use base class implementation for items
    super().scrollTo(index, hint)

setExpanded(index, expanded)

Set the expansion state of the category at index.

Source code in source/qextrawidgets/widgets/views/grouped_icon_view.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def setExpanded(self, index: QModelIndex, expanded: bool) -> None:
    """Set the expansion state of the category at index."""
    if not index.isValid():
        return

    p_index = QPersistentModelIndex(index)
    if expanded:
        self._expanded_items.add(p_index)
    else:
        self._expanded_items.discard(p_index)

    self._schedule_layout()

setHeaderHeight(height)

Set the height of the category headers.

Parameters:

Name Type Description Default
height int

The new header height in pixels.

required
Source code in source/qextrawidgets/widgets/views/grouped_icon_view.py
64
65
66
67
68
69
70
71
72
73
74
def setHeaderHeight(self, height: int) -> None:
    """
    Set the height of the category headers.

    Args:
        height (int): The new header height in pixels.
    """
    if self._header_height == height:
        return
    self._header_height = height
    self._schedule_layout()

setModel(model)

Set the model for the view.

Connects to necessary signals for handling layout updates and structural changes.

Parameters:

Name Type Description Default
model Optional[QAbstractItemModel]

The model to be set.

required
Source code in source/qextrawidgets/widgets/views/grouped_icon_view.py
151
152
153
154
155
156
157
158
159
160
161
def setModel(self, model: typing.Optional[QAbstractItemModel]) -> None:
    """
    Set the model for the view.

    Connects to necessary signals for handling layout updates and structural changes.

    Args:
        model (Optional[QAbstractItemModel]): The model to be set.
    """
    super().setModel(model)
    self._expanded_items.clear()

updateGeometries()

Recalculate the layout of item rectangles and update scrollbars.

Source code in source/qextrawidgets/widgets/views/grouped_icon_view.py
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
def updateGeometries(self) -> None:
    """
    Recalculate the layout of item rectangles and update scrollbars.
    """
    if not self.model():
        return

    self._item_rects.clear()
    width = self.viewport().width()
    y = 0

    item_w = self.iconSize().width()
    item_h = self.iconSize().height()

    effective_width = width - (2 * self._margin)
    cols = max(1, effective_width // (item_w + self._margin))
    root = self.rootIndex()

    row_count = self.model().rowCount(root)

    for r in range(row_count):
        cat_index = self.model().index(r, 0, root)
        if not cat_index.isValid():
            continue

        is_expanded = self.isExpanded(cat_index)

        self._item_rects[QPersistentModelIndex(cat_index)] = QRect(
            0, y, width, self._header_height
        )
        y += self._header_height

        if is_expanded:
            child_count = self.model().rowCount(cat_index)
            if child_count > 0:
                y += self._margin
                col_current = 0

                for c_row in range(child_count):
                    child = self.model().index(c_row, 0, cat_index)
                    if not child.isValid():
                        continue

                    px = self._margin + (col_current * (item_w + self._margin))
                    self._item_rects[QPersistentModelIndex(child)] = QRect(
                        px, y, item_w, item_h
                    )

                    col_current += 1
                    if col_current >= cols:
                        col_current = 0
                        y += item_h + self._margin

                if col_current != 0:
                    y += item_h + self._margin

    content_height = y
    scroll_range = max(0, content_height - self.viewport().height())

    self.verticalScrollBar().setRange(0, scroll_range)
    self.verticalScrollBar().setPageStep(self.viewport().height())
    self.verticalScrollBar().setSingleStep(self._header_height)

QListGridView

Bases: QListView

A customized QListView designed to display emojis in a grid layout.

Signals

left: Emitted when the mouse cursor leaves the grid area.

Source code in source/qextrawidgets/widgets/views/list_grid_view.py
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
class QListGridView(QListView):
    """A customized QListView designed to display emojis in a grid layout.

    Signals:
        left: Emitted when the mouse cursor leaves the grid area.
    """

    left = Signal()

    def __init__(self, parent: typing.Optional[QWidget] = None) -> None:
        """Initializes the emoji grid.

        Args:
            parent (QWidget, optional): Parent widget. Defaults to None.
        """
        super().__init__(parent)

        self.setMouseTracking(True)  # Essential for hover to work

        # Default settings
        self.setViewMode(QListView.ViewMode.IconMode)
        self.setResizeMode(QListView.ResizeMode.Adjust)
        self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.setUniformItemSizes(True)
        self.setWrapping(True)
        self.setDragEnabled(False)
        self.setSpacing(0)
        self.setTextElideMode(Qt.TextElideMode.ElideNone)

        # Turn off scroll bars (parent should scroll)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)

        # Size policy: Expands horizontally, Fixed vertically (based on sizeHint)
        self.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Minimum)

        # Native adjustment (helps, but sizeHint does the heavy lifting)
        self.setSizeAdjustPolicy(QAbstractScrollArea.SizeAdjustPolicy.AdjustToContents)

    def sizeHint(self) -> QSize:
        """Informs the layout of the ideal size for this widget.

        Calculates the height needed to display all items based on the current width.

        Returns:
            QSize: The calculated size hint.
        """
        if self.model() is None or self.model().rowCount() == 0:
            return QSize(0, 0)

        # Available width (if widget hasn't been shown yet, use a default value)
        width = self.width() if self.width() > 0 else 400

        # Grid dimensions
        grid_sz = self.gridSize()
        if grid_sz.isEmpty():
            grid_sz = QSize(40, 40)  # Fallback

        # Mathematical calculation
        item_width = grid_sz.width()
        item_height = grid_sz.height()

        # How many fit per row?
        items_per_row = max(1, width // item_width)

        # How many rows do we need?
        total_items = self.model().rowCount()
        rows = (total_items + items_per_row - 1) // items_per_row  # Ceil division

        height = rows * item_height + 5  # +5 safety padding

        return QSize(width, height)

    def leaveEvent(self, event: QEvent) -> None:
        """Handles the mouse leave event.

        Args:
            event (QEvent): The leave event.
        """
        super().leaveEvent(event)
        self.left.emit()

    def resizeEvent(self, event: QResizeEvent) -> None:
        """Handles the resize event.

        Triggers a geometry update to recalculate the size hint.

        Args:
            event (QResizeEvent): The resize event.
        """
        super().resizeEvent(event)
        self.updateGeometry()

__init__(parent=None)

Initializes the emoji grid.

Parameters:

Name Type Description Default
parent QWidget

Parent widget. Defaults to None.

None
Source code in source/qextrawidgets/widgets/views/list_grid_view.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
def __init__(self, parent: typing.Optional[QWidget] = None) -> None:
    """Initializes the emoji grid.

    Args:
        parent (QWidget, optional): Parent widget. Defaults to None.
    """
    super().__init__(parent)

    self.setMouseTracking(True)  # Essential for hover to work

    # Default settings
    self.setViewMode(QListView.ViewMode.IconMode)
    self.setResizeMode(QListView.ResizeMode.Adjust)
    self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
    self.setUniformItemSizes(True)
    self.setWrapping(True)
    self.setDragEnabled(False)
    self.setSpacing(0)
    self.setTextElideMode(Qt.TextElideMode.ElideNone)

    # Turn off scroll bars (parent should scroll)
    self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
    self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)

    # Size policy: Expands horizontally, Fixed vertically (based on sizeHint)
    self.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Minimum)

    # Native adjustment (helps, but sizeHint does the heavy lifting)
    self.setSizeAdjustPolicy(QAbstractScrollArea.SizeAdjustPolicy.AdjustToContents)

leaveEvent(event)

Handles the mouse leave event.

Parameters:

Name Type Description Default
event QEvent

The leave event.

required
Source code in source/qextrawidgets/widgets/views/list_grid_view.py
80
81
82
83
84
85
86
87
def leaveEvent(self, event: QEvent) -> None:
    """Handles the mouse leave event.

    Args:
        event (QEvent): The leave event.
    """
    super().leaveEvent(event)
    self.left.emit()

resizeEvent(event)

Handles the resize event.

Triggers a geometry update to recalculate the size hint.

Parameters:

Name Type Description Default
event QResizeEvent

The resize event.

required
Source code in source/qextrawidgets/widgets/views/list_grid_view.py
89
90
91
92
93
94
95
96
97
98
def resizeEvent(self, event: QResizeEvent) -> None:
    """Handles the resize event.

    Triggers a geometry update to recalculate the size hint.

    Args:
        event (QResizeEvent): The resize event.
    """
    super().resizeEvent(event)
    self.updateGeometry()

sizeHint()

Informs the layout of the ideal size for this widget.

Calculates the height needed to display all items based on the current width.

Returns:

Name Type Description
QSize QSize

The calculated size hint.

Source code in source/qextrawidgets/widgets/views/list_grid_view.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
def sizeHint(self) -> QSize:
    """Informs the layout of the ideal size for this widget.

    Calculates the height needed to display all items based on the current width.

    Returns:
        QSize: The calculated size hint.
    """
    if self.model() is None or self.model().rowCount() == 0:
        return QSize(0, 0)

    # Available width (if widget hasn't been shown yet, use a default value)
    width = self.width() if self.width() > 0 else 400

    # Grid dimensions
    grid_sz = self.gridSize()
    if grid_sz.isEmpty():
        grid_sz = QSize(40, 40)  # Fallback

    # Mathematical calculation
    item_width = grid_sz.width()
    item_height = grid_sz.height()

    # How many fit per row?
    items_per_row = max(1, width // item_width)

    # How many rows do we need?
    total_items = self.model().rowCount()
    rows = (total_items + items_per_row - 1) // items_per_row  # Ceil division

    height = rows * item_height + 5  # +5 safety padding

    return QSize(width, height)