Menu action with an option box button (Maya-style option box pattern).
Provides three classes that together implement a menu item composed of a standard action area (icon + label) and a small option button on the far right. Clicking the main area fires the normal triggered signal; clicking the option button emits OptionalAction.option_clicked instead.
Typical usage::
menu = QMenu("My Menu", parent)
action = OptionalAction(
label="Run Process",
icon_name="play_arrow",
use_option=True,
parent=menu,
)
action.triggered.connect(lambda: run_process())
action.option_clicked.connect(lambda: open_options_dialog())
menu.addAction(action)
Bases: QMenu
QMenu that paints itself using the AYON style.
Replicates :meth:QMenu.paintEvent but routes every primitive and control draw call through :func:get_ayon_style, so the menu is painted consistently with the rest of the AYON UI even when the application style isn't AYONStyle.
Source code in client/ayon_core/ui/components/option_action.py
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 | class AYMenu(QtWidgets.QMenu):
"""QMenu that paints itself using the AYON style.
Replicates :meth:`QMenu.paintEvent` but routes every primitive and
control draw call through :func:`get_ayon_style`, so the menu is
painted consistently with the rest of the AYON UI even when the
application style isn't AYONStyle.
"""
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
# Force the menu to use AYONStyle for drawing, even if the app's style
# is something else.
self.setStyle(get_ayon_style())
def paintEvent(self, arg__1: QtGui.QPaintEvent) -> None:
"""Paint the menu using AYON's QStyle implementation.
Mirrors Qt's own ``QMenu::paintEvent`` order:
1. Draw the menu panel background (``PE_PanelMenu``).
2. Draw each visible action row (``CE_MenuItem``).
3. Draw the menu frame on top (``PE_FrameMenu``).
Args:
arg__1: The paint event delivered by Qt.
"""
style = get_ayon_style()
painter = QtGui.QPainter(self)
# --- Shared base option (used for panel + frame) ---
menu_opt = QtWidgets.QStyleOptionMenuItem()
menu_opt.initFrom(self)
menu_opt.state = QtWidgets.QStyle.StateFlag.State_None
menu_opt.checkType = (
QtWidgets.QStyleOptionMenuItem.CheckType.NotCheckable
)
menu_opt.maxIconWidth = 0
try:
menu_opt.reservedShortcutWidth = 0
except AttributeError:
# Older Qt versions expose tabWidth instead.
menu_opt.tabWidth = 0
menu_opt.rect = self.rect()
menu_opt.menuRect = self.rect()
# --- 1. Panel background ---
style.drawPrimitive(
QtWidgets.QStyle.PrimitiveElement.PE_PanelMenu,
menu_opt,
painter,
self,
)
# --- 2. Action rows ---
event_region = arg__1.region()
for action in self.actions():
action_rect = self.actionGeometry(action)
if not event_region.intersects(action_rect):
continue
opt = QtWidgets.QStyleOptionMenuItem()
self.initStyleOption(opt, action)
opt.rect = action_rect
style.drawControl(
QtWidgets.QStyle.ControlElement.CE_MenuItem,
opt,
painter,
self,
)
# --- 3. Frame on top ---
frame_width = style.pixelMetric(
QtWidgets.QStyle.PixelMetric.PM_MenuPanelWidth, menu_opt, self
)
if frame_width > 0:
frame_opt = QtWidgets.QStyleOptionFrame()
frame_opt.initFrom(self)
frame_opt.rect = self.rect()
frame_opt.state = QtWidgets.QStyle.StateFlag.State_None
frame_opt.lineWidth = frame_width
frame_opt.midLineWidth = 0
style.drawPrimitive(
QtWidgets.QStyle.PrimitiveElement.PE_FrameMenu,
frame_opt,
painter,
self,
)
painter.end()
|
Paint the menu using AYON's QStyle implementation.
Mirrors Qt's own QMenu::paintEvent order: 1. Draw the menu panel background (PE_PanelMenu). 2. Draw each visible action row (CE_MenuItem). 3. Draw the menu frame on top (PE_FrameMenu).
Parameters:
| Name | Type | Description | Default |
arg__1 | QPaintEvent | The paint event delivered by Qt. | required |
Source code in client/ayon_core/ui/components/option_action.py
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 | def paintEvent(self, arg__1: QtGui.QPaintEvent) -> None:
"""Paint the menu using AYON's QStyle implementation.
Mirrors Qt's own ``QMenu::paintEvent`` order:
1. Draw the menu panel background (``PE_PanelMenu``).
2. Draw each visible action row (``CE_MenuItem``).
3. Draw the menu frame on top (``PE_FrameMenu``).
Args:
arg__1: The paint event delivered by Qt.
"""
style = get_ayon_style()
painter = QtGui.QPainter(self)
# --- Shared base option (used for panel + frame) ---
menu_opt = QtWidgets.QStyleOptionMenuItem()
menu_opt.initFrom(self)
menu_opt.state = QtWidgets.QStyle.StateFlag.State_None
menu_opt.checkType = (
QtWidgets.QStyleOptionMenuItem.CheckType.NotCheckable
)
menu_opt.maxIconWidth = 0
try:
menu_opt.reservedShortcutWidth = 0
except AttributeError:
# Older Qt versions expose tabWidth instead.
menu_opt.tabWidth = 0
menu_opt.rect = self.rect()
menu_opt.menuRect = self.rect()
# --- 1. Panel background ---
style.drawPrimitive(
QtWidgets.QStyle.PrimitiveElement.PE_PanelMenu,
menu_opt,
painter,
self,
)
# --- 2. Action rows ---
event_region = arg__1.region()
for action in self.actions():
action_rect = self.actionGeometry(action)
if not event_region.intersects(action_rect):
continue
opt = QtWidgets.QStyleOptionMenuItem()
self.initStyleOption(opt, action)
opt.rect = action_rect
style.drawControl(
QtWidgets.QStyle.ControlElement.CE_MenuItem,
opt,
painter,
self,
)
# --- 3. Frame on top ---
frame_width = style.pixelMetric(
QtWidgets.QStyle.PixelMetric.PM_MenuPanelWidth, menu_opt, self
)
if frame_width > 0:
frame_opt = QtWidgets.QStyleOptionFrame()
frame_opt.initFrom(self)
frame_opt.rect = self.rect()
frame_opt.state = QtWidgets.QStyle.StateFlag.State_None
frame_opt.lineWidth = frame_width
frame_opt.midLineWidth = 0
style.drawPrimitive(
QtWidgets.QStyle.PrimitiveElement.PE_FrameMenu,
frame_opt,
painter,
self,
)
painter.end()
|
AYOptionBox
Bases: AYButton
Option box widget used as the right-hand button in an action row.
Emits :attr:clicked when the user presses this button. It is a standard :class:AYButton styled with the Optional_Action variant.
Source code in client/ayon_core/ui/components/option_action.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54 | class AYOptionBox(AYButton):
"""Option box widget used as the right-hand button in an action row.
Emits :attr:`clicked` when the user presses this button. It is a
standard :class:`AYButton` styled with the ``Optional_Action``
variant.
"""
def __init__(
self,
icon_name: str = "check_box_outline_blank",
icon_size: int = 16,
parent: QtWidgets.QWidget | None = None,
) -> None:
super().__init__(
parent,
variant=AYButton.Variants.Optional_Action,
icon=icon_name,
icon_size=icon_size,
fixed_width=False,
)
|
AYOptionalAction
Bases: QWidgetAction
Menu action with an optional right-hand option box button.
Subclasses :class:QtWidgets.QWidgetAction to embed a custom :class:AYOptionalActionWidget inside a standard QMenu.
Set use_option=True to show the option box and connect to :attr:option_clicked for the secondary action.
Parameters:
| Name | Type | Description | Default |
label | str | | required |
icon_name | str | None | Material symbol icon name (or "none"). | 'none' |
use_option | bool | Whether to show the option box button. | True |
parent | QWidget | None | Parent widget (typically the owning menu). | None |
Source code in client/ayon_core/ui/components/option_action.py
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 | class AYOptionalAction(QtWidgets.QWidgetAction):
"""Menu action with an optional right-hand option box button.
Subclasses :class:`QtWidgets.QWidgetAction` to embed a custom
:class:`AYOptionalActionWidget` inside a standard ``QMenu``.
Set ``use_option=True`` to show the option box and connect to
:attr:`option_clicked` for the secondary action.
Args:
label: Display text.
icon_name: Material symbol icon name (or ``"none"``).
use_option: Whether to show the option box button.
parent: Parent widget (typically the owning menu).
"""
option_clicked = QtCore.Signal()
def __init__(
self,
label: str,
icon_name: str | None = "none",
use_option: bool = True,
parent: QtWidgets.QWidget | None = None,
) -> None:
super().__init__(parent)
self._label = label
self._icon_name = icon_name or "none"
self._use_option = use_option
self.widget: AYOptionalActionWidget | None = None
def createWidget(self, parent: QtWidgets.QWidget) -> QtWidgets.QWidget:
"""Instantiate and configure the custom action row widget.
Called by Qt when the action is added to a menu.
Args:
parent: The menu widget that will own the row widget.
Returns:
The newly created :class:`AYOptionalActionWidget`.
"""
widget = AYOptionalActionWidget(
self._label,
icon_name=self._icon_name,
parent=parent,
)
widget.setEnabled(self.isEnabled())
self.widget = widget
if self._use_option:
widget.option.clicked.connect(self.option_clicked.emit)
widget.option.clicked.connect(self._close_menu_chain)
else:
widget.option.setVisible(False)
return widget
def _close_menu_chain(self) -> None:
"""Close the menu (and any parent menus) hosting this action."""
w = self.widget
while w is not None:
if isinstance(w, QtWidgets.QMenu):
w.close()
w = w.parentWidget()
|
Instantiate and configure the custom action row widget.
Called by Qt when the action is added to a menu.
Parameters:
| Name | Type | Description | Default |
parent | QWidget | The menu widget that will own the row widget. | required |
Returns:
| Type | Description |
QWidget | The newly created :class:AYOptionalActionWidget. |
Source code in client/ayon_core/ui/components/option_action.py
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 | def createWidget(self, parent: QtWidgets.QWidget) -> QtWidgets.QWidget:
"""Instantiate and configure the custom action row widget.
Called by Qt when the action is added to a menu.
Args:
parent: The menu widget that will own the row widget.
Returns:
The newly created :class:`AYOptionalActionWidget`.
"""
widget = AYOptionalActionWidget(
self._label,
icon_name=self._icon_name,
parent=parent,
)
widget.setEnabled(self.isEnabled())
self.widget = widget
if self._use_option:
widget.option.clicked.connect(self.option_clicked.emit)
widget.option.clicked.connect(self._close_menu_chain)
else:
widget.option.setVisible(False)
return widget
|
Bases: QWidget
Row widget that combines a body area and an :class:AYOptionBox.
The body contains an icon label and a text label. The option box is pinned to the far right. Both sections respond to hover state via :meth:_set_row_hover and :meth:_sync_row_hover.
Parameters:
| Name | Type | Description | Default |
label | str | Display text for the action. | required |
icon_name | str | Material symbol icon name for the label. | 'none' |
parent | QWidget | None | | None |
Source code in client/ayon_core/ui/components/option_action.py
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 | class AYOptionalActionWidget(QtWidgets.QWidget):
"""Row widget that combines a body area and an :class:`AYOptionBox`.
The body contains an icon label and a text label. The option box
is pinned to the far right. Both sections respond to hover state
via :meth:`_set_row_hover` and :meth:`_sync_row_hover`.
Args:
label: Display text for the action.
icon_name: Material symbol icon name for the label.
parent: Optional parent widget.
"""
def __init__(
self,
label: str,
icon_name: str = "none",
parent: QtWidgets.QWidget | None = None,
) -> None:
super().__init__(parent)
_style = get_ayon_style().model.get_style(
"QLabel", variant=AYLabel.Variants.Optional_Action.value
)
icon_size = _style.get("icon-size", 16)
body_widget = AYFrame(self, variant=AYFrame.Variants.Contextual_Menu)
body_widget.setObjectName("OptionalActionBody")
label_wdgt = AYLabel(
label,
variant=AYLabel.Variants.Optional_Action,
icon=icon_name,
icon_size=icon_size,
icon_fill=False,
parent=body_widget,
)
min_h = int(
get_ayon_style().model.get_style("QMenu").get("min-item-height", 0)
)
self.setMinimumHeight(min_h)
option_box = AYOptionBox(icon_size=icon_size, parent=body_widget)
option_box.setObjectName("OptionalActionOption")
body_layout = AYHBoxLayout(body_widget, spacing=2, margin=0)
body_layout.addWidget(label_wdgt, stretch=1)
layout = AYHBoxLayout(self, spacing=0, margin=0)
layout.addWidget(body_widget)
layout.addWidget(option_box)
body_widget.setMouseTracking(True)
self.setMouseTracking(True)
self.icon: QtGui.QIcon = QtGui.QIcon()
self.label: AYLabel = label_wdgt
self.option: AYOptionBox = option_box
self.body: QtWidgets.QWidget = body_widget
# Watch the children's hover transitions so we can keep them in sync
# while the cursor moves between them.
self.label.installEventFilter(self)
self.option.installEventFilter(self)
# -- hover propagation ------------------------------------------------
def _set_row_hover(self, hovered: bool) -> None:
for child in (self.body, self.label, self.option):
child.setAttribute(QtCore.Qt.WA_UnderMouse, hovered)
child.update()
def _sync_row_hover(self) -> None:
# ``underMouse()`` on the parent stays True as long as the cursor is
# anywhere inside this row, even while crossing child borders.
self._set_row_hover(self.underMouse())
def enterEvent(self, event: QtCore.QEvent) -> None:
self._set_row_hover(True)
super().enterEvent(event)
def leaveEvent(self, event: QtCore.QEvent) -> None:
self._set_row_hover(False)
super().leaveEvent(event)
def eventFilter(self, obj: QtCore.QObject, event: QtCore.QEvent) -> bool:
if obj in (self.body, self.label, self.option) and event.type() in (
QtCore.QEvent.Type.Enter,
QtCore.QEvent.Type.Leave,
):
# Qt is about to flip WA_UnderMouse on this child. Defer to
# the next event-loop tick so Qt's own handling has finished,
# then re-assert hover state based on the parent.
QtCore.QTimer.singleShot(0, self._sync_row_hover)
return super().eventFilter(obj, event)
|