diff --git a/lib/usd/ui/CMakeLists.txt b/lib/usd/ui/CMakeLists.txt index d97fe217f1..307b7595a0 100644 --- a/lib/usd/ui/CMakeLists.txt +++ b/lib/usd/ui/CMakeLists.txt @@ -128,6 +128,7 @@ endif() set(HEADERS api.h initStringResources.h + undoChunkUtils.h ) mayaUsd_promoteHeaderList( diff --git a/lib/usd/ui/debugTools/CompositionEditorCmd.cpp b/lib/usd/ui/debugTools/CompositionEditorCmd.cpp index 680bb636a1..3e2d4a148b 100644 --- a/lib/usd/ui/debugTools/CompositionEditorCmd.cpp +++ b/lib/usd/ui/debugTools/CompositionEditorCmd.cpp @@ -17,6 +17,7 @@ #include #include +#include #include @@ -62,31 +63,6 @@ constexpr auto kReloadFlagLong = "-reload"; const MString WORKSPACE_CONTROL_NAME = "mayaUsdCompositionEditor"; -// RAII guard that opens a named Maya undo chunk -class UndoChunkContext -{ -private: - // Maya's undo chunk names cannot contain spaces (the name is split at the - // first space), so replace them with underscores before quoting. - MString cleanChunkName(const std::string& label) - { - std::string name = label.empty() ? "USD Composition Edit" : label; - std::replace(name.begin(), name.end(), ' ', '_'); - return MString("\"") + name.c_str() + "\""; - } - -public: - explicit UndoChunkContext(const std::string& label) - { - MGlobal::executeCommand( - MString("undoInfo -openChunk -chunkName ") + cleanChunkName(label), false, false); - } - ~UndoChunkContext() { MGlobal::executeCommand("undoInfo -closeChunk", false, false); } - - UndoChunkContext(const UndoChunkContext&) = delete; - UndoChunkContext& operator=(const UndoChunkContext&) = delete; -}; - QPointer g_compositionEditorWidget; Ufe::Observer::Ptr g_selectionObserver; @@ -167,8 +143,8 @@ class MayaCompositionEditorHost : public Adsk::UsdDebug::ApplicationHost UsdUfe::UsdUndoManager::instance().trackLayerStates(layer); } - UndoChunkContext undoChunk(editLabel); - MayaUsdUndoBlock undoBlock; + MayaUsdUI::UndoChunkGuard undoChunk(editLabel); + MayaUsdUndoBlock undoBlock; return edit(); } diff --git a/lib/usd/ui/layerEditor/mayaCommandHook.cpp b/lib/usd/ui/layerEditor/mayaCommandHook.cpp index a0fd4b9b66..b98e2c37f2 100644 --- a/lib/usd/ui/layerEditor/mayaCommandHook.cpp +++ b/lib/usd/ui/layerEditor/mayaCommandHook.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -46,9 +47,6 @@ namespace { std::string quote(const std::string& string) { return STR(" \"") + string + STR("\""); } -// maya doesn't support spaces in undo chunk names... -MString cleanChunkName(QString name) { return quote(name.replace(" ", "_").toStdString()).c_str(); } - std::string getProxyShapeName(const std::string& proxyShapePath) { std::size_t found = proxyShapePath.find_last_of("|"); @@ -102,7 +100,9 @@ void MayaCommandHook::setEditTarget(UsdLayer usdLayer) void MayaCommandHook::openUndoBracket(const QString& name) { MGlobal::executeCommand( - MString("undoInfo -openChunk -chunkName ") + cleanChunkName(name), false, false); + MString("undoInfo -openChunk -chunkName ") + MayaUsdUI::cleanChunkName(name.toStdString()), + false, + false); } // closes a complex undo operation in the host app. Please use UndoContext class to safely diff --git a/lib/usd/ui/renderSetup/CMakeLists.txt b/lib/usd/ui/renderSetup/CMakeLists.txt index b0537e9e2a..b5b668ab50 100644 --- a/lib/usd/ui/renderSetup/CMakeLists.txt +++ b/lib/usd/ui/renderSetup/CMakeLists.txt @@ -17,11 +17,13 @@ target_sources(${PROJECT_NAME} PRIVATE + mayaEditCommitter.cpp renderSetupWindowCmd.cpp ) mayaUsd_promoteHeaderList( HEADERS + mayaEditCommitter.h renderSetupWindowCmd.h BASEDIR ${PROJECT_NAME}/ui diff --git a/lib/usd/ui/renderSetup/mayaEditCommitter.cpp b/lib/usd/ui/renderSetup/mayaEditCommitter.cpp new file mode 100644 index 0000000000..ba129034fd --- /dev/null +++ b/lib/usd/ui/renderSetup/mayaEditCommitter.cpp @@ -0,0 +1,73 @@ +// +// Copyright 2026 Autodesk +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#include "mayaEditCommitter.h" + +#include +#include + +#include + +#include + +#include + +namespace MayaUsdRenderSetup { + +MayaEditCommitter::MayaEditCommitter(QObject* parent) + : QObject(parent) +{ +} + +void MayaEditCommitter::setStages(const std::vector& stages) +{ + _stages.clear(); + _stages.reserve(stages.size()); + for (const auto& hostStage : stages) { + if (hostStage.stage) { + _stages.push_back(hostStage.stage); + } + } +} + +void MayaEditCommitter::commit(const std::string& undoLabel, std::function doEdit) +{ + if (!doEdit || _stages.empty()) { + return; + } + + ++_inFlightCount; + // Schedule the decrement before doEdit() so it fires even if doEdit() throws. + // The one-event-loop-cycle delay keeps isLocalEditInFlight() true through the + // USD-notice burst that follows the edit, so the notice bridge treats the + // resulting refresh as self-originated and skips re-entrancy. + QTimer::singleShot(0, this, [this]() { + if (_inFlightCount > 0) { + --_inFlightCount; + } + }); + + UsdUfe::trackStagesEditTargets(_stages); + + const MayaUsdUI::UndoChunkGuard undoChunkGuard(undoLabel); + MayaUsd::MayaUsdUndoBlock block; + { + PXR_NS::SdfChangeBlock changeBlock; + doEdit(); + } +} + +} // namespace MayaUsdRenderSetup diff --git a/lib/usd/ui/renderSetup/mayaEditCommitter.h b/lib/usd/ui/renderSetup/mayaEditCommitter.h new file mode 100644 index 0000000000..2dae20ac47 --- /dev/null +++ b/lib/usd/ui/renderSetup/mayaEditCommitter.h @@ -0,0 +1,55 @@ +// +// Copyright 2026 Autodesk +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#ifndef MAYAUSDUI_USD_RENDERSETUP_MAYAEDITCOMMITTER_H +#define MAYAUSDUI_USD_RENDERSETUP_MAYAEDITCOMMITTER_H + +#include + +#include +#include +#include + +#include +#include +#include + +namespace MayaUsdRenderSetup { + +//! MayaUSD implementation of Adsk::IEditCommitter for the Render Setup UI. +//! Captures USD edits through UsdUfe and flushes them onto the Maya undo stack +//! via MayaUsdUndoBlock. +class MayaEditCommitter + : public QObject + , public Adsk::IEditCommitter +{ + Q_OBJECT +public: + explicit MayaEditCommitter(QObject* parent = nullptr); + + void setStages(const std::vector& stages); + + void commit(const std::string& undoLabel, std::function doEdit) override; + bool isLocalEditInFlight() const override { return _inFlightCount > 0; } + +private: + std::vector _stages; + int _inFlightCount { 0 }; +}; + +} // namespace MayaUsdRenderSetup + +#endif // MAYAUSDUI_USD_RENDERSETUP_MAYAEDITCOMMITTER_H diff --git a/lib/usd/ui/renderSetup/renderSetupWindowCmd.cpp b/lib/usd/ui/renderSetup/renderSetupWindowCmd.cpp index 0023c5ef91..afec1b21ec 100644 --- a/lib/usd/ui/renderSetup/renderSetupWindowCmd.cpp +++ b/lib/usd/ui/renderSetup/renderSetupWindowCmd.cpp @@ -15,6 +15,8 @@ // #include "renderSetupWindowCmd.h" +#include "mayaEditCommitter.h" + #include #include #include @@ -88,12 +90,17 @@ class RenderSetupWindow static void onSceneChangedCB(void* clientData); private: - void applyStages() { _tree->setStages(_hostStages); } + void applyStages() + { + _editCommitter->setStages(_hostStages); + _tree->setStages(_hostStages); + } private: - Adsk::RenderSetupWidget* _tree; - std::vector _hostStages; - std::vector _sceneCallbackIds; + Adsk::RenderSetupWidget* _tree; + MayaUsdRenderSetup::MayaEditCommitter* _editCommitter { nullptr }; + std::vector _hostStages; + std::vector _sceneCallbackIds; }; RenderSetupWindow::RenderSetupWindow(QWidget* parent) @@ -101,6 +108,8 @@ RenderSetupWindow::RenderSetupWindow(QWidget* parent) { // Create the render setup widget and set it as the central widget of the window. _tree = new Adsk::RenderSetupWidget(this); + _editCommitter = new MayaUsdRenderSetup::MayaEditCommitter(nullptr); + _tree->setEditCommitter(std::unique_ptr(_editCommitter)); setCentralWidget(_tree); _tree->show(); diff --git a/lib/usd/ui/undoChunkUtils.h b/lib/usd/ui/undoChunkUtils.h new file mode 100644 index 0000000000..785bade4e7 --- /dev/null +++ b/lib/usd/ui/undoChunkUtils.h @@ -0,0 +1,66 @@ +// +// Copyright 2026 Autodesk +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#ifndef MAYAUSDUI_UNDO_CHUNK_UTILS_H +#define MAYAUSDUI_UNDO_CHUNK_UTILS_H + +#include +#include + +#include +#include + +#include + +namespace MayaUsdUI { + +//! Sanitizes a user-facing undo label for use as a Maya undo chunk name. +//! Maya's -chunkName flag does not accept spaces; backslash and double-quote +//! are escaped so the result is safe to embed in a MEL double-quoted string. +inline MString cleanChunkName(const std::string& label) +{ + QString name = QString::fromStdString(label); + name.replace(QLatin1Char('\\'), QLatin1String("\\\\")); + name.replace(QLatin1Char('"'), QLatin1String("\\\"")); + name.replace(QLatin1Char(' '), QLatin1Char('_')); + return MString(("\"" + name.toStdString() + "\"").c_str()); +} + +//! RAII guard that opens a named Maya undo chunk on construction and closes it +//! on destruction, ensuring the chunk is always balanced even if an exception +//! is thrown inside the guarded scope. +struct UndoChunkGuard +{ + //! Opens the undo chunk. If \p label is empty, no chunk name is set. + explicit UndoChunkGuard(const std::string& label) + { + if (label.empty()) { + MGlobal::executeCommand("undoInfo -openChunk", false, false); + } else { + MGlobal::executeCommand( + MString("undoInfo -openChunk -chunkName ") + cleanChunkName(label), false, false); + } + } + + ~UndoChunkGuard() { MGlobal::executeCommand("undoInfo -closeChunk", false, false); } + + UndoChunkGuard(const UndoChunkGuard&) = delete; + UndoChunkGuard& operator=(const UndoChunkGuard&) = delete; +}; + +} // namespace MayaUsdUI + +#endif // MAYAUSDUI_UNDO_CHUNK_UTILS_H diff --git a/lib/usdUfe/ufe/CMakeLists.txt b/lib/usdUfe/ufe/CMakeLists.txt index 8866dfa1b4..87a76d6b56 100644 --- a/lib/usdUfe/ufe/CMakeLists.txt +++ b/lib/usdUfe/ufe/CMakeLists.txt @@ -49,6 +49,7 @@ target_sources(${PROJECT_NAME} UsdUndoVisibleCommand.cpp UsdUndoSetDefaultPrimCommand.cpp UsdUndoClearDefaultPrimCommand.cpp + UsdLabeledEditUndoableCommand.cpp UsdUndoableCommand.cpp Utils.cpp ) @@ -219,6 +220,7 @@ set(HEADERS UsdUndoVisibleCommand.h UsdUndoSetDefaultPrimCommand.h UsdUndoClearDefaultPrimCommand.h + UsdLabeledEditUndoableCommand.h UsdUndoableCommand.h Utils.h ) diff --git a/lib/usdUfe/ufe/UsdLabeledEditUndoableCommand.cpp b/lib/usdUfe/ufe/UsdLabeledEditUndoableCommand.cpp new file mode 100644 index 0000000000..c2173b284c --- /dev/null +++ b/lib/usdUfe/ufe/UsdLabeledEditUndoableCommand.cpp @@ -0,0 +1,36 @@ +// +// Copyright 2026 Autodesk +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#include "UsdLabeledEditUndoableCommand.h" + +namespace USDUFE_NS_DEF { + +UsdLabeledEditUndoableCommand::UsdLabeledEditUndoableCommand( + std::string label, + std::function edit) + : _label(std::move(label)) + , _edit(std::move(edit)) +{ +} + +void UsdLabeledEditUndoableCommand::executeImplementation() +{ + if (_edit) { + _edit(); + } +} + +} // namespace USDUFE_NS_DEF diff --git a/lib/usdUfe/ufe/UsdLabeledEditUndoableCommand.h b/lib/usdUfe/ufe/UsdLabeledEditUndoableCommand.h new file mode 100644 index 0000000000..08b399ca34 --- /dev/null +++ b/lib/usdUfe/ufe/UsdLabeledEditUndoableCommand.h @@ -0,0 +1,49 @@ +// +// Copyright 2026 Autodesk +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#ifndef USDUFE_USDLABELEDEDITUNDOABLECOMMAND_H +#define USDUFE_USDLABELEDEDITUNDOABLECOMMAND_H + +#include +#include +#include + +#include + +#include +#include + +namespace USDUFE_NS_DEF { + +//! UFE undo command that runs a host-supplied edit lambda and exposes +//! a human-readable undo label through commandString(). +class USDUFE_PUBLIC UsdLabeledEditUndoableCommand : public UsdUndoableCommand +{ +public: + UsdLabeledEditUndoableCommand(std::string label, std::function edit); + + void executeImplementation() override; + + UFE_V4(std::string commandString() const override { return _label; }) + +private: + std::string _label; + std::function _edit; +}; + +} // namespace USDUFE_NS_DEF + +#endif // USDUFE_USDLABELEDEDITUNDOABLECOMMAND_H diff --git a/lib/usdUfe/undo/CMakeLists.txt b/lib/usdUfe/undo/CMakeLists.txt index 042bb2fac0..abb56e8b3e 100644 --- a/lib/usdUfe/undo/CMakeLists.txt +++ b/lib/usdUfe/undo/CMakeLists.txt @@ -7,6 +7,7 @@ target_sources(${PROJECT_NAME} UsdUndoManager.cpp UsdUndoStateDelegate.cpp UsdUndoableItem.cpp + UsdUndoUtils.cpp ) # ----------------------------------------------------------------------------- @@ -17,6 +18,7 @@ set(HEADERS UsdUndoManager.h UsdUndoStateDelegate.h UsdUndoableItem.h + UsdUndoUtils.h ) # ----------------------------------------------------------------------------- diff --git a/lib/usdUfe/undo/UsdUndoUtils.cpp b/lib/usdUfe/undo/UsdUndoUtils.cpp new file mode 100644 index 0000000000..438885f0f2 --- /dev/null +++ b/lib/usdUfe/undo/UsdUndoUtils.cpp @@ -0,0 +1,33 @@ +// +// Copyright 2026 Autodesk +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#include "UsdUndoUtils.h" + +#include + +namespace USDUFE_NS_DEF { + +void trackStagesEditTargets(const std::vector& stages) +{ + for (const auto& stage : stages) { + if (!stage) { + continue; + } + UsdUndoManager::instance().trackLayerStates(stage->GetEditTarget().GetLayer()); + } +} + +} // namespace USDUFE_NS_DEF diff --git a/lib/usdUfe/undo/UsdUndoUtils.h b/lib/usdUfe/undo/UsdUndoUtils.h new file mode 100644 index 0000000000..ef05ce7ea7 --- /dev/null +++ b/lib/usdUfe/undo/UsdUndoUtils.h @@ -0,0 +1,34 @@ +// +// Copyright 2026 Autodesk +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#ifndef USDUFE_UNDO_USDUNDOUTILS_H +#define USDUFE_UNDO_USDUNDOUTILS_H + +#include + +#include + +#include + +namespace USDUFE_NS_DEF { + +//! Installs a UsdUndoStateDelegate on each stage's current edit-target +//! layer. Repeated calls for the same layer are idempotent. +USDUFE_PUBLIC void trackStagesEditTargets(const std::vector& stages); + +} // namespace USDUFE_NS_DEF + +#endif // USDUFE_UNDO_USDUNDOUTILS_H