Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions lib/usd/ui/layerEditor/layerTreeModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -297,13 +297,17 @@ void LayerTreeModel::setSessionState(SessionState* in_sessionState)
rebuildModelOnIdle(true);
}

void LayerTreeModel::rebuildModelOnIdle(bool dataChanged)
void LayerTreeModel::rebuildModelOnIdle(bool dataChanged, bool refreshLockState)
{
// Coalesce rebuilds within one event-loop turn into a single rebuild. Accumulate
// the strongest request across coalesced callers (data-changed and lock-refresh).
_selectedLayerDataChanged |= dataChanged;
_rebuildOnIdleRefreshLockState = _rebuildOnIdleRefreshLockState || refreshLockState;
if (!_rebuildOnIdlePending) {
_rebuildOnIdlePending = true;
QTimer::singleShot(0, this, [this]() {
bool refreshLockState = false;
const bool refreshLockState = _rebuildOnIdleRefreshLockState;
_rebuildOnIdleRefreshLockState = false;
this->rebuildModel(refreshLockState);
});
}
Expand Down Expand Up @@ -527,8 +531,9 @@ void LayerTreeModel::usd_layerDirtinessChanged(
// called from SessionState::currentStageChangedSignal
void LayerTreeModel::sessionStageChanged()
{
bool refreshLockState = true;
rebuildModel(refreshLockState);
// Coalesce: a burst of stage changes collapses into one rebuild instead of
// rebuilding synchronously on every change.
rebuildModelOnIdle(/*dataChanged*/ false, /*refreshLockState*/ true);
}

// called from SessionState::autoHideSessionLayerSignal
Expand Down
3 changes: 2 additions & 1 deletion lib/usd/ui/layerEditor/layerTreeModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,10 @@ class LayerTreeModel

mutable int _lastAskedAnonLayerNameSinceRebuild = 0;

void rebuildModelOnIdle(bool dataChanged = false);
void rebuildModelOnIdle(bool dataChanged = false, bool refreshLockState = false);
bool _rebuildOnIdlePending = false;
bool _selectedLayerDataChanged = false;
bool _rebuildOnIdleRefreshLockState = false;
void rebuildModel(bool refreshLockState = false);

void updateTargetLayer(InRebuildModel inRebuild);
Expand Down
16 changes: 15 additions & 1 deletion lib/usd/ui/layerEditor/layerTreeView.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include <maya/MGlobal.h>
#include <maya/MQtUtil.h>

#include <QtCore/QTimer>
#include <QtGui/QColor>
#include <QtGui/QCursor>
#include <QtWidgets/QMenu>
Expand Down Expand Up @@ -140,7 +141,7 @@ LayerTreeView::LayerTreeView(SessionState* in_sessionState, QWidget* in_parent)
_model->sessionState(),
&SessionState::stageListChangedSignal,
this,
&LayerTreeView::updateFromSessionState);
&LayerTreeView::updateFromSessionStateOnIdle);

auto buttonDefinitions = LayerTreeItem::actionButtonsDefinition();
auto muteActionIter = buttonDefinitions.find(LayerActionType::Mute);
Expand Down Expand Up @@ -352,6 +353,19 @@ void LayerViewMemento::restore(LayerTreeView& view, LayerTreeModel& model)
}
}

// Coalesce a burst of stageListChangedSignal notifications into a single refresh
// (updateFromSessionState re-scans all stages, so avoid doing it per stage).
void LayerTreeView::updateFromSessionStateOnIdle()
{
if (!_updateFromSessionStatePending) {
_updateFromSessionStatePending = true;
QTimer::singleShot(0, this, [this]() {
_updateFromSessionStatePending = false;
updateFromSessionState();
});
}
}

void LayerTreeView::updateFromSessionState()
{
if (_cachedModelState == nullptr) {
Expand Down
3 changes: 3 additions & 0 deletions lib/usd/ui/layerEditor/layerTreeView.h
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ class LayerTreeView

// Updates the _cachedModelState using stage data from the session
void updateFromSessionState();
// Coalesced updateFromSessionState (one refresh per event-loop turn).
void updateFromSessionStateOnIdle();
bool _updateFromSessionStatePending { false };

LayerTreeViewStyle _treeViewStyle;
QPointer<LayerTreeModel> _model;
Expand Down
80 changes: 38 additions & 42 deletions lib/usd/ui/layerEditor/stageSelectorWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,36 +36,31 @@
#include <ufe/selectionNotification.h>

#include <QtCore/QSignalBlocker>
#include <QtCore/QTimer>
#include <QtWidgets/QHBoxLayout>
#include <QtWidgets/QLabel>

Q_DECLARE_METATYPE(UsdLayerEditor::SessionState::StageEntry);

namespace {

int getEntryIndexById(
const std::string& id,
std::vector<UsdLayerEditor::SessionState::StageEntry> const& stages)
// Find the combo index whose stored StageEntry matches id, or -1. Used instead of
// SessionState::allStages() (a full DAG scan) so per-stage handlers stay O(combo).
int comboIndexById(const QComboBox* dropDown, const std::string& id)
{
auto it = std::find_if(
stages.begin(), stages.end(), [&id](UsdLayerEditor::SessionState::StageEntry stageEntry) {
return (id == stageEntry._id);
});

if (it != stages.end()) {
return std::distance(stages.begin(), it);
for (int i = 0, count = dropDown->count(); i < count; ++i) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to guard against input dropDown == nullptr?

@AramAzhari-adsk AramAzhari-adsk Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good suggestion. It seems that dropdown is only created in a constructor once and never reset (not even in the destructor). So at all the callers' location the dropDown is guaranteed non-null for the widget's entire lifetime.

I could still add this if it's blocking this PR, or take a note and improve it later.

   if (!dropDown)
        return -1;

// Bind itemData() to a named QVariant so value<>() uses the const-lvalue
// overload. Calling value<>() directly on the returned rvalue selects the
// move overload, which move-constructs StageEntry and trips a spurious
// -Werror=array-bounds under GCC.
const QVariant data = dropDown->itemData(i);
if (data.value<UsdLayerEditor::SessionState::StageEntry>()._id == id) {
return i;
}
}

return -1;
}

int getEntryIndexById(
UsdLayerEditor::SessionState::StageEntry const& entry,
std::vector<UsdLayerEditor::SessionState::StageEntry> const& stages)
{
return getEntryIndexById(entry._id, stages);
}

bool loadStagePinnedOption()
{
const MString optionName = PXR_NS::MayaUsdOptionVars->PinLayerEditorStage.GetText();
Expand Down Expand Up @@ -241,7 +236,7 @@ void StageSelectorWidget::setSessionState(SessionState* in_sessionState)
_sessionState,
&SessionState::stageListChangedSignal,
this,
&StageSelectorWidget::updateFromSessionState);
&StageSelectorWidget::updateFromSessionStateOnIdle);
connect(
_sessionState,
&SessionState::currentStageChangedSignal,
Expand Down Expand Up @@ -269,6 +264,23 @@ SessionState::StageEntry const StageSelectorWidget::selectedStage()

return SessionState::StageEntry();
}
// Coalesce a burst of stageListChangedSignal notifications into a single dropdown
// rebuild (last requested selection wins). Avoids a per-stage rebuild + DAG scan.
void StageSelectorWidget::updateFromSessionStateOnIdle(
SessionState::StageEntry const& entryToSelect)
{
_pendingSelectEntry = entryToSelect;
if (!_updateFromSessionStatePending) {
_updateFromSessionStatePending = true;
QTimer::singleShot(0, this, [this]() {
_updateFromSessionStatePending = false;
const SessionState::StageEntry entry = _pendingSelectEntry;
_pendingSelectEntry = SessionState::StageEntry();
updateFromSessionState(entry);
});
}
}

// repopulates the combo based on the session stage list
void StageSelectorWidget::updateFromSessionState(SessionState::StageEntry const& entryToSelect)
{
Expand Down Expand Up @@ -326,9 +338,6 @@ void StageSelectorWidget::selectionChanged()
if (!ufeGlobalSelection)
return;

std::vector<SessionState::StageEntry> allStages;
bool allStagesFilled = false;

// We will set the currently selected stage to be the stage of the first item
// that is a USD item. So if multiple stages are selected, the first one wins.
const Ufe::Selection& ufeSelection = *ufeGlobalSelection;
Expand All @@ -337,14 +346,9 @@ void StageSelectorWidget::selectionChanged()
if (!proxyShapePtr)
continue;

if (!allStagesFilled) {
allStages = _sessionState->allStages();
allStagesFilled = true;
}

MFnDagNode dagNode(proxyShapePtr->thisMObject());
const std::string id = dagNode.uuid().asString().asChar();
const int index = getEntryIndexById(id, allStages);
const int index = comboIndexById(_dropDown, id);
if (index == -1)
continue;

Expand Down Expand Up @@ -393,7 +397,7 @@ void StageSelectorWidget::updateContentButton()
void StageSelectorWidget::sessionStageChanged()
{
if (!_internalChange) {
auto index = getEntryIndexById(_sessionState->stageEntry(), _sessionState->allStages());
auto index = comboIndexById(_dropDown, _sessionState->stageEntry()._id);
if (index != -1) {
QSignalBlocker blocker(_dropDown);
_dropDown->setCurrentIndex(index);
Expand All @@ -403,7 +407,7 @@ void StageSelectorWidget::sessionStageChanged()

void StageSelectorWidget::stageRenamed(SessionState::StageEntry const& renamedEntry)
{
auto index = getEntryIndexById(renamedEntry, _sessionState->allStages());
auto index = comboIndexById(_dropDown, renamedEntry._id);
if (index != -1) {
_dropDown->setItemText(index, renamedEntry._displayName.c_str());
_dropDown->setItemData(index, QVariant::fromValue(renamedEntry));
Expand All @@ -412,18 +416,10 @@ void StageSelectorWidget::stageRenamed(SessionState::StageEntry const& renamedEn

void StageSelectorWidget::stageReset(SessionState::StageEntry const& entry)
{
// Individual combo box entries have a short display name and a reference to a stage,
// which is not a unique combination. By construction the combo box indices do line
// up with the SessionState StageEntry vector though, so in the case of resetting
// a proxy we will find the matching full proxy path in that vector and use its index
// to update the combo box.
auto count = _dropDown->count();
if (count <= 0) {
return;
}

auto index = getEntryIndexById(entry, _sessionState->allStages());
if (index >= 0 && index < count) {
// Refresh only the matching combo item's data, matched locally by id, instead
// of SessionState::allStages() (a full DAG scan) — this fires O(N) times.
const int index = comboIndexById(_dropDown, entry._id);
if (index != -1) {
_dropDown->setItemData(index, QVariant::fromValue(entry));
}
}
Expand Down
7 changes: 7 additions & 0 deletions lib/usd/ui/layerEditor/stageSelectorWidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ class StageSelectorWidget : public QWidget
// slot:
void updateFromSessionState(
SessionState::StageEntry const& entryToSelect = SessionState::StageEntry());
// Coalesced updateFromSessionState (one dropdown rebuild per event-loop turn).
void updateFromSessionStateOnIdle(
SessionState::StageEntry const& entryToSelect = SessionState::StageEntry());
void stageRenamed(SessionState::StageEntry const& renamedEntry);
void stageReset(SessionState::StageEntry const& entry);
void sessionStageChanged();
Expand All @@ -64,6 +67,10 @@ class StageSelectorWidget : public QWidget
QPushButton* _collapseContent { nullptr };
bool _internalChange { false }; // for notifications
bool _pinStageSelection { true };

// Coalescing state for updateFromSessionStateOnIdle().
bool _updateFromSessionStatePending { false };
SessionState::StageEntry _pendingSelectEntry;
};

} // namespace UsdLayerEditor
Expand Down
Loading