Skip to content

utils

QSignalBlocker

Block a QObject's signals for the duration of a with block.

QtCore.QSignalBlocker only implements the context-manager protocol on PySide6; on PySide2 with QtCore.QSignalBlocker(widget): fails with AttributeError: __enter__. This wrapper drives QObject.blockSignals() directly, which behaves identically on Qt 5 and Qt 6, so the same code runs under both bindings.

Unlike Qt's class, blocking starts on __enter__ rather than on construction: Python has no deterministic destructor, so a blocker created outside a with statement would leave signals blocked for however long the object happened to live.

The previous blocked state is restored rather than signals being unconditionally unblocked, so nesting blockers over the same object is safe.

Example

with QSignalBlocker(self.checkbox): self.checkbox.setChecked(True)

Parameters:

Name Type Description Default
*objects

QObjects to block. None entries are ignored so callers can pass widgets that may not exist yet.

()
Source code in client/ayon_core/ui/utils.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
52
53
54
55
56
57
58
class QSignalBlocker:
    """Block a QObject's signals for the duration of a ``with`` block.

    ``QtCore.QSignalBlocker`` only implements the context-manager protocol
    on PySide6; on PySide2 ``with QtCore.QSignalBlocker(widget):`` fails
    with ``AttributeError: __enter__``.  This wrapper drives
    ``QObject.blockSignals()`` directly, which behaves identically on Qt 5
    and Qt 6, so the same code runs under both bindings.

    Unlike Qt's class, blocking starts on ``__enter__`` rather than on
    construction: Python has no deterministic destructor, so a blocker
    created outside a ``with`` statement would leave signals blocked for
    however long the object happened to live.

    The previous blocked state is restored rather than signals being
    unconditionally unblocked, so nesting blockers over the same object is
    safe.

    Example:
        with QSignalBlocker(self.checkbox):
            self.checkbox.setChecked(True)

    Args:
        *objects: QObjects to block. ``None`` entries are ignored so
            callers can pass widgets that may not exist yet.
    """

    def __init__(self, *objects) -> None:
        self._objects = [obj for obj in objects if obj is not None]
        self._previous: list[bool] = []

    def __enter__(self) -> "QSignalBlocker":
        self._previous = [obj.blockSignals(True) for obj in self._objects]
        return self

    def __exit__(self, exc_type, exc_value, traceback) -> None:
        for obj, was_blocked in zip(self._objects, self._previous):
            obj.blockSignals(was_blocked)
        self._previous = []

clear_layout(layout)

Recursively deletes child QWidgets in the initial QLayout and in sub-layouts.

This function handles all types of QLayout subclasses (QVBoxLayout, QHBoxLayout, QGridLayout, etc.) and safely deletes all child widgets while maintaining the integrity of the parent layout structure.

Parameters:

Name Type Description Default
layout

QLayout instance to clear

required
Source code in client/ayon_core/ui/utils.py
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
def clear_layout(layout):
    """Recursively deletes child QWidgets in the initial QLayout and in
    sub-layouts.

    This function handles all types of QLayout subclasses (QVBoxLayout,
    QHBoxLayout, QGridLayout, etc.) and safely deletes all child widgets
    while maintaining the integrity of the parent layout structure.

    Args:
        layout: QLayout instance to clear
    """
    if layout is None:
        return

    # print(f"layout = {layout}")

    # Work backwards through the layout to avoid index issues when removing
    # items
    for i in reversed(range(layout.count())):
        item = layout.takeAt(i)
        if item is None:
            continue

        widget = item.widget()
        sub_layout = item.layout()

        if widget:
            widget.setVisible(False)
            widget.deleteLater()
        elif sub_layout:
            # Recursively clear the sub-layout
            clear_layout(sub_layout)
            # Delete the layout
            sub_layout.deleteLater()

process_activity_data(activity_data, project_data)

Preprocesses payload data to extract and parse comment activities.

This function processes the input data to extract activities, specifically focusing on comment activities. It parses these activities using the AYComment class and returns a list of parsed comment objects.

Parameters:

Name Type Description Default
activity_data dict

The input data containing project activities in a specific structure. Expected to have a nested structure with 'project' -> 'activities'.

required

Returns:

Name Type Description
list list[CommentModel | VersionPublishModel | StatusChangeModel]

A list of parsed comment objects. Returns an empty list if no activities are found or if there's an error during processing.

Source code in client/ayon_core/ui/utils.py
 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
def process_activity_data(
    activity_data: dict, project_data: ProjectData
) -> list[CommentModel | VersionPublishModel | StatusChangeModel]:
    """Preprocesses payload data to extract and parse comment activities.

    This function processes the input data to extract activities, specifically
    focusing on comment activities. It parses these activities using the
    AYComment class and returns a list of parsed comment objects.

    Args:
        activity_data (dict): The input data containing project activities in
            a specific structure. Expected to have a nested structure with
            'project' -> 'activities'.
        project_data (ProjectData)

    Returns:
        list: A list of parsed comment objects. Returns an empty list if
            no activities are found or if there's an error during processing.
    """
    try:
        activities: list[dict] = activity_data["project"]["activities"]
    except KeyError as err:
        logger.error(f"Could not extract activities: {err}")
        return []

    users = {d.short_name: d for d in project_data.users}
    category_colors = {c.name: c.color for c in project_data.comment_category}

    ui_data = []
    nothing = "n/a"
    for act in activities:
        act_data = act.get("activityData", {})
        if isinstance(act_data, str):
            act_data = json.loads(act_data)
            act["activityData"] = act_data

        activity_type = act.get("activityType", "")
        activity_id = act["activityId"]

        user_name = act.get("author", {}).get("name", nothing)
        user_full_name = user_name
        user = users.get(user_name)
        if user:
            user_full_name = user.full_name

        date = act.get("updatedAt", nothing)

        if activity_type == "comment":
            annotation_models, ranges = _parse_annotations(act_data, nothing)
            file_models = _parse_files(act, ranges, nothing)
            category = act_data.get("category", "")
            ui_data.append(
                CommentModel(
                    activity_id=activity_id,
                    user_full_name=user_full_name,
                    user_name=user_name,
                    comment=act.get("body", nothing),
                    comment_date=date,
                    category=category,
                    category_color=category_colors.get(category, ""),
                    files=file_models,
                    annotations=annotation_models,
                )
            )
        elif activity_type == "version.publish":
            ui_data.append(
                VersionPublishModel(
                    activity_id=activity_id,
                    user_full_name=user_full_name,
                    user_name=user_name,
                    version=str(
                        act_data.get("origin", {}).get("name", nothing)
                    ),
                    product=str(
                        act_data.get("context", {}).get("productName", nothing)
                    ),
                    date=date,
                )
            )
        elif activity_type == "status.change":
            version = act_data.get("origin", {}).get("name", nothing)
            parents: list[dict] = act_data.get("parents", {})
            product = next(
                (p["name"] for p in parents if p["type"] == "product"),
                nothing,
            )
            ui_data.append(
                StatusChangeModel(
                    activity_id=activity_id,
                    user_full_name=user_full_name,
                    user_name=user_name,
                    product=product,
                    version=version,
                    old_status=str(act_data.get("oldValue", nothing)),
                    new_status=str(act_data.get("newValue", nothing)),
                    date=date,
                )
            )

    return ui_data