Skip to content

tree_view

AYTreeView component module.

AYTreeView

Bases: StyleMixin, QTreeView

AYON-styled tree view.

Fully self-contained: uses AYONStyle for all painting, a custom item delegate that draws directly bypassing any parent QSS, and AYScrollBar instances for scrollbars.

The view shows an animated skeleton placeholder (AYSkeletonLoader) while its model is loading and has no rows to show yet. A model opts in by exposing a loading_changed = Signal(bool) signal and, optionally, an is_loading() -> bool method. The model is found automatically when set with setModel, also through proxy models. Loading state can be also set manually with set_loading.

Parameters:

Name Type Description Default
parent QWidget | None

Optional parent widget.

None
variant QTreeViewVariants

Visual style variant controlling background colour and item-state colours.

Default
Source code in client/ayon_core/ui/components/tree_view.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
 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
class AYTreeView(StyleMixin, QTreeView):
    """AYON-styled tree view.

    Fully self-contained: uses AYONStyle for all painting, a custom
    item delegate that draws directly bypassing any parent QSS, and
    AYScrollBar instances for scrollbars.

    The view shows an animated skeleton placeholder (`AYSkeletonLoader`)
    while its model is loading and has no rows to show yet. A model opts
    in by exposing a ``loading_changed = Signal(bool)`` signal and,
    optionally, an ``is_loading() -> bool`` method. The model is found
    automatically when set with ``setModel``, also through proxy models.
    Loading state can be also set manually with ``set_loading``.

    Args:
        parent: Optional parent widget.
        variant: Visual style variant controlling background colour and
            item-state colours.
    """

    Variants = QTreeViewVariants
    selection_changed = Signal(QItemSelection, QItemSelection)
    double_clicked = Signal(QMouseEvent)

    def __init__(
        self,
        parent: QWidget | None = None,
        variant: QTreeViewVariants = QTreeViewVariants.Default,
    ) -> None:
        self._variant_str: str = variant.value

        super().__init__(parent)

        style = get_ayon_style()

        header = AYHeaderView(
            Qt.Orientation.Horizontal,
            parent=self,
            style_model=style.model,
            variant=self._variant_str,
        )
        self.setHeader(header)

        self.setStyle(style)

        # Self-contained: do not inherit parent background or stylesheet.
        self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
        self.setAttribute(Qt.WidgetAttribute.WA_Hover, True)
        self.setMouseTracking(True)

        # Viewport must also be opaque with our colour.
        self.viewport().setAttribute(
            Qt.WidgetAttribute.WA_TranslucentBackground, False
        )
        self.viewport().setAttribute(Qt.WidgetAttribute.WA_Hover, True)
        self.viewport().setMouseTracking(True)
        self.viewport().installEventFilter(self)
        self._mouse_pos: QPoint = QPoint(-1, -1)
        self._sync_viewport_palette()

        # Custom item delegate — paints items directly, avoids QSS.
        delegate = TreeViewItemDelegate(
            parent=self,
            style_model=style.model,
            variant=self._variant_str,
        )
        self.setItemDelegate(delegate)

        # Styled scrollbars.
        vsb = AYScrollBar(Qt.Orientation.Vertical, self)
        self.setVerticalScrollBar(vsb)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
        hsb = AYScrollBar(Qt.Orientation.Horizontal, self)
        self.setHorizontalScrollBar(hsb)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)

        # No header — single-column hierarchical browser.

        # Indentation from style data.
        tv_style = style.model.get_style("QTreeView", self._variant_str)
        self.setIndentation(int(tv_style.get("indent", 20)))

        # Selection behaviour.
        self.setSelectionMode(QTreeView.SelectionMode.ExtendedSelection)
        self.setSelectionBehavior(QTreeView.SelectionBehavior.SelectRows)

        # No default frame — drawn manually in paintEvent.
        self.setFrameShape(QTreeView.Shape.NoFrame)

        # Loading placeholder, created lazily when first needed.
        self._skeleton: AYSkeletonLoader | None = None
        self._skeleton_delay: int = 80
        self._loading: bool = False
        self._loading_model: QAbstractItemModel | None = None

    def setModel(self, model: QAbstractItemModel | None) -> None:
        """Set model and connect to its loading state if it has one.

        Args:
            model: Model to show, can be a proxy model.
        """
        self._disconnect_loading_model()
        super().setModel(model)
        self._connect_loading_model(model)

    def is_loading(self) -> bool:
        """Whether the view is in loading state.

        Returns:
            bool: True if the view's data are loading.
        """
        return self._loading

    def set_loading(self, loading: bool) -> None:
        """Show animated placeholder rows while data are loading.

        The placeholder shows only when the model has no rows, a refresh
        of visible rows happens in place. Called automatically when
        the model has ``loading_changed`` signal.

        Args:
            loading: Whether the model is loading data.
        """
        self._loading = loading
        self._update_skeleton()

    def set_loading_delay(self, delay: int) -> None:
        """Delay before the loading placeholder appears.

        Loads finishing sooner never show the placeholder.

        Args:
            delay: Delay in milliseconds.
        """
        self._skeleton_delay = delay
        if self._skeleton is not None:
            self._skeleton.set_show_delay(delay)

    def _update_skeleton(self) -> None:
        show = self._loading
        if show:
            model = self._loading_model or self.model()
            show = model is None or model.rowCount() == 0

        if self._skeleton is None:
            if not show:
                return
            self._skeleton = AYSkeletonLoader(
                self,
                variant=QTreeViewVariants(self._variant_str),
                show_delay=self._skeleton_delay,
            )
        self._skeleton.set_loading(show)

    def _on_loading_model_rows_changed(self, *args) -> None:
        # Rows appeared or were removed while loading
        if self._loading:
            self._update_skeleton()

    @staticmethod
    def _find_loading_model(
        model: QAbstractItemModel | None,
    ) -> QAbstractItemModel | None:
        """Find model with 'loading_changed' signal, look through proxies.

        Args:
            model: Model set to the view.

        Returns:
            QAbstractItemModel | None: Model reporting its loading state.
        """
        while model is not None:
            if hasattr(model, "loading_changed"):
                return model
            if not isinstance(model, QAbstractProxyModel):
                break
            model = model.sourceModel()
        return None

    def _connect_loading_model(
        self, model: QAbstractItemModel | None
    ) -> None:
        loading_model = self._find_loading_model(model)
        self._loading_model = loading_model
        if loading_model is None:
            self.set_loading(False)
            return

        loading_model.loading_changed.connect(self.set_loading)
        for signal in self._loading_model_row_signals(loading_model):
            signal.connect(self._on_loading_model_rows_changed)

        is_loading = getattr(loading_model, "is_loading", None)
        self.set_loading(bool(is_loading()) if is_loading else False)

    def _disconnect_loading_model(self) -> None:
        loading_model, self._loading_model = self._loading_model, None
        if loading_model is None:
            return
        try:
            loading_model.loading_changed.disconnect(self.set_loading)
            for signal in self._loading_model_row_signals(loading_model):
                signal.disconnect(self._on_loading_model_rows_changed)
        except (RuntimeError, TypeError):
            # Model was already deleted or signals were not connected
            pass

    @staticmethod
    def _loading_model_row_signals(model: QAbstractItemModel) -> list:
        return [
            model.rowsInserted,
            model.rowsRemoved,
            model.modelReset,
            model.layoutChanged,
        ]

    def _sync_viewport_palette(self) -> None:
        """Apply the variant background colour to the viewport palette."""
        style = get_ayon_style()
        tv_style = style.model.get_style("QTreeView", self._variant_str)
        bg = QColor(tv_style.get("background-color", "#252a31"))
        p = self.viewport().palette()
        p.setColor(QPalette.ColorRole.Base, bg)
        p.setColor(QPalette.ColorRole.Window, bg)
        self.viewport().setPalette(p)

    def paintEvent(self, event: QPaintEvent) -> None:
        """Draw the outer container background before the items.

        Args:
            event: The paint event.
        """
        painter = QPainter(self.viewport())
        painter.setRenderHint(QPainter.RenderHint.Antialiasing, False)
        style = get_ayon_style()
        tv_style = style.model.get_style("QTreeView", self._variant_str)
        bg = QColor(tv_style.get("background-color", "#252a31"))
        painter.fillRect(self.viewport().rect(), bg)
        painter.end()

        # Let QTreeView draw its items on top.
        super().paintEvent(event)

    def eventFilter(self, obj, event):
        if obj is self.viewport():
            if event.type() == QEvent.Type.MouseMove:
                self._mouse_pos = event.pos()
            elif event.type() == QEvent.Type.Leave:
                self._mouse_pos = QPoint(-1, -1)
        return super().eventFilter(obj, event)

    def drawBranches(self, painter, rect, index):
        """Draw branch indicators with AYONStyle directly.

        Bypasses ``self.style()`` because, when an application-level QSS is
        active, Qt wraps the widget's style in a ``QStyleSheetStyle`` proxy
        which would otherwise intercept ``PE_IndicatorBranch`` and apply
        QSS ``QTreeView::branch`` rules on top of (or instead of) ours.
        """
        style = get_ayon_style()  # the raw AYONStyle, never wrapped

        opt = QStyleOption()
        opt.rect = rect
        opt.palette = self.palette()
        state = QStyle.StateFlag.State_Item
        if self.model() is not None and self.model().hasChildren(index):
            state |= QStyle.StateFlag.State_Children
        if self.isExpanded(index):
            state |= QStyle.StateFlag.State_Open
        if self.selectionModel().isSelected(index):
            state |= QStyle.StateFlag.State_Selected
        if self.isEnabled():
            state |= QStyle.StateFlag.State_Enabled

        # Row-level hover: is the cursor on the same row as `index`?
        hovered_idx = self.indexAt(self._mouse_pos)
        idx_rows = self._get_index_rows(index)
        if idx_rows == self._get_index_rows(hovered_idx):
            state |= QStyle.StateFlag.State_MouseOver

        opt.state = state

        # Call our drawer directly, not through self.style().
        style.drawers[
            enum_to_str(
                QStyle.PrimitiveElement,
                QStyle.PrimitiveElement.PE_IndicatorBranch,
                "QTreeView",
            )
        ](opt, painter, self)

    def _get_index_rows(self, index: QModelIndex) -> list[int]:
        """Return a list of row numbers from the given index to the root."""
        rows = []
        while index.isValid():
            rows.append(index.row())
            index = index.parent()
        return rows

    def mouseDoubleClickEvent(self, event: QMouseEvent) -> None:
        """Emit double_clicked signal on double-click."""
        self.double_clicked.emit(event)
        super().mouseDoubleClickEvent(event)

    def mousePressEvent(self, event) -> None:
        """Deselect all items when clicking in an empty area."""
        index = self.indexAt(event.pos())
        if not index.isValid():
            self.clearSelection()
            self.setCurrentIndex(self.rootIndex())
            return
        super().mousePressEvent(event)

    def selectionChanged(
        self,
        selected: QItemSelection,
        deselected: QItemSelection,
    ) -> None:
        """Override to emit a public signal on selection change.

        Args:
            selected: Newly selected items.
            deselected: Newly deselected items.
        """
        super().selectionChanged(selected, deselected)
        self.selection_changed.emit(selected, deselected)

drawBranches(painter, rect, index)

Draw branch indicators with AYONStyle directly.

Bypasses self.style() because, when an application-level QSS is active, Qt wraps the widget's style in a QStyleSheetStyle proxy which would otherwise intercept PE_IndicatorBranch and apply QSS QTreeView::branch rules on top of (or instead of) ours.

Source code in client/ayon_core/ui/components/tree_view.py
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
def drawBranches(self, painter, rect, index):
    """Draw branch indicators with AYONStyle directly.

    Bypasses ``self.style()`` because, when an application-level QSS is
    active, Qt wraps the widget's style in a ``QStyleSheetStyle`` proxy
    which would otherwise intercept ``PE_IndicatorBranch`` and apply
    QSS ``QTreeView::branch`` rules on top of (or instead of) ours.
    """
    style = get_ayon_style()  # the raw AYONStyle, never wrapped

    opt = QStyleOption()
    opt.rect = rect
    opt.palette = self.palette()
    state = QStyle.StateFlag.State_Item
    if self.model() is not None and self.model().hasChildren(index):
        state |= QStyle.StateFlag.State_Children
    if self.isExpanded(index):
        state |= QStyle.StateFlag.State_Open
    if self.selectionModel().isSelected(index):
        state |= QStyle.StateFlag.State_Selected
    if self.isEnabled():
        state |= QStyle.StateFlag.State_Enabled

    # Row-level hover: is the cursor on the same row as `index`?
    hovered_idx = self.indexAt(self._mouse_pos)
    idx_rows = self._get_index_rows(index)
    if idx_rows == self._get_index_rows(hovered_idx):
        state |= QStyle.StateFlag.State_MouseOver

    opt.state = state

    # Call our drawer directly, not through self.style().
    style.drawers[
        enum_to_str(
            QStyle.PrimitiveElement,
            QStyle.PrimitiveElement.PE_IndicatorBranch,
            "QTreeView",
        )
    ](opt, painter, self)

is_loading()

Whether the view is in loading state.

Returns:

Name Type Description
bool bool

True if the view's data are loading.

Source code in client/ayon_core/ui/components/tree_view.py
150
151
152
153
154
155
156
def is_loading(self) -> bool:
    """Whether the view is in loading state.

    Returns:
        bool: True if the view's data are loading.
    """
    return self._loading

mouseDoubleClickEvent(event)

Emit double_clicked signal on double-click.

Source code in client/ayon_core/ui/components/tree_view.py
344
345
346
347
def mouseDoubleClickEvent(self, event: QMouseEvent) -> None:
    """Emit double_clicked signal on double-click."""
    self.double_clicked.emit(event)
    super().mouseDoubleClickEvent(event)

mousePressEvent(event)

Deselect all items when clicking in an empty area.

Source code in client/ayon_core/ui/components/tree_view.py
349
350
351
352
353
354
355
356
def mousePressEvent(self, event) -> None:
    """Deselect all items when clicking in an empty area."""
    index = self.indexAt(event.pos())
    if not index.isValid():
        self.clearSelection()
        self.setCurrentIndex(self.rootIndex())
        return
    super().mousePressEvent(event)

paintEvent(event)

Draw the outer container background before the items.

Parameters:

Name Type Description Default
event QPaintEvent

The paint event.

required
Source code in client/ayon_core/ui/components/tree_view.py
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
def paintEvent(self, event: QPaintEvent) -> None:
    """Draw the outer container background before the items.

    Args:
        event: The paint event.
    """
    painter = QPainter(self.viewport())
    painter.setRenderHint(QPainter.RenderHint.Antialiasing, False)
    style = get_ayon_style()
    tv_style = style.model.get_style("QTreeView", self._variant_str)
    bg = QColor(tv_style.get("background-color", "#252a31"))
    painter.fillRect(self.viewport().rect(), bg)
    painter.end()

    # Let QTreeView draw its items on top.
    super().paintEvent(event)

selectionChanged(selected, deselected)

Override to emit a public signal on selection change.

Parameters:

Name Type Description Default
selected QItemSelection

Newly selected items.

required
deselected QItemSelection

Newly deselected items.

required
Source code in client/ayon_core/ui/components/tree_view.py
358
359
360
361
362
363
364
365
366
367
368
369
370
def selectionChanged(
    self,
    selected: QItemSelection,
    deselected: QItemSelection,
) -> None:
    """Override to emit a public signal on selection change.

    Args:
        selected: Newly selected items.
        deselected: Newly deselected items.
    """
    super().selectionChanged(selected, deselected)
    self.selection_changed.emit(selected, deselected)

setModel(model)

Set model and connect to its loading state if it has one.

Parameters:

Name Type Description Default
model QAbstractItemModel | None

Model to show, can be a proxy model.

required
Source code in client/ayon_core/ui/components/tree_view.py
140
141
142
143
144
145
146
147
148
def setModel(self, model: QAbstractItemModel | None) -> None:
    """Set model and connect to its loading state if it has one.

    Args:
        model: Model to show, can be a proxy model.
    """
    self._disconnect_loading_model()
    super().setModel(model)
    self._connect_loading_model(model)

set_loading(loading)

Show animated placeholder rows while data are loading.

The placeholder shows only when the model has no rows, a refresh of visible rows happens in place. Called automatically when the model has loading_changed signal.

Parameters:

Name Type Description Default
loading bool

Whether the model is loading data.

required
Source code in client/ayon_core/ui/components/tree_view.py
158
159
160
161
162
163
164
165
166
167
168
169
def set_loading(self, loading: bool) -> None:
    """Show animated placeholder rows while data are loading.

    The placeholder shows only when the model has no rows, a refresh
    of visible rows happens in place. Called automatically when
    the model has ``loading_changed`` signal.

    Args:
        loading: Whether the model is loading data.
    """
    self._loading = loading
    self._update_skeleton()

set_loading_delay(delay)

Delay before the loading placeholder appears.

Loads finishing sooner never show the placeholder.

Parameters:

Name Type Description Default
delay int

Delay in milliseconds.

required
Source code in client/ayon_core/ui/components/tree_view.py
171
172
173
174
175
176
177
178
179
180
181
def set_loading_delay(self, delay: int) -> None:
    """Delay before the loading placeholder appears.

    Loads finishing sooner never show the placeholder.

    Args:
        delay: Delay in milliseconds.
    """
    self._skeleton_delay = delay
    if self._skeleton is not None:
        self._skeleton.set_show_delay(delay)

TreeViewItemDelegate

Bases: StyleMixin, QStyledItemDelegate

Item delegate for AYTreeView that paints directly, bypassing QSS.

Reads style data from the QTreeView style entry to draw item backgrounds (hover, selected) and text/icons. The paint() method uses raw QPainter calls so that a parent-level QStyleSheet cannot intercept and override the colours.

Parameters:

Name Type Description Default
parent QWidget | None

The parent widget (expected to be an AYTreeView instance).

None
style_model StyleData | None

StyleData instance providing colour/dimension data.

None
variant str

The variant string used to look up the correct style.

'default'
Source code in client/ayon_core/ui/components/tree_view.py
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
class TreeViewItemDelegate(StyleMixin, QStyledItemDelegate):
    """Item delegate for AYTreeView that paints directly, bypassing QSS.

    Reads style data from the QTreeView style entry to draw item
    backgrounds (hover, selected) and text/icons.  The paint() method
    uses raw QPainter calls so that a parent-level QStyleSheet cannot
    intercept and override the colours.

    Args:
        parent: The parent widget (expected to be an AYTreeView instance).
        style_model: StyleData instance providing colour/dimension data.
        variant: The variant string used to look up the correct style.
    """

    def __init__(
        self,
        parent: QWidget | None = None,
        style_model: StyleData | None = None,
        variant: str = "default",
    ) -> None:
        super().__init__(parent)
        self._style_model = style_model
        self._variant_str = variant
        self._icon_cache: dict[str, QIcon] = {}
        self._styles_cache: dict[str, dict] | None = None

    def _tv_styles(self) -> dict[str, dict]:
        """Return *base*, *hover* and *selected* style dicts at once.

        Resolved once and kept: every state lookup deep-copies the style
        data, which is too costly to repeat for each painted row.  The
        underlying style data is loaded once per process, so the cached
        result never goes stale.
        """
        if self._styles_cache is None:
            if self._style_model is None:
                return {
                    "base": {},
                    "hover": {},
                    "selected": {},
                    "selected-hover": {},
                }
            self._styles_cache = self._style_model.get_styles(
                "QTreeView",
                self._variant_str,
                ["base", "hover", "selected", "selected-hover"],
            )
        return self._styles_cache

    def initStyleOption(
        self,
        option: QStyleOptionViewItem,
        index: QModelIndex | QPersistentModelIndex,
    ) -> None:
        """Initialize the style option with the default implementation, then
        override any properties needed for our custom painting.

        Args:
            option: The style option to initialize.
            index: The model index of the item.
        """
        super().initStyleOption(option, index)
        option.font = self.font()
        option.fontMetrics = self.fontMetrics()
        option.decorationAlignment = Qt.AlignmentFlag.AlignCenter
        option.displayAlignment = (
            Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft
        )

    def sizeHint(
        self,
        option: QStyleOptionViewItem,
        index: QModelIndex | QPersistentModelIndex,
    ) -> QSize:
        """Return a fixed row height from the style data.

        Args:
            option: The style option for the item.
            index: The model index of the item.

        Returns:
            The size hint for the item.
        """
        sh = index.data(Qt.SizeHintRole)
        if sh is not None:
            return sh

        h = int(self._tv_styles()["base"].get("item-height", 28))
        return QSize(option.rect.width(), h)

    def _get_reserved_right_width(
        self,
        option: QStyleOptionViewItem,
        index: QModelIndex | QPersistentModelIndex,
    ) -> int:
        """Width on the right side of the item kept free of icon and text.

        Allows subclasses to paint additional content on the far right of
        the item without overlapping the text, which is elided earlier.

        Args:
            option: The initialized style option for the item.
            index: The model index of the item.

        Returns:
            Reserved width in pixels.
        """
        return 0

    def paint(
        self,
        painter: QPainter,
        option: QStyleOptionViewItem,
        index: QModelIndex | QPersistentModelIndex,
    ) -> None:
        """Paint a tree-view item directly, bypassing QStyle completely.

        Args:
            painter: The QPainter to use.
            option: The style option for the item.
            index: The model index of the item.
        """
        painter.save()
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)
        painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)

        opt = QStyleOptionViewItem(option)
        self.initStyleOption(opt, index)

        state = opt.state
        is_selected = bool(state & QStyle.StateFlag.State_Selected)
        is_hovered = bool(state & QStyle.StateFlag.State_MouseOver)

        styles = self._tv_styles()
        base_style = styles["base"]
        colors_style = {}
        if is_selected and is_hovered:
            colors_style = styles["selected-hover"]
        elif is_selected:
            colors_style = styles["selected"]
        elif is_hovered:
            colors_style = styles["hover"]

        # --- bg and fg colors ------------------------------------------
        if state & QStyle.StateFlag.State_Enabled:
            bg_color = QColor(colors_style.get(
                "background-color",
                base_style.get("background-color", "transparent")
            ))
            text_color = QColor(colors_style.get(
                "color",
                base_style.get("color", "#f4f5f5"),
            ))
        else:
            # Use base colors for disabled state
            bg_color = QColor(
                base_style.get("background-color", "transparent")
            )
            text_color = QColor(base_style.get("color", "#f4f5f5"))
            # - apply disabled opacity to text color
            text_color.setAlpha(
                int(
                    text_color.alpha()
                    * base_style.get("disabled-opacity", 0.5)
                )
            )

        painter.setBrush(QBrush(bg_color))
        painter.setPen(Qt.PenStyle.NoPen)
        painter.drawRect(opt.rect)

        # --- icon + text layout ----------------------------------------
        item_padding = base_style.get("item-padding", [4, 8])
        icon_text_spacing = int(base_style.get("icon-text-spacing", 6))
        content_rect = QRect(opt.rect).adjusted(
            item_padding[1],
            item_padding[0],
            -item_padding[1],
            -item_padding[0],
        )
        content_rect.setRight(
            content_rect.right() - self._get_reserved_right_width(opt, index)
        )

        icon = opt.icon
        icon_offset = 0
        content_left = content_rect.left()
        if not icon.isNull():
            icon_size = opt.decorationSize
            icon_rect = QRect(content_rect)
            icon_rect.setSize(icon_size)
            if opt.decorationAlignment & Qt.AlignmentFlag.AlignBottom:
                icon_rect.moveTop(
                    (content_rect.bottom() - icon_size.height()) + 1
                )
            elif opt.decorationAlignment & Qt.AlignmentFlag.AlignVCenter:
                icon_rect.moveTop(
                    (
                        content_rect.center().y() - (icon_size.height() // 2)
                    ) + 1
                )

            icon_offset = icon_rect.width()
            if opt.text:
                icon_offset += icon_text_spacing

            content_left = icon_rect.right() + icon_text_spacing
            mode = (
                QIcon.Mode.Normal
                if state & QStyle.StateFlag.State_Enabled
                else QIcon.Mode.Disabled
            )
            icon.paint(
                painter,
                icon_rect,
                opt.decorationAlignment,
                mode,
            )

        if opt.text:
            metrics = opt.fontMetrics
            bound = metrics.boundingRect(opt.text)

            content_width = icon_offset + bound.width()
            if content_width > content_rect.width():
                # Text is too long to fit in the available space, so elide it.
                metrics = opt.fontMetrics
                elided_text = metrics.elidedText(
                    opt.text,
                    opt.textElideMode,
                    content_rect.width() - icon_offset,
                )
                opt.text = elided_text

        if opt.text:
            text_rect = QRect(content_rect)
            text_rect.setLeft(content_left)
            painter.setPen(text_color)
            painter.setFont(opt.font)
            painter.drawText(
                text_rect,
                opt.displayAlignment,
                opt.text,
            )

        painter.restore()

initStyleOption(option, index)

Initialize the style option with the default implementation, then override any properties needed for our custom painting.

Parameters:

Name Type Description Default
option QStyleOptionViewItem

The style option to initialize.

required
index QModelIndex | QPersistentModelIndex

The model index of the item.

required
Source code in client/ayon_core/ui/components/tree_view.py
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
def initStyleOption(
    self,
    option: QStyleOptionViewItem,
    index: QModelIndex | QPersistentModelIndex,
) -> None:
    """Initialize the style option with the default implementation, then
    override any properties needed for our custom painting.

    Args:
        option: The style option to initialize.
        index: The model index of the item.
    """
    super().initStyleOption(option, index)
    option.font = self.font()
    option.fontMetrics = self.fontMetrics()
    option.decorationAlignment = Qt.AlignmentFlag.AlignCenter
    option.displayAlignment = (
        Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft
    )

paint(painter, option, index)

Paint a tree-view item directly, bypassing QStyle completely.

Parameters:

Name Type Description Default
painter QPainter

The QPainter to use.

required
option QStyleOptionViewItem

The style option for the item.

required
index QModelIndex | QPersistentModelIndex

The model index of the item.

required
Source code in client/ayon_core/ui/components/tree_view.py
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
def paint(
    self,
    painter: QPainter,
    option: QStyleOptionViewItem,
    index: QModelIndex | QPersistentModelIndex,
) -> None:
    """Paint a tree-view item directly, bypassing QStyle completely.

    Args:
        painter: The QPainter to use.
        option: The style option for the item.
        index: The model index of the item.
    """
    painter.save()
    painter.setRenderHint(QPainter.RenderHint.Antialiasing)
    painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)

    opt = QStyleOptionViewItem(option)
    self.initStyleOption(opt, index)

    state = opt.state
    is_selected = bool(state & QStyle.StateFlag.State_Selected)
    is_hovered = bool(state & QStyle.StateFlag.State_MouseOver)

    styles = self._tv_styles()
    base_style = styles["base"]
    colors_style = {}
    if is_selected and is_hovered:
        colors_style = styles["selected-hover"]
    elif is_selected:
        colors_style = styles["selected"]
    elif is_hovered:
        colors_style = styles["hover"]

    # --- bg and fg colors ------------------------------------------
    if state & QStyle.StateFlag.State_Enabled:
        bg_color = QColor(colors_style.get(
            "background-color",
            base_style.get("background-color", "transparent")
        ))
        text_color = QColor(colors_style.get(
            "color",
            base_style.get("color", "#f4f5f5"),
        ))
    else:
        # Use base colors for disabled state
        bg_color = QColor(
            base_style.get("background-color", "transparent")
        )
        text_color = QColor(base_style.get("color", "#f4f5f5"))
        # - apply disabled opacity to text color
        text_color.setAlpha(
            int(
                text_color.alpha()
                * base_style.get("disabled-opacity", 0.5)
            )
        )

    painter.setBrush(QBrush(bg_color))
    painter.setPen(Qt.PenStyle.NoPen)
    painter.drawRect(opt.rect)

    # --- icon + text layout ----------------------------------------
    item_padding = base_style.get("item-padding", [4, 8])
    icon_text_spacing = int(base_style.get("icon-text-spacing", 6))
    content_rect = QRect(opt.rect).adjusted(
        item_padding[1],
        item_padding[0],
        -item_padding[1],
        -item_padding[0],
    )
    content_rect.setRight(
        content_rect.right() - self._get_reserved_right_width(opt, index)
    )

    icon = opt.icon
    icon_offset = 0
    content_left = content_rect.left()
    if not icon.isNull():
        icon_size = opt.decorationSize
        icon_rect = QRect(content_rect)
        icon_rect.setSize(icon_size)
        if opt.decorationAlignment & Qt.AlignmentFlag.AlignBottom:
            icon_rect.moveTop(
                (content_rect.bottom() - icon_size.height()) + 1
            )
        elif opt.decorationAlignment & Qt.AlignmentFlag.AlignVCenter:
            icon_rect.moveTop(
                (
                    content_rect.center().y() - (icon_size.height() // 2)
                ) + 1
            )

        icon_offset = icon_rect.width()
        if opt.text:
            icon_offset += icon_text_spacing

        content_left = icon_rect.right() + icon_text_spacing
        mode = (
            QIcon.Mode.Normal
            if state & QStyle.StateFlag.State_Enabled
            else QIcon.Mode.Disabled
        )
        icon.paint(
            painter,
            icon_rect,
            opt.decorationAlignment,
            mode,
        )

    if opt.text:
        metrics = opt.fontMetrics
        bound = metrics.boundingRect(opt.text)

        content_width = icon_offset + bound.width()
        if content_width > content_rect.width():
            # Text is too long to fit in the available space, so elide it.
            metrics = opt.fontMetrics
            elided_text = metrics.elidedText(
                opt.text,
                opt.textElideMode,
                content_rect.width() - icon_offset,
            )
            opt.text = elided_text

    if opt.text:
        text_rect = QRect(content_rect)
        text_rect.setLeft(content_left)
        painter.setPen(text_color)
        painter.setFont(opt.font)
        painter.drawText(
            text_rect,
            opt.displayAlignment,
            opt.text,
        )

    painter.restore()

sizeHint(option, index)

Return a fixed row height from the style data.

Parameters:

Name Type Description Default
option QStyleOptionViewItem

The style option for the item.

required
index QModelIndex | QPersistentModelIndex

The model index of the item.

required

Returns:

Type Description
QSize

The size hint for the item.

Source code in client/ayon_core/ui/components/tree_view.py
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
def sizeHint(
    self,
    option: QStyleOptionViewItem,
    index: QModelIndex | QPersistentModelIndex,
) -> QSize:
    """Return a fixed row height from the style data.

    Args:
        option: The style option for the item.
        index: The model index of the item.

    Returns:
        The size hint for the item.
    """
    sh = index.data(Qt.SizeHintRole)
    if sh is not None:
        return sh

    h = int(self._tv_styles()["base"].get("item-height", 28))
    return QSize(option.rect.width(), h)