Skip to content

frame

AYFrame

Bases: StyleMixin, QFrame

Source code in client/ayon_core/ui/components/frame.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
class AYFrame(StyleMixin, QtWidgets.QFrame):
    Variants = QFrameVariants

    def __init__(
        self,
        *args,
        bg=False,
        variant: Variants = Variants.Default,
        margin=0,
        bg_tint="",
        hover_enabled=False,
        **kwargs,
    ):
        # Convert enum to string if needed
        self._bg: bool = bg
        self._variant_str = variant.value
        self._bg_tint = bg_tint
        self._bg_color = None
        self._hover_enabled = hover_enabled

        super().__init__(*args, **kwargs)
        self.setStyle(get_ayon_style())

        self.setAttribute(
            QtCore.Qt.WidgetAttribute.WA_TranslucentBackground, True
        )
        if hover_enabled:
            self.setAttribute(QtCore.Qt.WidgetAttribute.WA_Hover, True)
            self.setMouseTracking(True)
        self.setContentsMargins(margin, margin, margin, margin)

    def paintEvent(self, arg__1: QtGui.QPaintEvent) -> None:
        p = QtGui.QPainter(self)
        option = QtWidgets.QStyleOptionFrame()
        self.initStyleOption(option)
        # print(f"opt: {option}")
        get_ayon_style().drawControl(
            QtWidgets.QStyle.ControlElement.CE_ShapedFrame, option, p, self
        )

    def get_bg_color(self, base_color: str):
        if not self._bg_color:
            if self._bg_tint:
                self._bg_color = color_blend(base_color, self._bg_tint, 0.1)
            else:
                return base_color
        return self._bg_color

    def _set_row_state_bit(
        self, bit: QtWidgets.QStyle.StateFlag, on: bool
    ) -> None:
        state = (
            self.property("row_state")
            or QtWidgets.QStyle.StateFlag.State_None
        )
        state = (state | bit) if on else (state & ~bit)
        self.setProperty("row_state", state)
        self.update()

    def set_selected(self, selected: bool) -> None:
        """Mark this frame as selected via the ``row_state`` property.

        The frame drawer reads this to pick the current variant's
        ``selected`` style block (background, border, ...), so a whole
        frame can carry a highlight the way a single-row control would,
        instead of only an inner button doing so.
        """
        self._set_row_state_bit(
            QtWidgets.QStyle.StateFlag.State_Selected, selected
        )

    def set_hovered(self, hovered: bool) -> None:
        """Mark this frame as hovered via the ``row_state`` property.

        Companion to :meth:`set_selected`: driven explicitly (see
        :class:`RowHoverTracker`) instead of relying on
        ``underMouse()``, since an interactive ``QPushButton`` child
        sitting on top of the frame doesn't reliably cascade hover
        state to it.
        """
        self._set_row_state_bit(
            QtWidgets.QStyle.StateFlag.State_MouseOver, hovered
        )

set_hovered(hovered)

Mark this frame as hovered via the row_state property.

Companion to :meth:set_selected: driven explicitly (see :class:RowHoverTracker) instead of relying on underMouse(), since an interactive QPushButton child sitting on top of the frame doesn't reliably cascade hover state to it.

Source code in client/ayon_core/ui/components/frame.py
84
85
86
87
88
89
90
91
92
93
94
95
def set_hovered(self, hovered: bool) -> None:
    """Mark this frame as hovered via the ``row_state`` property.

    Companion to :meth:`set_selected`: driven explicitly (see
    :class:`RowHoverTracker`) instead of relying on
    ``underMouse()``, since an interactive ``QPushButton`` child
    sitting on top of the frame doesn't reliably cascade hover
    state to it.
    """
    self._set_row_state_bit(
        QtWidgets.QStyle.StateFlag.State_MouseOver, hovered
    )

set_selected(selected)

Mark this frame as selected via the row_state property.

The frame drawer reads this to pick the current variant's selected style block (background, border, ...), so a whole frame can carry a highlight the way a single-row control would, instead of only an inner button doing so.

Source code in client/ayon_core/ui/components/frame.py
72
73
74
75
76
77
78
79
80
81
82
def set_selected(self, selected: bool) -> None:
    """Mark this frame as selected via the ``row_state`` property.

    The frame drawer reads this to pick the current variant's
    ``selected`` style block (background, border, ...), so a whole
    frame can carry a highlight the way a single-row control would,
    instead of only an inner button doing so.
    """
    self._set_row_state_bit(
        QtWidgets.QStyle.StateFlag.State_Selected, selected
    )

HoverReveal

Bases: QObject

Event filter that shows widget only while its row is hovered.

Give the revealed widget a size policy with setRetainSizeWhenHidden(True) so the row keeps its width and nothing shifts as the widget appears.

Parameters:

Name Type Description Default
widget QWidget

The widget to reveal/hide.

required
parent QObject

The watched widget (also used as the filter's parent).

required
force_visible Callable[[], bool] | None

Optional callable; when it returns True the widget stays visible on Leave instead of being hidden.

None
Source code in client/ayon_core/ui/components/frame.py
 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
class HoverReveal(QtCore.QObject):
    """Event filter that shows *widget* only while its row is hovered.

    Give the revealed widget a size policy with
    ``setRetainSizeWhenHidden(True)`` so the row keeps its width and
    nothing shifts as the widget appears.

    Args:
        widget: The widget to reveal/hide.
        parent: The watched widget (also used as the filter's parent).
        force_visible: Optional callable; when it returns ``True`` the
            widget stays visible on Leave instead of being hidden.
    """

    def __init__(
        self,
        widget: QtWidgets.QWidget,
        parent: QtCore.QObject,
        force_visible: Callable[[], bool] | None = None,
    ) -> None:
        super().__init__(parent)
        self._widget = widget
        self._force_visible = force_visible

    def eventFilter(
        self, obj: QtCore.QObject, event: QtCore.QEvent
    ) -> bool:
        if event.type() == QtCore.QEvent.Type.Enter:
            self._widget.setVisible(True)
        elif event.type() == QtCore.QEvent.Type.Leave:
            self._widget.setVisible(
                bool(self._force_visible and self._force_visible())
            )
        # never consume the event
        return False

RowHoverTracker

Bases: QObject

Keeps an :class:AYFrame hover-highlighted via set_hovered.

Install on the frame itself and on each interactive child (buttons) it contains, via :meth:watch. A QPushButton child can intercept mouse tracking in a way that prevents Qt's own Enter/Leave events from reliably reaching an ancestor frame, so this tracks hover directly instead of depending on that propagation.

On every Leave from any watched widget, it re-checks the actual cursor position against the frame's own rect rather than assuming "left a child" means "left the frame" — moving from one child to a sibling (or to the frame's own background) fires a Leave too, and naively clearing hover on that would flicker or stick off.

Source code in client/ayon_core/ui/components/frame.py
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
class RowHoverTracker(QtCore.QObject):
    """Keeps an :class:`AYFrame` hover-highlighted via ``set_hovered``.

    Install on the frame itself *and* on each interactive child
    (buttons) it contains, via :meth:`watch`. A ``QPushButton`` child
    can intercept mouse tracking in a way that prevents Qt's own
    Enter/Leave events from reliably reaching an ancestor frame, so
    this tracks hover directly instead of depending on that
    propagation.

    On every Leave from any watched widget, it re-checks the actual
    cursor position against the frame's own rect rather than assuming
    "left a child" means "left the frame" — moving from one child to a
    sibling (or to the frame's own background) fires a Leave too, and
    naively clearing hover on that would flicker or stick off.
    """

    def __init__(self, frame: AYFrame) -> None:
        super().__init__(frame)
        self._frame = frame

    def watch(self, *widgets: QtWidgets.QWidget) -> "RowHoverTracker":
        for widget in (self._frame, *widgets):
            widget.installEventFilter(self)
        return self

    def eventFilter(
        self, obj: QtCore.QObject, event: QtCore.QEvent
    ) -> bool:
        if event.type() == QtCore.QEvent.Type.Enter:
            self._frame.set_hovered(True)
        elif event.type() == QtCore.QEvent.Type.Leave:
            local = self._frame.mapFromGlobal(QtGui.QCursor.pos())
            if isinstance(local, QtCore.QPointF):
                local = local.toPoint()
            if not self._frame.rect().contains(local):
                self._frame.set_hovered(False)
        # never consume the event
        return False