diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 6a07ea92d2..81ab734b6a 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -1,4 +1,5 @@ add_subdirectory(usdUfe) +add_subdirectory(usdLayerEditor) if (BUILD_MAYAUSD_LIBRARY) add_subdirectory(mayaUsd) add_subdirectory(usd) diff --git a/lib/mayaUsd/CMakeLists.txt b/lib/mayaUsd/CMakeLists.txt index 3b8198cc31..e62f52dcd2 100644 --- a/lib/mayaUsd/CMakeLists.txt +++ b/lib/mayaUsd/CMakeLists.txt @@ -272,11 +272,18 @@ target_link_libraries(${PROJECT_NAME} $<$:MaterialXGenGlsl> ) +target_link_libraries(${PROJECT_NAME} PRIVATE usdLayerEditor Qt::Core) +target_compile_definitions(${PROJECT_NAME} PRIVATE MAYAUSD_USE_SHARED_LAYER_EDITOR=1) + # ----------------------------------------------------------------------------- # run-time search paths # ----------------------------------------------------------------------------- if(IS_MACOSX OR IS_LINUX) mayaUsd_init_rpath(rpath "lib") + # Search libmayaUsd's own directory so it can resolve sibling libraries it + # links privately (e.g. usdLayerEditor). DT_RUNPATH is non-transitive, so + # such siblings are not found via the loading module's rpath. + mayaUsd_add_rpath(rpath ".") if(DEFINED MAYAUSD_TO_USD_RELATIVE_PATH) mayaUsd_add_rpath(rpath "../${MAYAUSD_TO_USD_RELATIVE_PATH}/lib") elseif(DEFINED PXR_USD_LOCATION) diff --git a/lib/mayaUsd/commands/CMakeLists.txt b/lib/mayaUsd/commands/CMakeLists.txt index 40b716f132..957c8ae538 100644 --- a/lib/mayaUsd/commands/CMakeLists.txt +++ b/lib/mayaUsd/commands/CMakeLists.txt @@ -10,6 +10,7 @@ target_sources(${PROJECT_NAME} editTargetCommand.cpp layerEditorCommand.cpp layerEditorWindowCommand.cpp + mayaLayerEditorDCCFunctions.cpp schemaCommand.cpp ) @@ -22,6 +23,7 @@ set(HEADERS editTargetCommand.h layerEditorCommand.h layerEditorWindowCommand.h + mayaLayerEditorDCCFunctions.h schemaCommand.h ) diff --git a/lib/mayaUsd/commands/layerEditorCommand.cpp b/lib/mayaUsd/commands/layerEditorCommand.cpp index a337c56693..11aa8bca49 100644 --- a/lib/mayaUsd/commands/layerEditorCommand.cpp +++ b/lib/mayaUsd/commands/layerEditorCommand.cpp @@ -16,1451 +16,82 @@ #include "layerEditorCommand.h" -#include -#include -#ifdef WANT_ADSK_USD_EDIT_FORWARD_BUILD -#include -#endif -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include -#include -#include -#include -#include -#include -#include - -PXR_NAMESPACE_USING_DIRECTIVE - -namespace { -const char kInsertSubPathFlag[] = "is"; -const char kInsertSubPathFlagL[] = "insertSubPath"; -const char kRemoveSubPathFlag[] = "rs"; -const char kRemoveSubPathFlagL[] = "removeSubPath"; -const char kReplaceSubPathFlag[] = "rp"; -const char kReplaceSubPathFlagL[] = "replaceSubPath"; -const char kMoveSubPathFlag[] = "mv"; -const char kMoveSubPathFlagL[] = "moveSubPath"; -const char kDiscardEditsFlag[] = "de"; -const char kDiscardEditsFlagL[] = "discardEdits"; -const char kClearLayerFlag[] = "cl"; -const char kClearLayerFlagL[] = "clear"; -const char kFlattenLayerFlag[] = "fl"; -const char kFlattenLayerFlagL[] = "flatten"; -const char kAddAnonSublayerFlag[] = "aa"; -const char kAddAnonSublayerFlagL[] = "addAnonymous"; -const char kMuteLayerFlag[] = "mt"; -const char kMuteLayerFlagL[] = "muteLayer"; -const char kLockLayerFlag[] = "lk"; -const char kLockLayerFlagL[] = "lockLayer"; -const char kSkipSystemLockedFlag[] = "ssl"; -const char kSkipSystemLockedFlagL[] = "skipSystemLocked"; -const char kRefreshSystemLockFlag[] = "rl"; -const char kRefreshSystemLockFlagL[] = "refreshSystemLock"; -const char kStitchLayersFlag[] = "sl"; -const char kStitchLayersFlagL[] = "stitchLayers"; - -// Disables updateEditTarget's functionality is set. -// Areas that will be affected are: -// - Mute layer -// - Lock layer -// - System lock layer -TF_DEFINE_ENV_SETTING( - MAYAUSD_LAYEREDITOR_DISABLE_AUTOTARGET, - false, - "When set, disables auto retargeting of layers based on the file and permission status."); -} // namespace - -namespace MAYAUSD_NS_DEF { - -namespace Impl { - -enum class CmdId -{ - kInsert, - kRemove, - kMove, - kReplace, - kDiscardEdit, - kClearLayer, - kFlattenLayer, - kAddAnonLayer, - kMuteLayer, - kLockLayer, - kRefreshSystemLock, - kStitchLayers -}; - -class BaseCmd -{ -public: - BaseCmd(CmdId id) - : _cmdId(id) - { - } - virtual ~BaseCmd() { } - virtual bool doIt(SdfLayerHandle layer) = 0; - virtual bool undoIt(SdfLayerHandle layer) = 0; - - std::string _cmdResult; // set if the command returns something - -protected: - CmdId _cmdId; - // we need to hold on to dirty subLayers if we remove them - std::vector _subLayersRefs; - void holdOntoSubLayers(SdfLayerHandle layer); - void releaseSubLayers() { _subLayersRefs.clear(); } - void holdOnPathIfDirty(SdfLayerHandle layer, std::string path); - void updateEditTarget(const PXR_NS::UsdStageWeakPtr stage); -}; - -void BaseCmd::holdOnPathIfDirty(SdfLayerHandle layer, std::string path) -{ - auto subLayerHandle = SdfLayer::FindRelativeToLayer(layer, path); - if (subLayerHandle != nullptr) { - if (subLayerHandle->IsDirty() || subLayerHandle->IsAnonymous()) { - _subLayersRefs.push_back(subLayerHandle); - } - holdOntoSubLayers(subLayerHandle); // we'll need to hold onto children as well - } -} - -// hold references to any anon or dirty sublayer -void BaseCmd::holdOntoSubLayers(SdfLayerHandle layer) -{ - const std::vector& sublayers = layer->GetSubLayerPaths(); - for (auto path : sublayers) { - holdOnPathIfDirty(layer, path); - } -} - -// Set the edit target to Session layer if no other layers are modifiable, -// unless the user has disabled this feature with an env var. -void BaseCmd::updateEditTarget(const PXR_NS::UsdStageWeakPtr stage) -{ - //// User-controlled environment variable to disable automatic target change. - if (TfGetEnvSetting(MAYAUSD_LAYEREDITOR_DISABLE_AUTOTARGET)) { - return; - } - - if (!stage) - return; - -#ifdef WANT_ADSK_USD_EDIT_FORWARD_BUILD - // If edit forwarding is active and the fallback target is now locked, redirect the - // fallback to the session layer so unmatched edits still have a writable destination, - // then skip the normal auto-targeting (in EF mode the stage edit target is already the - // session layer). - if (auto controller = MayaUsdEditForwardController::GetForStage(UsdStageRefPtr(stage))) { - if (controller->isForwardingActive()) { - auto fallback = controller->fallbackTarget(); - if (fallback && MayaUsd::isLayerLocked(fallback)) { - std::string errMsg; - if (!UsdUfe::isAnyLayerModifiable(stage, &errMsg)) { - MPxCommand::displayInfo(errMsg.c_str()); - controller->setFallbackTarget(stage->GetSessionLayer()); - } - } - return; - } - } -#endif - - if (stage->GetEditTarget().GetLayer() == stage->GetSessionLayer()) - return; - - // If the currently targeted layer isn't locked, we don't need to change it. - if (!MayaUsd::isLayerLocked(stage->GetEditTarget().GetLayer())) - return; - - // If there are no target-able layers, we set the target to session layer. - std::string errMsg; - if (!UsdUfe::isAnyLayerModifiable(stage, &errMsg)) { - MPxCommand::displayInfo(errMsg.c_str()); - stage->SetEditTarget(stage->GetSessionLayer()); - } -} - -class InsertRemoveSubPathBase : public BaseCmd -{ -public: - int _index = -1; - std::string _subPath; - std::string _proxyShapePath; - - InsertRemoveSubPathBase(CmdId id) - : BaseCmd(id) - { - } - - bool doIt(SdfLayerHandle layer) override - { - if (_cmdId == CmdId::kInsert || _cmdId == CmdId::kAddAnonLayer) { - if (_index == -1) { - _index = (int)layer->GetNumSubLayerPaths(); - } - if (_index != 0) { - if (!validateAndReportIndex(layer, _index, (int)layer->GetNumSubLayerPaths() + 1)) { - return false; - } - } - - // According to USD codebase, we should always call SdfLayer::InsertSubLayerPath() - // with a layer's identifier. So, if the layer exists, override _subPath with the - // identifier in case this command was called with a filesystem path. Otherwise, - // adding the layer with the filesystem path can cause issue when muting the layer - // on Windows if the path is absolute and start with a capital drive letter. - // - // Note: It's possible that SdfLayer::FindOrOpen() fail because we - // allow user to add layer that does not exists. - auto layerToAdd = SdfLayer::FindOrOpen(_subPath); - if (layerToAdd) { - _subPath = layerToAdd->GetIdentifier(); - } - - layer->InsertSubLayerPath(_subPath, _index); - TF_VERIFY( - (static_cast(_index) < layer->GetSubLayerPaths().size()) - && layer->GetSubLayerPaths()[_index] == _subPath); - } else { - TF_VERIFY(_cmdId == CmdId::kRemove); - if (!validateAndReportIndex(layer, _index, (int)layer->GetNumSubLayerPaths())) { - return false; - } - saveSelection(); - _subPath = layer->GetSubLayerPaths()[_index]; - holdOnPathIfDirty(layer, _subPath); - - // if the current edit target is the layer to remove or - // a sublayer of the layer to remove, - // set the root layer as the current edit target - auto layerToRemove = SdfLayer::FindRelativeToLayer(layer, _subPath); - auto stage = getStage(); - auto currentTarget = stage->GetEditTarget().GetLayer(); - - // Helper function to find if a layer is in the - // hierarchy of another layer - // - // rootLayer: The root layer of the hierarchy - // layer: The layer to find - // ignore : Optional layer used has the root of a hierarchy that - // we don't want to check in. - // ignoreSubPath : Optional subpath used whith ignore layer. - auto isInHierarchy = [](const SdfLayerHandle& rootLayer, - const SdfLayerHandle& layer, - const SdfLayerHandle* ignore = nullptr, - const std::string* ignoreSubPath = nullptr) { - // Impl used for recursive call - auto isInHierarchyImpl = [](const SdfLayerHandle& rootLayer, - const SdfLayerHandle& layer, - const SdfLayerHandle* ignore, - const std::string* ignoreSubPath, - auto& implRef) { - if (!rootLayer || !layer) - return false; - - if (rootLayer->GetIdentifier() == layer->GetIdentifier()) - return true; - - const auto subLayerPaths = rootLayer->GetSubLayerPaths(); - for (const auto& subLayerPath : subLayerPaths) { - - if (ignore && ignoreSubPath - && (*ignore)->GetIdentifier() == rootLayer->GetIdentifier() - && *ignoreSubPath == subLayerPath) - continue; - - const auto subLayer - = SdfLayer::FindRelativeToLayer(rootLayer, subLayerPath); - if (implRef(subLayer, layer, ignore, ignoreSubPath, implRef)) - return true; - } - return false; - }; - return isInHierarchyImpl( - rootLayer, layer, ignore, ignoreSubPath, isInHierarchyImpl); - }; - - if (isInHierarchy(layerToRemove, currentTarget)) { - // The current edit layer is in the hierarchy of the layer to remove, - // now we need to be sure the edit target layer is not also a sublayer - // of another layer in the stage. - if (!isInHierarchy(stage->GetRootLayer(), currentTarget, &layer, &_subPath)) { - _editTargetPath = currentTarget->GetIdentifier(); - stage->SetEditTarget(stage->GetRootLayer()); - } - } - - layer->RemoveSubLayerPath(_index); - } - return true; - } - bool undoIt(SdfLayerHandle layer) override - { - if (_cmdId == CmdId::kInsert || _cmdId == CmdId::kAddAnonLayer) { - auto index = _index; - if (index == -1) { - index = static_cast(layer->GetNumSubLayerPaths() - 1); - } - if (validateUndoIndex(layer, _index)) { - TF_VERIFY(layer->GetSubLayerPaths()[index] == _subPath); - layer->RemoveSubLayerPath(index); - } else { - return false; - } - } else { - TF_VERIFY(_index != -1); - if (validateUndoIndex(layer, _index)) { - layer->InsertSubLayerPath(_subPath, _index); - - // if the removed layer was the edit target, - // set it back to the current edit target - if (!_editTargetPath.empty()) { - auto stage = getStage(); - auto subLayerHandle = SdfLayer::FindRelativeToLayer(layer, _editTargetPath); - stage->SetEditTarget(subLayerHandle); - } - } else { - return false; - } - restoreSelection(); - } - return true; - } - static bool validateUndoIndex(SdfLayerHandle layer, int index) - { // allow re-inserting at the last index + 1, but -1 should have been changed to 0 - return !(index < 0 || index > (int)layer->GetNumSubLayerPaths()); - } - - static bool validateAndReportIndex(SdfLayerHandle layer, int index, int maxIndex) - { - if (index < 0 || index >= maxIndex) { - std::string message = std::string("Index ") + std::to_string(index) - + std::string(" out-of-bound for ") + layer->GetIdentifier(); - MPxCommand::displayError(message.c_str()); - return false; - } else { - return true; - } - } - - void saveSelection() - { - // Make a copy of the global selection, to restore it on undo. - auto globalSn = Ufe::GlobalSelection::get(); - _savedSn.replaceWith(*globalSn); - // Filter the global selection, removing items below our proxy shape. - // We know the path to the proxy shape has a single segment. Not - // using Ufe::PathString::path() for UFE v1 compatibility, which - // unfortunately reveals leading "world" path component implementation - // detail. - Ufe::Path path( - Ufe::PathSegment("world" + _proxyShapePath, MayaUsd::ufe::getMayaRunTimeId(), '|')); - globalSn->replaceWith(UsdUfe::removeDescendants(_savedSn, path)); - } - - void restoreSelection() - { - // Restore the saved selection to the global selection. If a saved - // selection item started with the proxy shape path, re-create it. - // We know the path to the proxy shape has a single segment. Not - // using Ufe::PathString::path() for UFE v1 compatibility, which - // unfortunately reveals leading "world" path component implementation - // detail. - Ufe::Path path( - Ufe::PathSegment("world" + _proxyShapePath, MayaUsd::ufe::getMayaRunTimeId(), '|')); - auto globalSn = Ufe::GlobalSelection::get(); - globalSn->replaceWith(UsdUfe::recreateDescendants(_savedSn, path)); - } - -protected: - std::string _editTargetPath; - Ufe::Selection _savedSn; - - UsdStageWeakPtr getStage() - { - auto prim = UsdMayaQuery::GetPrim(_proxyShapePath.c_str()); - auto stage = prim.GetStage(); - return stage; - } -}; - -class InsertSubPath : public InsertRemoveSubPathBase -{ -public: - InsertSubPath() - : InsertRemoveSubPathBase(CmdId::kInsert) - { - } -}; - -class RemoveSubPath : public InsertRemoveSubPathBase -{ -public: - RemoveSubPath() - : InsertRemoveSubPathBase(CmdId::kRemove) - { - } -}; - -// Move a sublayer into another layer. -// @param path The layer's path to move. -// @param newParentLayer The new parent layer's path -// @param newIndex The index where the moved layer will be in the new parent. -class MoveSubPath : public BaseCmd -{ -public: - MoveSubPath(const std::string& path, const std::string& newParentLayer, unsigned newIndex) - : BaseCmd(CmdId::kMove) - , _path(path) - , _newParentLayer(newParentLayer) - , _newIndex(newIndex) - { - } - - bool doIt(SdfLayerHandle layer) override - { - auto proxy = layer->GetSubLayerPaths(); - auto subPathIndex = proxy.Find(_path); - if (subPathIndex == size_t(-1)) { - std::string message = std::string("path ") + _path + std::string(" not found on layer ") - + layer->GetIdentifier(); - MPxCommand::displayError(message.c_str()); - return false; - } - - _oldIndex = subPathIndex; // save for undo - - SdfLayerHandle newParentLayer; - std::string newPath = _path; - - if (layer->GetIdentifier() == _newParentLayer) { - - if (_newIndex > layer->GetNumSubLayerPaths() - 1) { - std::string message = std::string("Index ") + std::to_string(_newIndex) - + std::string(" out-of-bound for ") + layer->GetIdentifier(); - MPxCommand::displayError(message.c_str()); - return false; - } - - newParentLayer = layer; - } else { - newParentLayer = SdfLayer::Find(_newParentLayer); - if (!newParentLayer) { - std::string message - = std::string("Layer ") + _newParentLayer + std::string(" not found!"); - return false; - } - - if (_newIndex > newParentLayer->GetNumSubLayerPaths()) { - std::string message = std::string("Index ") + std::to_string(_newIndex) - + std::string(" out-of-bound for ") + newParentLayer->GetIdentifier(); - MPxCommand::displayError(message.c_str()); - return false; - } - - // See if the path should be reparented - fs::filesystem::path filePath(_path); - bool needsRepathing = !SdfLayer::IsAnonymousLayerIdentifier(_path); - needsRepathing &= filePath.is_relative(); - needsRepathing &= !layer->GetRealPath().empty(); - needsRepathing &= !newParentLayer->GetRealPath().empty(); - - // Reparent the path if needed - if (needsRepathing) { - auto oldLayerDir = fs::filesystem::path(layer->GetRealPath()).remove_filename(); - auto newLayerDir - = fs::filesystem::path(newParentLayer->GetRealPath()).remove_filename(); - - std::string absolutePath - = (oldLayerDir / filePath).lexically_normal().generic_string(); - auto result = UsdMayaUtilFileSystem::makePathRelativeTo( - absolutePath, newLayerDir.lexically_normal().generic_string()); - - if (result.second) { - newPath = result.first; - } else { - newPath = absolutePath; - TF_WARN( - "File name (%s) cannot be resolved as relative to the layer %s, " - "using the absolute path.", - absolutePath.c_str(), - newParentLayer->GetIdentifier().c_str()); - } - } - - // make sure the subpath is not already in the new parent layer. - // Otherwise, the SdfLayer::InsertSubLayerPath() below will do nothing - // and the subpath will be removed from it's current parent. - if (newParentLayer->GetSubLayerPaths().Find(newPath) != size_t(-1)) { - std::string message = std::string("SubPath ") + newPath - + std::string(" already exist in layer ") + newParentLayer->GetIdentifier(); - MPxCommand::displayError(message.c_str()); - return false; - } - } - - // When the subLayer is moved inside the current parent, - // Remove it from it's current location and insert it into it's - // new location. The order of remove / insert is important - // oterwise InsertSubLayerPath() will fail because the subLayer - // already exists. - layer->RemoveSubLayerPath(subPathIndex); - newParentLayer->InsertSubLayerPath(newPath, _newIndex); - - return true; - } - - bool undoIt(SdfLayerHandle layer) override - { - if (layer->GetIdentifier() == _newParentLayer) { - // When the subLayer is moved inside the current parent, - // Remove it from it's current location and insert it into it's - // new location. The order of remove / insert is important - // oterwise InsertSubLayerPath() will fail because the subLayer - // already exists. - layer->RemoveSubLayerPath(_newIndex); - layer->InsertSubLayerPath(_path, _oldIndex); - } else { - auto newParentLayer = SdfLayer::Find(_newParentLayer); - newParentLayer->RemoveSubLayerPath(_newIndex); - layer->InsertSubLayerPath(_path, _oldIndex); - } - - return true; - } - -private: - std::string _path; - std::string _newParentLayer; - unsigned int _newIndex; - unsigned int _oldIndex { 0 }; -}; - -class ReplaceSubPath : public BaseCmd -{ -public: - ReplaceSubPath() - : BaseCmd(CmdId::kReplace) - { - } - - bool doIt(SdfLayerHandle layer) override - { - auto proxy = layer->GetSubLayerPaths(); - if (proxy.Find(_oldPath) == size_t(-1)) { - std::string message = std::string("path ") + _oldPath - + std::string(" not found on layer ") + layer->GetIdentifier(); - MPxCommand::displayError(message.c_str()); - return false; - } - - holdOnPathIfDirty(layer, _oldPath); - proxy.Replace(_oldPath, _newPath); - return true; - } - - bool undoIt(SdfLayerHandle layer) override - { - auto proxy = layer->GetSubLayerPaths(); - proxy.Replace(_newPath, _oldPath); - releaseSubLayers(); - holdOnPathIfDirty(layer, _newPath); - return true; - } - - std::string _oldPath, _newPath; -}; - -class AddAnonSubLayer : public InsertRemoveSubPathBase -{ -public: - AddAnonSubLayer() - : InsertRemoveSubPathBase(CmdId::kAddAnonLayer) {}; - - bool doIt(SdfLayerHandle layer) override - { - // the first time, USD will create a layer with a certain identifier - // on undo(), we will remove the path, but hold onto the layer - // on redo, we want to put back that same identifier, for later commands - if (_anonIdentifier.empty()) { - _anonLayer = SdfLayer::CreateAnonymous(_anonName); - _anonIdentifier = _anonLayer->GetIdentifier(); - } - _subPath = _anonIdentifier; - _index = 0; - _cmdResult = _subPath; - return InsertRemoveSubPathBase::doIt(layer); - } - - bool undoIt(SdfLayerHandle layer) override { return InsertRemoveSubPathBase::undoIt(layer); } - - std::string _anonName; - -protected: - PXR_NS::SdfLayerRefPtr _anonLayer; - std::string _anonIdentifier; -}; - -class BackupLayerBase : public BaseCmd -{ - // commands that need to backup the whole layer for undo -public: - BackupLayerBase(CmdId id) - : BaseCmd(id) - { - } - - bool doIt(SdfLayerHandle layer) override - { - backupLayer(layer); - - // using reload will correctly reset the dirty bit - holdOntoSubLayers(layer); - - if (_cmdId == CmdId::kDiscardEdit) { - layer->Reload(); - } else if (_cmdId == CmdId::kClearLayer) { - layer->Clear(); - } else if (_cmdId == CmdId::kFlattenLayer) { - // Create a tempStage to get a PcpLayerStack with this layer as the root. - PXR_NS::UsdStageRefPtr tempStage = PXR_NS::UsdStage::Open(layer); - if (!tempStage) { - MPxCommand::displayError("Failed to open stage for layer"); - return false; - } - - // Get the PcpLayerStackRefPtr to be used in the flatten method. - PXR_NS::PcpLayerStackRefPtr layerStack; - PXR_NS::UsdPrim rootPrim = tempStage->GetPseudoRoot(); - if (rootPrim) { - PXR_NS::PcpPrimIndex primIndex = rootPrim.GetPrimIndex(); - if (primIndex.IsValid()) { - PXR_NS::PcpNodeRef rootNode = primIndex.GetRootNode(); - if (rootNode) { - layerStack = rootNode.GetLayerStack(); - } - } - } - - if (!layerStack) { - MPxCommand::displayError("Cannot flatten layer: could not determine layer stack"); - return false; - } - - PXR_NS::SdfLayerRefPtr flattenedLayer = PXR_NS::UsdFlattenLayerStack(layerStack); - if (!flattenedLayer) { - MPxCommand::displayError("Failed to flatten layer stack"); - return false; - } - - layer->TransferContent(flattenedLayer); - } - - // Note: backup the edit targets after the layer is cleared because we use - // the fact that a stage edit target is now invalid to decide to backup - // that edit target. - backupEditTargets(layer); - - return true; - } - - bool undoIt(SdfLayerHandle layer) override - { - restoreLayer(layer); - - // Note: restore edit targets after the layers are restored so that the backup - // edit targets are now valid. - restoreEditTargets(); - - releaseSubLayers(); - - return true; - } - -protected: - // Backup and restore edit targets of stages that were targeting the sub-layers - // of the cleared layer to support undo and redo. - void backupEditTargets(SdfLayerHandle layer) - { - _editTargetBackups.clear(); - - if (!layer) - return; - - const UsdMayaStageCache::Caches& caches = UsdMayaStageCache::GetAllCaches(); - for (const PXR_NS::UsdStageCache& cache : caches) { - const std::vector stages = cache.GetAllStages(); - for (const PXR_NS::UsdStageRefPtr& stage : stages) { - if (!stage) - continue; - const PXR_NS::UsdEditTarget target = stage->GetEditTarget(); - // Note: this is the check that UsdStage::SetTargetLayer would do - // which is how we would detect that the edit target is now - // invalid. Unfortunately, there is no USD function to check - // if an edit target is valid outside of trying to set it as - // the edit target, but we would not want to set it. (Also, - // knowing if the stage checks that the edit target is already - // set to the same target before validating it is an implementation - // detail that we would raher not rely on.) - if (stage->HasLocalLayer(target.GetLayer())) - continue; - _editTargetBackups[stage] = target; - - // Set a valid target. The only layer we can count on is the root - // layer, so set the target to that. - stage->SetEditTarget(stage->GetRootLayer()); - } - } - } - - void restoreEditTargets() - { - for (const auto& weakStageAndTarget : _editTargetBackups) { - const PXR_NS::UsdStageRefPtr stage = weakStageAndTarget.first; - if (!stage) - continue; - - PXR_NS::UsdEditTarget target = weakStageAndTarget.second; - stage->SetEditTarget(target); - } - } - -private: - // Backup dirty layer to support undo and redo. - void backupLayer(SdfLayerHandle layer) - { - if (!layer) - return; - - if (layer->IsDirty() || _cmdId != CmdId::kDiscardEdit) { - _backupLayer = SdfLayer::CreateAnonymous(); - _backupLayer->TransferContent(layer); - } - } - - void restoreLayer(SdfLayerHandle layer) - { - if (!layer) - return; - - if (_backupLayer) { - layer->TransferContent(_backupLayer); - _backupLayer = nullptr; - } else { - layer->Reload(); - } - } - - // Edit targets that were made invalid after the layer was cleared. - // The stages are kept with weak pointer to avoid forcing to stay valid. - using EditTargetBackups = std::map; - EditTargetBackups _editTargetBackups; - - PXR_NS::SdfLayerRefPtr _backupLayer; -}; - -class DiscardEdit : public BackupLayerBase -{ -public: - DiscardEdit() - : BackupLayerBase(CmdId::kDiscardEdit) - { - } -}; - -class ClearLayer : public BackupLayerBase -{ -public: - ClearLayer() - : BackupLayerBase(CmdId::kClearLayer) - { - } -}; - -class FlattenLayer : public BackupLayerBase -{ -public: - FlattenLayer() - : BackupLayerBase(CmdId::kFlattenLayer) - { - } -}; - -class StitchLayers : public BackupLayerBase -{ -public: - StitchLayers( - const std::vector& layerIdentifiers, - const std::string& newParentLayer) - : BackupLayerBase(CmdId::kStitchLayers) - , _layerIdentifiersByStrength(layerIdentifiers) - , _proxyShapePath(newParentLayer) - { - } - - bool doIt(SdfLayerHandle /*targetLayer*/) override - { - if (_layerIdentifiersByStrength.empty()) - return true; - - const auto prim = UsdMayaQuery::GetPrim(_proxyShapePath.c_str()); - if (!prim.IsValid()) { - TF_RUNTIME_ERROR("Invalid proxy shape path: %s", _proxyShapePath.c_str()); - return false; - } - - const UsdStageWeakPtr stage = prim.GetStage(); - if (!stage) { - TF_RUNTIME_ERROR("Cannot get stage for proxy shape: %s", _proxyShapePath.c_str()); - return false; - } - - const SdfLayerHandleVector stageLayers = stage->GetLayerStack(); - - // Sort the selected layers by their strength (strongest first). - { - std::unordered_map layerStrengthMap; - layerStrengthMap.reserve(stageLayers.size()); - for (size_t i = 0; i < stageLayers.size(); ++i) - layerStrengthMap[stageLayers[i]->GetIdentifier()] = i; - - std::sort( - _layerIdentifiersByStrength.begin(), - _layerIdentifiersByStrength.end(), - [&layerStrengthMap](const std::string& a, const std::string& b) { - const auto itA = layerStrengthMap.find(a); - const auto itB = layerStrengthMap.find(b); - if (itA == layerStrengthMap.end()) { - TF_WARN("Layer '%s' not found in stage layer stack", a.c_str()); - return false; - } - if (itB == layerStrengthMap.end()) { - TF_WARN("Layer '%s' not found in stage layer stack", b.c_str()); - return true; - } - return itA->second < itB->second; - }); - } - - // We will analyze locked layers before doing any modification, - // report all problem and abort the command if all layers are locked. - bool hasProblems = false; - - // Convert the list of layer identifier to a list of layer handles. - SdfLayerHandleVector layersByStrength; - { - layersByStrength.reserve(_layerIdentifiersByStrength.size()); - for (const auto& layerIdentifier : _layerIdentifiersByStrength) { - auto layer = SdfLayer::FindOrOpen(layerIdentifier); - if (!layer) { - TF_RUNTIME_ERROR("Cannot find layer: %s", layerIdentifier.c_str()); - return false; - } - layersByStrength.push_back(layer); - } - } - - if (layersByStrength.empty()) { - TF_RUNTIME_ERROR("No valid layer found to stitch"); - return false; - } - - const SdfLayerHandle strongestLayer = layersByStrength[0]; - if (!strongestLayer->PermissionToEdit()) { - TF_WARN( - "Cannot merge into layer '%s' because it is locked.", - strongestLayer->GetDisplayName().c_str()); - hasProblems = true; - } - - // Create a map from layer identifier to its parent layer and subLayerPath, - // to be used for stitching and removals. - std::map> parentInfoByLayer; - for (const auto& potentialParent : stageLayers) { - const std::vector& subLayerPaths = potentialParent->GetSubLayerPaths(); - for (size_t i = 0; i < subLayerPaths.size(); ++i) { - auto subLayer = SdfLayer::FindRelativeToLayer(potentialParent, subLayerPaths[i]); - if (subLayer) { - parentInfoByLayer[subLayer->GetIdentifier()] - = std::make_pair(potentialParent, subLayerPaths[i]); - } - } - } - - // Filter the layers to be merged, only keeping those that have unlocked parents. - // - // Keep a map of parent layers to their removed children because multiple - // selected weak layers may share the same parent, so batch removals by - // parent to avoid calling SetSubLayerPaths more than once per parent layer. - std::map> removalsByParent; - { - SdfLayerHandleVector layersToBeMergedAndRemoved; - for (size_t i = 1; i < layersByStrength.size(); ++i) { - const SdfLayerHandle& weakLayer = layersByStrength[i]; - const std::string weakLayerId = weakLayer->GetIdentifier(); - - const auto& it = parentInfoByLayer.find(weakLayerId); - if (it == parentInfoByLayer.end()) { - TF_WARN( - "Could not find parent for layer: %s", weakLayer->GetDisplayName().c_str()); - hasProblems = true; - continue; - } - - SdfLayerHandle parenttLayer = it->second.first; - if (!parenttLayer->PermissionToEdit()) { - TF_WARN( - "Cannot merge layer '%s' because its parent '%s' is locked.", - weakLayer->GetDisplayName().c_str(), - parenttLayer->GetDisplayName().c_str()); - continue; - } - - auto parentLayerId = parenttLayer->GetIdentifier(); - auto removeLayerPath = it->second.second; - layersToBeMergedAndRemoved.push_back(weakLayer); - removalsByParent[parentLayerId].push_back(removeLayerPath); - } - layersByStrength.swap(layersToBeMergedAndRemoved); - } - - if (hasProblems) { - return false; - } - - // Recheck if there are now no layer to merege from. - if (layersByStrength.empty()) { - TF_RUNTIME_ERROR("No valid layer found to stitch"); - return false; - } - - holdOntoSubLayers(strongestLayer); - - // Keep a hold of references for all selected layers, needed for undo(). - for (auto& layer : layersByStrength) - _subLayersRefs.push_back(layer); - - UsdUfe::UsdUndoManager::instance().trackLayerStates(strongestLayer); - for (auto& layer : layersByStrength) - UsdUfe::UsdUndoManager::instance().trackLayerStates(layer); - for (const auto& entry : removalsByParent) { - auto parentLayer = SdfLayer::Find(entry.first); - if (parentLayer) { - UsdUfe::UsdUndoManager::instance().trackLayerStates(parentLayer); - } - } - - { - UsdUfe::UsdUndoBlock undoBlock(&_undoItem); - - std::vector> movedSubLayers; - movedSubLayers.reserve(layersByStrength.size()); - for (auto& weakLayer : layersByStrength) { - movedSubLayers.push_back(weakLayer->GetSubLayerPaths()); - UsdUtilsStitchLayers(strongestLayer, weakLayer); - } - - // Add all collected subLayers to the strongest layer, preventing duplicates. - // Non-selected weak subLayers are added to the end of the subPathList of the - // strongestLayer. Note: This means there are cases where relative strength is not fully - // preserved. - auto strongLayerSubLayers = strongestLayer->GetSubLayerPaths(); - - // Creates a set of the added subLayers, prevent duplicates. - std::set addedSublayerIds; - for (const auto path : strongLayerSubLayers) { - const auto existingLayer = SdfLayer::FindRelativeToLayer(strongestLayer, path); - if (existingLayer) { - addedSublayerIds.insert(existingLayer->GetIdentifier()); - } - } - - // Adds any moved sub layers to the strong layers sub layers to prevent layers from - // being lost when the weak layer is deleted. - for (const auto& subLayerList : movedSubLayers) { - for (const auto& subLayerPath : subLayerList) { - const auto subLayer - = SdfLayer::FindRelativeToLayer(strongestLayer, subLayerPath); - if (subLayer - && addedSublayerIds.find(subLayer->GetIdentifier()) - == addedSublayerIds.end()) { - strongLayerSubLayers.push_back(subLayerPath); - addedSublayerIds.insert(subLayer->GetIdentifier()); - } - } - } - - // Remove any merged weak layers from the sublayer list before setting, to prevent - // them from being both stitched (merged) and referenced as subLayers. - for (const auto& weakLayer : layersByStrength) { - const std::string weakLayerId = weakLayer->GetIdentifier(); - const auto it = std::find( - strongLayerSubLayers.begin(), strongLayerSubLayers.end(), weakLayerId); - if (it != strongLayerSubLayers.end()) - strongLayerSubLayers.erase(it); - } - - strongestLayer->SetSubLayerPaths(strongLayerSubLayers); - - // Removes the selected weak layers from their parents. - for (auto& entry : removalsByParent) { - const auto parentLayer = SdfLayer::Find(entry.first); - if (!parentLayer) - continue; - - auto subLayerPaths = parentLayer->GetSubLayerPaths(); - for (const auto& pathToRemove : entry.second) { - auto it = std::find(subLayerPaths.begin(), subLayerPaths.end(), pathToRemove); - if (it != subLayerPaths.end()) - subLayerPaths.erase(it); - } - parentLayer->SetSubLayerPaths(subLayerPaths); - } - } - - backupEditTargets(strongestLayer); - - return true; - } - - bool undoIt(SdfLayerHandle /*targetLayer*/) override - { - _undoItem.undo(); - - restoreEditTargets(); - releaseSubLayers(); - - return true; - } - -private: - UsdUfe::UsdUndoableItem _undoItem; - std::vector _layerIdentifiersByStrength; - std::string _proxyShapePath; -}; - -class MuteLayer : public BaseCmd -{ -public: - MuteLayer() - : BaseCmd(CmdId::kMuteLayer) - { - } - - bool doIt(SdfLayerHandle layer) override - { - auto stage = getStage(); - if (!stage) - return false; - - if (_muteIt) { - // We prefer not holding to pointers needlessly, but we need to hold on - // to the muted layer. OpenUSD let go of muted layers, so anonymous - // layers and any dirty children would be lost if not explicitly held on. - // This is done before really muting the layer to ensure no sublayer is - // gone after the mute change. - _didAddOrRemMutedLayer = addMutedLayer(layer); - - // Muting a layer will cause all scene items under the proxy shape - // to be stale. - saveSelection(); - stage->MuteLayer(layer->GetIdentifier()); - } else { - stage->UnmuteLayer(layer->GetIdentifier()); - - // We can release the now unmuted layers. - _didAddOrRemMutedLayer = removeMutedLayer(layer); - - restoreSelection(); - } - - updateEditTarget(stage); - - return true; - } - - bool undoIt(SdfLayerHandle layer) override - { - auto stage = getStage(); - if (!stage) - return false; - if (_muteIt) { - stage->UnmuteLayer(layer->GetIdentifier()); - - // We can release the now unmuted layers. - if (_didAddOrRemMutedLayer) { - removeMutedLayer(layer); - _didAddOrRemMutedLayer = false; - } - - restoreSelection(); - } else { - // Hold back the layer about to be re-muted. - if (_didAddOrRemMutedLayer) { - addMutedLayer(layer); - _didAddOrRemMutedLayer = false; - } - - // Muting a layer will cause all scene items under the proxy shape - // to be stale. - saveSelection(); - stage->MuteLayer(layer->GetIdentifier()); - } - - updateEditTarget(stage); - - return true; - } - - std::string _proxyShapePath; - bool _muteIt = true; - -private: - UsdStageWeakPtr getStage() - { - auto prim = UsdMayaQuery::GetPrim(_proxyShapePath.c_str()); - auto stage = prim.GetStage(); - return stage; - } - - void saveSelection() - { - // Make a copy of the global selection, to restore it on unmute. - auto globalSn = Ufe::GlobalSelection::get(); - _savedSn.replaceWith(*globalSn); - // Filter the global selection, removing items below our proxy shape. - // We know the path to the proxy shape has a single segment. Not - // using Ufe::PathString::path() for UFE v1 compatibility, which - // unfortunately reveals leading "world" path component implementation - // detail. - Ufe::Path path( - Ufe::PathSegment("world" + _proxyShapePath, MayaUsd::ufe::getMayaRunTimeId(), '|')); - globalSn->replaceWith(UsdUfe::removeDescendants(_savedSn, path)); - } - - void restoreSelection() - { - // Restore the saved selection to the global selection. If a saved - // selection item started with the proxy shape path, re-create it. - // We know the path to the proxy shape has a single segment. Not - // using Ufe::PathString::path() for UFE v1 compatibility, which - // unfortunately reveals leading "world" path component implementation - // detail. - Ufe::Path path( - Ufe::PathSegment("world" + _proxyShapePath, MayaUsd::ufe::getMayaRunTimeId(), '|')); - auto globalSn = Ufe::GlobalSelection::get(); - globalSn->replaceWith(UsdUfe::recreateDescendants(_savedSn, path)); - } - - Ufe::Selection _savedSn; - bool _didAddOrRemMutedLayer = false; -}; - -class LockLayer : public BaseCmd -{ -public: - LockLayer() - : BaseCmd(CmdId::kLockLayer) - { - } - - bool doIt(SdfLayerHandle layer) override - { - auto stage = getStage(); - if (!stage) - return false; - - std::set layersToUpdate; - if (_includeSublayers) { - // If _includeSublayers is True, we attempt to refresh the system lock status of all - // layers under the given layer. This is specially useful when reloading a stage. - bool includeTopLayer = true; - layersToUpdate = MayaUsd::getAllSublayerRefs(layer, includeTopLayer); - } else { - layersToUpdate.insert(layer); - } - - for (auto layerIt : layersToUpdate) { - if (MayaUsd::isLayerLocked(layerIt)) { - _previousStates.push_back(MayaUsd::LayerLockType::LayerLock_Locked); - } else if (MayaUsd::isLayerSystemLocked(layerIt)) { - _previousStates.push_back(MayaUsd::LayerLockType::LayerLock_SystemLocked); - } else { - _previousStates.push_back(MayaUsd::LayerLockType::LayerLock_Unlocked); - } - _layers.push_back(layerIt); - } - - // Execute lock commands - for (size_t layerIndex = 0; layerIndex < _layers.size(); layerIndex++) { - auto curLayer = _layers[layerIndex]; - // Note: per design, we refuse to affect the lock status of system-locked - // sub-layers from the UI. The skip-system-locked flag is used for that. - if (_skipSystemLockedLayers) { - if (curLayer != layer) { - if (_lockType != MayaUsd::LayerLockType::LayerLock_SystemLocked) { - if (MayaUsd::isLayerSystemLocked(curLayer)) { - continue; - } - } - } - } - - MayaUsd::lockLayer(_proxyShapePath, curLayer, _lockType, true); - } - - if (_updateEditTarget) { - updateEditTarget(stage); - } - - return true; - } - - bool undoIt(SdfLayerHandle layer) override - { - auto stage = getStage(); - if (!stage) - return false; - - if (_layers.size() != _previousStates.size()) { - return false; - } - // Execute lock commands - for (size_t layerIndex = 0; layerIndex < _layers.size(); layerIndex++) { - // Note: the undo of system-locked is unlocked by design. - if (_lockType == MayaUsd::LayerLockType::LayerLock_SystemLocked) { - MayaUsd::lockLayer( - _proxyShapePath, - _layers[layerIndex], - MayaUsd::LayerLockType::LayerLock_Unlocked, - true); - } else { - MayaUsd::lockLayer( - _proxyShapePath, _layers[layerIndex], _previousStates[layerIndex], true); - } - } - - if (_updateEditTarget) { - updateEditTarget(stage); - } - - return true; - } - - MayaUsd::LayerLockType _lockType = MayaUsd::LayerLockType::LayerLock_Locked; - bool _includeSublayers = false; - bool _skipSystemLockedLayers = false; - bool _updateEditTarget = true; - std::string _proxyShapePath; - -private: - UsdStageWeakPtr getStage() - { - auto prim = UsdMayaQuery::GetPrim(_proxyShapePath.c_str()); - auto stage = prim.GetStage(); - return stage; - } - - std::vector _previousStates; - SdfLayerHandleVector _layers; -}; - -class RefreshSystemLockLayer : public BaseCmd -{ -public: - RefreshSystemLockLayer() - : BaseCmd(CmdId::kRefreshSystemLock) - { - } - - bool doIt(SdfLayerHandle layer) override - { - auto stage = getStage(); - if (!stage) - return false; - - if (_refreshSubLayers) { - // If refreshSubLayers is True, we attempt to refresh the system lock status of all - // layers under the given layer. This is specially useful when reloading a stage. - bool includeTopLayer = true; - auto allLayers = MayaUsd::getAllSublayerRefs(layer, includeTopLayer); - for (auto layerIt : allLayers) { - _refreshLayerSystemLock(layerIt); - } - } else { - // Only check and refresh the system lock status of the current layer. - _refreshLayerSystemLock(layer); - } - - // Execute lock commands - for (size_t layerIndex = 0; layerIndex < _layers.size(); layerIndex++) { - if (!_lockCommands[layerIndex]->doIt(_layers[layerIndex])) { - return false; - } - } - - if (!_layers.empty()) { - _notifySystemLockIsRefreshed(); - - // Finally update edit target after layer locks were changed - // by the command or a callback. - updateEditTarget(stage); - } - - return true; - } - - // The command itself doesn't retain its state. However, the underlying logic contains commands - // that are undoable. - bool undoIt(SdfLayerHandle layer) override - { - auto stage = getStage(); - if (!stage) - return false; - - // Execute lock commands - for (size_t layerIndex = 0; layerIndex < _layers.size(); layerIndex++) { - if (!_lockCommands[layerIndex]->undoIt(_layers[layerIndex])) { - return false; - } - } - - if (!_layers.empty()) { - _notifySystemLockIsRefreshed(); - - // Finally update edit target after layer locks were changed - // by the command or a callback. - updateEditTarget(stage); - } - - return true; - } +#include +#include - std::string _proxyShapePath; - bool _refreshSubLayers = false; - std::vector> _lockCommands; - SdfLayerHandleVector _layers; - -private: - std::string _quote(const std::string& string) - { - return std::string(" \"") + string + std::string("\""); - } - - // Checks if the file layer or its subLayer are accessible on disk, and adds the layer - // to _layers along with the _lockCommands to updates the system-lock status. - void _refreshLayerSystemLock(SdfLayerHandle usdLayer) - { - // Anonymous layers do not need to be checked. - if (usdLayer && !usdLayer->IsAnonymous()) { - // Check if the layer's write permissions have changed. - std::string assetPath = usdLayer->GetResolvedPath(); - std::replace(assetPath.begin(), assetPath.end(), '\\', '/'); - - if (!assetPath.empty()) { - MString commandString; - commandString.format("filetest -w \"^1s\"", MString(assetPath.c_str())); - MIntArray result; - // filetest is NOT undoable - MGlobal::executeCommand(commandString, result, /*display*/ false, /*undo*/ false); - if (result.length() > 0) { - - if (result[0] == 1 && MayaUsd::isLayerSystemLocked(usdLayer)) { - // If the file has write permissions and the layer is currently - // system-locked: Unlock the layer - - // Create the lock command - auto cmd = std::make_shared(); - cmd->_lockType = MayaUsd::LayerLockType::LayerLock_Unlocked; - cmd->_includeSublayers = false; - cmd->_proxyShapePath = _proxyShapePath; - // Edit target will be updated once at the end of the refresh command. - cmd->_updateEditTarget = false; - - // Add the lock command and its parameter to be executed - _lockCommands.push_back(std::move(cmd)); - _layers.push_back(usdLayer); - } else if (result[0] == 0 && !MayaUsd::isLayerSystemLocked(usdLayer)) { - // If the file doesn't have write permissions and the layer is currently not - // system-locked: System-lock the layer - - // Create the lock command - auto cmd = std::make_shared(); - cmd->_lockType = MayaUsd::LayerLockType::LayerLock_SystemLocked; - cmd->_includeSublayers = false; - cmd->_proxyShapePath = _proxyShapePath; - // Edit target will be updated once at the end of the refresh command. - cmd->_updateEditTarget = false; +#include +#include +#ifdef WANT_ADSK_USD_EDIT_FORWARD_BUILD +#include +#endif +#include +#include +#include +#include +#include +#include - // Add the lock command and its parameter to be executed - _lockCommands.push_back(std::move(cmd)); - _layers.push_back(usdLayer); - } - } - } - } - } +#include +#include +#include +#include +#include - void _notifySystemLockIsRefreshed() - { - if (!UsdUfe::isUICallbackRegistered(TfToken("onRefreshSystemLock"))) - return; +#include +#include +#include +#include +#include +#include - PXR_NS::VtDictionary callbackContext; - callbackContext["proxyShapePath"] = PXR_NS::VtValue(_proxyShapePath.c_str()); - PXR_NS::VtDictionary callbackData; +#include +#include +#include +#include +#include +#include +#include +#include - std::vector affectedLayers; - affectedLayers.reserve(_layers.size()); - for (size_t layerIndex = 0; layerIndex < _layers.size(); layerIndex++) { - affectedLayers.push_back(_layers[layerIndex]->GetIdentifier()); - } +#include - VtStringArray lockedArray(affectedLayers.begin(), affectedLayers.end()); - callbackData["affectedLayerIds"] = lockedArray; +#include +#include +#include +#include +#include +#include +#include - UsdUfe::triggerUICallback(TfToken("onRefreshSystemLock"), callbackContext, callbackData); - } +PXR_NAMESPACE_USING_DIRECTIVE - UsdStageWeakPtr getStage() - { - auto prim = UsdMayaQuery::GetPrim(_proxyShapePath.c_str()); - auto stage = prim.GetStage(); - return stage; - } -}; +namespace { +const char kInsertSubPathFlag[] = "is"; +const char kInsertSubPathFlagL[] = "insertSubPath"; +const char kRemoveSubPathFlag[] = "rs"; +const char kRemoveSubPathFlagL[] = "removeSubPath"; +const char kReplaceSubPathFlag[] = "rp"; +const char kReplaceSubPathFlagL[] = "replaceSubPath"; +const char kMoveSubPathFlag[] = "mv"; +const char kMoveSubPathFlagL[] = "moveSubPath"; +const char kDiscardEditsFlag[] = "de"; +const char kDiscardEditsFlagL[] = "discardEdits"; +const char kClearLayerFlag[] = "cl"; +const char kClearLayerFlagL[] = "clear"; +const char kFlattenLayerFlag[] = "fl"; +const char kFlattenLayerFlagL[] = "flatten"; +const char kAddAnonSublayerFlag[] = "aa"; +const char kAddAnonSublayerFlagL[] = "addAnonymous"; +const char kMuteLayerFlag[] = "mt"; +const char kMuteLayerFlagL[] = "muteLayer"; +const char kLockLayerFlag[] = "lk"; +const char kLockLayerFlagL[] = "lockLayer"; +const char kSkipSystemLockedFlag[] = "ssl"; +const char kSkipSystemLockedFlagL[] = "skipSystemLocked"; +const char kRefreshSystemLockFlag[] = "rl"; +const char kRefreshSystemLockFlagL[] = "refreshSystemLock"; +const char kStitchLayersFlag[] = "sl"; +const char kStitchLayersFlagL[] = "stitchLayers"; // We assume the indexes given to the command are the original indexes // of the layers. Since each command is executed individually and in @@ -1515,7 +146,9 @@ class IndexAdjustments std::map _indexAdjustments; }; -} // namespace Impl +} // namespace + +namespace MAYAUSD_NS_DEF { const char LayerEditorCommand::commandName[] = "mayaUsdLayerEditor"; @@ -1591,212 +224,194 @@ MStatus LayerEditorCommand::parseArgs(const MArgList& argList) _layerIdentifier = objects[0].asChar(); if (!isQuery()) { - Impl::IndexAdjustments indexAdjustments; + + auto layer = SdfLayer::FindOrOpen(_layerIdentifier); + if (!layer) { + displayError(MString("Layer not found: ") + _layerIdentifier.c_str()); + return MS::kInvalidParameter; + } + + IndexAdjustments indexAdjustments; const bool skipSystemLockedLayers = argParser.isFlagSet(kSkipSystemLockedFlag); if (argParser.isFlagSet(kInsertSubPathFlag)) { auto count = argParser.numberOfFlagUses(kInsertSubPathFlag); for (unsigned i = 0; i < count; i++) { - auto cmd = std::make_shared(); - MArgList listOfArgs; argParser.getFlagArgumentList(kInsertSubPathFlag, i, listOfArgs); - const int originalIndex = listOfArgs.asInt(0); const int adjustedIndex = indexAdjustments.insertionAdjustment(originalIndex); - - cmd->_index = adjustedIndex; - cmd->_subPath = listOfArgs.asString(1).asUTF8(); - - _subCommands.push_back(std::move(cmd)); + _subCommands.push_back(std::make_shared( + UsdStageRefPtr {}, layer, listOfArgs.asString(1).asUTF8(), adjustedIndex)); } } if (argParser.isFlagSet(kRemoveSubPathFlag)) { auto count = argParser.numberOfFlagUses(kRemoveSubPathFlag); for (unsigned i = 0; i < count; i++) { - MArgList listOfArgs; argParser.getFlagArgumentList(kRemoveSubPathFlag, i, listOfArgs); - auto shapePath = listOfArgs.asString(1); auto prim = UsdMayaQuery::GetPrim(shapePath.asChar()); if (prim == UsdPrim()) { displayError(MString("Invalid proxy shape \"") + shapePath.asChar() + "\""); return MS::kInvalidParameter; } - - const int originalIndex = listOfArgs.asInt(0); - const int adjustedIndex = indexAdjustments.removalAdjustment(originalIndex); - - auto cmd = std::make_shared(); - cmd->_index = adjustedIndex; - cmd->_proxyShapePath = shapePath.asChar(); - _subCommands.push_back(std::move(cmd)); + UsdStageRefPtr stage = prim.GetStage(); + const int originalIndex = listOfArgs.asInt(0); + const int adjustedIndex = indexAdjustments.removalAdjustment(originalIndex); + _subCommands.push_back( + std::make_shared(stage, layer, adjustedIndex)); } } if (argParser.isFlagSet(kReplaceSubPathFlag)) { auto count = argParser.numberOfFlagUses(kReplaceSubPathFlag); for (unsigned i = 0; i < count; i++) { - auto cmd = std::make_shared(); - MArgList listOfArgs; argParser.getFlagArgumentList(kReplaceSubPathFlag, i, listOfArgs); - cmd->_oldPath = listOfArgs.asString(0).asUTF8(); - cmd->_newPath = listOfArgs.asString(1).asUTF8(); - _subCommands.push_back(std::move(cmd)); + auto oldPath = listOfArgs.asString(0).asUTF8(); + auto newPath = listOfArgs.asString(1).asUTF8(); + _subCommands.push_back(std::make_shared( + layer, oldPath, newPath)); } } if (argParser.isFlagSet(kMoveSubPathFlag)) { MString subPath; argParser.getFlagArgument(kMoveSubPathFlag, 0, subPath); - - MString newParentLayer; - argParser.getFlagArgument(kMoveSubPathFlag, 1, newParentLayer); - + MString newParentLayerStr; + argParser.getFlagArgument(kMoveSubPathFlag, 1, newParentLayerStr); int originalIndex { 0 }; argParser.getFlagArgument(kMoveSubPathFlag, 2, originalIndex); const int adjustedIndex = indexAdjustments.removalAdjustment(originalIndex); - auto cmd = std::make_shared( - subPath.asUTF8(), newParentLayer.asUTF8(), adjustedIndex); - _subCommands.push_back(std::move(cmd)); + SdfLayerHandle newParentLayerH; + if (layer->GetIdentifier() == newParentLayerStr.asUTF8()) { + newParentLayerH = layer; + } else { + newParentLayerH = SdfLayer::Find(newParentLayerStr.asUTF8()); + if (!newParentLayerH) { + displayError(MString("Layer not found: ") + newParentLayerStr); + return MS::kInvalidParameter; + } + } + _subCommands.push_back(std::make_shared( + layer, newParentLayerH, subPath.asUTF8(), adjustedIndex)); } if (argParser.isFlagSet(kDiscardEditsFlag)) { - auto cmd = std::make_shared(); - _subCommands.push_back(std::move(cmd)); + _subCommands.push_back(std::make_shared(layer)); } if (argParser.isFlagSet(kClearLayerFlag)) { - auto cmd = std::make_shared(); - _subCommands.push_back(std::move(cmd)); + _subCommands.push_back(std::make_shared(layer)); } if (argParser.isFlagSet(kFlattenLayerFlag)) { - auto cmd = std::make_shared(); - _subCommands.push_back(std::move(cmd)); + _subCommands.push_back(std::make_shared(layer)); } if (argParser.isFlagSet(kAddAnonSublayerFlag)) { auto count = argParser.numberOfFlagUses(kAddAnonSublayerFlag); for (unsigned i = 0; i < count; i++) { - auto cmd = std::make_shared(); - MArgList listOfArgs; argParser.getFlagArgumentList(kAddAnonSublayerFlag, i, listOfArgs); + // AddAnonSubLayer only inserts into the parent layer; it never uses the stage, + // and no proxy shape is supplied with this flag, so pass an empty stage. + auto cmd = std::make_shared( + UsdStageRefPtr {}, layer); cmd->_anonName = listOfArgs.asString(0).asUTF8(); _subCommands.push_back(std::move(cmd)); } } + if (argParser.isFlagSet(kMuteLayerFlag)) { bool muteIt = true; argParser.getFlagArgument(kMuteLayerFlag, 0, muteIt); - MString proxyShapeName; argParser.getFlagArgument(kMuteLayerFlag, 1, proxyShapeName); - auto prim = UsdMayaQuery::GetPrim(proxyShapeName.asChar()); if (prim == UsdPrim()) { displayError( - MString("Invalid proxy shape \"") + MString(proxyShapeName.asChar()) + "\""); + MString("Invalid proxy shape \"") + proxyShapeName.asChar() + "\""); return MS::kInvalidParameter; } - - auto cmd = std::make_shared(); - cmd->_muteIt = muteIt; - cmd->_proxyShapePath = proxyShapeName.asChar(); - _subCommands.push_back(std::move(cmd)); + UsdStageRefPtr stage = prim.GetStage(); + _subCommands.push_back( + std::make_shared(stage, layer, muteIt)); } + if (argParser.isFlagSet(kLockLayerFlag)) { int lockValue = 0; // 0 = Unlocked // 1 = Locked // 2 = SystemLocked argParser.getFlagArgument(kLockLayerFlag, 0, lockValue); - bool includeSublayers = false; argParser.getFlagArgument(kLockLayerFlag, 1, includeSublayers); - MString proxyShapeName; argParser.getFlagArgument(kLockLayerFlag, 2, proxyShapeName); - auto prim = UsdMayaQuery::GetPrim(proxyShapeName.asChar()); if (prim == UsdPrim()) { displayError( - MString("Invalid proxy shape \"") + MString(proxyShapeName.asChar()) + "\""); + MString("Invalid proxy shape \"") + proxyShapeName.asChar() + "\""); return MS::kInvalidParameter; } - - auto cmd = std::make_shared(); + UsdStageRefPtr stage = prim.GetStage(); + UsdLayerEditor::LayerLockType lockType; switch (lockValue) { - case 1: { - cmd->_lockType = MayaUsd::LayerLockType::LayerLock_Locked; - break; + case 1: lockType = UsdLayerEditor::LayerLock_Locked; break; + case 2: lockType = UsdLayerEditor::LayerLock_SystemLocked; break; + default: lockType = UsdLayerEditor::LayerLock_Unlocked; break; } - case 2: { - cmd->_lockType = MayaUsd::LayerLockType::LayerLock_SystemLocked; - break; - } - default: { - cmd->_lockType = MayaUsd::LayerLockType::LayerLock_Unlocked; - break; - } - } - cmd->_includeSublayers = includeSublayers; - cmd->_skipSystemLockedLayers = skipSystemLockedLayers; - cmd->_proxyShapePath = proxyShapeName.asChar(); - _subCommands.push_back(std::move(cmd)); + _subCommands.push_back(std::make_shared( + stage, layer, lockType, includeSublayers, skipSystemLockedLayers)); } + if (argParser.isFlagSet(kRefreshSystemLockFlag)) { MString proxyShapeName; argParser.getFlagArgument(kRefreshSystemLockFlag, 0, proxyShapeName); bool refreshSubLayers = true; argParser.getFlagArgument(kRefreshSystemLockFlag, 1, refreshSubLayers); - auto prim = UsdMayaQuery::GetPrim(proxyShapeName.asChar()); if (prim == UsdPrim()) { displayError( - MString("Invalid proxy shape \"") + MString(proxyShapeName.asChar()) + "\""); + MString("Invalid proxy shape \"") + proxyShapeName.asChar() + "\""); return MS::kInvalidParameter; } - - auto cmd = std::make_shared(); - cmd->_proxyShapePath = proxyShapeName.asChar(); - cmd->_refreshSubLayers = refreshSubLayers; + UsdStageRefPtr stage = prim.GetStage(); + auto cmd = std::make_shared( + stage, layer, refreshSubLayers); + cmd->addCallbackContext( + "proxyShapePath", PXR_NS::VtValue(std::string(proxyShapeName.asChar()))); _subCommands.push_back(std::move(cmd)); } + if (argParser.isFlagSet(kStitchLayersFlag)) { std::vector layerIdentifiers; const auto layerCount = argParser.numberOfFlagUses(kStitchLayersFlag); MString proxyShapeName; - for (unsigned i = 0; i < layerCount; ++i) { MArgList listOfArgs; argParser.getFlagArgumentList(kStitchLayersFlag, i, listOfArgs); - if (i == 0) proxyShapeName = listOfArgs.asString(0); - - const MString layerIdentifier = listOfArgs.asString(1); - layerIdentifiers.push_back(layerIdentifier.asChar()); + const std::string layerIdentifier = listOfArgs.asString(1).asChar(); + layerIdentifiers.push_back(layerIdentifier); } - const UsdPrim prim = UsdMayaQuery::GetPrim(proxyShapeName.asChar()); if (prim == UsdPrim()) { displayError( - MString("Invalid proxy shape \"") + MString(proxyShapeName.asChar()) + "\""); + MString("Invalid proxy shape \"") + proxyShapeName.asChar() + "\""); return MS::kInvalidParameter; } - - auto cmd - = std::make_shared(layerIdentifiers, proxyShapeName.asChar()); - - _subCommands.push_back(std::move(cmd)); + UsdStageRefPtr stage = prim.GetStage(); + _subCommands.push_back( + std::make_shared(stage, layerIdentifiers)); } + } return MS::kSuccess; @@ -1821,42 +436,21 @@ MStatus LayerEditorCommand::doIt(const MArgList& argList) // main MPxCommand execution point MStatus LayerEditorCommand::redoIt() { - - auto layer = SdfLayer::FindOrOpen(_layerIdentifier); - if (!layer) { - return MS::kInvalidParameter; - } - - for (auto it = _subCommands.begin(); it != _subCommands.end(); ++it) { - if (!(*it)->doIt(layer)) { - return MS::kFailure; - } - const auto& result = (*it)->_cmdResult; - if (!result.empty()) { - appendToResult(result.c_str()); - } + for (auto& cmd : _subCommands) { + cmd->redo(); + // AddAnonSubLayerCmd is the only command that returns a result + // (the new anonymous layer identifier). + if (auto* anon = dynamic_cast(cmd.get())) + appendToResult(anon->addedLayer().c_str()); } - return MS::kSuccess; } // main MPxCommand execution point MStatus LayerEditorCommand::undoIt() { - - auto layer = SdfLayer::FindOrOpen(_layerIdentifier); - if (!layer) { - return MS::kInvalidParameter; - } - - // clang-format off - for (auto it = _subCommands.rbegin(); it != _subCommands.rend(); ++it) { - if (!(*it)->undoIt(layer)) { - return MS::kFailure; - } - } - - // clang-format on + for (auto it = _subCommands.rbegin(); it != _subCommands.rend(); ++it) + (*it)->undo(); return MS::kSuccess; } diff --git a/lib/mayaUsd/commands/layerEditorCommand.h b/lib/mayaUsd/commands/layerEditorCommand.h index e1c8ee8980..039eb962dc 100644 --- a/lib/mayaUsd/commands/layerEditorCommand.h +++ b/lib/mayaUsd/commands/layerEditorCommand.h @@ -20,7 +20,7 @@ #include #include -#include +#include #include #include @@ -31,10 +31,6 @@ namespace MAYAUSD_NS_DEF { -namespace Impl { -class BaseCmd; -} - class MAYAUSD_CORE_PUBLIC LayerEditorCommand : public MPxCommand { public: @@ -62,8 +58,8 @@ class MAYAUSD_CORE_PUBLIC LayerEditorCommand : public MPxCommand bool isEdit() const { return _cmdMode == Mode::Edit; } bool isQuery() const { return _cmdMode == Mode::Query; } - std::string _layerIdentifier; - std::vector> _subCommands; + std::string _layerIdentifier; + std::vector> _subCommands; }; } // namespace MAYAUSD_NS_DEF diff --git a/lib/mayaUsd/commands/mayaLayerEditorDCCFunctions.cpp b/lib/mayaUsd/commands/mayaLayerEditorDCCFunctions.cpp new file mode 100644 index 0000000000..4597a13e1f --- /dev/null +++ b/lib/mayaUsd/commands/mayaLayerEditorDCCFunctions.cpp @@ -0,0 +1,321 @@ +// +// 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 "mayaLayerEditorDCCFunctions.h" + +#include +#include // UsdLayerEditorOptionVars + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#ifdef WANT_ADSK_USD_EDIT_FORWARD_BUILD +#include + +#include + +#include +#endif + +PXR_NAMESPACE_USING_DIRECTIVE + +namespace UsdLayerEditor { + +void registerLayerEditorDCCFunctions() +{ + ComponentFns component; + component.saveComponent + = [](const PXR_NS::UsdStageRefPtr& /*stage*/, const std::string& dccObjectPath) { + MayaUsd::ComponentUtils::saveAdskUsdComponent(dccObjectPath); + }; + component.reloadComponent = [](const std::string& dccObjectPath) { + MayaUsd::ComponentUtils::reloadAdskUsdComponent(dccObjectPath); + }; + component.isStageAComponent = [](const std::string& dccObjectPath) { + if (dccObjectPath.empty()) + return false; + return MayaUsd::ComponentUtils::isAdskUsdComponent(dccObjectPath); + }; + component.isUnsavedComponent = [](const PXR_NS::UsdStageRefPtr& stage) { + return MayaUsd::ComponentUtils::isUnsavedAdskUsdComponent(stage); + }; + component.shouldDisplayComponentInitialSaveDialog + = [](const PXR_NS::UsdStageRefPtr& stage, const std::string& dccObjectPath) { + return MayaUsd::ComponentUtils::shouldDisplayComponentInitialSaveDialog( + stage, dccObjectPath); + }; + component.moveComponent = [](const std::string& saveLocation, + const std::string& componentName, + const std::string& dccObjectPath) { + return MayaUsd::ComponentUtils::moveAdskUsdComponent( + saveLocation, componentName, dccObjectPath); + }; + component.previewComponentSave = [](const std::string& saveLocation, + const std::string& componentName, + const std::string& dccObjectPath) { + return MayaUsd::ComponentUtils::previewSaveAdskUsdComponent( + saveLocation, componentName, dccObjectPath); + }; + component.getComponentLayersToSave = [](const std::string& dccObjectPath) { + return MayaUsd::ComponentUtils::getAdskUsdComponentLayersToSave(dccObjectPath); + }; + setComponentFns(component); + + DccObjectFns dccObject; + dccObject.isDccObjectStageIncoming = [](const std::string& dccObjectPath) { + return UsdMayaUtil::GetBooleanAttributeOnProxyShape(dccObjectPath, "stageIncoming"); + }; + dccObject.isDccObjectSharedStage = [](const std::string& dccObjectPath) { + return UsdMayaUtil::GetBooleanAttributeOnProxyShape(dccObjectPath, "shareStage"); + }; + dccObject.renameObject + = [](const std::string& oldDccObjectPath, const std::string& newName) -> std::string { + if (oldDccObjectPath.empty() || newName.empty()) + return {}; + MObject proxyNode; + if (PXR_NS::UsdMayaUtil::GetMObjectByName(oldDccObjectPath, proxyNode) != MStatus::kSuccess) + return {}; + MDagModifier dagMod; + if (dagMod.renameNode(proxyNode, newName.c_str()) != MStatus::kSuccess || dagMod.doIt() != MStatus::kSuccess) + return {}; + MDagPath newPath; + if (MDagPath::getAPathTo(proxyNode, newPath) != MStatus::kSuccess) + return {}; + return newPath.fullPathName().asUTF8(); + }; + setDccObjectFns(dccObject); + + SaveOptionFns saveOption; + saveOption.requireUsdPathsRelativeToSceneFile = []() { + return MGlobal::optionVarExists("mayaUsd_MakePathRelativeToSceneFile") + && MGlobal::optionVarIntValue("mayaUsd_MakePathRelativeToSceneFile") != 0; + }; + saveOption.requireUsdPathsRelativeToParentLayer = []() { + return MGlobal::optionVarExists("mayaUsd_MakePathRelativeToParentLayer") + && MGlobal::optionVarIntValue("mayaUsd_MakePathRelativeToParentLayer") != 0; + }; + saveOption.requireUsdPathsRelativeToEditTargetLayer = []() { + return MGlobal::optionVarExists("mayaUsd_MakePathRelativeToEditTargetLayer") + && MGlobal::optionVarIntValue("mayaUsd_MakePathRelativeToEditTargetLayer") != 0; + }; + saveOption.wantReferenceCompositionArc = []() { + return MGlobal::optionVarExists("mayaUsd_WantReferenceCompositionArc") + && MGlobal::optionVarIntValue("mayaUsd_WantReferenceCompositionArc") != 0; + }; + saveOption.wantPrependCompositionArc = []() { + return MGlobal::optionVarExists("mayaUsd_WantPrependCompositionArc") + && MGlobal::optionVarIntValue("mayaUsd_WantPrependCompositionArc") != 0; + }; + saveOption.wantPayloadLoaded = []() { + return MGlobal::optionVarExists("mayaUsd_WantPayloadLoaded") + && MGlobal::optionVarIntValue("mayaUsd_WantPayloadLoaded") != 0; + }; + saveOption.getReferencedPrimPath = []() -> std::string { + if (!MGlobal::optionVarExists("mayaUsd_ReferencedPrimPath")) + return {}; + return MGlobal::optionVarStringValue("mayaUsd_ReferencedPrimPath").asChar(); + }; + saveOption.setRequireUsdPathsRelativeToSceneFile + = [](bool v) { MGlobal::setOptionVarValue("mayaUsd_MakePathRelativeToSceneFile", v ? 1 : 0); }; + saveOption.setRequireUsdPathsRelativeToParentLayer + = [](bool v) { MGlobal::setOptionVarValue("mayaUsd_MakePathRelativeToParentLayer", v ? 1 : 0); }; + saveOption.confirmExistingFileSave = []() { + static const MString k = UsdLayerEditorOptionVars->ConfirmExistingFileSave.GetText(); + return !MGlobal::optionVarExists(k) || MGlobal::optionVarIntValue(k) != 0; // default true + }; + saveOption.getSaveLayerFormatBinary = []() { + static const MString k = UsdLayerEditorOptionVars->SaveLayerFormatArgBinaryOption.GetText(); + return !MGlobal::optionVarExists(k) || MGlobal::optionVarIntValue(k) != 0; // default true + }; + saveOption.setSaveLayerFormatBinary = [](bool v) { + static const MString k = UsdLayerEditorOptionVars->SaveLayerFormatArgBinaryOption.GetText(); + MGlobal::setOptionVarValue(k, v ? 1 : 0); + }; + saveOption.getSerializedUsdEditsLocation = []() -> int { + static const MString k = UsdLayerEditorOptionVars->SerializedUsdEditsLocation.GetText(); + return MGlobal::optionVarExists(k) ? MGlobal::optionVarIntValue(k) : 1; // kSaveToUSDFiles + }; + saveOption.setSerializedUsdEditsLocation = [](int v) { + static const MString k = UsdLayerEditorOptionVars->SerializedUsdEditsLocation.GetText(); + MGlobal::setOptionVarValue(k, v); + }; + setSaveOptionFns(saveOption); + + EnvironmentFns environment; + environment.getPinLayerEditorStage = []() { + static const MString k = UsdLayerEditorOptionVars->PinLayerEditorStage.GetText(); + return MGlobal::optionVarExists(k) && MGlobal::optionVarIntValue(k) != 0; + }; + environment.setPinLayerEditorStage = [](bool v) { + static const MString k = UsdLayerEditorOptionVars->PinLayerEditorStage.GetText(); + MGlobal::setOptionVarValue(k, v ? 1 : 0); + }; + environment.isInteractiveDCCSession + = []() { return MGlobal::mayaState() == MGlobal::kInteractive; }; + environment.shouldExpandOrCollapseAll = []() { + int modifiers = 0; + MGlobal::executeCommand("getModifiers", modifiers); + return (modifiers % 2) != 0; // magic constant: SHIFT held + }; + environment.layerContentsArraySizeLimit = []() -> int64_t { + const MString k + = PXR_NS::UsdMayaUtil::convert(MayaUsdOptionVars->LayerContentsArraySizeLimit); + return MGlobal::optionVarExists(k) ? MGlobal::optionVarIntValue(k) : 8; + }; + environment.layerContentsTimeSamplesSizeLimit = []() -> int64_t { + const MString k + = PXR_NS::UsdMayaUtil::convert(MayaUsdOptionVars->LayerContentsTimeSamplesSizeLimit); + return MGlobal::optionVarExists(k) ? MGlobal::optionVarIntValue(k) : 8; + }; + environment.displayError = [](const std::string& error) { + MGlobal::displayError(error.c_str()); + }; + setEnvironmentFns(environment); + +#ifdef WANT_ADSK_USD_EDIT_FORWARD_BUILD + EditForwardingFns editForwarding; + editForwarding.supportsEditForwarding = []() { return true; }; + editForwarding.echoEditForwarding = []() { + const MString optVar + = PXR_NS::UsdMayaUtil::convert(MayaUsdOptionVars->LayerEditorEchoEditForwarding); + return MGlobal::optionVarExists(optVar) && MGlobal::optionVarIntValue(optVar) != 0; + }; + editForwarding.setEchoEditForwarding = [](bool echo) { + const MString optVar + = PXR_NS::UsdMayaUtil::convert(MayaUsdOptionVars->LayerEditorEchoEditForwarding); + MGlobal::setOptionVarValue(optVar, echo ? 1 : 0); + if (auto host = std::dynamic_pointer_cast( + AdskUsdEditForward::Host::GetInstance())) { + host->SetWantsEcho(echo); + } + }; + editForwarding.handleEFEditTargetUpdate = [](const UsdStageRefPtr& stage) -> bool { + auto controller = MayaUsdEditForwardController::GetForStage(stage); + if (!controller || !controller->isForwardingActive()) + return false; // not forwarding: let normal auto-targeting run + + // If edit forwarding is active and the fallback target is now locked, redirect the + // fallback to the session layer so unmatched edits still have a writable destination. + // The stage edit target itself is already the session layer in EF mode. + auto fallback = controller->fallbackTarget(); + if (fallback && UsdLayerEditor::isLayerLocked(fallback)) { + std::string errMsg; + if (!UsdUfe::isAnyLayerModifiable(stage, &errMsg)) { + controller->setFallbackTarget(stage->GetSessionLayer()); + } + } + return true; // forwarding active: skip normal auto-targeting + }; + setEditForwardingFns(editForwarding); +#endif + + FileSystemFns fileSystem; + fileSystem.getDCCSceneDir + = []() { return UsdMayaUtilFileSystem::getMayaSceneFileDir(); }; + fileSystem.getDCCWorkspaceScenesDir + = []() { return std::string(UsdMayaUtil::GetCurrentMayaWorkspacePath().asChar()); }; + fileSystem.sceneFolder = []() { return MayaUsd::utils::getSceneFolder(); }; + fileSystem.prepareLayerSaveUILayer = [](const std::string& relativeAnchor) -> bool { + const char* script = "import mayaUsd_USDRootFileRelative as murel\n" + "murel.usdFileRelative.setRelativeFilePathRoot(r'''%s''')"; + const std::string commandString = PXR_NS::TfStringPrintf(script, relativeAnchor.c_str()); + return MGlobal::executePythonCommand(commandString.c_str()); + }; + fileSystem.checkWriteAccess = [](const std::string& filePath) -> bool { + const fs::filesystem::path p(filePath); + if (!fs::filesystem::exists(p)) + return true; + const auto perms = fs::filesystem::status(p).permissions(); + return (perms & fs::filesystem::perms::owner_write) != fs::filesystem::perms::none; + }; + setFileSystemFns(fileSystem); + + SerializationFns serialization; + serialization.getStageCaches = []() { + std::vector caches; + for (PXR_NS::UsdStageCache& cache : UsdMayaStageCache::GetAllCaches()) + caches.push_back(&cache); + return caches; + }; + serialization.getAllStages = []() { return MayaUsd::ufe::ProxyShapeHandler::getAllStages(); }; + serialization.setLayerUpAxisAndUnits = [](const PXR_NS::SdfLayerRefPtr& layer) { + const PXR_NS::TfToken upAxis + = MGlobal::isZAxisUp() ? PXR_NS::UsdGeomTokens->z : PXR_NS::UsdGeomTokens->y; + const double metersPerUnit + = UsdMayaUtil::ConvertMDistanceUnitToUsdGeomLinearUnit(MDistance::internalUnit()); + layer->SetField( + PXR_NS::SdfPath::AbsoluteRootPath(), + PXR_NS::UsdGeomTokens->metersPerUnit, + metersPerUnit); + layer->SetField( + PXR_NS::SdfPath::AbsoluteRootPath(), PXR_NS::UsdGeomTokens->upAxis, upAxis); + }; + serialization.updateDCCObjectRootLayer + = [](const std::string& proxyPath, + const std::string& layerPath, + const PXR_NS::SdfLayerRefPtr& layer, + bool wasTargetLayer, + DccObjectRootLayerPathMode pathMode) { + const MayaUsd::utils::ProxyPathMode proxyPathMode + = (pathMode == DccObjectRootLayerPathMode::ForceAbsolute) + ? MayaUsd::utils::ProxyPathMode::kProxyPathAbsolute + : MayaUsd::utils::kProxyPathFollowProxyShape; + MayaUsd::utils::setNewProxyPath( + MString(proxyPath.c_str()), + MString(layerPath.c_str()), + proxyPathMode, + layer, + wasTargetLayer); + }; + serialization.captureSessionLayer + = [](const std::string& dccObjectPath) -> PXR_NS::SdfLayerRefPtr { + auto stage = UsdMayaUtil::GetStageByProxyName(dccObjectPath); + return stage ? PXR_NS::SdfLayerRefPtr(stage->GetSessionLayer()) : PXR_NS::SdfLayerRefPtr {}; + }; + serialization.transferSessionLayer + = [](const PXR_NS::SdfLayerRefPtr& sourceSessionLayer, const std::string& dstDccObjectPath) { + auto newStage = UsdMayaUtil::GetStageByProxyName(dstDccObjectPath); + if (sourceSessionLayer && newStage) + newStage->GetSessionLayer()->TransferContent(sourceSessionLayer); + }; + setSerializationFns(serialization); +} + +void deregisterLayerEditorDCCFunctions() +{ + setLayerEditorDCCFunctions(LayerEditorDCCFunctions {}); +} + +} // namespace UsdLayerEditor diff --git a/lib/mayaUsd/commands/mayaLayerEditorDCCFunctions.h b/lib/mayaUsd/commands/mayaLayerEditorDCCFunctions.h new file mode 100644 index 0000000000..706471affa --- /dev/null +++ b/lib/mayaUsd/commands/mayaLayerEditorDCCFunctions.h @@ -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. +// +#ifndef MAYAUSD_MAYA_LAYER_EDITOR_DCC_FUNCTIONS_H +#define MAYAUSD_MAYA_LAYER_EDITOR_DCC_FUNCTIONS_H + +#include + +namespace UsdLayerEditor { + +// Populates the shared layer-editor DCC-functions registry with the Maya +// implementations that do not depend on Qt (Component Creator, DCC object/stage +// queries, save options, file system, serialization, and the non-UI parts of +// Edit Forwarding). Safe to call in headless/batch sessions. Call once at Maya +// plugin initialization, before any layer-editor command runs. In Qt builds, +// initializeUi() adds the remaining UI-dependent functions on top of these. +MAYAUSD_CORE_PUBLIC void registerLayerEditorDCCFunctions(); + +// Clears the registry back to defaults. Call at plugin unload. +MAYAUSD_CORE_PUBLIC void deregisterLayerEditorDCCFunctions(); + +} // namespace UsdLayerEditor + +#endif // MAYAUSD_MAYA_LAYER_EDITOR_DCC_FUNCTIONS_H diff --git a/lib/mayaUsd/utils/layerMuting.cpp b/lib/mayaUsd/utils/layerMuting.cpp index 96731b9397..557cdb0a8c 100644 --- a/lib/mayaUsd/utils/layerMuting.cpp +++ b/lib/mayaUsd/utils/layerMuting.cpp @@ -18,49 +18,13 @@ #include -#include - -namespace MAYAUSD_NS_DEF { +#include -MStatus copyLayerMutingToAttribute(const PXR_NS::UsdStage& stage, MayaUsdProxyShapeBase& proxyShape) -{ - return proxyShape.setMutedLayers(stage.GetMutedLayers()); -} - -MStatus copyLayerMutingFromAttribute( - const MayaUsdProxyShapeBase& proxyShape, - const LayerNameMap& nameMap, - PXR_NS::UsdStage& stage) -{ - std::vector muted = proxyShape.getMutedLayers(); - - // Remap the muted layer names in case the layer were renamed when reloaded. - for (std::string& name : muted) { - auto iter = nameMap.find(name); - if (iter != nameMap.end()) { - name = iter->second; - } - } +#include - // Add muted layers to the retained muted layer set to avoid losing them. - // This is necessary because USD only keeps layers in memory if at least one - // referencing pointer holds it, but muting in the stage makes the stage no - // longer reference the layer, so the layer would be lost otherwise. - // - // Use a set to accelerate lookup of muted layers. - PXR_NS::SdfLayerHandleVector layers = stage.GetLayerStack(); - std::set mutedSet(muted.begin(), muted.end()); - for (const auto& layer : layers) { - const auto iter = mutedSet.find(layer->GetIdentifier()); - if (iter != mutedSet.end()) { - addMutedLayer(layer); - } - } +#include - const std::vector unmuted; - stage.MuteAndUnmuteLayers(muted, unmuted); - return MS::kSuccess; -} +namespace MAYAUSD_NS_DEF { namespace { @@ -77,92 +41,55 @@ struct SceneResetListener : public PXR_NS::TfWeakBase { // Make sure we don't hold onto muted layers now that the // Maya scene is reset. - forgetMutedLayers(); + UsdLayerEditor::forgetMutedLayers(); } }; -// Maps muted layer identifier -> set of held layers (root + descendants). -// -// Kept in a function to avoid problem with the order of construction -// of global variables in C++. -using MutedLayers = std::unordered_map; -MutedLayers& getMutedLayers() +// The muted layers live in the DCC-agnostic UsdLayerEditor store, which has no +// notion of a Maya scene. This Maya-side listener clears that store on scene +// reset. +std::unique_ptr sSceneResetListener; + +} // namespace + +void registerLayerMutingSceneResetListener() { - // Note: C++ guarantees correct multi-thread protection for static - // variables initialization in functions. - static SceneResetListener onSceneResetListener; - static MutedLayers layers; - return layers; + if (!sSceneResetListener) + sSceneResetListener = std::make_unique(); } -void holdMutedLayers(const PXR_NS::SdfLayerRefPtr& layer, LayerRefSet& heldLayers) -{ - // Non-dirty, non-anonymous layers can be reloaded, so we - // won't hold onto them. - const bool needHolding = (layer->IsDirty() || layer->IsAnonymous()); - if (needHolding) { - heldLayers.insert(layer); - } +void unregisterLayerMutingSceneResetListener() { sSceneResetListener.reset(); } - // Hold onto sub-layers as well, in case they are dirty or anonymous. - // Note: the GetSubLayerPaths function returns proxies, so we have to - // hold the std::string by value, not reference. - for (const std::string subLayerPath : layer->GetSubLayerPaths()) { - auto subLayer = SdfLayer::FindRelativeToLayer(layer, subLayerPath); - if (subLayer) { - holdMutedLayers(subLayer, heldLayers); - } - } +MStatus copyLayerMutingToAttribute(const PXR_NS::UsdStage& stage, MayaUsdProxyShapeBase& proxyShape) +{ + return proxyShape.setMutedLayers(stage.GetMutedLayers()); } -} // namespace +MStatus copyLayerMutingFromAttribute( + const MayaUsdProxyShapeBase& proxyShape, + const LayerNameMap& nameMap, + PXR_NS::UsdStage& stage) +{ + const std::vector muted = proxyShape.getMutedLayers(); + UsdLayerEditor::loadLayerMuteState(muted, nameMap, stage); + return MS::kSuccess; +} bool addMutedLayer(const PXR_NS::SdfLayerRefPtr& layer) { - if (!layer) - return false; - - MutedLayers& mutedLayers = getMutedLayers(); - - // Hold the layer dirty graph, only the first time we see this mute ancestor. - auto ret = mutedLayers.emplace(layer->GetIdentifier(), LayerRefSet {}); - - if (ret.second) { - holdMutedLayers(layer, ret.first->second); - } - - return ret.second; + return UsdLayerEditor::addMutedLayer(layer); } bool removeMutedLayer(const PXR_NS::SdfLayerRefPtr& layer) { - if (!layer) - return false; - - MutedLayers& layers = getMutedLayers(); - - // Stop holding the layers rooted at this layer. - return (layers.erase(layer->GetIdentifier()) > 0); + return UsdLayerEditor::removeMutedLayer(layer); } const LayerRefSet& getMutedLayers(const std::string& mutedIdentifier) { - const MutedLayers& mutedLayers = getMutedLayers(); - - const auto foundSet = mutedLayers.find(mutedIdentifier); - - if (foundSet == mutedLayers.end()) { - static const LayerRefSet kEmpty; - return kEmpty; - } - - return foundSet->second; + return UsdLayerEditor::getMutedLayers(mutedIdentifier); } -void forgetMutedLayers() -{ - MutedLayers& layers = getMutedLayers(); - layers.clear(); -} +void forgetMutedLayers() { UsdLayerEditor::forgetMutedLayers(); } } // namespace MAYAUSD_NS_DEF diff --git a/lib/mayaUsd/utils/layerMuting.h b/lib/mayaUsd/utils/layerMuting.h index d09a3b6a34..d2a786d987 100644 --- a/lib/mayaUsd/utils/layerMuting.h +++ b/lib/mayaUsd/utils/layerMuting.h @@ -89,6 +89,15 @@ using LayerRefSet = std::set; MAYAUSD_CORE_PUBLIC const LayerRefSet& getMutedLayers(const std::string& mutedIdentifier); +/*! \brief register/unregister the scene-reset listener that clears recorded + muted layers when the Maya scene is reset. Called from plugin init/uninit. + */ +MAYAUSD_CORE_PUBLIC +void registerLayerMutingSceneResetListener(); + +MAYAUSD_CORE_PUBLIC +void unregisterLayerMutingSceneResetListener(); + } // namespace MAYAUSD_NS_DEF #endif diff --git a/lib/mayaUsd/utils/util.cpp b/lib/mayaUsd/utils/util.cpp index 04117f27dc..ce0a271bc2 100644 --- a/lib/mayaUsd/utils/util.cpp +++ b/lib/mayaUsd/utils/util.cpp @@ -275,6 +275,31 @@ UsdStageRefPtr UsdMayaUtil::GetStageByProxyName(const std::string& proxyPath) return pShape ? pShape->getUsdStage() : nullptr; } +std::string UsdMayaUtil::GetProxyShapeName(const std::string& proxyShapePath) +{ + std::size_t found = proxyShapePath.find_last_of("|"); + return (std::string::npos != found) ? proxyShapePath.substr(found + 1) : proxyShapePath; +} + +bool UsdMayaUtil::GetBooleanAttributeOnProxyShape( + const std::string& proxyShapePath, + const std::string& attributeName) +{ + if (proxyShapePath.empty()) + return false; + + MObject mobj; + MStatus status = UsdMayaUtil::GetMObjectByName(GetProxyShapeName(proxyShapePath), mobj); + if (status == MStatus::kSuccess) { + MFnDependencyNode fn; + fn.setObject(mobj); + bool attribute; + if (UsdMayaUtil::getPlugValue(fn, attributeName.c_str(), &attribute)) + return attribute; + } + return false; +} + MStatus UsdMayaUtil::GetPlugByName(const std::string& attrPath, MPlug& plug) { std::vector comps = TfStringSplit(attrPath, "."); diff --git a/lib/mayaUsd/utils/util.h b/lib/mayaUsd/utils/util.h index 08c72fef57..f87cce5231 100644 --- a/lib/mayaUsd/utils/util.h +++ b/lib/mayaUsd/utils/util.h @@ -202,6 +202,17 @@ MayaUsdProxyShapeBase* GetProxyShapeByProxyName(const std::string& nodeName); MAYAUSD_CORE_PUBLIC UsdStageRefPtr GetStageByProxyName(const std::string& nodeName); +/// Gets the proxy shape node name from its full DAG path \p proxyShapePath. +MAYAUSD_CORE_PUBLIC +std::string GetProxyShapeName(const std::string& proxyShapePath); + +/// Reads the boolean attribute \p attributeName from the proxy shape at +/// \p proxyShapePath, returning false if the proxy or attribute is not found. +MAYAUSD_CORE_PUBLIC +bool GetBooleanAttributeOnProxyShape( + const std::string& proxyShapePath, + const std::string& attributeName); + /// Gets the Maya MPlug for the given \p attrPath. /// The attribute path should be specified as "nodeName.attrName" (the format /// used by MEL). diff --git a/lib/usd/ui/CMakeLists.txt b/lib/usd/ui/CMakeLists.txt index 307b7595a0..43fde7e4d0 100644 --- a/lib/usd/ui/CMakeLists.txt +++ b/lib/usd/ui/CMakeLists.txt @@ -41,11 +41,6 @@ endif() # ----------------------------------------------------------------------------- # sources # ----------------------------------------------------------------------------- -target_sources(${PROJECT_NAME} - PRIVATE - initStringResources.cpp -) - # ----------------------------------------------------------------------------- # compiler configuration # ----------------------------------------------------------------------------- @@ -127,7 +122,6 @@ endif() # ----------------------------------------------------------------------------- set(HEADERS api.h - initStringResources.h undoChunkUtils.h ) diff --git a/lib/usd/ui/layerEditor/CMakeLists.txt b/lib/usd/ui/layerEditor/CMakeLists.txt index c91f7e4869..6de024468c 100644 --- a/lib/usd/ui/layerEditor/CMakeLists.txt +++ b/lib/usd/ui/layerEditor/CMakeLists.txt @@ -18,24 +18,7 @@ target_sources(${PROJECT_NAME} PRIVATE batchSaveLayersUIDelegate.cpp - componentSaveWidget.cpp - componentSaveWidget.h - dirtyLayersCountBadge.cpp - generatedIconButton.cpp - layerContentsWidget.cpp - layerContentsWidget.h - layerEditorWidget.cpp - layerEditorWidget.h - layerTreeItem.cpp - layerTreeItem.h - layerTreeItemDelegate.cpp - layerTreeItemDelegate.h - layerTreeModel.cpp - layerTreeModel.h - layerTreeView.cpp - layerTreeView.h - loadLayersDialog.cpp - loadLayersDialog.h + batchSaveLayersUIDelegate.h mayaCommandHook.cpp mayaCommandHook.h mayaLayerEditorWindow.cpp @@ -44,24 +27,10 @@ target_sources(${PROJECT_NAME} mayaSessionState.h mayaQtUtils.cpp mayaQtUtils.h - pathChecker.cpp - pathChecker.h - qtUtils.cpp - qtUtils.h - resources.qrc - saveLayersDialog.cpp - saveLayersDialog.h - sessionState.cpp - sessionState.h - stageSelectorWidget.cpp - stageSelectorWidget.h - stringResources.cpp - stringResources.h - usdSyntaxHighlighter.cpp - usdSyntaxHighlighter.h - warningDialogs.cpp - warningDialogs.h + mayaLayerEditorUi.h ) +target_link_libraries(${PROJECT_NAME} PRIVATE usdLayerEditor) +target_compile_definitions(${PROJECT_NAME} PRIVATE MAYAUSD_USE_SHARED_LAYER_EDITOR=1) # ----------------------------------------------------------------------------- # MSVC fix: avoid ambiguous qfloat16 arithmetic operators pulled by Qt headers @@ -79,6 +48,7 @@ endif() # ----------------------------------------------------------------------------- set(HEADERS batchSaveLayersUIDelegate.h + mayaLayerEditorUi.h ) mayaUsd_promoteHeaderList( diff --git a/lib/usd/ui/layerEditor/batchSaveLayersUIDelegate.cpp b/lib/usd/ui/layerEditor/batchSaveLayersUIDelegate.cpp index b031124fb9..3c10120f58 100644 --- a/lib/usd/ui/layerEditor/batchSaveLayersUIDelegate.cpp +++ b/lib/usd/ui/layerEditor/batchSaveLayersUIDelegate.cpp @@ -17,7 +17,10 @@ #include "batchSaveLayersUIDelegate.h" #include "mayaQtUtils.h" -#include "saveLayersDialog.h" + +#include +#include +#include #include #include @@ -25,14 +28,33 @@ #include #include +#include + +namespace { -void UsdLayerEditor::initialize() +// Adapt Maya's StageSavingInfo (carrying an MDagPath) to the shared, +// DCC-agnostic UsdLayerEditor::StageSavingInfo (carrying a dccObjectPath +// string + stageName). +std::vector +toSharedInfos(const std::vector& mayaInfos) { - if (nullptr == UsdLayerEditor::utils) { - UsdLayerEditor::utils = new MayaQtUtils(); + std::vector sharedInfos; + sharedInfos.reserve(mayaInfos.size()); + for (const auto& mi : mayaInfos) { + UsdLayerEditor::StageSavingInfo si; + si.stage = mi.stage; + si.dccObjectPath = mi.dagPath.fullPathName().asChar(); + // Use the leaf name of the dag path as a friendly stage name. + si.stageName = mi.dagPath.partialPathName().asChar(); + si.shareable = mi.shareable; + si.isIncoming = mi.isIncoming; + sharedInfos.push_back(si); } + return sharedInfos; } +} // namespace + MayaUsd::BatchSaveResult UsdLayerEditor::batchSaveLayersUIDelegate( const std::vector& infos, bool isExporting) @@ -76,7 +98,8 @@ MayaUsd::BatchSaveResult UsdLayerEditor::batchSaveLayersUIDelegate( if (showConfirmDgl) { - UsdLayerEditor::SaveLayersDialog dlg(nullptr, infos, isExporting); + const auto sharedInfos = toSharedInfos(infos); + UsdLayerEditor::SaveLayersDialog dlg(nullptr, sharedInfos, isExporting); // The SaveLayers dialog only handles choosing new names for anonymous layers and // making sure that they are remapped correctly in either their parent layer or by @@ -101,8 +124,10 @@ MayaUsd::BatchSaveResult UsdLayerEditor::batchSaveLayersUIDelegate( } if (hasComponentStages) { - const bool componentsOnly = true; - UsdLayerEditor::SaveLayersDialog dlg(nullptr, infos, isExporting, componentsOnly); + const bool componentsOnly = true; + const auto sharedInfos = toSharedInfos(infos); + UsdLayerEditor::SaveLayersDialog dlg( + nullptr, sharedInfos, isExporting, componentsOnly); // Execute the dialog and return partially completed even if the dialog is closed. dlg.exec(); diff --git a/lib/usd/ui/layerEditor/batchSaveLayersUIDelegate.h b/lib/usd/ui/layerEditor/batchSaveLayersUIDelegate.h index ef0a190782..84663528cf 100644 --- a/lib/usd/ui/layerEditor/batchSaveLayersUIDelegate.h +++ b/lib/usd/ui/layerEditor/batchSaveLayersUIDelegate.h @@ -31,9 +31,6 @@ PXR_NAMESPACE_USING_DIRECTIVE namespace UsdLayerEditor { -MAYAUSD_UI_PUBLIC -void initialize(); - MAYAUSD_UI_PUBLIC MayaUsd::BatchSaveResult batchSaveLayersUIDelegate(const std::vector& infos, bool isExporting); diff --git a/lib/usd/ui/layerEditor/layerContentsWidget.h b/lib/usd/ui/layerEditor/layerContentsWidget.h index 4d4ea69a2b..5771e8d9d3 100644 --- a/lib/usd/ui/layerEditor/layerContentsWidget.h +++ b/lib/usd/ui/layerEditor/layerContentsWidget.h @@ -19,6 +19,8 @@ // Needs to come first when used with VS2017 and Qt5. #include "pxr/usd/sdf/layer.h" +#include + #include #include @@ -32,7 +34,7 @@ namespace UsdLayerEditor { * @brief Widget used to display the contents of a layer. Owned by the LayerEditorWidget * */ -class LayerContentsWidget : public QWidget +class MAYAUSD_UI_PUBLIC LayerContentsWidget : public QWidget { Q_OBJECT diff --git a/lib/usd/ui/layerEditor/layerTreeItem.cpp b/lib/usd/ui/layerEditor/layerTreeItem.cpp index 4b606b0e09..813b07b00d 100644 --- a/lib/usd/ui/layerEditor/layerTreeItem.cpp +++ b/lib/usd/ui/layerEditor/layerTreeItem.cpp @@ -215,6 +215,10 @@ bool LayerTreeItem::isIdenticalItem(const LayerTreeItem* other) const return false; } + if (_isSharedStage != other->_isSharedStage) { + return false; + } + auto myChildren = childrenVector(); auto otherChildren = other->childrenVector(); if (myChildren.size() != otherChildren.size()) { diff --git a/lib/usd/ui/layerEditor/mayaCommandHook.cpp b/lib/usd/ui/layerEditor/mayaCommandHook.cpp index b98e2c37f2..4cae7ee37b 100644 --- a/lib/usd/ui/layerEditor/mayaCommandHook.cpp +++ b/lib/usd/ui/layerEditor/mayaCommandHook.cpp @@ -16,9 +16,10 @@ #include "mayaCommandHook.h" -#include "abstractCommandHook.h" #include "mayaSessionState.h" +#include + #include #include #include @@ -29,6 +30,7 @@ #include #include +#include #include #include #include @@ -47,38 +49,6 @@ namespace { std::string quote(const std::string& string) { return STR(" \"") + string + STR("\""); } -std::string getProxyShapeName(const std::string& proxyShapePath) -{ - std::size_t found = proxyShapePath.find_last_of("|"); - if (std::string::npos != found) { - return proxyShapePath.substr(found + 1); - } else { - return proxyShapePath; - } -} - -bool getBooleanAttributeOnProxyShape( - const std::string& proxyShapePath, - const std::string& attributeName) -{ - if (proxyShapePath.empty()) { - return false; - } - - MObject mobj; - MStatus status = PXR_NS::UsdMayaUtil::GetMObjectByName(getProxyShapeName(proxyShapePath), mobj); - if (status == MStatus::kSuccess) { - MFnDependencyNode fn; - fn.setObject(mobj); - bool attribute; - if (PXR_NS::UsdMayaUtil::getPlugValue(fn, attributeName.c_str(), &attribute)) { - return attribute; - } - } - - return false; -} - } // namespace namespace UsdLayerEditor { @@ -218,9 +188,9 @@ void MayaCommandHook::muteSubLayer(UsdLayer usdLayer, bool muteIt) // lock, system-lock or unlock the given layer void MayaCommandHook::lockLayer( - UsdLayer usdLayer, - MayaUsd::LayerLockType lockState, - bool includeSubLayers) + UsdLayer usdLayer, + LayerLockType lockState, + bool includeSubLayers) { // Per design, we refuse to change the lock state of system-locked // layers through the UI. @@ -243,7 +213,7 @@ void MayaCommandHook::refreshLayerSystemLock(UsdLayer usdLayer, bool refreshSubL return; MObject mobj; - if (PXR_NS::UsdMayaUtil::GetMObjectByName(getProxyShapeName(shapePath), mobj) + if (PXR_NS::UsdMayaUtil::GetMObjectByName(PXR_NS::UsdMayaUtil::GetProxyShapeName(shapePath), mobj) != MStatus::kSuccess) return; @@ -309,16 +279,6 @@ void MayaCommandHook::selectPrimsWithSpec(UsdLayer usdLayer) MayaUsd::UfeSelectionUndoItem::select("selectPrimsWithSpec", sn); } -bool MayaCommandHook::isProxyShapeStageIncoming(const std::string& proxyShapePath) -{ - return getBooleanAttributeOnProxyShape(proxyShapePath, "stageIncoming"); -} - -bool MayaCommandHook::isProxyShapeSharedStage(const std::string& proxyShapePath) -{ - return getBooleanAttributeOnProxyShape(proxyShapePath, "shareStage"); -} - std::string MayaCommandHook::executeMel(const std::string& commandString, bool undoable) { if (areCommandsDelayed()) { diff --git a/lib/usd/ui/layerEditor/mayaCommandHook.h b/lib/usd/ui/layerEditor/mayaCommandHook.h index 4ce9f145e6..5eae90cbeb 100644 --- a/lib/usd/ui/layerEditor/mayaCommandHook.h +++ b/lib/usd/ui/layerEditor/mayaCommandHook.h @@ -17,7 +17,9 @@ #ifndef MAYACOMMANDHOOK_H #define MAYACOMMANDHOOK_H -#include "abstractCommandHook.h" +#include + +#include #include @@ -67,8 +69,7 @@ class MayaCommandHook : public AbstractCommandHook void muteSubLayer(UsdLayer usdLayer, bool muteIt) override; // lock, system-lock or unlock the given layer - void - lockLayer(UsdLayer usdLayer, MayaUsd::LayerLockType lockState, bool includeSubLayers) override; + void lockLayer(UsdLayer usdLayer, LayerLockType lockState, bool includeSubLayers) override; // Checks if the file layer or its sublayers are accessible on disk, and updates the system-lock // status. @@ -90,14 +91,6 @@ class MayaCommandHook : public AbstractCommandHook // this method is used to select the prims with spec in a layer void selectPrimsWithSpec(UsdLayer usdLayer) override; - // this method is used to check if the stage in the proxy shape is from - // an incoming connection (using instage data or cache id for example) - bool isProxyShapeStageIncoming(const std::string& proxyShapePath) override; - - // this method is used to check if the proxy shape is sharing the composition - // or has an owned root - bool isProxyShapeSharedStage(const std::string& proxyShapePath) override; - protected: std::string proxyShapePath(); diff --git a/lib/usd/ui/initStringResources.h b/lib/usd/ui/layerEditor/mayaLayerEditorUi.h similarity index 59% rename from lib/usd/ui/initStringResources.h rename to lib/usd/ui/layerEditor/mayaLayerEditorUi.h index cf62b1bd02..1bdde17ea0 100644 --- a/lib/usd/ui/initStringResources.h +++ b/lib/usd/ui/layerEditor/mayaLayerEditorUi.h @@ -13,19 +13,19 @@ // See the License for the specific language governing permissions and // limitations under the License. // -#ifndef INIT_STRING_RESOURCES_H -#define INIT_STRING_RESOURCES_H -#include -#include +#ifndef MAYALAYEREDITORUI_H +#define MAYALAYEREDITORUI_H -#include +#include -namespace MAYAUSD_NS_DEF { +namespace UsdLayerEditor { -// register all string -MAYAUSD_UI_PUBLIC MStatus initStringResources(); +// Registers the Qt-dependent layer-editor DCC functions (main window parent, +// Edit Forwarding dialog) and installs MayaQtUtils as the Qt utils provider. +// Call once at Maya plugin initialization, after registerLayerEditorDCCFunctions(). +MAYAUSD_UI_PUBLIC void initializeUi(); -} // namespace MAYAUSD_NS_DEF +} // namespace UsdLayerEditor -#endif // INIT_STRING_RESOURCES_H +#endif // MAYALAYEREDITORUI_H diff --git a/lib/usd/ui/layerEditor/mayaLayerEditorWindow.cpp b/lib/usd/ui/layerEditor/mayaLayerEditorWindow.cpp index badb1768f5..54b5edb1d9 100644 --- a/lib/usd/ui/layerEditor/mayaLayerEditorWindow.cpp +++ b/lib/usd/ui/layerEditor/mayaLayerEditorWindow.cpp @@ -18,12 +18,14 @@ #include "mayaLayerEditorWindow.h" -#include "layerEditorWidget.h" -#include "layerTreeModel.h" -#include "layerTreeView.h" #include "mayaQtUtils.h" #include "mayaSessionState.h" -#include "sessionState.h" + +#include +#include +#include +#include +#include #include @@ -96,8 +98,8 @@ MayaLayerEditorWindow::MayaLayerEditorWindow(const char* panelName, QWidget* par { // Normally this will be set from the MayaUsd plugin, but only // when building with UFE (for the batch save case). - if (!UsdLayerEditor::utils) { - UsdLayerEditor::utils = new MayaQtUtils(); + if (!UsdLayerEditor::getQtUtils()) { + UsdLayerEditor::setQtUtils(new MayaQtUtils()); } onCreateUI(); @@ -174,13 +176,13 @@ bool MayaLayerEditorWindow::layerHasSubLayers() { CALL_CURRENT_ITEM(hasSubLayers std::string MayaLayerEditorWindow::proxyShapeName(const bool fullPath) const { auto stageEntry = _sessionState.stageEntry(); - return fullPath ? stageEntry._proxyShapePath : stageEntry._displayName; + return fullPath ? stageEntry._dccObjectPath : stageEntry._displayName; } void MayaLayerEditorWindow::removeSubLayer() { QString name = "Remove"; - treeView()->callMethodOnSelectionNoDelay(name, &LayerTreeItem::removeSubLayer); + treeView()->callMethodOnSelection(name, &LayerTreeItem::removeSubLayer); } void MayaLayerEditorWindow::saveEdits() diff --git a/lib/usd/ui/layerEditor/mayaLayerEditorWindow.h b/lib/usd/ui/layerEditor/mayaLayerEditorWindow.h index eaa3293aeb..2ec27de02e 100644 --- a/lib/usd/ui/layerEditor/mayaLayerEditorWindow.h +++ b/lib/usd/ui/layerEditor/mayaLayerEditorWindow.h @@ -17,9 +17,10 @@ #ifndef MAYALAYEREDITORWINDOW_H #define MAYALAYEREDITORWINDOW_H -#include "layerTreeView.h" #include "mayaSessionState.h" +#include + #include #include diff --git a/lib/usd/ui/layerEditor/mayaQtUtils.cpp b/lib/usd/ui/layerEditor/mayaQtUtils.cpp index 74776682ee..528d18477e 100644 --- a/lib/usd/ui/layerEditor/mayaQtUtils.cpp +++ b/lib/usd/ui/layerEditor/mayaQtUtils.cpp @@ -16,10 +16,71 @@ #include "mayaQtUtils.h" +#include "mayaLayerEditorUi.h" + +#if defined(MAYAUSD_USE_SHARED_LAYER_EDITOR) +#include + +#include + +PXR_NAMESPACE_USING_DIRECTIVE +#endif + #include +#ifdef WANT_ADSK_USD_EDIT_FORWARD_BUILD +#include + +#include + +#include + +#include + +namespace { +QPointer g_editForwardDialog; +} // namespace +#endif + namespace UsdLayerEditor { +void initializeUi() +{ +#if defined(MAYAUSD_USE_SHARED_LAYER_EDITOR) + // The read-modify-write below assumes registerLayerEditorDCCFunctions() already ran; otherwise it + // writes back empty groups. isInteractiveDCCSession is always set there, so use it as the sentinel. + TF_VERIFY( + layerEditorDCCFunctions().environment.isInteractiveDCCSession, + "initializeUi() must be called after registerLayerEditorDCCFunctions()."); + + auto environment = layerEditorDCCFunctions().environment; + environment.mainWindowParent = []() -> QWidget* { return MQtUtil::mainWindow(); }; + setEnvironmentFns(environment); + +#ifdef WANT_ADSK_USD_EDIT_FORWARD_BUILD + auto editForwarding = layerEditorDCCFunctions().editForwarding; + editForwarding.openEditForwardDialog = [](const PXR_NS::UsdStageRefPtr& stage) { + if (!g_editForwardDialog) { + g_editForwardDialog = new UsdEditForwardConfig::EditForwardDialog( + StringResources::getAsQString(StringResources::kConfigureEditForwardingTitle), + MQtUtil::mainWindow()); + } + g_editForwardDialog->setActiveStage(stage); + g_editForwardDialog->show(); + g_editForwardDialog->raise(); + g_editForwardDialog->activateWindow(); + }; + editForwarding.isEditForwardDialogOpen + = []() { return g_editForwardDialog && g_editForwardDialog->isVisible(); }; + setEditForwardingFns(editForwarding); +#endif + + if (nullptr == getQtUtils()) { + setQtUtils(new MayaQtUtils()); + } +#endif +} + double MayaQtUtils::dpiScale() { return MQtUtil::dpiScale(1.0f); } QIcon MayaQtUtils::createIcon(const char* iconName) diff --git a/lib/usd/ui/layerEditor/mayaQtUtils.h b/lib/usd/ui/layerEditor/mayaQtUtils.h index 9104a38408..090114952b 100644 --- a/lib/usd/ui/layerEditor/mayaQtUtils.h +++ b/lib/usd/ui/layerEditor/mayaQtUtils.h @@ -17,7 +17,11 @@ #ifndef MAYAQTUTILS_H #define MAYAQTUTILS_H +#if defined(MAYAUSD_USE_SHARED_LAYER_EDITOR) +#include +#else #include "qtUtils.h" +#endif namespace UsdLayerEditor { diff --git a/lib/usd/ui/layerEditor/mayaSessionState.cpp b/lib/usd/ui/layerEditor/mayaSessionState.cpp index 5688cd3a45..aaef8fc954 100644 --- a/lib/usd/ui/layerEditor/mayaSessionState.cpp +++ b/lib/usd/ui/layerEditor/mayaSessionState.cpp @@ -16,21 +16,28 @@ #include "mayaSessionState.h" -#include "saveLayersDialog.h" -#include "stringResources.h" +#include +#include #include #include #include #include +#include +#include #include +#include #ifdef WANT_ADSK_USD_EDIT_FORWARD_BUILD #include - -#include #endif +#include +#include +#include +#include +#include + #include #include #include @@ -55,10 +62,6 @@ namespace { MString PROXY_NODE_TYPE = "mayaUsdProxyShapeBase"; MString AUTO_HIDE_OPTION_VAR = UsdMayaUtil::convert(MayaUsdOptionVars->LayerEditorAutoHideSessionLayer); -#ifdef WANT_ADSK_USD_EDIT_FORWARD_BUILD -MString ECHO_EDIT_FORWARDING_OPTION_VAR - = UsdMayaUtil::convert(MayaUsdOptionVars->LayerEditorEchoEditForwarding); -#endif MString DISPLAY_LAYER_CONTENTS_OPTION_VAR = UsdMayaUtil::convert(MayaUsdOptionVars->LayerEditorDisplayLayerContents); MString DISPLAY_LAYER_EXPAND_ALL_VALUES_OPTION_VAR @@ -73,11 +76,6 @@ MayaSessionState::MayaSessionState() if (MGlobal::optionVarExists(AUTO_HIDE_OPTION_VAR)) { _autoHideSessionLayer = MGlobal::optionVarIntValue(AUTO_HIDE_OPTION_VAR) != 0; } -#ifdef WANT_ADSK_USD_EDIT_FORWARD_BUILD - if (MGlobal::optionVarExists(ECHO_EDIT_FORWARDING_OPTION_VAR)) { - _echoEditForwarding = MGlobal::optionVarIntValue(ECHO_EDIT_FORWARDING_OPTION_VAR) != 0; - } -#endif if (MGlobal::optionVarExists(DISPLAY_LAYER_CONTENTS_OPTION_VAR)) { _displayLayerContents = MGlobal::optionVarIntValue(DISPLAY_LAYER_CONTENTS_OPTION_VAR) != 0; } @@ -106,7 +104,7 @@ void MayaSessionState::setStageEntry(StageEntry const& inEntry) } if (!_inLoad) - MayaUsd::LayerManager::setSelectedStage(_currentStageEntry._proxyShapePath); + MayaUsd::LayerManager::setSelectedStage(_currentStageEntry._dccObjectPath); } bool MayaSessionState::getStageEntry(StageEntry* out_stageEntry, const MString& shapePath) @@ -140,7 +138,7 @@ bool MayaSessionState::getStageEntry(StageEntry* out_stageEntry, const MString& out_stageEntry->_id = dagNode.uuid().asString().asChar(); out_stageEntry->_stage = stage; out_stageEntry->_displayName = niceName.toStdString(); - out_stageEntry->_proxyShapePath = shapePath.asChar(); + out_stageEntry->_dccObjectPath = shapePath.asChar(); return true; } return false; @@ -220,6 +218,10 @@ void MayaSessionState::registerNotifications() TfWeakPtr me(this); _stageResetNoticeKey = TfNotice::Register(me, &MayaSessionState::mayaUsdStageReset); +#ifdef WANT_ADSK_USD_EDIT_FORWARD_BUILD + _efFallbackTargetChangedNoticeKey + = TfNotice::Register(me, &MayaSessionState::usd_efFallbackTargetChanged); +#endif loadSelectedStage(); } @@ -236,18 +238,21 @@ void MayaSessionState::unregisterNotifications() _callbackIds.clear(); TfNotice::Revoke(_stageResetNoticeKey); +#ifdef WANT_ADSK_USD_EDIT_FORWARD_BUILD + TfNotice::Revoke(_efFallbackTargetChangedNoticeKey); +#endif } void MayaSessionState::refreshCurrentStageEntry() { - refreshStageEntry(_currentStageEntry._proxyShapePath); + refreshStageEntry(_currentStageEntry._dccObjectPath); } void MayaSessionState::refreshStageEntry(std::string const& proxyShapePath) { StageEntry entry; if (getStageEntry(&entry, proxyShapePath.c_str())) { - if (entry._proxyShapePath == _currentStageEntry._proxyShapePath) { + if (entry._dccObjectPath == _currentStageEntry._dccObjectPath) { QTimer::singleShot(0, this, [this, entry]() { mayaUsdStageResetCBOnIdle(entry); setStageEntry(entry); @@ -463,8 +468,9 @@ std::string MayaSessionState::defaultLoadPath() const // in this case, the stage needs to be re-created on the new file void MayaSessionState::rootLayerPathChanged(std::string const& in_path) { - if (!_currentStageEntry._proxyShapePath.empty()) { - MString proxyShape(_currentStageEntry._proxyShapePath.c_str()); + const std::string& proxyPath = _currentStageEntry._dccObjectPath; + if (!proxyPath.empty()) { + MString proxyShape(proxyPath.c_str()); MString newValue(in_path.c_str()); MayaUsd::utils::setNewProxyPath( proxyShape, newValue, MayaUsd::utils::kProxyPathFollowProxyShape, nullptr, false); @@ -479,16 +485,6 @@ void MayaSessionState::setAutoHideSessionLayer(bool hideIt) } #ifdef WANT_ADSK_USD_EDIT_FORWARD_BUILD -void MayaSessionState::setEchoEditForwarding(bool echo) -{ - MGlobal::setOptionVarValue(ECHO_EDIT_FORWARDING_OPTION_VAR, echo ? 1 : 0); - if (auto host = std::dynamic_pointer_cast( - AdskUsdEditForward::Host::GetInstance())) { - host->SetWantsEcho(echo); - } - PARENT_CLASS::setEchoEditForwarding(echo); -} - bool MayaSessionState::isEditForwardMode() const { const auto& stage = stageEntry()._stage; @@ -513,6 +509,19 @@ PXR_NS::SdfLayerRefPtr MayaSessionState::effectiveTargetLayer() const } return targetLayer(); } + +void MayaSessionState::usd_efFallbackTargetChanged( + const MayaUsdEFFallbackTargetChangedNotice& notice) +{ + if (notice.GetStage() != stage()) + return; + + // The notice fires while the edit-forward host is mutating the stage, so defer the + // emit to idle. + QTimer::singleShot(0, this, [this]() { + Q_EMIT editForwardingFallbackTargetChanged(); + }); +} #endif void MayaSessionState::setDisplayLayerContents(bool showIt) @@ -534,14 +543,13 @@ void MayaSessionState::printLayer(const PXR_NS::SdfLayerRefPtr& layer) const MString result, temp; temp.format( - StringResources::getAsMString(StringResources::kUsdLayerIdentifier), + MString(StringResources::kUsdLayerIdentifier.value.c_str()), layer->GetIdentifier().c_str()); result += temp; result += "\n"; if (layer->GetRealPath() != layer->GetIdentifier()) { temp.format( - StringResources::getAsMString(StringResources::kRealPath), - layer->GetRealPath().c_str()); + MString(StringResources::kRealPath.value.c_str()), layer->GetRealPath().c_str()); result += temp; result += "\n"; } @@ -551,4 +559,37 @@ void MayaSessionState::printLayer(const PXR_NS::SdfLayerRefPtr& layer) const MGlobal::displayInfo(result); } +std::vector MayaSessionState::selectedStages() const +{ + std::vector result; + + const Ufe::GlobalSelection::Ptr& ufeGlobalSelection = Ufe::GlobalSelection::get(); + if (!ufeGlobalSelection) + return result; + + const std::vector allEntries = allStages(); + auto findEntryById = [&](const std::string& stageId) -> const StageEntry* { + for (const auto& candidateEntry : allEntries) { + if (candidateEntry._id == stageId) + return &candidateEntry; + } + return nullptr; + }; + + const Ufe::Selection& ufeSelection = *ufeGlobalSelection; + for (const auto& item : ufeSelection) { + PXR_NS::MayaUsdProxyShapeBase* proxyShapePtr + = MayaUsd::ufe::getProxyShapeFromItemOrChildren(item); + if (!proxyShapePtr) + continue; + + MFnDagNode dagNode(proxyShapePtr->thisMObject()); + const std::string stageId = dagNode.uuid().asString().asChar(); + if (const StageEntry* entry = findEntryById(stageId)) { + result.push_back(*entry); + } + } + return result; +} + } // namespace UsdLayerEditor diff --git a/lib/usd/ui/layerEditor/mayaSessionState.h b/lib/usd/ui/layerEditor/mayaSessionState.h index 48d91f7c85..ef79dd91b2 100644 --- a/lib/usd/ui/layerEditor/mayaSessionState.h +++ b/lib/usd/ui/layerEditor/mayaSessionState.h @@ -18,7 +18,8 @@ #define MAYASESSIONSTATE_H #include "mayaCommandHook.h" -#include "sessionState.h" + +#include #include #include @@ -30,6 +31,10 @@ #include +#ifdef WANT_ADSK_USD_EDIT_FORWARD_BUILD +class MayaUsdEFFallbackTargetChangedNotice; +#endif + namespace UsdLayerEditor { PXR_NAMESPACE_USING_DIRECTIVE @@ -54,7 +59,6 @@ class MayaSessionState void setStageEntry(StageEntry const& in_entry) override; void setAutoHideSessionLayer(bool hide) override; #ifdef WANT_ADSK_USD_EDIT_FORWARD_BUILD - void setEchoEditForwarding(bool echo) override; bool isEditForwardMode() const override; PXR_NS::SdfLayerRefPtr effectiveTargetLayer() const override; #endif @@ -63,6 +67,7 @@ class MayaSessionState AbstractCommandHook* commandHook() override; std::vector allStages() const override; + std::vector selectedStages() const override; // path to default load layer dialogs to std::string defaultLoadPath() const override; // ui that returns a list of paths to load @@ -81,10 +86,10 @@ class MayaSessionState // in this case, the stage needs to be re-created on the new file void rootLayerPathChanged(std::string const& in_path) override; - void refreshCurrentStageEntry(); - void refreshStageEntry(std::string const& proxyShapePath); + void refreshCurrentStageEntry() override; + void refreshStageEntry(std::string const& proxyShapePath) override; - std::string proxyShapePath() { return _currentStageEntry._proxyShapePath; } + std::string proxyShapePath() { return _currentStageEntry._dccObjectPath; } Q_SIGNALS: void clearUIOnSceneResetSignal(); @@ -115,12 +120,22 @@ class MayaSessionState void mayaUsdStageReset(const MayaUsdProxyStageSetNotice& notice); void mayaUsdStageResetCBOnIdle(StageEntry const& entry); +#ifdef WANT_ADSK_USD_EDIT_FORWARD_BUILD + // Bridges the edit-forward host's fallback-target-changed notice to the + // shared SessionState Qt signals so the layer editor refreshes its target + // highlight and the EF toggle button when forwarding state changes. + void usd_efFallbackTargetChanged(const MayaUsdEFFallbackTargetChangedNotice& notice); +#endif + void loadSelectedStage(); std::vector _callbackIds; TfNotice::Key _stageResetNoticeKey; - MayaCommandHook _mayaCommandHook; - bool _inLoad = false; +#ifdef WANT_ADSK_USD_EDIT_FORWARD_BUILD + TfNotice::Key _efFallbackTargetChangedNoticeKey; +#endif + MayaCommandHook _mayaCommandHook; + bool _inLoad = false; }; } // namespace UsdLayerEditor diff --git a/lib/usd/ui/layerEditor/qtUtils.h b/lib/usd/ui/layerEditor/qtUtils.h index da5b57af27..42de76e91e 100644 --- a/lib/usd/ui/layerEditor/qtUtils.h +++ b/lib/usd/ui/layerEditor/qtUtils.h @@ -16,6 +16,8 @@ #ifndef LAYEREDITOR_QTUTILS_H #define LAYEREDITOR_QTUTILS_H +#include + class QPushButton; class QSize; class QString; @@ -92,7 +94,7 @@ const bool IS_MAC_OS = true; const bool IS_MAC_OS = false; #endif -extern QtUtils* utils; +MAYAUSD_UI_PUBLIC extern QtUtils* utils; template inline T DPIScale(T pixel) { return static_cast(pixel * utils->dpiScale()); } diff --git a/lib/usd/ui/layerEditor/sessionState.h b/lib/usd/ui/layerEditor/sessionState.h index 5972d44a36..fe0404e85b 100644 --- a/lib/usd/ui/layerEditor/sessionState.h +++ b/lib/usd/ui/layerEditor/sessionState.h @@ -18,6 +18,8 @@ #include "abstractCommandHook.h" +#include + #include #include @@ -37,7 +39,7 @@ namespace UsdLayerEditor { * stage, and app-specific UI * */ -class SessionState : public QObject +class MAYAUSD_UI_PUBLIC SessionState : public QObject { Q_OBJECT public: diff --git a/lib/usd/ui/layerEditor/stageSelectorWidget.cpp b/lib/usd/ui/layerEditor/stageSelectorWidget.cpp index bbe744b94b..a848ed4df4 100644 --- a/lib/usd/ui/layerEditor/stageSelectorWidget.cpp +++ b/lib/usd/ui/layerEditor/stageSelectorWidget.cpp @@ -219,6 +219,7 @@ void StageSelectorWidget::createUI() connect(_pinStage, &QAbstractButton::clicked, this, &StageSelectorWidget::stagePinClicked); _collapseContent = new QPushButton(); + _collapseContent->setObjectName("collapseContentButton"); _collapseContent->move(0, higButtonYOffset); QtUtils::setupButtonWithHIGBitmaps(_collapseContent, ":/UsdLayerEditor/contents_on"); _collapseContent->setFixedSize(buttonSize, buttonSize); diff --git a/lib/usd/ui/layerEditor/warningDialogs.cpp b/lib/usd/ui/layerEditor/warningDialogs.cpp index 1fbb7de452..354b802838 100644 --- a/lib/usd/ui/layerEditor/warningDialogs.cpp +++ b/lib/usd/ui/layerEditor/warningDialogs.cpp @@ -19,6 +19,12 @@ namespace { +UsdLayerEditor::ModalDialogTestHandler& modalDialogTestHandler() +{ + static UsdLayerEditor::ModalDialogTestHandler handler; + return handler; +} + // returns a bullet-ed html text for a list of layers tree items // used for dialog boxes, or an empty string if none QString getLayerBulletList(const QStringList* in_list) @@ -38,6 +44,14 @@ QString getLayerBulletList(const QStringList* in_list) namespace UsdLayerEditor { +ModalDialogTestHandler setModalDialogTestHandler(ModalDialogTestHandler handler) +{ + auto& current = modalDialogTestHandler(); + auto prev = std::move(current); + current = std::move(handler); + return prev; +} + bool confirmDialog_internal( bool okCancel, QWidget* parent, @@ -47,6 +61,9 @@ bool confirmDialog_internal( const QString* okButtonText, QMessageBox::Icon icon) { + if (auto& testHandler = modalDialogTestHandler()) + return testHandler(title, message); + QMessageBox msgBox(parent); // there is no title bar text on mac, instead it's bold text if (IS_MAC_OS) diff --git a/lib/usd/ui/layerEditor/warningDialogs.h b/lib/usd/ui/layerEditor/warningDialogs.h index 96140510a4..a4daff0ba4 100644 --- a/lib/usd/ui/layerEditor/warningDialogs.h +++ b/lib/usd/ui/layerEditor/warningDialogs.h @@ -20,6 +20,8 @@ #include #include +#include + /** * @brief Helpers to easily pop a properly formatted and sized message box * @@ -43,6 +45,11 @@ void warningDialog( const QStringList* bulletList = nullptr, QMessageBox::Icon icon = QMessageBox::Icon::NoIcon); +// Test seam: when a handler is installed, confirmDialog/warningDialog return its +// result instead of showing a real dialog. Pass nullptr to uninstall. +using ModalDialogTestHandler = std::function; +ModalDialogTestHandler setModalDialogTestHandler(ModalDialogTestHandler handler); + } // namespace UsdLayerEditor #endif // WARNINGDIALOGS_H diff --git a/lib/usd/ui/renderSetup/renderSetupWindowCmd.cpp b/lib/usd/ui/renderSetup/renderSetupWindowCmd.cpp index afec1b21ec..7d30674a38 100644 --- a/lib/usd/ui/renderSetup/renderSetupWindowCmd.cpp +++ b/lib/usd/ui/renderSetup/renderSetupWindowCmd.cpp @@ -90,24 +90,20 @@ class RenderSetupWindow static void onSceneChangedCB(void* clientData); private: - void applyStages() - { - _editCommitter->setStages(_hostStages); - _tree->setStages(_hostStages); - } + void applyStages() { /*_editCommitter->setStages(_hostStages); _tree->setStages(_hostStages);*/ } private: - Adsk::RenderSetupWidget* _tree; - MayaUsdRenderSetup::MayaEditCommitter* _editCommitter { nullptr }; - std::vector _hostStages; - std::vector _sceneCallbackIds; + //Adsk::RenderSetupWidget* _tree; + //MayaUsdRenderSetup::MayaEditCommitter* _editCommitter { nullptr }; + std::vector _hostStages; + std::vector _sceneCallbackIds; }; RenderSetupWindow::RenderSetupWindow(QWidget* parent) : PARENT_CLASS(parent) { // Create the render setup widget and set it as the central widget of the window. - _tree = new Adsk::RenderSetupWidget(this); + /*_tree = new Adsk::RenderSetupWidget(this); _editCommitter = new MayaUsdRenderSetup::MayaEditCommitter(nullptr); _tree->setEditCommitter(std::unique_ptr(_editCommitter)); setCentralWidget(_tree); @@ -135,7 +131,7 @@ RenderSetupWindow::RenderSetupWindow(QWidget* parent) _sceneCallbackIds.push_back( MSceneMessage::addCallback(MSceneMessage::kAfterNew, onSceneChangedCB, this)); _sceneCallbackIds.push_back( - MNodeMessage::addNameChangedCallback(MObject::kNullObj, nodeRenamedCB, this)); + MNodeMessage::addNameChangedCallback(MObject::kNullObj, nodeRenamedCB, this));*/ } RenderSetupWindow::~RenderSetupWindow() diff --git a/lib/usdLayerEditor/.claude/context/maya-to-max-mapping.md b/lib/usdLayerEditor/.claude/context/maya-to-max-mapping.md deleted file mode 100644 index d1855b4706..0000000000 --- a/lib/usdLayerEditor/.claude/context/maya-to-max-mapping.md +++ /dev/null @@ -1,67 +0,0 @@ -### Things to keep in mind: -- Maya and Max have some core differences, while the logic of the feature may be the same between the two. You can expect some code duplication. -- The core implementation of the layer editor is found within the git submodule usd-layer-editor. -- In maya-usd, commands are implemented as sub-classes of BaseCmd inside layerEditorCommand.cpp, each with doIt()/undoIt() methods, wrapped by the LayerEditorCommand MPxCommand that parses MEL flags and dispatches to them. This command is registered under the name mayaUsdLayerEditor, making it invokable from Python via maya.cmds.mayaUsdLayerEditor(). In max's usd-layer-editor, the layer editor routes all operations through an AbstractCommandHook interface, which the UfeCommandHook implements by creating Ufe::UndoableCommand subclasses that are executed through UFE's undo/redo manager, and these commands are also exposed to Python via boost.python bindings in python/wrapUsdLayerEditor.cpp. -- `LayerTreeView::callMethodOnSelection` in the 3dsMax submodule only accepts `simpleLayerMethod = void (LayerTreeItem::*)()` — no parameters. Maya's simpleLayerMethod is typedef'd as `void (LayerTreeItem::*)(QWidget* in_parent)`, but that does not exist here. If a Maya port declares a `LayerTreeItem` method with a `QWidget*` parameter intended for use with `callMethodOnSelection`, drop the parameter as 3dsMax typedef requires a no-parameter method pointer. -- When adding Python bindings (`wrapUsdLayerEditor.cpp`), always build **both** configurations: `python build-scripts/build-solution.py 2026` and `python build-scripts/build-solution.py hybrid 2026`. - -### Key files for 3dsmax-component-usd: - Core layer editor wrapper: - - src/MaxUsdObjects/LayerEditor/MaxLayerEditor.h/.cpp - - src/MaxUsdObjects/LayerEditor/MaxLayerEditorWindow.h/.cpp - - src/MaxUsdObjects/LayerEditor/MaxLayerEditorItemDelegate.h/.cpp - - src/MaxUsdObjects/LayerEditor/MaxSessionState.h/.cpp - - src/MaxUsdObjects/LayerEditor/USDLayerManager.h/.cpp - - Menu & actions: - - src/MaxUsdObjects/Views/UsdMenu.h/.cpp - - src/ApplicationPlugins/usd-component/Contents/scripts/registerMenu.ms/.mcr - - Layer editor commands (the actual operations): - - layerEditorCommands.h/.cpp - - abstractCommandHook.h - - ufeCommandHook.h/.cpp - - Layer editor operations: - - layers.h/.cpp - - layerMuting.h/.cpp - - layerLocking.h/.cpp - - customLayerData.h/.cpp - - Layer editor UI: - - layerEditorWindow.h/.cpp, abstractLayerEditorWindow.h - - layerEditorWidget.h/.cpp - - layerTreeView.h/.cpp, layerTreeModel.h/.cpp, layerTreeItem.h/.cpp, layerTreeItemDelegate.h/.cpp - - saveLayersDialog.h/.cpp, loadLayersDialog.h/.cpp - - sessionState.h/.cpp - - Python bindings: - - usd-layer-editor/python/wrapUsdLayerEditor.cpp - - usd-layer-editor/python/module.cpp - - Tests: - - usd-layer-editor/test/layer_editor_test.py - - src/Tests/Integration/layer_editor_test.ms - - src/Tests/Integration/usd_layer_editor_test.ms - -### Key files for usd-maya layer editor - Commands: - - lib/mayaUsd/commands/layerEditorCommand.cpp - - lib/mayaUsd/commands/layerEditorWindowCommand.cpp - - Layer Editor UI: - - lib/mayaUsd/ui/layer_editor/abstractLayerEditorWindow.h - - lib/mayaUsd/ui/layer_editor/abstractCommandHook.h - - lib/mayaUsd/ui/layer_editor/layerTreeItem.h - - lib/mayaUsd/ui/layer_editor/layerTreeItem.cpp - - lib/mayaUsd/ui/layer_editor/mayaCommandHook.h - - lib/mayaUsd/ui/layer_editor/mayaCommandHook.cpp - - lib/mayaUsd/ui/layer_editor/mayaLayerEditorWindow.h - - lib/mayaUsd/ui/layer_editor/mayaLayerEditorWindow.cpp - - UI / Strings: - - lib/mayaUsd/ui/mayaUsdMenu.mel - - lib/mayaUsd/ui/mayaUSDRegisterStrings.py - - Tests: - - test/lib/testMayaUsdLayerEditorCommands.py \ No newline at end of file diff --git a/lib/usdLayerEditor/.claude/settings.local.json b/lib/usdLayerEditor/.claude/settings.local.json deleted file mode 100644 index 07fc9ee61f..0000000000 --- a/lib/usdLayerEditor/.claude/settings.local.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(curl -s \"https://raw.githubusercontent.com/Autodesk/maya-usd/a3a7c03fe794aba1bdba9fcbf81bc50c7ed96834/test/lib/testMayaUsdLayerEditorCommands.py\")", - "Bash(python build-scripts/build-solution.py 2026)", - "Bash(python build-scripts/build-solution.py hybrid 2026)", - "Bash(python run_tests.py)", - "Bash(xargs grep:*)", - "Bash(python -c ':*)" - ] - } -} diff --git a/lib/usdLayerEditor/.claude/skills/port-maya-to-max/SKILL.md b/lib/usdLayerEditor/.claude/skills/port-maya-to-max/SKILL.md deleted file mode 100644 index ad1db0a28c..0000000000 --- a/lib/usdLayerEditor/.claude/skills/port-maya-to-max/SKILL.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -name: port-maya-to-max -description: Looks at a PR(s) or commit sha1 from the maya-usd repo and ports the changes to 3dsmax-component-usd, keeping the logic but integrating it with 3dsMax. Use when user asks to port a maya feature or pull request to 3dsmax or if the user mentions "port from maya", "port-to-max", "max integration". -argument-hint: [pr-link-or-sha] -allowed-tools: bash, grep, find, read, gh pr:view, gh pr:diff ---- - -Arguments: $pr-link-or-sha - -# Port Maya to Max - -## Instructions -1. If $pr-link-or-sha is a link to a PR, verify it is from `https://github.com/Autodesk/maya-usd`. Before making any network calls, check for a local maya-usd clone by searching common sibling directories (e.g. `../maya-usd`, `../../maya-usd`, or any path matching `**/maya-usd` near the current repo root). If a local clone exists, use `git -C fetch origin` then `git -C show` or `git -C diff` to read the changes without network calls. Only fall back to `gh pr view $pr-link-or-sha` and `gh pr diff $pr-link-or-sha` if no local clone is found. If $pr-link-or-sha is instead a commit sha (40 hex characters), first check whether the commit exists in a local maya-usd clone via `git -C show $pr-link-or-sha`; only fall back to WebFetch at `https://github.com/Autodesk/maya-usd/commit/$pr-link-or-sha.diff` if no local clone has the commit. -2. Read `.claude/context/maya-to-max-mapping.md` for architectural differences and key file locations. -3. Create a plan for implementing it in 3dsmax-component-usd. You may search through both the maya-usd repo and the 3dsmax-component-usd repo. -4. Implement the changes necessary in 3dsMax to replicate the Maya pull request. Mirror the Maya PR's design as closely as possible (class hierarchy, inheritance relationships, member visibility (public/protected/private), and use of shared utilities (e.g. UsdUndoableItem, UsdUndoBlock)). Only deviate when the 3dsMax architecture strictly requires it. If the Maya PR makes a base class member protected, do the same in 3dsMax. If the Maya PR uses a particular undo mechanism, use the equivalent in 3dsMax. Do not invent alternative designs. If in doubt, follow the Maya PR. -5. Apply the code changes. -6. Implement all of the integration tests from the Maya pull request into 3dsMax. - - First, list every new test method added in the Maya PR diff (e.g. `testFoo`, `testBar`, …) and count them. - - Then port each one, checking them off as you go. - - Before moving on, verify your count matches. Do not proceed until every test from the PR has been ported. -7. Build the entire project, using `python build-scripts\build-solution.py`, ensuring that it builds without error. Build both Release and Hybrid. -8. Run the integration tests, ensuring no integration tests fail. -9. Clean up logs and run_tests.py file(s). - -## Running Tests - -Tests require launching 3dsMax. Create `run_tests.py` in the repo root: -Before writing the file, glob for `artifacts_*.xml` in the repo root and extract all year numbers. List them highest-first in max_candidates so the newest installed version is preferred. - -```python -import subprocess, sys, os, tempfile - -repo_root = os.path.dirname(os.path.abspath(__file__)) -script = os.path.join(repo_root, "src", "Tests", "run_integration_tests.py") -logpath = os.path.join(tempfile.gettempdir(), "test_log.txt") - -# Find 3dsmax.exe - check common install locations or MAXPATH env var. -# Entries below are generated from all artifacts_*.xml found in the repo root, highest year first. -max_candidates = [ - os.environ.get("MAXPATH", ""), - # r"C:\Program Files\Autodesk\3ds Max \3dsmax.exe", - # r"C:\Program Files\Autodesk\3ds Max \3dsmax.exe", - # ... one entry per artifacts_YEAR.xml, descending -] - -maxpath = next((p for p in max_candidates if p and os.path.exists(p)), None) -if not maxpath: - print("ERROR: 3dsmax.exe not found. Set MAXPATH env var or add your install path to max_candidates.") - sys.exit(1) - -result = subprocess.run( - [sys.executable, script, "--maxpath", maxpath, - "--tests", ".ms", "--logpath", logpath], - capture_output=True, text=True, timeout=900 -) -print("Return code:", result.returncode) -print("Log:", logpath) -``` - -Run with `python run_tests.py`, then read the log at the path you specified. - -**Notes:** -- 3dsMax takes a minute to start, use at least 900s timeout -- Return code `4294967295` (0xFFFFFFFF) is normal; actual pass/fail is in the log file -- Check git log on the test file to identify pre-existing known failures before assuming a failure is yours -- `test_layer_editor_selection` in `usd-layer-editor/test/layer_editor_test.py` (line ~1152) fails with `AssertionError: 0 != 1`. The test itself has a comment: `# KNOWN ISSUE: due to QTimer::singleShot in modelrebuild being called async`. When running integration tests and seeing only this failure, it is not a regression. Ignore it and consider the test run successful for porting purposes. -- UsdUfe::UsdUndoBlock + UsdUndoManager::trackLayerStates captures most USD layer edits (attribute sets, prim creation/removal, sublayer list edits, etc.) and should be used when mirroring Maya. However, SdfLayer::TransferContent is not captured by this mechanism. For commands that rely on TransferContent, manual layer backup (e.g., CreateAnonymous + TransferContent for snapshot, then restore on undo) is required instead of or in addition to UsdUndoableItem. Do not blindly follow Maya's UsdUndoableItem usage if the operation involves TransferContent — verify that the undo mechanism actually captures the changes. -This prevents two failure modes: - 1. Using manual snapshots when UsdUndoBlock would work (over-engineering, diverges from Maya's design). - 2. Using UsdUndoBlock alone for TransferContent-based operations and silently having broken undo. -The distinction is: tracked layer state changes (edits through the SDF change notification system) are captured; bulk buffer copies like TransferContent bypass that tracking entirely. diff --git a/lib/usdLayerEditor/CMakeLists.txt b/lib/usdLayerEditor/CMakeLists.txt index dca8906c35..ea37c165bc 100644 --- a/lib/usdLayerEditor/CMakeLists.txt +++ b/lib/usdLayerEditor/CMakeLists.txt @@ -1,9 +1,20 @@ -cmake_minimum_required (VERSION 3.20) +# +# 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. +# -add_definitions(-DBOOST_ALL_NO_LIB) +# Top-level CMakeLists for the shared usdLayerEditor component (DCC-agnostic). -project ("UsdLayerEditor") - -list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/Modules") add_subdirectory(lib) add_subdirectory(python) diff --git a/lib/usdLayerEditor/README.md b/lib/usdLayerEditor/README.md deleted file mode 100644 index ebd44c7d31..0000000000 --- a/lib/usdLayerEditor/README.md +++ /dev/null @@ -1,59 +0,0 @@ -# usd-layer-editor - -A DCC agnostic USD Layer editor, extracted from [maya-usd](https://git.autodesk.com/maya3d/maya-usd/tree/dev/lib/usd/ui/layerEditor). - -This is a work in progress, features from the maya-usd layer editor will be progressively enabled. -Code that needs attention in that regard is flagged with `TODO LE-EXTRACT` to make it easy to find. - -# Build Instructions -Download and install : -- [CMake](https://cmake.org/download/) >= 3.20 - -The usd-layer-editor depends on : -- USD -- UFE -- UsdUfe -- Python -- QT - -The following CMAKE options can be used to configure dependencies : - -Name | Description ---- | --- -PXR_USD_LOCATION | Path to USD root directory -UFE_INCLUDE_ROOT | Path to UFE include directory -UFE_LIB_ROOT | Path to UFE lib directory. -USDUFE_ROOT_DIR | Path to UsdUfe root directory -Python3_ROOT_DIR | Path to the python root directory -BOOST_LIB_TOOLSET | Boost library toolset to link against -QT_VERSION | QT version to use, support 5 or 6 -CMAKE_PREFIX_PATH | Path to the QT cmake folder - -For example : - -```bash -git clone git@git.autodesk.com:media-and-entertainment/usd-layer-editor.git -cd usd-layer-editor -mkdir build && cd build - -cmake -G "Visual Studio 16 2019" ^ - -DPXR_USD_LOCATION=D:/repos/3dsmax-component-usd/artifacts/2025/Pixar_USD/Release ^ - -DUFE_INCLUDE_ROOT=D:/repos/3dsmax-component-usd/artifacts/2025/ufe/ufe-5.3.0/common/include ^ - -DUFE_LIB_ROOT=D:/repos/3dsmax-component-usd/artifacts/2025/ufe/ufe-5.3.0/platform/Windows/RelWithDebInfo/lib ^ - -DUSDUFE_ROOT_DIR=D:/repos/3dsmax-component-usd/artifacts/2025/UsdUfe ^ - -DPython3_ROOT_DIR=D:/repos/3dsmax-component-usd/artifacts/2025/Python ^ - -DPython3_FIND_STRATEGY=LOCATION ^ - -DPython3_FIND_FRAMEWORK=NEVER ^ - -DPython3_FIND_REGISTRY=NEVER ^ - -DBOOST_LIB_TOOLSET="vc142" ^ - -DQT_VERSION=6 ^ - -DCMAKE_PREFIX_PATH=D:/artifactory/unzipped/Qt/6.5.0-3dsmax-001-vc142-test7/qt/6.5.0/lib/cmake/Qt6 ^ - -DCMAKE_INSTALL_PREFIX=../maxBuilds/2025 ^ - .. - -cmake --build . --config RelWithDebInfo --target install - -cd .. -``` - -bat scripts are is provided as a convenience to build the usd-layer-editor for 3dsMax USD on Windows. \ No newline at end of file diff --git a/lib/usdLayerEditor/buildMaxAll.bat b/lib/usdLayerEditor/buildMaxAll.bat deleted file mode 100644 index 701b1cc4a7..0000000000 --- a/lib/usdLayerEditor/buildMaxAll.bat +++ /dev/null @@ -1,5 +0,0 @@ -call buildMax2023.bat -call buildMax2024.bat -call buildMax2025.bat -call buildMax2026.bat -call buildMax2027.bat \ No newline at end of file diff --git a/lib/usdLayerEditor/lib/CMakeLists.txt b/lib/usdLayerEditor/lib/CMakeLists.txt index bb345ea3e0..244d7363ce 100644 --- a/lib/usdLayerEditor/lib/CMakeLists.txt +++ b/lib/usdLayerEditor/lib/CMakeLists.txt @@ -1,152 +1,159 @@ - -# Find QT -if(QT_VERSION EQUAL "5") - find_package(Qt5 REQUIRED COMPONENTS Core Gui Widgets) -elseif(QT_VERSION EQUAL "6") - find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets) -else() - message(SEND_ERROR "Invalid QT Version : ${QT_VERSION}.") -endif() - -find_package(Python3 COMPONENTS Interpreter Development) -find_package(USD REQUIRED) -find_package(UFE REQUIRED) -find_package(USDUFE REQUIRED) +# +# 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. +# set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_AUTOUIC ON) -# Add source to this project's executable. -add_library (UsdLayerEditor SHARED - # Code - abstractCommandHook.h - abstractLayerEditorWindow.h - batchSaveLayersUIDelegate.cpp - batchSaveLayersUIDelegate.h - customLayerData.cpp - customLayerData.h - dirtyLayersCountBadge.cpp - dirtyLayersCountBadge.h - generatedIconButton.cpp - generatedIconButton.h - layerEditorAPI.h - layerEditorCommands.cpp - LayerEditorCommands.h - layerEditorWidget.cpp - layerEditorWidget.h - layerEditorWidgetManager.cpp - layerEditorWidgetManager.h - layerEditorWindow.h - layerEditorWindow.cpp - layerLocking.cpp - layerLocking.h - layerMuting.cpp - layerMuting.h - layers.cpp - layers.h - layerTreeItem.cpp - layerTreeItem.h - layerTreeItemDelegate.cpp - layerTreeItemDelegate.h - layerTreeModel.cpp - layerTreeModel.h - layerTreeView.cpp - layerTreeView.h - layerTreeViewStyle.h - loadLayersDialog.cpp - loadLayersDialog.h - pathChecker.cpp - pathChecker.h - saveLayersDialog.cpp - saveLayersDialog.h - sessionState.cpp - sessionState.h - stageSelectorWidget.cpp - stageSelectorWidget.h - stringResources.cpp - stringResources.h - tokens.cpp - tokens.h - ufeCommandHook.h - ufeCommandHook.cpp - utilOptions.h - utilString.h - utilUI.cpp - utilUI.h - utilFileSystem.cpp - utilFileSystem.h - utilSerialization.cpp - utilSerialization.h - utilQT.cpp - utilQT.h - warningDialogs.cpp - # Icon resources - resources.qrc +add_library(usdLayerEditor SHARED + abstractCommandHook.h + abstractLayerEditorWindow.h + batchSaveLayersUIDelegate.cpp + batchSaveLayersUIDelegate.h + componentSaveWidget.cpp + componentSaveWidget.h + customLayerData.cpp + customLayerData.h + dirtyLayersCountBadge.cpp + dirtyLayersCountBadge.h + generatedIconButton.cpp + generatedIconButton.h + layerContentsWidget.cpp + layerContentsWidget.h + layerEditorAPI.h + LayerEditorCommands.h + layerEditorCommands.cpp + layerEditorDCCFunctions.cpp + layerEditorDCCFunctions.h + layerEditorWidget.cpp + layerEditorWidget.h + layerEditorWidgetManager.cpp + layerEditorWidgetManager.h + layerEditorWindow.cpp + layerEditorWindow.h + layerLocking.cpp + layerLocking.h + layerMuting.cpp + layerMuting.h + layers.cpp + layers.h + layerTreeItem.cpp + layerTreeItem.h + layerTreeItemDelegate.cpp + layerTreeItemDelegate.h + layerTreeModel.cpp + layerTreeModel.h + layerTreeView.cpp + layerTreeView.h + layerTreeViewStyle.h + loadLayersDialog.cpp + loadLayersDialog.h + pathChecker.cpp + pathChecker.h + resources.qrc + saveLayersDialog.cpp + saveLayersDialog.h + sessionState.cpp + sessionState.h + stageSelectorWidget.cpp + stageSelectorWidget.h + stringResources.cpp + stringResources.h + tokens.cpp + tokens.h + ufeCommandHook.cpp + ufeCommandHook.h + usdSyntaxHighlighter.cpp + usdSyntaxHighlighter.h + utilFileSystem.cpp + utilFileSystem.h + utilQT.cpp + utilQT.h + utilSerialization.cpp + utilSerialization.h + utilString.h + utilUI.cpp + utilUI.h + warningDialogs.cpp + warningDialogs.h ) -target_include_directories( UsdLayerEditor PUBLIC - ${UFE_INCLUDE_DIR} - ${USDUFE_INCLUDE_DIR} - ${Python3_INCLUDE_DIRS} -) - -# Link QT -if(QT_VERSION EQUAL 5) - target_link_libraries(UsdLayerEditor Qt5::Core Qt5::Gui Qt5::Widgets) +# QT_NO_KEYWORDS: match mayaUsdUI convention (forbids `signals`/`slots`/`emit` macros) +get_target_property(qt_core_aliased Qt::Core ALIASED_TARGET) +if (qt_core_aliased) + set_target_properties(${qt_core_aliased} PROPERTIES INTERFACE_COMPILE_DEFINITIONS QT_NO_KEYWORDS) else() - target_link_libraries(UsdLayerEditor Qt6::Core Qt6::Gui Qt6::Widgets) + set_target_properties(Qt::Core PROPERTIES INTERFACE_COMPILE_DEFINITIONS QT_NO_KEYWORDS) endif() -# Link USD -target_link_libraries(UsdLayerEditor - usd - sdf - usdUtils -) - -# Link UFE/UsdUfe -target_link_libraries(UsdLayerEditor - ${UFE_LIBRARY} - ${USDUFE_LIBRARY} - ${Python3_LIBRARIES} +target_compile_definitions(usdLayerEditor + PRIVATE + LAYEREDITOR_EXPORTS + $<$:WIN32> + $<$:LINUX> + $<$:OSMac_> ) -set_property(TARGET UsdLayerEditor PROPERTY CXX_STANDARD 17) -set_property(TARGET UsdLayerEditor PROPERTY AUTOMOC ON) -set_property(TARGET UsdLayerEditor PROPERTY AUTORCC ON) -set_property(TARGET UsdLayerEditor PROPERTY AUTOUIC ON) +mayaUsd_compile_config(usdLayerEditor) -if (MSVC) - set(MSVC_DEFINITIONS - LAYEREDITOR_EXPORTS - NOMINMAX - _CRT_SECURE_NO_WARNINGS - _SCL_SECURE_NO_WARNINGS - TBB_SUPPRESS_DEPRECATED_MESSAGES - ) +target_include_directories(usdLayerEditor + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${UFE_INCLUDE_DIR} +) + +target_link_libraries(usdLayerEditor + PUBLIC + usdUfe + ${UFE_LIBRARY} + sdf + tf + usd + PRIVATE + Qt::Core + Qt::Gui + Qt::Widgets +) - target_compile_definitions(UsdLayerEditor - PUBLIC - ${MSVC_DEFINITIONS} - ) - - # Disable some warnings that arise alot from USD. - add_definitions(/wd26495 /wd4305 /wd4244) - - # Parallel compile - add_definitions(/MP) +# run-time search paths (mirrors usdUfe) +if(IS_MACOSX OR IS_LINUX) + mayaUsd_init_rpath(rpath "lib") + if(DEFINED MAYAUSD_TO_USD_RELATIVE_PATH) + mayaUsd_add_rpath(rpath "../${MAYAUSD_TO_USD_RELATIVE_PATH}/lib") + elseif(DEFINED PXR_USD_LOCATION) + mayaUsd_add_rpath(rpath "${PXR_USD_LOCATION}/lib") + endif() + mayaUsd_install_rpath(rpath usdLayerEditor) endif() -target_compile_definitions(UsdLayerEditor - PUBLIC - QT_NO_KEYWORDS - BOOST_LIB_TOOLSET=${BOOST_LIB_TOOLSET} +install(TARGETS usdLayerEditor + LIBRARY DESTINATION ${CMAKE_INSTALL_PREFIX}/lib + ARCHIVE DESTINATION ${CMAKE_INSTALL_PREFIX}/lib + RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/lib ) -install(TARGETS UsdLayerEditor - DESTINATION lib) -install(FILES $ DESTINATION lib) +# Install the USD syntax highlighting configuration file. The cpp resolves the path +# at runtime via MAYAUSD_LIB_LOCATION + /syntaxHighlight/usdSyntaxConfig.json (or via +# the MAYAUSD_USD_SYNTAX_HIGHLIGHTING_CONFIG env var for user overrides). +install(FILES "usdSyntaxConfig.json" + DESTINATION "${CMAKE_INSTALL_PREFIX}/lib/syntaxHighlight" +) -file(GLOB HEADER_FILES "./*.h") -install(FILES ${HEADER_FILES} DESTINATION include/UsdLayerEditor) \ No newline at end of file +if(IS_WINDOWS) + install(FILES $ + DESTINATION ${CMAKE_INSTALL_PREFIX}/lib OPTIONAL + ) +endif() diff --git a/lib/usdLayerEditor/lib/LayerEditorCommands.h b/lib/usdLayerEditor/lib/LayerEditorCommands.h index c12863e717..845a8f3928 100644 --- a/lib/usdLayerEditor/lib/LayerEditorCommands.h +++ b/lib/usdLayerEditor/lib/LayerEditorCommands.h @@ -13,6 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. // +#ifndef LAYER_EDITOR_COMMANDS_H +#define LAYER_EDITOR_COMMANDS_H #include "abstractCommandHook.h" #include "layerEditorAPI.h" @@ -22,6 +24,11 @@ #include #include +#include +#include + +#include + namespace UsdLayerEditor { enum class CmdId @@ -198,10 +205,10 @@ class LayerEditorAPI LockLayerCmd : public BaseCmd bool includeSubLayers = false, bool skipSystemLockedLayers = false) : BaseCmd(CmdId::kLockLayer, layer) - , _stage(stage) + , _lockType(lockState) , _includeSublayers(includeSubLayers) , _skipSystemLockedLayers(skipSystemLockedLayers) - , _lockType(lockState) + , _stage(stage) { } @@ -277,7 +284,6 @@ class LayerEditorAPI InsertSubPathCmd : public InsertRemoveSubPathBaseCmd class LayerEditorAPI RemoveSubPathCmd : public InsertRemoveSubPathBaseCmd { public: - // Constructor using subpath index. RemoveSubPathCmd( const pxr::UsdStageRefPtr& stage, const pxr::SdfLayerRefPtr& layer, @@ -286,7 +292,6 @@ class LayerEditorAPI RemoveSubPathCmd : public InsertRemoveSubPathBaseCmd { } - // Constructor using subpath. RemoveSubPathCmd( const pxr::UsdStageRefPtr& stage, const pxr::SdfLayerRefPtr& layer, @@ -298,6 +303,55 @@ class LayerEditorAPI RemoveSubPathCmd : public InsertRemoveSubPathBaseCmd std::string commandString() const override { return "Remove USD sublayer"; } }; +class LayerEditorAPI ReplaceSubPathCmd : public BaseCmd +{ +public: + ReplaceSubPathCmd( + const pxr::SdfLayerRefPtr& layer, + const std::string& oldPath, + const std::string& newPath) + : BaseCmd(CmdId::kReplace, layer) + , _oldPath(oldPath) + , _newPath(newPath) + { + } + + bool doIt(const pxr::SdfLayerHandle& layer) override; + bool undoIt(const pxr::SdfLayerHandle& layer) override; + std::string commandString() const override { return "Replace USD sublayer path"; } + +private: + std::string _oldPath; + std::string _newPath; +}; + +class LayerEditorAPI MoveSubPathCmd : public BaseCmd +{ +public: + MoveSubPathCmd( + const pxr::SdfLayerRefPtr& layer, + const pxr::SdfLayerRefPtr& newParentLayer, + const std::string& subPath, + int newIndex) + : BaseCmd(CmdId::kMove, layer) + , _newParent(newParentLayer) + , _subPath(subPath) + , _newIndex(newIndex) + { + } + + bool doIt(const pxr::SdfLayerHandle& layer) override; + bool undoIt(const pxr::SdfLayerHandle& layer) override; + std::string commandString() const override { return "Move USD sublayer"; } + +private: + pxr::SdfLayerRefPtr _newParent; + std::string _subPath; + std::string _newPath; + int _newIndex; + int _oldIndex { -1 }; +}; + class LayerEditorAPI RefreshSystemLockLayerCmd : public BaseCmd { public: @@ -306,8 +360,8 @@ class LayerEditorAPI RefreshSystemLockLayerCmd : public BaseCmd const pxr::SdfLayerHandle& layer, bool refreshSubLayers) : BaseCmd(CmdId::kRefreshSystemLock, layer) - , _stage(stage) , _refreshSubLayers(refreshSubLayers) + , _stage(stage) { } @@ -319,9 +373,17 @@ class LayerEditorAPI RefreshSystemLockLayerCmd : public BaseCmd std::string commandString() const override { return "Refresh System Lock"; } + // Adds a DCC-specific entry (e.g. "proxyShapePath") to the context sent with + // the onRefreshSystemLock UI callback. + void addCallbackContext(const std::string& key, const pxr::VtValue& value); + + const pxr::VtDictionary& extraCallbackContext() const { return _extraCallbackContext; } + private: std::string _quote(const std::string& string); + pxr::VtDictionary _extraCallbackContext; + // Checks if the file layer or its sublayers are accessible on disk, and adds the layer // to _layers along with the _lockCommands to update the system-lock status. void _refreshLayerSystemLock(const pxr::SdfLayerHandle& usdLayer); @@ -341,8 +403,8 @@ class LayerEditorAPI StitchLayersCmd : public BackupLayerBaseCmd const pxr::UsdStageRefPtr& stage, const std::vector& layerIdentifiers) : BackupLayerBaseCmd(CmdId::kStitchLayers, pxr::SdfLayerRefPtr()) - , _stage(stage) , _layerIdentifiersByStrength(layerIdentifiers) + , _stage(stage) { } @@ -397,4 +459,6 @@ class LayerEditorAPI AddAnonSubLayerCmd : public InsertRemoveSubPathBaseCmd std::string _anonIdentifier; }; -} // namespace UsdLayerEditor \ No newline at end of file +} // namespace UsdLayerEditor + +#endif // LAYER_EDITOR_COMMANDS_H \ No newline at end of file diff --git a/lib/usdLayerEditor/lib/abstractCommandHook.h b/lib/usdLayerEditor/lib/abstractCommandHook.h index d413b393da..9043d43164 100644 --- a/lib/usdLayerEditor/lib/abstractCommandHook.h +++ b/lib/usdLayerEditor/lib/abstractCommandHook.h @@ -106,14 +106,6 @@ class AbstractCommandHook // this method is used to select the prims with spec in a layer virtual void selectPrimsWithSpec(UsdLayer usdLayer) = 0; - // this method is used to check if the stage in the dcc stage object is from - // an incoming connection (using instage data or cache id for example) - virtual bool isDccObjectStageIncoming(const std::string& dccObjectPath) { return false; }; - - // this method is used to check if the dcc stage object is sharing the composition - // or has an owned root - virtual bool isDccObjectSharedStage(const std::string& dccObjectPath) { return true; }; - // Increase the count tracking if command executions are delayed. void increaseDelayedCommands() { _delayCount += 1; } diff --git a/lib/usdLayerEditor/lib/batchSaveLayersUIDelegate.cpp b/lib/usdLayerEditor/lib/batchSaveLayersUIDelegate.cpp index 9b408ae9e8..cbf9317a88 100644 --- a/lib/usdLayerEditor/lib/batchSaveLayersUIDelegate.cpp +++ b/lib/usdLayerEditor/lib/batchSaveLayersUIDelegate.cpp @@ -16,57 +16,64 @@ #include "batchSaveLayersUIDelegate.h" +#include "layerEditorDCCFunctions.h" #include "saveLayersDialog.h" #include "tokens.h" -#include "utilOptions.h" #include "utilQT.h" #include "utilSerialization.h" -#include - UsdLayerEditor::BatchSaveResult UsdLayerEditor::batchSaveLayersUIDelegate( const std::vector& infos, bool isExporting) { - // TODO LE-EXTRACT Maya non-interactive mode. - // if (MGlobal::kInteractive == MGlobal::mayaState()) { + if (UsdLayerEditor::isInteractiveDCCSession()) { + + auto opt = Serialization::serializeUsdEditsLocationOption(); + if (Serialization::kSaveToUSDFiles == opt) { - auto opt = Serialization::serializeUsdEditsLocationOption(); - if (Serialization::kSaveToUSDFiles == opt) { + bool showConfirmDglOption = UsdLayerEditor::confirmExistingFileSave(); - static const std::string kConfirmExistingFileSave - = UsdLayerEditorOptionVars->ConfirmExistingFileSave.GetText(); - bool showConfirmDgl = Options::optionVarExists(kConfirmExistingFileSave) - && Options::optionVarIntValue(kConfirmExistingFileSave) != 0; + bool atLeastOneLayerToSave = false; + bool atLeastOneAnonToSave = false; - // if at least one stage contains anonymous layers, you need to show the comfirm dialog - // so the user can choose where to save the anonymous layers. - if (!showConfirmDgl) { for (const auto& info : infos) { Serialization::StageLayersToSave StageLayersToSave; - const auto dccObjectPath = UsdUfe::stagePath(info.stage).string(); - Serialization::getLayersToSaveFromDCCObject(dccObjectPath, StageLayersToSave); + Serialization::getLayersToSaveFromDCCObject(info.dccObjectPath, StageLayersToSave); if (!StageLayersToSave._anonLayers.empty()) { - showConfirmDgl = true; + atLeastOneAnonToSave = true; + atLeastOneLayerToSave = true; break; } + if (!StageLayersToSave._dirtyFileBackedLayers.empty()) { + atLeastOneLayerToSave = true; + // If the option is set to show the confirmation dialog, + // we can stop here, we already know we will have to show it + // below, no need to complete the search for atLeastOneAnonToSave. + if (showConfirmDglOption) { + break; + } + } } - } - if (showConfirmDgl) { - UsdLayerEditor::SaveLayersDialog dlg(nullptr, infos, isExporting); + // if at least one stage contains anonymous layers, you need to show the comfirm dialog + // so the user can choose where to save the anonymous layers. + bool showConfirmDgl + = (showConfirmDglOption || atLeastOneAnonToSave) && atLeastOneLayerToSave; + + if (showConfirmDgl) { + UsdLayerEditor::SaveLayersDialog dlg(nullptr, infos, isExporting); - // The SaveLayers dialog only handles choosing new names for anonymous layers and - // making sure that they are remapped correctly in either their parent layer or by - // the owning proxy shape. The SaveLayers dialog itself does not currently handle - // the saving of file-backed layers, so for now we will return that we only partially - // completed saving. This will trigger the LayerManager to double check what needs - // to be saved and to complete the saving of all file-backed layers. - // - return (QDialog::Rejected == dlg.exec()) ? kAbort : kPartiallyCompleted; + // The SaveLayers dialog only handles choosing new names for anonymous layers and + // making sure that they are remapped correctly in either their parent layer or by + // the owning proxy shape. The SaveLayers dialog itself does not currently handle + // the saving of file-backed layers, so for now we will return that we only + // partially completed saving. This will trigger the LayerManager to double check + // what needs to be saved and to complete the saving of all file-backed layers. + // + return (QDialog::Rejected == dlg.exec()) ? kAbort : kPartiallyCompleted; + } } } - //} return BatchSaveResult::kNotHandled; } diff --git a/lib/usdLayerEditor/lib/batchSaveLayersUIDelegate.h b/lib/usdLayerEditor/lib/batchSaveLayersUIDelegate.h index be97de19f1..691bb69fe8 100644 --- a/lib/usdLayerEditor/lib/batchSaveLayersUIDelegate.h +++ b/lib/usdLayerEditor/lib/batchSaveLayersUIDelegate.h @@ -37,13 +37,15 @@ enum BatchSaveResult // Manager should continue to look for unsaved stages. }; -// TODO LE-EXTRACT Batch save layers. /*! \brief Information about the stages that need to be saved. */ struct StageSavingInfo { UsdStageRefPtr stage; std::string stageName; + // DCC-side object path (e.g. proxy shape path on the Maya side) that + // owns the stage. May be empty if no DCC object is associated. + std::string dccObjectPath; bool shareable = true; bool isIncoming = false; }; diff --git a/lib/usdLayerEditor/lib/componentSaveWidget.cpp b/lib/usdLayerEditor/lib/componentSaveWidget.cpp new file mode 100644 index 0000000000..56f92fe6d3 --- /dev/null +++ b/lib/usdLayerEditor/lib/componentSaveWidget.cpp @@ -0,0 +1,410 @@ +// +// Copyright 2025 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 "componentSaveWidget.h" + +#include "generatedIconButton.h" +#include "layerEditorDCCFunctions.h" +#include "sessionState.h" +#include "utilQT.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace { +const char* SHOW_MORE_TEXT = "Show More"; +const char* SHOW_LESS_TEXT = "Show Less"; + +} // namespace + +namespace UsdLayerEditor { + +ComponentSaveWidget::ComponentSaveWidget( + QWidget* in_parent, + SessionState* in_sessionState, + const std::string& dccObjectPath) + : QWidget(in_parent) + , _sessionState(in_sessionState) + , _nameEdit(nullptr) + , _locationEdit(nullptr) + , _browseButton(nullptr) + , _showMoreLabel(nullptr) + , _nameLabel(nullptr) + , _locationLabel(nullptr) + , _treeScrollArea(nullptr) + , _treeWidget(nullptr) + , _treeContainer(nullptr) + , _isExpanded(false) + , _isCompact(false) + , _originalHeight(0) + , _dccObjectPath(dccObjectPath) + , _lastComponentName() +{ + setupUI(); + if (_sessionState) { + setFolderLocation(QString::fromStdString(UsdLayerEditor::sceneFolder())); + } +} + +ComponentSaveWidget::~ComponentSaveWidget() { } + +void ComponentSaveWidget::setupUI() +{ + // Main vertical layout + auto mainLayout = new QVBoxLayout(); + QtUtils::initLayoutMargins(mainLayout); + mainLayout->setSpacing(0); + + // Content widget with padding + auto contentWidget = new QWidget(this); + auto contentLayout = new QGridLayout(); + + // Set padding: left, top, right, bottom + contentLayout->setContentsMargins(0, 0, 0, 0); + contentLayout->setSpacing(DPIScale(10)); + + // Column stretch factors: 1/6, 4/6, 1/24, 3/24 + // Convert to integers: 4/24, 16/24, 1/24, 3/24 + // Use 24 as base: 4, 16, 1, 3 + contentLayout->setColumnStretch(0, 4); // 1/6 = 4/24 + contentLayout->setColumnStretch(1, 16); // 4/6 = 16/24 + contentLayout->setColumnStretch(2, 1); // 1/24 + contentLayout->setColumnStretch(3, 3); // 3/24 + + // First row, first column: "Name" label + _nameLabel = new QLabel("Name", this); + contentLayout->addWidget(_nameLabel, 0, 0); + + // First row, second column: "Location" label + _locationLabel = new QLabel("Location", this); + contentLayout->addWidget(_locationLabel, 0, 1); + + // Second row, first column: Name textbox + _nameEdit = new QLineEdit(this); + + QValidator* compNameValidator = new ValidTfIdentifierValidator(this); + _nameEdit->setValidator(compNameValidator); + + contentLayout->addWidget(_nameEdit, 1, 0); + + // Second row, second column: Location textbox + _locationEdit = new QLineEdit(this); + contentLayout->addWidget(_locationEdit, 1, 1); + + // Second row, third column: Folder browse button + auto qtUtils = getQtUtils(); + QIcon folderIcon = qtUtils ? qtUtils->createIcon(":/UsdLayerEditor/LE_fileOpen.png") : QIcon(); + _browseButton = new GeneratedIconButton(this, folderIcon); + _browseButton->setToolTip("Browse for folder"); + _browseButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + connect(_browseButton, &QAbstractButton::clicked, this, &ComponentSaveWidget::onBrowseFolder); + contentLayout->addWidget(_browseButton, 1, 2); + + // Second row, fourth column: "Show More" clickable label + _showMoreLabel = new QLabel(SHOW_MORE_TEXT, this); + _showMoreLabel->setOpenExternalLinks(false); + _showMoreLabel->setTextFormat(Qt::RichText); + connect(_showMoreLabel, &QLabel::linkActivated, this, &ComponentSaveWidget::onShowMore); + contentLayout->addWidget(_showMoreLabel, 1, 3, Qt::AlignLeft | Qt::AlignVCenter); + + contentWidget->setLayout(contentLayout); + mainLayout->addWidget(contentWidget); + + // Tree view container (initially hidden) + _treeContainer = new QWidget(this); + auto treeLayout = new QVBoxLayout(); + treeLayout->setContentsMargins(DPIScale(20), DPIScale(10), DPIScale(20), DPIScale(15)); + treeLayout->setSpacing(DPIScale(10)); + + auto treeLabel = new QLabel("The following file structure is created on save.", this); + treeLayout->addWidget(treeLabel); + + _treeScrollArea = new QScrollArea(this); + _treeScrollArea->setWidgetResizable(true); + _treeScrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); + _treeScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + _treeScrollArea->setMinimumHeight(DPIScale(250)); + _treeScrollArea->setMaximumHeight(DPIScale(300)); + + // Use QStackedWidget to switch between tree and "nothing to preview" label + auto stackedWidget = new QStackedWidget(this); + + // Tree widget page + _treeWidget = new QTreeWidget(this); + _treeWidget->setHeaderHidden(true); + _treeWidget->setRootIsDecorated(true); + _treeWidget->setAlternatingRowColors(false); + stackedWidget->addWidget(_treeWidget); + + // "Nothing to preview" label page - centered + auto nothingToPreviewWidget = new QWidget(this); + nothingToPreviewWidget->setMinimumHeight(DPIScale(250)); + auto nothingLayout = new QVBoxLayout(); + nothingLayout->setContentsMargins(0, 0, 0, 0); + nothingLayout->setSpacing(0); + auto nothingToPreviewLabel = new QLabel( + "Nothing to preview since no information about the template is available.", + nothingToPreviewWidget); + nothingToPreviewLabel->setAlignment(Qt::AlignCenter); + nothingLayout->addStretch(); + nothingLayout->addWidget(nothingToPreviewLabel, 0, Qt::AlignCenter); + nothingLayout->addStretch(); + nothingToPreviewWidget->setLayout(nothingLayout); + stackedWidget->addWidget(nothingToPreviewWidget); + + // Start with tree widget visible + stackedWidget->setCurrentWidget(_treeWidget); + + _treeScrollArea->setWidget(stackedWidget); + treeLayout->addWidget(_treeScrollArea); + + _treeContainer->setLayout(treeLayout); + _treeContainer->setVisible(false); + mainLayout->addWidget(_treeContainer); + + setLayout(mainLayout); + setMinimumWidth(DPIScale(600)); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); +} + +void ComponentSaveWidget::setComponentName(const QString& name) +{ + if (_nameEdit) { + _nameEdit->setText(name); + } +} + +void ComponentSaveWidget::setFolderLocation(const QString& location) +{ + if (_locationEdit) { + QString normalized = location; + normalized.replace('\\', '/'); + _locationEdit->setText(normalized); + } +} + +QString ComponentSaveWidget::componentName() const +{ + return _nameEdit ? _nameEdit->text() : QString(); +} + +QString ComponentSaveWidget::folderLocation() const +{ + return _locationEdit ? _locationEdit->text() : QString(); +} + +void ComponentSaveWidget::onBrowseFolder() +{ + QString currentPath = _locationEdit->text(); + // Default to the directory of the current DCC scene if it's saved, + // otherwise default to scene folder reported by SessionState. + if (currentPath.isEmpty() && _sessionState) { + currentPath = QString::fromStdString(UsdLayerEditor::sceneFolder()); + } + + QString selectedFolder = QFileDialog::getExistingDirectory( + this, + "Select Folder", + currentPath, + QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + + if (!selectedFolder.isEmpty()) { + _locationEdit->setText(selectedFolder); + } +} + +void ComponentSaveWidget::keyPressEvent(QKeyEvent* event) +{ + // Intercept Enter/Return key when focus is on _nameEdit + if ((event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) && _nameEdit + && _nameEdit->hasFocus()) { + // If tree view is minimized (not expanded), let parent handle it + if (!_isExpanded) { + QWidget::keyPressEvent(event); + return; + } + + // If tree view is expanded, check if name has changed + QString currentName = _nameEdit->text(); + if (currentName != _lastComponentName) { + // Name has changed - update tree view and prevent event propagation + updateTreeView(); + event->accept(); + return; + } + // Name hasn't changed - let parent handle it + } + QWidget::keyPressEvent(event); +} + +void ComponentSaveWidget::populateTreeView(const QJsonObject& jsonObj, QTreeWidgetItem* parentItem) +{ + // Get standard icons for folders and files + QFileIconProvider iconProvider; + QIcon folderIcon = iconProvider.icon(QFileIconProvider::Folder); + QIcon fileIcon = iconProvider.icon(QFileIconProvider::File); + + // Fallback to style icons if file icon provider doesn't work + if (folderIcon.isNull()) { + folderIcon = style()->standardIcon(QStyle::SP_DirIcon); + } + if (fileIcon.isNull()) { + fileIcon = style()->standardIcon(QStyle::SP_FileIcon); + } + + for (auto it = jsonObj.begin(); it != jsonObj.end(); ++it) { + QString key = it.key(); + QJsonValue value = it.value(); + + QTreeWidgetItem* item = nullptr; + if (parentItem) { + item = new QTreeWidgetItem(parentItem); + } else { + item = new QTreeWidgetItem(_treeWidget); + } + item->setText(0, key); + + if (value.isBool()) { + // Boolean value represents a file + item->setIcon(0, fileIcon); + } else if (value.isObject()) { + // Object represents a folder + item->setIcon(0, folderIcon); + item->setExpanded(true); + populateTreeView(value.toObject(), item); + } + } +} + +void ComponentSaveWidget::toggleExpandedState() +{ + if (_isExpanded) { + // Collapsing: hide tree + _isExpanded = false; + _showMoreLabel->setText(SHOW_MORE_TEXT); + _treeContainer->setVisible(false); + } else { + // Expanding: show tree + _isExpanded = true; + // Capture current height before expanding (compact state) + if (_originalHeight == 0) { + _originalHeight = height(); + } + + _showMoreLabel->setText(SHOW_LESS_TEXT); + _treeContainer->setVisible(true); + } + + // Emit signal to notify parent of state change + Q_EMIT expandedStateChanged(_isExpanded); +} + +void ComponentSaveWidget::updateTreeView() +{ + std::string saveLocation(_locationEdit->text().toStdString()); + std::string componentName(_nameEdit->text().toStdString()); + + std::string result; + if (_sessionState) { + result = UsdLayerEditor::previewComponentSave(saveLocation, componentName, _dccObjectPath); + } + + QString jsonStr = QString::fromStdString(result); + + // Clear existing tree first + _treeWidget->clear(); + + QStackedWidget* stackedWidget = qobject_cast(_treeScrollArea->widget()); + + QJsonParseError parseError; + QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8(), &parseError); + QJsonObject jsonObj; + bool hasData = false; + + if (parseError.error == QJsonParseError::NoError && doc.isObject() && !doc.isEmpty()) { + jsonObj = doc.object(); + hasData = !jsonObj.isEmpty(); + } + + if (hasData) { + // Populate tree view with JSON data and show tree + populateTreeView(jsonObj); + if (stackedWidget) { + stackedWidget->setCurrentIndex(0); + } + } else { + // Show "Nothing to preview" message + if (stackedWidget) { + stackedWidget->setCurrentIndex(1); + } + } + + // Update last component name + _lastComponentName = _nameEdit->text(); +} + +void ComponentSaveWidget::onShowMore() +{ + // If collapsing, just toggle + if (_isExpanded) { + toggleExpandedState(); + return; + } + + // If expanding, fetch data first + updateTreeView(); + + // Always expand the dialog + toggleExpandedState(); +} + +void ComponentSaveWidget::setCompactMode(bool compact) +{ + if (_isCompact == compact) { + return; + } + + _isCompact = compact; + + // Show/hide Name and Location labels based on compact mode + if (_nameLabel) { + _nameLabel->setVisible(!_isCompact); + } + if (_locationLabel) { + _locationLabel->setVisible(!_isCompact); + } +} + +} // namespace UsdLayerEditor diff --git a/lib/usdLayerEditor/lib/componentSaveWidget.h b/lib/usdLayerEditor/lib/componentSaveWidget.h new file mode 100644 index 0000000000..14370eca19 --- /dev/null +++ b/lib/usdLayerEditor/lib/componentSaveWidget.h @@ -0,0 +1,125 @@ +// +// Copyright 2025 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 USDLAYEREDITOR_COMPONENTSAVEWIDGET_H +#define USDLAYEREDITOR_COMPONENTSAVEWIDGET_H + +#include "layerEditorAPI.h" + +#include +#include + +#include + +class QJsonObject; +class QKeyEvent; +class QLabel; +class QLineEdit; +class QScrollArea; +class QStackedWidget; +class QTreeWidget; +class QTreeWidgetItem; + +namespace UsdLayerEditor { + +class GeneratedIconButton; +class SessionState; + +/** + * @brief Widget for component save form with name, location, and preview tree view. + * This widget can be reused in multiple dialogs or contexts. + * + * The widget is DCC-agnostic. The DCC-specific bits (default save location, + * preview hierarchy) are routed through SessionState virtuals. + */ +class LayerEditorAPI ComponentSaveWidget : public QWidget +{ + Q_OBJECT + +public: + ComponentSaveWidget( + QWidget* in_parent = nullptr, + SessionState* in_sessionState = nullptr, + const std::string& dccObjectPath = std::string()); + ~ComponentSaveWidget() override; + + // Set the component name programmatically + void setComponentName(const QString& name); + + // Set the folder location programmatically + void setFolderLocation(const QString& location); + + // Get the component name + QString componentName() const; + + // Get the folder location + QString folderLocation() const; + + // Get the current expanded state + bool isExpanded() const { return _isExpanded; } + + // Get the original height (before expansion) + int originalHeight() const { return _originalHeight; } + + // Set the original height (used by parent dialog for sizing) + void setOriginalHeight(int height) { _originalHeight = height; } + + // Set compact representation mode (hides Name/Location labels) + void setCompactMode(bool compact); + + // Get compact representation mode + bool isCompactMode() const { return _isCompact; } + + // Get the DCC object path (proxy shape path on the Maya side) + const std::string& dccObjectPath() const { return _dccObjectPath; } + +Q_SIGNALS: + // Emitted when the widget expands or collapses + void expandedStateChanged(bool isExpanded); + +protected: + void keyPressEvent(QKeyEvent* event) override; + +private Q_SLOTS: + void onBrowseFolder(); + void onShowMore(); + +private: + void setupUI(); + void populateTreeView(const QJsonObject& jsonObj, QTreeWidgetItem* parentItem = nullptr); + void toggleExpandedState(); + void updateTreeView(); + + SessionState* _sessionState; + QLineEdit* _nameEdit; + QLineEdit* _locationEdit; + GeneratedIconButton* _browseButton; + QLabel* _showMoreLabel; + QLabel* _nameLabel; + QLabel* _locationLabel; + QScrollArea* _treeScrollArea; + QTreeWidget* _treeWidget; + QWidget* _treeContainer; + bool _isExpanded; + bool _isCompact; + int _originalHeight; + std::string _dccObjectPath; + QString _lastComponentName; +}; + +} // namespace UsdLayerEditor + +#endif // USDLAYEREDITOR_COMPONENTSAVEWIDGET_H diff --git a/lib/usdLayerEditor/lib/customLayerData.h b/lib/usdLayerEditor/lib/customLayerData.h index 8df5fe11df..1e36cf33ac 100644 --- a/lib/usdLayerEditor/lib/customLayerData.h +++ b/lib/usdLayerEditor/lib/customLayerData.h @@ -16,7 +16,7 @@ #ifndef USDLAYEREDITOR_CUSTOMLAYERDATA_H #define USDLAYEREDITOR_CUSTOMLAYERDATA_H -#include "LayerEditorAPI.h" +#include "layerEditorAPI.h" #include #include diff --git a/lib/usdLayerEditor/lib/generatedIconButton.h b/lib/usdLayerEditor/lib/generatedIconButton.h index 01af708e1b..ca9677cd81 100644 --- a/lib/usdLayerEditor/lib/generatedIconButton.h +++ b/lib/usdLayerEditor/lib/generatedIconButton.h @@ -17,6 +17,8 @@ #ifndef USDLAYEREDITOR_GENERATEDICONBUTTON_H #define USDLAYEREDITOR_GENERATEDICONBUTTON_H +#include "layerEditorAPI.h" + #include #include @@ -27,7 +29,7 @@ namespace UsdLayerEditor { * */ -class GeneratedIconButton : public QAbstractButton +class LayerEditorAPI GeneratedIconButton : public QAbstractButton { public: GeneratedIconButton(QWidget* in_parent, const QIcon& in_icon, int in_size = -1); diff --git a/lib/usdLayerEditor/lib/layerContentsWidget.cpp b/lib/usdLayerEditor/lib/layerContentsWidget.cpp new file mode 100644 index 0000000000..48041d3309 --- /dev/null +++ b/lib/usdLayerEditor/lib/layerContentsWidget.cpp @@ -0,0 +1,363 @@ +// +// Copyright 2025 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 "layerContentsWidget.h" + +#include "layerEditorDCCFunctions.h" +#include "layerTreeModel.h" +#include "stringResources.h" +#include "usdSyntaxHighlighter.h" + +#include +#include +#include +#include +#include +#include +#include + +#if PXR_VERSION < 2508 +#include "pxr/usd/sdf/textFileFormat.h" +#else +#include +#include +#endif + +#if PXR_VERSION < 2403 +#include +#else +#include +#endif + +#include +#include +#include + +namespace UsdLayerEditor { + +namespace OutputType { + +PXR_NAMESPACE_USING_DIRECTIVE + +// clang-format off + +// This code was copied from Pixar OpenUSD's sdffilter.cpp, with some modifications to fit our use case. +// I disabled the clang-format for this section to preserve the original formatting of the code, which makes +// it easier to maintain when comparing to the original source. + +// A file format for the human readable "pseudoLayer" output. We use this so +// that the terse human-readable output we produce is not a valid layer nor may +// be mistaken for one. +#if PXR_VERSION < 2508 +#define LCWBASEFILEFORMAT SdfTextFileFormat +#define LCWBASEFILEFORMATTOKENS SdfTextFileFormatTokens +#else +#define LCWBASEFILEFORMAT SdfUsdaFileFormat +#define LCWBASEFILEFORMATTOKENS SdfUsdFileFormatTokens +#endif +class SdfFilterPseudoFileFormat : public LCWBASEFILEFORMAT +{ +private: + SDF_FILE_FORMAT_FACTORY_ACCESS; + +public: + SdfFilterPseudoFileFormat(std::string description="<< human readable >>") + : LCWBASEFILEFORMAT(TfToken("pseudousda"), + TfToken(description), + LCWBASEFILEFORMATTOKENS->Target) {} +private: + + virtual ~SdfFilterPseudoFileFormat() {} +}; + +TF_REGISTRY_FUNCTION(TfType) +{ + SDF_DEFINE_FILE_FORMAT(SdfFilterPseudoFileFormat, LCWBASEFILEFORMAT); +} + +// An enum representing the type of output to produce. +enum OutputType { + OutputPseudoLayer, // report as human readable text, as close to a + // valid layer as possible + OutputLayer // produce a valid layer as output. +}; + +// We use this structure to represent all the parameters for reporting. +struct ReportParams +{ + std::shared_ptr pathMatcher; + std::shared_ptr fieldMatcher; + + // Default to the most human readable output (pseudoLayer) which is the only + // time we use ReportParams. + OutputType outputType = OutputType::OutputPseudoLayer; + + int64_t arraySizeLimit = 8; + int64_t timeSamplesSizeLimit = 8; +}; + +// Find all the paths in layer that match, or all paths if matcher is null. +std::vector +CollectMatchingSpecPaths(SdfLayerHandle const &layer, + TfPatternMatcher const *matcher) +{ + std::vector result; + layer->Traverse(SdfPath::AbsoluteRootPath(), + [&result, &matcher](SdfPath const &path) { + if (!matcher || matcher->Match(path.GetString())) + result.push_back(path); + }); + return result; +} + +// Closeness check with relative tolerance. +bool IsClose(double a, double b, double tol) +{ + double absDiff = fabs(a-b); + return absDiff <= fabs(tol*a) || absDiff < fabs(tol*b); +} + +// Get a suitable value for the report specified by p. In particular, for +// non-layer output, make a value that shows only array type & size for large +// arrays. +VtValue +GetReportValue(VtValue const &value, ReportParams const &p) +{ + if (p.outputType != OutputLayer && + p.arraySizeLimit >= 0 && + value.IsArrayValued() && + value.GetArraySize() > static_cast(p.arraySizeLimit)) { + return VtValue( + SdfHumanReadableValue( + TfStringPrintf( + "%s[%zu]", + ArchGetDemangled(value.GetElementTypeid()).c_str(), + value.GetArraySize()))); + } + return value; +} + +// Get a suitable value for timeSamples for the report specified by p. In +// particular, for non-layer output, make a value that shows number of samples +// and their time range. +VtValue +GetReportTimeSamplesValue(SdfLayerHandle const &layer, + SdfPath const &path, ReportParams const &p) +{ + auto times = layer->ListTimeSamplesForPath(path); + std::vector selectedTimes; + selectedTimes.reserve(times.size()); + + selectedTimes.assign(times.begin(), times.end()); + + if (selectedTimes.empty()) + return VtValue(); + + SdfTimeSampleMap result; + + VtValue val; + if (p.outputType != OutputLayer && + p.timeSamplesSizeLimit >= 0 && + selectedTimes.size() > static_cast(p.timeSamplesSizeLimit)) { + return VtValue( + SdfHumanReadableValue( + TfStringPrintf("%zu samples in [%s, %s]", + times.size(), + TfStringify(*times.begin()).c_str(), + TfStringify(*(--times.end())).c_str()) + ) + ); + } + else { + for (auto time: selectedTimes) { + TF_VERIFY(layer->QueryTimeSample(path, time, &val)); + result[time] = GetReportValue(val, p); + } + } + return VtValue(result); +} + +// Get a suitable value for the report specified by p. In particular, for +// non-layer output, make a value that shows only array type & size for large +// arrays or number of time samples and time range for large timeSamples. +VtValue +GetReportFieldValue(SdfLayerHandle const &layer, + SdfPath const &path, TfToken const &field, + ReportParams const &p) +{ + VtValue result; + // Handle timeSamples specially: + if (field == SdfFieldKeys->TimeSamples) { + result = GetReportTimeSamplesValue(layer, path, p); + } else { + TF_VERIFY(layer->HasField(path, field, &result)); + result = GetReportValue(result, p); + } + return result; +} + +// Utility function to filter a layer by the params p. This copies fields, +// replacing large arrays and timeSamples with human readable values if +// appropriate, and skipping paths and fields that do not match the matchers in +// p. +void +FilterLayer(SdfLayerHandle const &inLayer, + SdfLayerHandle const &outLayer, + ReportParams const &p) +{ + namespace ph = std::placeholders; + auto copyValueFn = [&p]( + SdfSpecType specType, TfToken const &field, + SdfLayerHandle const &srcLayer, const SdfPath& srcPath, bool fieldInSrc, + const SdfLayerHandle& dstLayer, const SdfPath& dstPath, bool fieldInDst, +#if PXR_VERSION >= 2403 + std::optional *valueToCopy) +#else + boost::optional *valueToCopy) +#endif + { + if (!p.fieldMatcher || p.fieldMatcher->Match(field.GetString())) { + *valueToCopy = GetReportFieldValue( + srcLayer, srcPath, field, p); + return !(*valueToCopy)->IsEmpty(); + } + else { + return false; + } + }; + + std::vector paths = + CollectMatchingSpecPaths(inLayer, p.pathMatcher.get()); + for (auto const &path: paths) { + if (path == SdfPath::AbsoluteRootPath() || + path.IsPrimPath()) { + if (path.IsPrimPath()) { + SdfPrimSpecHandle outPrim = SdfCreatePrimInLayer(outLayer, path); + } + SdfCopySpec(inLayer, path, outLayer, path, + copyValueFn, + std::bind(SdfShouldCopyChildren, + std::cref(path), std::cref(path), + ph::_1, ph::_2, ph::_3, ph::_4, ph::_5, + ph::_6, ph::_7, ph::_8, ph::_9)); + } + } +} + +// clang-format on + +} // namespace OutputType + +struct SuspendUsdNotices +{ + SuspendUsdNotices() { LayerTreeModel::suspendUsdNotices(true); } + ~SuspendUsdNotices() { LayerTreeModel::suspendUsdNotices(false); } +}; + +LayerContentsWidget::LayerContentsWidget(QWidget* in_parent) + : QWidget(in_parent) +{ + createUI(); +} + +LayerContentsWidget::~LayerContentsWidget() { } + +void LayerContentsWidget::createUI() +{ + auto mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->setSpacing(0); + + _layerContents = new QTextEdit; + _layerContents->setFont(QFont("Courier New")); + _layerContents->setAcceptRichText(true); + _layerContents->setFrameStyle(QFrame::NoFrame); + _layerContents->setPlaceholderText( + StringResources::getAsQString(StringResources::kDisplayLayerContentsEmpty)); + _layerContents->setReadOnly(true); + + // Apply USD syntax highlighting + _syntaxHighlighter = new UsdSyntaxHighlighter(_layerContents->document()); + + mainLayout->addWidget(_layerContents); + setLayout(mainLayout); +} + +void LayerContentsWidget::setLayer( + const PXR_NS::SdfLayerRefPtr in_layer, + bool expandAllValues /*= false*/) +{ + if (_layerContents) { + // Always clear the contents first as an input layer of nullptr means clear. + // Note: that will be the case when there is no layer selected, or if there is + // more than one layer selected. We only display the contents of a layer + // when there is exactly one layer selected. + _layerContents->clear(); + if (nullptr != in_layer) { + std::string layerText; + // Note: potentially slow operation for large layers. + // We could consider doing this in a worker thread if needed. + + bool layerExported { false }; + if (!expandAllValues) { + layerExported = exportPseudoLayer(in_layer, layerText); + } else { + layerExported = in_layer->ExportToString(&layerText); + } + + if (layerExported) { + _layerContents->setPlainText(QString::fromStdString(layerText)); + _isEmpty = false; + } + } + } +} + +bool LayerContentsWidget::exportPseudoLayer( + const PXR_NS::SdfLayerRefPtr in_layer, + std::string& out_contents) +{ + // Suspend all notices while we do this, since the pseudo layer is not a real layer and may have + // fields that would trigger notices that we don't want which cause the LE to rebuild. + SuspendUsdNotices suspendNotices; + + // Make the pseudo layer and copy into it, then export. + PXR_NS::SdfFileFormatConstRefPtr fmt; + OutputType::ReportParams params; + + // DCC integrations can override the array/timeSample display limits (e.g. via + // Maya optionVars); absent an override these return the ReportParams defaults. + params.arraySizeLimit = UsdLayerEditor::layerContentsArraySizeLimit(); + params.timeSamplesSizeLimit = UsdLayerEditor::layerContentsTimeSamplesSizeLimit(); + + fmt = PXR_NS::TfCreateRefPtr(new OutputType::SdfFilterPseudoFileFormat( + PXR_NS::TfStringPrintf("from @%s@", in_layer->GetIdentifier().c_str()))); + auto pseudoLayer = PXR_NS::SdfLayer::CreateAnonymous(".pseudousda", fmt); + + // Generate the layer content. + OutputType::FilterLayer(in_layer, pseudoLayer, params); + + return pseudoLayer->ExportToString(&out_contents); +} + +void LayerContentsWidget::clear() +{ + if (_layerContents) { + _layerContents->clear(); + _isEmpty = true; + } +} + +} // namespace UsdLayerEditor diff --git a/lib/usdLayerEditor/lib/layerContentsWidget.h b/lib/usdLayerEditor/lib/layerContentsWidget.h new file mode 100644 index 0000000000..56f63e191d --- /dev/null +++ b/lib/usdLayerEditor/lib/layerContentsWidget.h @@ -0,0 +1,62 @@ +// +// Copyright 2025 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 USDLAYEREDITOR_LAYERCONTENTSWIDGET_H +#define USDLAYEREDITOR_LAYERCONTENTSWIDGET_H + +// Needs to come first when used with VS2017 and Qt5. +#include "pxr/usd/sdf/layer.h" + +#include "layerEditorAPI.h" + +#include +#include + +QT_BEGIN_NAMESPACE +class QSyntaxHighlighter; +QT_END_NAMESPACE + +namespace UsdLayerEditor { + +/** + * @brief Widget used to display the contents of a layer. Owned by the LayerEditorWidget + * + */ +class LayerEditorAPI LayerContentsWidget : public QWidget +{ + Q_OBJECT + +public: + LayerContentsWidget(QWidget* in_parent); + ~LayerContentsWidget() override; + + void setLayer(const PXR_NS::SdfLayerRefPtr, bool expandAllValues = false); + + void clear(); + bool isEmpty() const { return _isEmpty; } + +private: + void createUI(); + + bool exportPseudoLayer(const PXR_NS::SdfLayerRefPtr in_layer, std::string& out_contents); + + QPointer _layerContents; + QPointer _syntaxHighlighter; + bool _isEmpty { true }; +}; + +} // namespace UsdLayerEditor + +#endif // USDLAYEREDITOR_LAYERCONTENTSWIDGET_H diff --git a/lib/usdLayerEditor/lib/layerEditorAPI.h b/lib/usdLayerEditor/lib/layerEditorAPI.h index a46f92b4dd..e02475b060 100644 --- a/lib/usdLayerEditor/lib/layerEditorAPI.h +++ b/lib/usdLayerEditor/lib/layerEditorAPI.h @@ -14,8 +14,12 @@ // limitations under the License. // -#ifdef LAYEREDITOR_EXPORTS -#define LayerEditorAPI __declspec(dllexport) +#ifdef _WIN32 +# ifdef LAYEREDITOR_EXPORTS +# define LayerEditorAPI __declspec(dllexport) +# else +# define LayerEditorAPI __declspec(dllimport) +# endif #else -#define LayerEditorAPI __declspec(dllimport) +# define LayerEditorAPI #endif \ No newline at end of file diff --git a/lib/usdLayerEditor/lib/layerEditorCommands.cpp b/lib/usdLayerEditor/lib/layerEditorCommands.cpp index 1067a1048f..a7368f8d13 100644 --- a/lib/usdLayerEditor/lib/layerEditorCommands.cpp +++ b/lib/usdLayerEditor/lib/layerEditorCommands.cpp @@ -1,901 +1,1074 @@ -// -// Copyright 2020 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 "LayerEditorCommands.h" - -#include "layerMuting.h" -#include "utilFileSystem.h" -#include "utilUI.h" - -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include -#include - -#include -#include - -#include -#include -#include -#include -#include - -PXR_NAMESPACE_USING_DIRECTIVE - -namespace UsdLayerEditor { - -void BaseCmd::holdOnPathIfDirty(const SdfLayerHandle& layer, const std::string& path) -{ - auto subLayerHandle = SdfLayer::FindRelativeToLayer(layer, path); - if (subLayerHandle != nullptr) { - if (subLayerHandle->IsDirty() || subLayerHandle->IsAnonymous()) { - _subLayersRefs.push_back(subLayerHandle); - } - holdOntoSubLayers(subLayerHandle); // we'll need to hold onto children as well - } -} - -void BaseCmd::undo() -{ - // Signal command failure by throwing - as per UFE pattern. - if (!undoIt(_layer)) { - std::string msg = commandString() + " command undo failed."; - throw std::runtime_error(msg); - }; -} - -void BaseCmd::redo() -{ - // Signal command failure by throwing - as per UFE pattern. - if (!doIt(_layer)) { - std::string msg = commandString() + " command failed."; - throw std::runtime_error(msg); - }; -} - -// hold references to any anon or dirty sublayer -void BaseCmd::holdOntoSubLayers(const SdfLayerHandle& layer) -{ - const std::vector& sublayers = layer->GetSubLayerPaths(); - for (auto path : sublayers) { - holdOnPathIfDirty(layer, path); - } -} - -// Set the edit target to Session layer if no other layers are modifiable -void BaseCmd::updateEditTarget(const PXR_NS::UsdStageWeakPtr stage) -{ - if (!stage) - return; - - if (stage->GetEditTarget().GetLayer() == stage->GetSessionLayer()) - return; - - // If there are no target-able layers, we set the target to session layer. - std::string errMsg; - if (!UsdUfe::isAnyLayerModifiable(stage, &errMsg)) { - - // TODO LE-EXTRACT Error message when no available edit target. - // MPxCommand::displayInfo(errMsg.c_str()); - stage->SetEditTarget(stage->GetSessionLayer()); - } -} - -bool BackupLayerBaseCmd::doIt(const SdfLayerHandle& layer) -{ - backupLayer(layer); - - // using reload will correctly reset the dirty bit - holdOntoSubLayers(layer); - - if (_cmdId == CmdId::kDiscardEdit) { - layer->Reload(); - } else if (_cmdId == CmdId::kClearLayer) { - layer->Clear(); - } else if (_cmdId == CmdId::kFlattenLayer) { - // Create a temp stage to get a PcpLayerStack with this layer as the root. - PXR_NS::UsdStageRefPtr tempStage = PXR_NS::UsdStage::Open(layer); - if (!tempStage) { - UIUtils::displayError("Failed to open stage for layer"); - return false; - } - - // Get the PcpLayerStackRefPtr to be used in the flatten method. - PXR_NS::PcpLayerStackRefPtr layerStack; - PXR_NS::UsdPrim rootPrim = tempStage->GetPseudoRoot(); - if (rootPrim) { - PXR_NS::PcpPrimIndex primIndex = rootPrim.GetPrimIndex(); - if (primIndex.IsValid()) { - PXR_NS::PcpNodeRef rootNode = primIndex.GetRootNode(); - if (rootNode) { - layerStack = rootNode.GetLayerStack(); - } - } - } - - if (!layerStack) { - UIUtils::displayError("Cannot flatten layer: could not determine layer stack"); - return false; - } - - PXR_NS::SdfLayerRefPtr flattenedLayer = PXR_NS::UsdFlattenLayerStack(layerStack); - if (!flattenedLayer) { - UIUtils::displayError("Failed to flatten layer stack"); - return false; - } - - layer->TransferContent(flattenedLayer); - } - - // Note: backup the edit targets after the layer is cleared because we use - // the fact that a stage edit target is now invalid to decide to backup - // that edit target. - backupEditTargets(layer); - - return true; -} - -bool BackupLayerBaseCmd::undoIt(const SdfLayerHandle& layer) -{ - restoreLayer(layer); - - // Note: restore edit targets after the layers are restored so that the backup - // edit targets are now valid. - restoreEditTargets(); - - releaseSubLayers(); - - return true; -} - -void BackupLayerBaseCmd::backupLayer(const SdfLayerHandle& layer) -{ - if (!layer) - return; - - if (layer->IsDirty() || _cmdId != CmdId::kDiscardEdit) { - _backupLayer = SdfLayer::CreateAnonymous(); - _backupLayer->TransferContent(layer); - } -} - -void BackupLayerBaseCmd::restoreLayer(const SdfLayerHandle& layer) -{ - if (!layer) - return; - - if (_backupLayer) { - layer->TransferContent(_backupLayer); - _backupLayer = nullptr; - } else { - layer->Reload(); - } -} - -void BackupLayerBaseCmd::backupEditTargets(const SdfLayerHandle& layer) -{ - _editTargetBackups.clear(); - - if (!layer) - return; - - // TODO LE-EXTRACT : Maya has multiple caches, not just the global one. - const std::vector stages = UsdUtilsStageCache::Get().GetAllStages(); - for (const PXR_NS::UsdStageRefPtr& stage : stages) { - if (!stage) - continue; - const PXR_NS::UsdEditTarget target = stage->GetEditTarget(); - // Note: this is the check that UsdStage::SetTargetLayer would do - // which is how we would detect that the edit target is now - // invalid. Unfortunately, there is no USD function to check - // if an edit target is valid outside of trying to set it as - // the edit target, but we would not want to set it. (Also, - // knowing if the stage checks that the edit target is already - // set to the same target before validating it is an implementation - // detail that we would raher not rely on.) - if (stage->HasLocalLayer(target.GetLayer())) - continue; - _editTargetBackups[stage] = target; - - // Set a valid target. The only layer we can count on is the root - // layer, so set the target to that. - stage->SetEditTarget(stage->GetRootLayer()); - } -} - -void BackupLayerBaseCmd::restoreEditTargets() -{ - for (const auto& weakStageAndTarget : _editTargetBackups) { - const PXR_NS::UsdStageRefPtr stage = weakStageAndTarget.first; - if (!stage) - continue; - - PXR_NS::UsdEditTarget target = weakStageAndTarget.second; - stage->SetEditTarget(target); - } -} - -bool LockLayerCmd::doIt(const SdfLayerHandle& layer) -{ - auto stage = getStage(); - if (!stage) - return false; - - std::set layersToUpdate; - if (_includeSublayers) { - // If _includeSublayers is True, we attempt to refresh the system lock status of all - // layers under the given layer. This is specially useful when reloading a stage. - bool includeTopLayer = true; - layersToUpdate = UsdUfe::getAllSublayerRefs(layer, includeTopLayer); - } else { - layersToUpdate.insert(layer); - } - - for (auto layerIt : layersToUpdate) { - if (isLayerLocked(layerIt)) { - _previousStates.push_back(LayerLockType::LayerLock_Locked); - } else if (isLayerSystemLocked(layerIt)) { - _previousStates.push_back(LayerLockType::LayerLock_SystemLocked); - } else { - _previousStates.push_back(LayerLockType::LayerLock_Unlocked); - } - _layers.push_back(layerIt); - } - - // Execute lock commands - for (size_t layerIndex = 0; layerIndex < _layers.size(); layerIndex++) { - auto curLayer = _layers[layerIndex]; - // Note: per design, we refuse to affect the lock status of system-locked - // sub-layers from the UI. The skip-system-locked flag is used for that. - if (_skipSystemLockedLayers) { - if (curLayer != layer) { - if (_lockType != LayerLockType::LayerLock_SystemLocked) { - if (isLayerSystemLocked(curLayer)) { - continue; - } - } - } - } - - const auto dccObjectPath = UsdUfe::stagePath(stage).string(); - lockLayer(dccObjectPath, curLayer, _lockType, true); - } - - if (_updateEditTarget) { - updateEditTarget(stage); - } - - return true; -} - -bool LockLayerCmd::undoIt(const SdfLayerHandle& layer) -{ - auto stage = getStage(); - if (!stage) - return false; - - if (_layers.size() != _previousStates.size()) { - return false; - } - - const auto dccObjectPath = UsdUfe::stagePath(stage).string(); - - // Execute lock commands - for (size_t layerIndex = 0; layerIndex < _layers.size(); layerIndex++) { - // Note: the undo of system-locked is unlocked by design. - if (_lockType == LayerLockType::LayerLock_SystemLocked) { - lockLayer(dccObjectPath, _layers[layerIndex], LayerLockType::LayerLock_Unlocked, true); - } else { - lockLayer(dccObjectPath, _layers[layerIndex], _previousStates[layerIndex], true); - } - } - - if (_updateEditTarget) { - updateEditTarget(stage); - } - - return true; -} - -UsdStageWeakPtr LockLayerCmd::getStage() { return _stage; } - -bool MuteLayerCmd::doIt(const SdfLayerHandle& layer) -{ - auto stage = getStage(); - if (!stage) - return false; - if (_muteIt) { - // Muting a layer will cause all scene items under the proxy shape - // to be stale. - saveSelection(); - stage->MuteLayer(layer->GetIdentifier()); - } else { - stage->UnmuteLayer(layer->GetIdentifier()); - restoreSelection(); - } - - // We prefer not holding to pointers needlessly, but we need to hold on - // to the muted layer. OpenUSD lets go of muted layers, so anonymous - // layers and any dirty children would be lost if not explicitly held on. - addMutedLayer(layer); - - updateEditTarget(stage); - - return true; -} - -bool MuteLayerCmd::undoIt(const SdfLayerHandle& layer) -{ - auto stage = getStage(); - if (!stage) - return false; - if (_muteIt) { - stage->UnmuteLayer(layer->GetIdentifier()); - restoreSelection(); - } else { - // Muting a layer will cause all scene items under the proxy shape - // to be stale. - saveSelection(); - stage->MuteLayer(layer->GetIdentifier()); - } - - // We can release the now unmuted layers. - removeMutedLayer(layer); - - updateEditTarget(stage); - - return true; -} - -UsdStageWeakPtr MuteLayerCmd::getStage() { return _stage; } - -void MuteLayerCmd::saveSelection() -{ - // Make a copy of the global selection, to restore it on unmute. - auto globalSn = Ufe::GlobalSelection::get(); - _savedSn.replaceWith(*globalSn); - // Filter the global selection, removing items below our DCC object. - auto path = UsdUfe::stagePath(_stage); - globalSn->replaceWith(UsdUfe::removeDescendants(_savedSn, path)); -} - -void MuteLayerCmd::restoreSelection() -{ - // Restore the saved selection to the global selection. - auto path = UsdUfe::stagePath(_stage); - auto globalSn = Ufe::GlobalSelection::get(); - globalSn->replaceWith(UsdUfe::recreateDescendants(_savedSn, path)); -} - -InsertRemoveSubPathBaseCmd::InsertRemoveSubPathBaseCmd( - CmdId id, - const pxr::UsdStageRefPtr& stage, - const pxr::SdfLayerRefPtr& layer, - const std::string& subpath, - int index) - : BaseCmd(id, layer) - , _stage(stage) - , _subPath(subpath) - , _index(index) -{ -} - -bool InsertRemoveSubPathBaseCmd::doIt(const pxr::SdfLayerHandle& layer) -{ - if (_cmdId == CmdId::kInsert || _cmdId == CmdId::kAddAnonLayer) { - if (_index == -1) { - _index = (int)layer->GetNumSubLayerPaths(); - } - if (_index != 0) { - if (!validateAndReportIndex(layer, _index, (int)layer->GetNumSubLayerPaths() + 1)) { - return false; - } - } - - // According to USD codebase, we should always call SdfLayer::InsertSubLayerPath() - // with a layer's identifier. So, if the layer exists, override _subPath with the - // identifier in case this command was called with a filesystem path. Otherwise, - // adding the layer with the filesystem path can cause issue when muting the layer - // on Windows if the path is absolute and start with a capital drive letter. - // - // Note: It's possible that SdfLayer::FindOrOpen() fail because we - // allow user to add layer that does not exists. - auto layerToAdd = SdfLayer::FindOrOpen(_subPath); - if (layerToAdd) { - _subPath = layerToAdd->GetIdentifier(); - } - - layer->InsertSubLayerPath(_subPath, _index); - TF_VERIFY( - (static_cast(_index) < layer->GetSubLayerPaths().size()) - && layer->GetSubLayerPaths()[_index] == _subPath); - } else { - TF_VERIFY(_cmdId == CmdId::kRemove); - - // If we build the remove layer command using a path - find matching sublayer index. - if (_index == -1) { - _index = layer->GetSubLayerPaths().Find(_subPath); - } - - if (!validateAndReportIndex(layer, _index, (int)layer->GetNumSubLayerPaths())) { - return false; - } - saveSelection(); - - // If we build the remove layer command using an index - find matching sublayer path. - if (_subPath.empty()) { - _subPath = layer->GetSubLayerPaths()[_index]; - } - - holdOnPathIfDirty(layer, _subPath); - - // if the current edit target is the layer to remove or - // a sublayer of the layer to remove, - // set the root layer as the current edit target - auto layerToRemove = SdfLayer::FindRelativeToLayer(layer, _subPath); - auto currentTarget = _stage->GetEditTarget().GetLayer(); - - // Helper function to find if a layer is in the - // hierarchy of another layer - // - // rootLayer: The root layer of the hierarchy - // layer: The layer to find - // ignore : Optional layer used has the root of a hierarchy that - // we don't want to check in. - // ignoreSubPath : Optional subpath used whith ignore layer. - auto isInHierarchy = [](const SdfLayerHandle& rootLayer, - const SdfLayerHandle& layer, - const SdfLayerHandle* ignore = nullptr, - const std::string* ignoreSubPath = nullptr) { - // Impl used for recursive call - auto isInHierarchyImpl = [](const SdfLayerHandle& rootLayer, - const SdfLayerHandle& layer, - const SdfLayerHandle* ignore, - const std::string* ignoreSubPath, - auto& implRef) { - if (!rootLayer || !layer) - return false; - - if (rootLayer->GetIdentifier() == layer->GetIdentifier()) - return true; - - const auto subLayerPaths = rootLayer->GetSubLayerPaths(); - for (const auto& subLayerPath : subLayerPaths) { - - if (ignore && ignoreSubPath - && (*ignore)->GetIdentifier() == rootLayer->GetIdentifier() - && *ignoreSubPath == subLayerPath) - continue; - - const auto subLayer = SdfLayer::FindRelativeToLayer(rootLayer, subLayerPath); - if (implRef(subLayer, layer, ignore, ignoreSubPath, implRef)) - return true; - } - return false; - }; - return isInHierarchyImpl(rootLayer, layer, ignore, ignoreSubPath, isInHierarchyImpl); - }; - - if (isInHierarchy(layerToRemove, currentTarget)) { - // The current edit layer is in the hierarchy of the layer to remove, - // now we need to be sure the edit target layer is not also a sublayer - // of another layer in the stage. - if (!isInHierarchy(_stage->GetRootLayer(), currentTarget, &layer, &_subPath)) { - _editTargetPath = currentTarget->GetIdentifier(); - _stage->SetEditTarget(_stage->GetRootLayer()); - } - } - - layer->RemoveSubLayerPath(_index); - } - return true; -} - -bool InsertRemoveSubPathBaseCmd::undoIt(const pxr::SdfLayerHandle& layer) -{ - if (_cmdId == CmdId::kInsert || _cmdId == CmdId::kAddAnonLayer) { - auto index = _index; - if (index == -1) { - index = static_cast(layer->GetNumSubLayerPaths() - 1); - } - if (validateUndoIndex(layer, _index)) { - TF_VERIFY(layer->GetSubLayerPaths()[index] == _subPath); - layer->RemoveSubLayerPath(index); - } else { - return false; - } - } else { - TF_VERIFY(_index != -1); - if (validateUndoIndex(layer, _index)) { - layer->InsertSubLayerPath(_subPath, _index); - - // if the removed layer was the edit target, - // set it back to the current edit target - if (!_editTargetPath.empty()) { - auto subLayerHandle = SdfLayer::FindRelativeToLayer(layer, _editTargetPath); - _stage->SetEditTarget(subLayerHandle); - } - } else { - return false; - } - restoreSelection(); - } - return true; -} - -bool InsertRemoveSubPathBaseCmd::validateUndoIndex(const pxr::SdfLayerHandle& layer, int index) -{ // allow re-inserting at the last index + 1, but -1 should have been changed to 0 - return !(index < 0 || index > (int)layer->GetNumSubLayerPaths()); -} - -bool InsertRemoveSubPathBaseCmd::validateAndReportIndex( - const pxr::SdfLayerHandle& layer, - int index, - int maxIndex) -{ - if (index < 0 || index >= maxIndex) { - std::string message = std::string("Index ") + std::to_string(index) - + std::string(" out-of-bound for ") + layer->GetIdentifier(); - UIUtils::displayError(message.c_str()); - return false; - } else { - return true; - } -} - -void InsertRemoveSubPathBaseCmd::saveSelection() -{ - // Make a copy of the global selection, to restore it on unlock. - auto globalSn = Ufe::GlobalSelection::get(); - _savedSn.replaceWith(*globalSn); - // Filter the global selection, removing items below our DCC object. - auto path = UsdUfe::stagePath(_stage); - globalSn->replaceWith(UsdUfe::removeDescendants(_savedSn, path)); -} - -void InsertRemoveSubPathBaseCmd::restoreSelection() -{ - // Restore the saved selection to the global selection. If a saved - // selection item started with the proxy shape path, re-create it. - auto globalSn = Ufe::GlobalSelection::get(); - auto path = UsdUfe::stagePath(_stage); - globalSn->replaceWith(UsdUfe::recreateDescendants(_savedSn, path)); -} - -bool RefreshSystemLockLayerCmd::doIt(const pxr::SdfLayerHandle& layer) -{ - if (!_stage) { - return false; - } - - if (_refreshSubLayers) { - // If refreshSubLayers is True, we attempt to refresh the system lock status of all - // layers under the given layer. This is specially useful when reloading a stage. - bool includeTopLayer = true; - auto allLayers = UsdUfe::getAllSublayerRefs(layer, includeTopLayer); - for (auto layerIt : allLayers) { - _refreshLayerSystemLock(layerIt); - } - } else { - // Only check and refresh the system lock status of the current layer. - _refreshLayerSystemLock(layer); - } - - // Execute lock commands - for (size_t layerIndex = 0; layerIndex < _layers.size(); layerIndex++) { - if (!_lockCommands[layerIndex]->doIt(_layers[layerIndex])) { - return false; - } - } - - if (!_layers.empty()) { - _notifySystemLockIsRefreshed(); - - // Finally update edit target after layer locks were changed - // by the command or a callback. - updateEditTarget(_stage); - } - - return true; -} - -bool RefreshSystemLockLayerCmd::undoIt(const pxr::SdfLayerHandle& layer) -{ - if (!_stage) { - return false; - } - - // Execute lock commands - for (size_t layerIndex = 0; layerIndex < _layers.size(); layerIndex++) { - if (!_lockCommands[layerIndex]->undoIt(_layers[layerIndex])) { - return false; - } - } - - if (!_layers.empty()) { - _notifySystemLockIsRefreshed(); - - // Finally update edit target after layer locks were changed - // by the command or a callback. - updateEditTarget(_stage); - } - - return true; -} - -std::string RefreshSystemLockLayerCmd::_quote(const std::string& string) -{ - return std::string(" \"") + string + std::string("\""); -} - -void RefreshSystemLockLayerCmd::_refreshLayerSystemLock(const pxr::SdfLayerHandle& usdLayer) -{ - // Anonymous layers do not need to be checked. - if (usdLayer && !usdLayer->IsAnonymous()) { - // Check if the layer's write permissions have changed. - std::string assetPath = usdLayer->GetResolvedPath(); - std::replace(assetPath.begin(), assetPath.end(), '\\', '/'); - - if (!assetPath.empty()) { - - auto writeAccess = UsdLayerEditor::FileSystem::checkWriteAccess(assetPath); - - if (writeAccess && isLayerSystemLocked(usdLayer)) { - // If the file has write permissions and the layer is currently - // system-locked: Unlock the layer - - // Create the lock command - auto cmd - = std::make_shared(_stage, usdLayer, LayerLock_Unlocked, false); - // Edit target will be updated once at the end of the refresh command. - cmd->SetUpdateEditTarget(false); - - // Add the lock command and its parameter to be executed - _lockCommands.push_back(std::move(cmd)); - _layers.push_back(usdLayer); - } else if (!writeAccess && !isLayerSystemLocked(usdLayer)) { - // If the file doesn't have write permissions and the layer is currently not - // system-locked: System-lock the layer - - // Create the lock command - auto cmd = std::make_shared( - _stage, usdLayer, LayerLock_SystemLocked, false); - // Edit target will be updated once at the end of the refresh command. - cmd->SetUpdateEditTarget(false); - - // Add the lock command and its parameter to be executed - _lockCommands.push_back(std::move(cmd)); - _layers.push_back(usdLayer); - } - } - } -} - -void RefreshSystemLockLayerCmd::_notifySystemLockIsRefreshed() -{ - if (!UsdUfe::isUICallbackRegistered(TfToken("onRefreshSystemLock"))) { - return; - } - - PXR_NS::VtDictionary callbackContext; - callbackContext["objectPath"] = PXR_NS::VtValue(UsdUfe::stagePath(_stage).string().c_str()); - PXR_NS::VtDictionary callbackData; - - std::vector affectedLayers; - affectedLayers.reserve(_layers.size()); - for (size_t layerIndex = 0; layerIndex < _layers.size(); layerIndex++) { - affectedLayers.push_back(_layers[layerIndex]->GetIdentifier()); - } - - VtStringArray lockedArray(affectedLayers.begin(), affectedLayers.end()); - callbackData["affectedLayerIds"] = lockedArray; - - UsdUfe::triggerUICallback(TfToken("onRefreshSystemLock"), callbackContext, callbackData); -} - -bool StitchLayersCmd::doIt(const SdfLayerHandle& /*layer*/) -{ - if (_layerIdentifiersByStrength.empty()) - return true; - - if (!_stage) { - TF_RUNTIME_ERROR("Cannot stitch layers: no valid stage"); - return false; - } - - const SdfLayerHandleVector stageLayers = _stage->GetLayerStack(); - - // Sort the selected layers by their strength (strongest first). - std::unordered_map layerStrengthMap; - layerStrengthMap.reserve(stageLayers.size()); - for (size_t i = 0; i < stageLayers.size(); ++i) - layerStrengthMap[stageLayers[i]->GetIdentifier()] = i; - - std::sort( - _layerIdentifiersByStrength.begin(), - _layerIdentifiersByStrength.end(), - [&layerStrengthMap](const std::string& a, const std::string& b) { - const auto itA = layerStrengthMap.find(a); - const auto itB = layerStrengthMap.find(b); - if (itA == layerStrengthMap.end()) { - TF_WARN("Layer '%s' not found in stage layer stack", a.c_str()); - return false; - } - if (itB == layerStrengthMap.end()) { - TF_WARN("Layer '%s' not found in stage layer stack", b.c_str()); - return true; - } - return itA->second < itB->second; - }); - - SdfLayerHandleVector layersByStrength; - layersByStrength.reserve(_layerIdentifiersByStrength.size()); - for (const auto& layerIdentifier : _layerIdentifiersByStrength) { - auto foundLayer = SdfLayer::FindOrOpen(layerIdentifier); - if (!foundLayer) { - TF_RUNTIME_ERROR("Cannot find layer: %s", layerIdentifier.c_str()); - return false; - } - layersByStrength.push_back(foundLayer); - } - - const SdfLayerHandle strongestLayer = layersByStrength[0]; - - holdOntoSubLayers(strongestLayer); - - // Keep a hold of references for all selected layers, needed for undo(). - for (size_t i = 1; i < layersByStrength.size(); ++i) - _subLayersRefs.push_back(layersByStrength[i]); - - std::map> parentInfoByLayer; - for (const auto& potentialParent : stageLayers) { - const std::vector& subLayerPaths = potentialParent->GetSubLayerPaths(); - for (size_t i = 0; i < subLayerPaths.size(); ++i) { - auto subLayer = SdfLayer::FindRelativeToLayer(potentialParent, subLayerPaths[i]); - if (subLayer) { - parentInfoByLayer[subLayer->GetIdentifier()] - = std::make_pair(potentialParent, subLayerPaths[i]); - } - } - } - - // Multiple selected weak layers may share the same parent, so batch removals by parent - // to avoid calling SetSubLayerPaths more than once per parent layer. - std::map> removalsByParent; - for (size_t i = 1; i < layersByStrength.size(); ++i) { - const SdfLayerHandle& weakLayer = layersByStrength[i]; - const std::string weakLayerId = weakLayer->GetIdentifier(); - - const auto& it = parentInfoByLayer.find(weakLayerId); - if (it != parentInfoByLayer.end()) { - removalsByParent[it->second.first->GetIdentifier()].push_back(it->second.second); - } else { - TF_WARN("Could not find parent for layer: %s", weakLayerId.c_str()); - } - } - - UsdUfe::UsdUndoManager::instance().trackLayerStates(strongestLayer); - for (size_t i = 1; i < layersByStrength.size(); ++i) - UsdUfe::UsdUndoManager::instance().trackLayerStates(layersByStrength[i]); - for (const auto& entry : removalsByParent) { - auto parentLayer = SdfLayer::Find(entry.first); - if (parentLayer) { - UsdUfe::UsdUndoManager::instance().trackLayerStates(parentLayer); - } - } - - { - UsdUfe::UsdUndoBlock undoBlock(&_undoItem); - - std::vector> movedSubLayers; - movedSubLayers.reserve(layersByStrength.size() - 1); - for (size_t i = 1; i < layersByStrength.size(); ++i) { - const SdfLayerHandle& weakLayer = layersByStrength[i]; - movedSubLayers.push_back(weakLayer->GetSubLayerPaths()); - UsdUtilsStitchLayers(strongestLayer, weakLayer); - } - - // Add all collected subLayers to the strongest layer, preventing duplicates. - // Non-selected weak subLayers are added to the end of the subPathList of the - // strongestLayer. Note: This means there are cases where relative strength is not fully - // preserved. - auto strongLayerSubLayers = strongestLayer->GetSubLayerPaths(); - - // Creates a set of the added subLayers, prevent duplicates. - std::set addedSublayerIds; - for (const auto path : strongLayerSubLayers) { - const auto existingLayer = SdfLayer::FindRelativeToLayer(strongestLayer, path); - if (existingLayer) { - addedSublayerIds.insert(existingLayer->GetIdentifier()); - } - } - - // Adds any moved sub layers to the strong layers sub layers to prevent layers from - // being lost when the weak layer is deleted. - for (const auto& subLayerList : movedSubLayers) { - for (const auto& subLayerPath : subLayerList) { - const auto subLayer - = SdfLayer::FindRelativeToLayer(strongestLayer, subLayerPath); - if (subLayer - && addedSublayerIds.find(subLayer->GetIdentifier()) - == addedSublayerIds.end()) { - strongLayerSubLayers.push_back(subLayerPath); - addedSublayerIds.insert(subLayer->GetIdentifier()); - } - } - } - - // Remove any merged weak layers from the sublayer list before setting, to prevent - // them from being both stitched (merged) and referenced as subLayers. - for (size_t i = 1; i < layersByStrength.size(); ++i) { - const std::string weakLayerId = layersByStrength[i]->GetIdentifier(); - const auto it = std::find( - strongLayerSubLayers.begin(), strongLayerSubLayers.end(), weakLayerId); - if (it != strongLayerSubLayers.end()) - strongLayerSubLayers.erase(it); - } - - strongestLayer->SetSubLayerPaths(strongLayerSubLayers); - - // Removes the selected weak layers from their parents. - for (auto& entry : removalsByParent) { - const auto parentLayer = SdfLayer::Find(entry.first); - if (parentLayer) { - auto subLayerPaths = parentLayer->GetSubLayerPaths(); - for (const auto& pathToRemove : entry.second) { - auto it - = std::find(subLayerPaths.begin(), subLayerPaths.end(), pathToRemove); - if (it != subLayerPaths.end()) - subLayerPaths.erase(it); - } - if (parentLayer && parentLayer->PermissionToEdit()) { - parentLayer->SetSubLayerPaths(subLayerPaths); - } else { - TF_WARN( - "Cannot update layer '%s' because it is locked.", - strongestLayer->GetIdentifier().c_str()); - } - } - } - } - - backupEditTargets(strongestLayer); - - return true; -} - -bool StitchLayersCmd::undoIt(const SdfLayerHandle& /*layer*/) -{ - _undoItem.undo(); - - restoreEditTargets(); - releaseSubLayers(); - - return true; -} - +// +// Copyright 2020 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 "LayerEditorCommands.h" + +#include "layerEditorDCCFunctions.h" +#include "layerLocking.h" +#include "layerMuting.h" +#include "utilFileSystem.h" +#include "utilUI.h" + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include +#include +#include +#include + +PXR_NAMESPACE_USING_DIRECTIVE + +namespace UsdLayerEditor { + +void BaseCmd::holdOnPathIfDirty(const SdfLayerHandle& layer, const std::string& path) +{ + auto subLayerHandle = SdfLayer::FindRelativeToLayer(layer, path); + if (subLayerHandle != nullptr) { + if (subLayerHandle->IsDirty() || subLayerHandle->IsAnonymous()) { + _subLayersRefs.push_back(subLayerHandle); + } + holdOntoSubLayers(subLayerHandle); // we'll need to hold onto children as well + } +} + +void BaseCmd::undo() +{ + // Signal command failure by throwing - as per UFE pattern. + if (!undoIt(_layer)) { + std::string msg = commandString() + " command undo failed."; + throw std::runtime_error(msg); + }; +} + +void BaseCmd::redo() +{ + // Signal command failure by throwing - as per UFE pattern. + if (!doIt(_layer)) { + std::string msg = commandString() + " command failed."; + throw std::runtime_error(msg); + }; +} + +// hold references to any anon or dirty sublayer +void BaseCmd::holdOntoSubLayers(const SdfLayerHandle& layer) +{ + const std::vector& sublayers = layer->GetSubLayerPaths(); + for (auto path : sublayers) { + holdOnPathIfDirty(layer, path); + } +} + +// Set the edit target to Session layer if no other layers are modifiable +void BaseCmd::updateEditTarget(const PXR_NS::UsdStageWeakPtr stage) +{ + if (!stage) + return; + + // Edit-forwarding integrations manage their own edit target: when forwarding is + // active they keep the stage edit target on the session layer and redirect the + // (possibly locked) fallback target. In that case skip the normal auto-targeting. + if (handleEFEditTargetUpdate(PXR_NS::UsdStageRefPtr(stage))) + return; + + if (stage->GetEditTarget().GetLayer() == stage->GetSessionLayer()) + return; + + // If the currently targeted layer is still editable (neither user-locked nor + // system-locked), we don't need to change the edit target. + if (stage->GetEditTarget().GetLayer()->PermissionToEdit()) + return; + + // If there are no target-able layers, we set the target to session layer. + std::string errMsg; + if (!UsdUfe::isAnyLayerModifiable(stage, &errMsg)) { + stage->SetEditTarget(stage->GetSessionLayer()); + UIUtils::displayError(errMsg); + } +} + +bool BackupLayerBaseCmd::doIt(const SdfLayerHandle& layer) +{ + backupLayer(layer); + + // using reload will correctly reset the dirty bit + holdOntoSubLayers(layer); + + if (_cmdId == CmdId::kDiscardEdit) { + layer->Reload(); + } else if (_cmdId == CmdId::kClearLayer) { + layer->Clear(); + } else if (_cmdId == CmdId::kFlattenLayer) { + // Create a temp stage to get a PcpLayerStack with this layer as the root. + PXR_NS::UsdStageRefPtr tempStage = PXR_NS::UsdStage::Open(layer); + if (!tempStage) { + UIUtils::displayError("Failed to open stage for layer"); + return false; + } + + // Get the PcpLayerStackRefPtr to be used in the flatten method. + PXR_NS::PcpLayerStackRefPtr layerStack; + PXR_NS::UsdPrim rootPrim = tempStage->GetPseudoRoot(); + if (rootPrim) { + PXR_NS::PcpPrimIndex primIndex = rootPrim.GetPrimIndex(); + if (primIndex.IsValid()) { + PXR_NS::PcpNodeRef rootNode = primIndex.GetRootNode(); + if (rootNode) { + layerStack = rootNode.GetLayerStack(); + } + } + } + + if (!layerStack) { + UIUtils::displayError("Cannot flatten layer: could not determine layer stack"); + return false; + } + + PXR_NS::SdfLayerRefPtr flattenedLayer = PXR_NS::UsdFlattenLayerStack(layerStack); + if (!flattenedLayer) { + UIUtils::displayError("Failed to flatten layer stack"); + return false; + } + + layer->TransferContent(flattenedLayer); + } + + // Note: backup the edit targets after the layer is cleared because we use + // the fact that a stage edit target is now invalid to decide to backup + // that edit target. + backupEditTargets(layer); + + return true; +} + +bool BackupLayerBaseCmd::undoIt(const SdfLayerHandle& layer) +{ + restoreLayer(layer); + + // Note: restore edit targets after the layers are restored so that the backup + // edit targets are now valid. + restoreEditTargets(); + + releaseSubLayers(); + + return true; +} + +void BackupLayerBaseCmd::backupLayer(const SdfLayerHandle& layer) +{ + if (!layer) + return; + + if (layer->IsDirty() || _cmdId != CmdId::kDiscardEdit) { + _backupLayer = SdfLayer::CreateAnonymous(); + _backupLayer->TransferContent(layer); + } +} + +void BackupLayerBaseCmd::restoreLayer(const SdfLayerHandle& layer) +{ + if (!layer) + return; + + if (_backupLayer) { + layer->TransferContent(_backupLayer); + _backupLayer = nullptr; + } else { + layer->Reload(); + } +} + +void BackupLayerBaseCmd::backupEditTargets(const SdfLayerHandle& layer) +{ + _editTargetBackups.clear(); + + if (!layer) + return; + + const std::vector stages = UsdLayerEditor::getAllStages(); + + for (const PXR_NS::UsdStageRefPtr& stage : stages) { + if (!stage) + continue; + const PXR_NS::UsdEditTarget target = stage->GetEditTarget(); + // Note: this is the check that UsdStage::SetTargetLayer would do + // which is how we would detect that the edit target is now + // invalid. Unfortunately, there is no USD function to check + // if an edit target is valid outside of trying to set it as + // the edit target, but we would not want to set it. (Also, + // knowing if the stage checks that the edit target is already + // set to the same target before validating it is an implementation + // detail that we would raher not rely on.) + if (stage->HasLocalLayer(target.GetLayer())) + continue; + _editTargetBackups[stage] = target; + + // Set a valid target. The only layer we can count on is the root + // layer, so set the target to that. + stage->SetEditTarget(stage->GetRootLayer()); + } +} + +void BackupLayerBaseCmd::restoreEditTargets() +{ + for (const auto& weakStageAndTarget : _editTargetBackups) { + const PXR_NS::UsdStageRefPtr stage = weakStageAndTarget.first; + if (!stage) + continue; + + PXR_NS::UsdEditTarget target = weakStageAndTarget.second; + stage->SetEditTarget(target); + } +} + +bool LockLayerCmd::doIt(const SdfLayerHandle& layer) +{ + auto stage = getStage(); + if (!stage) + return false; + + std::set layersToUpdate; + if (_includeSublayers) { + // If _includeSublayers is True, we attempt to refresh the system lock status of all + // layers under the given layer. This is specially useful when reloading a stage. + bool includeTopLayer = true; + layersToUpdate = UsdUfe::getAllSublayerRefs(layer, includeTopLayer); + } else { + layersToUpdate.insert(layer); + } + + for (auto layerIt : layersToUpdate) { + if (isLayerLocked(layerIt)) { + _previousStates.push_back(LayerLockType::LayerLock_Locked); + } else if (isLayerSystemLocked(layerIt)) { + _previousStates.push_back(LayerLockType::LayerLock_SystemLocked); + } else { + _previousStates.push_back(LayerLockType::LayerLock_Unlocked); + } + _layers.push_back(layerIt); + } + + // Execute lock commands + for (size_t layerIndex = 0; layerIndex < _layers.size(); layerIndex++) { + auto curLayer = _layers[layerIndex]; + // Note: per design, we refuse to affect the lock status of system-locked + // sub-layers from the UI. The skip-system-locked flag is used for that. + if (_skipSystemLockedLayers) { + if (curLayer != layer) { + if (_lockType != LayerLockType::LayerLock_SystemLocked) { + if (isLayerSystemLocked(curLayer)) { + continue; + } + } + } + } + + const auto dccObjectPath = UsdUfe::stagePath(stage).string(); + lockLayer(dccObjectPath, curLayer, _lockType, true); + } + + if (_updateEditTarget) { + updateEditTarget(stage); + } + + return true; +} + +bool LockLayerCmd::undoIt(const SdfLayerHandle& layer) +{ + auto stage = getStage(); + if (!stage) + return false; + + if (_layers.size() != _previousStates.size()) { + return false; + } + + const auto dccObjectPath = UsdUfe::stagePath(stage).string(); + + // Execute lock commands + for (size_t layerIndex = 0; layerIndex < _layers.size(); layerIndex++) { + // Note: the undo of system-locked is unlocked by design. + if (_lockType == LayerLockType::LayerLock_SystemLocked) { + lockLayer(dccObjectPath, _layers[layerIndex], LayerLockType::LayerLock_Unlocked, true); + } else { + lockLayer(dccObjectPath, _layers[layerIndex], _previousStates[layerIndex], true); + } + } + + if (_updateEditTarget) { + updateEditTarget(stage); + } + + return true; +} + +UsdStageWeakPtr LockLayerCmd::getStage() { return _stage; } + +bool MuteLayerCmd::doIt(const SdfLayerHandle& layer) +{ + auto stage = getStage(); + if (!stage) + return false; + if (_muteIt) { + // We prefer not holding to pointers needlessly, but we need to hold on + // to the muted layer. OpenUSD lets go of muted layers, so anonymous + // layers and any dirty children would be lost if not explicitly held on. + // This is done before really muting the layer to ensure no sublayer is + // gone after the mute change. + addMutedLayer(layer); + + // Muting a layer will cause all scene items under the proxy shape + // to be stale. + saveSelection(); + stage->MuteLayer(layer->GetIdentifier()); + } else { + stage->UnmuteLayer(layer->GetIdentifier()); + + // We can release the now unmuted layer. + removeMutedLayer(layer); + + restoreSelection(); + } + + updateEditTarget(stage); + + return true; +} + +bool MuteLayerCmd::undoIt(const SdfLayerHandle& layer) +{ + auto stage = getStage(); + if (!stage) + return false; + if (_muteIt) { + stage->UnmuteLayer(layer->GetIdentifier()); + + // We can release the now unmuted layer. + removeMutedLayer(layer); + + restoreSelection(); + } else { + // Hold the layer before re-muting it, mirroring doIt (so no sublayer is + // gone after the mute change). + addMutedLayer(layer); + + // Muting a layer will cause all scene items under the proxy shape + // to be stale. + saveSelection(); + stage->MuteLayer(layer->GetIdentifier()); + } + + updateEditTarget(stage); + + return true; +} + +UsdStageWeakPtr MuteLayerCmd::getStage() { return _stage; } + +void MuteLayerCmd::saveSelection() +{ + // Make a copy of the global selection, to restore it on unmute. + auto globalSn = Ufe::GlobalSelection::get(); + _savedSn.replaceWith(*globalSn); + // Filter the global selection, removing items below our DCC object. + auto path = UsdUfe::stagePath(_stage); + globalSn->replaceWith(UsdUfe::removeDescendants(_savedSn, path)); +} + +void MuteLayerCmd::restoreSelection() +{ + // Restore the saved selection to the global selection. + auto path = UsdUfe::stagePath(_stage); + auto globalSn = Ufe::GlobalSelection::get(); + globalSn->replaceWith(UsdUfe::recreateDescendants(_savedSn, path)); +} + +InsertRemoveSubPathBaseCmd::InsertRemoveSubPathBaseCmd( + CmdId id, + const pxr::UsdStageRefPtr& stage, + const pxr::SdfLayerRefPtr& layer, + const std::string& subpath, + int index) + : BaseCmd(id, layer) + , _stage(stage) + , _subPath(subpath) + , _index(index) +{ +} + +bool InsertRemoveSubPathBaseCmd::doIt(const pxr::SdfLayerHandle& layer) +{ + if (_cmdId == CmdId::kInsert || _cmdId == CmdId::kAddAnonLayer) { + if (_index == -1) { + _index = (int)layer->GetNumSubLayerPaths(); + } + if (_index != 0) { + if (!validateAndReportIndex(layer, _index, (int)layer->GetNumSubLayerPaths() + 1)) { + return false; + } + } + + // According to USD codebase, we should always call SdfLayer::InsertSubLayerPath() + // with a layer's identifier. So, if the layer exists, override _subPath with the + // identifier in case this command was called with a filesystem path. Otherwise, + // adding the layer with the filesystem path can cause issue when muting the layer + // on Windows if the path is absolute and start with a capital drive letter. + // + // Note: It's possible that SdfLayer::FindOrOpen() fail because we + // allow user to add layer that does not exists. + auto layerToAdd = SdfLayer::FindOrOpen(_subPath); + if (layerToAdd) { + _subPath = layerToAdd->GetIdentifier(); + } + + layer->InsertSubLayerPath(_subPath, _index); + TF_VERIFY( + (static_cast(_index) < layer->GetSubLayerPaths().size()) + && layer->GetSubLayerPaths()[_index] == _subPath); + } else { + TF_VERIFY(_cmdId == CmdId::kRemove); + + // If we build the remove layer command using a path - find matching sublayer index. + if (_index == -1) { + _index = layer->GetSubLayerPaths().Find(_subPath); + } + + if (!validateAndReportIndex(layer, _index, (int)layer->GetNumSubLayerPaths())) { + return false; + } + saveSelection(); + + // If we build the remove layer command using an index - find matching sublayer path. + if (_subPath.empty()) { + _subPath = layer->GetSubLayerPaths()[_index]; + } + + holdOnPathIfDirty(layer, _subPath); + + // if the current edit target is the layer to remove or + // a sublayer of the layer to remove, + // set the root layer as the current edit target + auto layerToRemove = SdfLayer::FindRelativeToLayer(layer, _subPath); + auto currentTarget = _stage->GetEditTarget().GetLayer(); + + // Helper function to find if a layer is in the + // hierarchy of another layer + // + // rootLayer: The root layer of the hierarchy + // layer: The layer to find + // ignore : Optional layer used has the root of a hierarchy that + // we don't want to check in. + // ignoreSubPath : Optional subpath used whith ignore layer. + auto isInHierarchy = [](const SdfLayerHandle& rootLayer, + const SdfLayerHandle& layer, + const SdfLayerHandle* ignore = nullptr, + const std::string* ignoreSubPath = nullptr) { + // Impl used for recursive call + auto isInHierarchyImpl = [](const SdfLayerHandle& rootLayer, + const SdfLayerHandle& layer, + const SdfLayerHandle* ignore, + const std::string* ignoreSubPath, + auto& implRef) { + if (!rootLayer || !layer) + return false; + + if (rootLayer->GetIdentifier() == layer->GetIdentifier()) + return true; + + const auto subLayerPaths = rootLayer->GetSubLayerPaths(); + for (const auto& subLayerPath : subLayerPaths) { + + if (ignore && ignoreSubPath + && (*ignore)->GetIdentifier() == rootLayer->GetIdentifier() + && *ignoreSubPath == subLayerPath) + continue; + + const auto subLayer = SdfLayer::FindRelativeToLayer(rootLayer, subLayerPath); + if (implRef(subLayer, layer, ignore, ignoreSubPath, implRef)) + return true; + } + return false; + }; + return isInHierarchyImpl(rootLayer, layer, ignore, ignoreSubPath, isInHierarchyImpl); + }; + + if (isInHierarchy(layerToRemove, currentTarget)) { + // The current edit layer is in the hierarchy of the layer to remove, + // now we need to be sure the edit target layer is not also a sublayer + // of another layer in the stage. + if (!isInHierarchy(_stage->GetRootLayer(), currentTarget, &layer, &_subPath)) { + _editTargetPath = currentTarget->GetIdentifier(); + _stage->SetEditTarget(_stage->GetRootLayer()); + } + } + + layer->RemoveSubLayerPath(_index); + } + return true; +} + +bool InsertRemoveSubPathBaseCmd::undoIt(const pxr::SdfLayerHandle& layer) +{ + if (_cmdId == CmdId::kInsert || _cmdId == CmdId::kAddAnonLayer) { + auto index = _index; + if (index == -1) { + index = static_cast(layer->GetNumSubLayerPaths() - 1); + } + if (validateUndoIndex(layer, _index)) { + TF_VERIFY(layer->GetSubLayerPaths()[index] == _subPath); + layer->RemoveSubLayerPath(index); + } else { + return false; + } + } else { + TF_VERIFY(_index != -1); + if (validateUndoIndex(layer, _index)) { + layer->InsertSubLayerPath(_subPath, _index); + + // if the removed layer was the edit target, + // set it back to the current edit target + if (!_editTargetPath.empty()) { + auto subLayerHandle = SdfLayer::FindRelativeToLayer(layer, _editTargetPath); + _stage->SetEditTarget(subLayerHandle); + } + } else { + return false; + } + restoreSelection(); + } + return true; +} + +bool InsertRemoveSubPathBaseCmd::validateUndoIndex(const pxr::SdfLayerHandle& layer, int index) +{ // allow re-inserting at the last index + 1, but -1 should have been changed to 0 + return !(index < 0 || index > (int)layer->GetNumSubLayerPaths()); +} + +bool InsertRemoveSubPathBaseCmd::validateAndReportIndex( + const pxr::SdfLayerHandle& layer, + int index, + int maxIndex) +{ + if (index < 0 || index >= maxIndex) { + std::string message = std::string("Index ") + std::to_string(index) + + std::string(" out-of-bound for ") + layer->GetIdentifier(); + UIUtils::displayError(message.c_str()); + return false; + } else { + return true; + } +} + +void InsertRemoveSubPathBaseCmd::saveSelection() +{ + // Make a copy of the global selection, to restore it on unlock. + auto globalSn = Ufe::GlobalSelection::get(); + _savedSn.replaceWith(*globalSn); + // Filter the global selection, removing items below our DCC object. + auto path = UsdUfe::stagePath(_stage); + globalSn->replaceWith(UsdUfe::removeDescendants(_savedSn, path)); +} + +void InsertRemoveSubPathBaseCmd::restoreSelection() +{ + // Restore the saved selection to the global selection. If a saved + // selection item started with the proxy shape path, re-create it. + auto globalSn = Ufe::GlobalSelection::get(); + auto path = UsdUfe::stagePath(_stage); + globalSn->replaceWith(UsdUfe::recreateDescendants(_savedSn, path)); +} + +bool ReplaceSubPathCmd::doIt(const SdfLayerHandle& layer) +{ + auto proxy = layer->GetSubLayerPaths(); + if (proxy.Find(_oldPath) == static_cast(-1)) { + std::string message = std::string("path ") + _oldPath + + std::string(" not found on layer ") + layer->GetIdentifier(); + UIUtils::displayError(message.c_str()); + return false; + } + holdOnPathIfDirty(layer, _oldPath); + proxy.Replace(_oldPath, _newPath); + return true; +} + +bool ReplaceSubPathCmd::undoIt(const SdfLayerHandle& layer) +{ + auto proxy = layer->GetSubLayerPaths(); + proxy.Replace(_newPath, _oldPath); + releaseSubLayers(); + holdOnPathIfDirty(layer, _newPath); + return true; +} + +bool MoveSubPathCmd::doIt(const pxr::SdfLayerHandle& layer) +{ + auto proxy = layer->GetSubLayerPaths(); + auto subPathIndex = proxy.Find(_subPath); + if (subPathIndex == size_t(-1)) { + TF_RUNTIME_ERROR( + "path %s not found on layer %s", + _subPath.c_str(), + layer->GetIdentifier().c_str()); + return false; + } + _oldIndex = static_cast(subPathIndex); + + std::string newPath = _subPath; + + if (layer->GetIdentifier() == _newParent->GetIdentifier()) { + // Same-parent reorder: bounds-check against current count (before removal) + if (_newIndex > static_cast(layer->GetNumSubLayerPaths()) - 1) { + TF_RUNTIME_ERROR( + "Index %d out-of-bound for %s", + _newIndex, + layer->GetIdentifier().c_str()); + return false; + } + } else { + // Cross-parent move: append is allowed, so bound is GetNumSubLayerPaths() + if (_newIndex > static_cast(_newParent->GetNumSubLayerPaths())) { + TF_RUNTIME_ERROR( + "Index %d out-of-bound for %s", + _newIndex, + _newParent->GetIdentifier().c_str()); + return false; + } + + // Reparent relative file paths + fs::filesystem::path filePath(_subPath); + bool needsRepathing = !SdfLayer::IsAnonymousLayerIdentifier(_subPath) + && filePath.is_relative() && !layer->GetRealPath().empty() + && !_newParent->GetRealPath().empty(); + + if (needsRepathing) { + auto oldLayerDir = fs::filesystem::path(layer->GetRealPath()).remove_filename(); + auto newLayerDir = fs::filesystem::path(_newParent->GetRealPath()).remove_filename(); + std::string absolutePath + = (oldLayerDir / filePath).lexically_normal().generic_string(); + auto result = FileSystem::makePathRelativeTo( + absolutePath, newLayerDir.lexically_normal().generic_string()); + if (result.second) { + newPath = result.first; + } else { + newPath = absolutePath; + TF_WARN( + "File name (%s) cannot be resolved as relative to layer %s, using " + "absolute path.", + absolutePath.c_str(), + _newParent->GetIdentifier().c_str()); + } + } + + if (_newParent->GetSubLayerPaths().Find(newPath) != size_t(-1)) { + TF_RUNTIME_ERROR( + "SubPath %s already exists in layer %s", + newPath.c_str(), + _newParent->GetIdentifier().c_str()); + return false; + } + } + + _newPath = newPath; + layer->RemoveSubLayerPath(_oldIndex); + _newParent->InsertSubLayerPath(_newPath, _newIndex); + return true; +} + +bool MoveSubPathCmd::undoIt(const pxr::SdfLayerHandle& layer) +{ + _newParent->RemoveSubLayerPath(_newIndex); + layer->InsertSubLayerPath(_subPath, _oldIndex); + return true; +} + +bool RefreshSystemLockLayerCmd::doIt(const pxr::SdfLayerHandle& layer) +{ + if (!_stage) { + return false; + } + + if (_refreshSubLayers) { + // If refreshSubLayers is True, we attempt to refresh the system lock status of all + // layers under the given layer. This is specially useful when reloading a stage. + bool includeTopLayer = true; + auto allLayers = UsdUfe::getAllSublayerRefs(layer, includeTopLayer); + for (auto layerIt : allLayers) { + _refreshLayerSystemLock(layerIt); + } + } else { + // Only check and refresh the system lock status of the current layer. + _refreshLayerSystemLock(layer); + } + + // Execute lock commands + for (size_t layerIndex = 0; layerIndex < _layers.size(); layerIndex++) { + if (!_lockCommands[layerIndex]->doIt(_layers[layerIndex])) { + return false; + } + } + + if (!_layers.empty()) { + _notifySystemLockIsRefreshed(); + + // Finally update edit target after layer locks were changed + // by the command or a callback. + updateEditTarget(_stage); + } + + return true; +} + +bool RefreshSystemLockLayerCmd::undoIt(const pxr::SdfLayerHandle& layer) +{ + if (!_stage) { + return false; + } + + // Execute lock commands + for (size_t layerIndex = 0; layerIndex < _layers.size(); layerIndex++) { + if (!_lockCommands[layerIndex]->undoIt(_layers[layerIndex])) { + return false; + } + } + + if (!_layers.empty()) { + _notifySystemLockIsRefreshed(); + + // Finally update edit target after layer locks were changed + // by the command or a callback. + updateEditTarget(_stage); + } + + return true; +} + +std::string RefreshSystemLockLayerCmd::_quote(const std::string& string) +{ + return std::string(" \"") + string + std::string("\""); +} + +void RefreshSystemLockLayerCmd::addCallbackContext( + const std::string& key, const pxr::VtValue& value) +{ + _extraCallbackContext[key] = value; +} + +void RefreshSystemLockLayerCmd::_refreshLayerSystemLock(const pxr::SdfLayerHandle& usdLayer) +{ + // Anonymous layers do not need to be checked. + if (usdLayer && !usdLayer->IsAnonymous()) { + // Check if the layer's write permissions have changed. + std::string assetPath = usdLayer->GetResolvedPath(); + std::replace(assetPath.begin(), assetPath.end(), '\\', '/'); + + if (!assetPath.empty()) { + + auto writeAccess = UsdLayerEditor::FileSystem::checkWriteAccess(assetPath); + + if (writeAccess && isLayerSystemLocked(usdLayer)) { + // If the file has write permissions and the layer is currently + // system-locked: Unlock the layer + + // Create the lock command + auto cmd + = std::make_shared(_stage, usdLayer, LayerLock_Unlocked, false); + // Edit target will be updated once at the end of the refresh command. + cmd->SetUpdateEditTarget(false); + + // Add the lock command and its parameter to be executed + _lockCommands.push_back(std::move(cmd)); + _layers.push_back(usdLayer); + } else if (!writeAccess && !isLayerSystemLocked(usdLayer)) { + // If the file doesn't have write permissions and the layer is currently not + // system-locked: System-lock the layer + + // Create the lock command + auto cmd = std::make_shared( + _stage, usdLayer, LayerLock_SystemLocked, false); + // Edit target will be updated once at the end of the refresh command. + cmd->SetUpdateEditTarget(false); + + // Add the lock command and its parameter to be executed + _lockCommands.push_back(std::move(cmd)); + _layers.push_back(usdLayer); + } + } + } +} + +void RefreshSystemLockLayerCmd::_notifySystemLockIsRefreshed() +{ + if (!UsdUfe::isUICallbackRegistered(TfToken("onRefreshSystemLock"))) { + return; + } + + PXR_NS::VtDictionary callbackContext; + callbackContext["objectPath"] = PXR_NS::VtValue(UsdUfe::stagePath(_stage).string().c_str()); + for (const auto& entry : _extraCallbackContext) { + callbackContext[entry.first] = entry.second; + } + PXR_NS::VtDictionary callbackData; + + std::vector affectedLayers; + affectedLayers.reserve(_layers.size()); + for (size_t layerIndex = 0; layerIndex < _layers.size(); layerIndex++) { + affectedLayers.push_back(_layers[layerIndex]->GetIdentifier()); + } + + VtStringArray lockedArray(affectedLayers.begin(), affectedLayers.end()); + callbackData["affectedLayerIds"] = lockedArray; + + UsdUfe::triggerUICallback(TfToken("onRefreshSystemLock"), callbackContext, callbackData); +} + +bool StitchLayersCmd::doIt(const SdfLayerHandle& /*layer*/) +{ + if (_layerIdentifiersByStrength.empty()) + return true; + + if (!_stage) { + TF_RUNTIME_ERROR("Cannot stitch layers: no valid stage"); + return false; + } + + const SdfLayerHandleVector stageLayers = _stage->GetLayerStack(); + + // Sort the selected layers by their strength (strongest first). + std::unordered_map layerStrengthMap; + layerStrengthMap.reserve(stageLayers.size()); + for (size_t i = 0; i < stageLayers.size(); ++i) + layerStrengthMap[stageLayers[i]->GetIdentifier()] = i; + + std::sort( + _layerIdentifiersByStrength.begin(), + _layerIdentifiersByStrength.end(), + [&layerStrengthMap](const std::string& a, const std::string& b) { + const auto itA = layerStrengthMap.find(a); + const auto itB = layerStrengthMap.find(b); + if (itA == layerStrengthMap.end()) { + TF_WARN("Layer '%s' not found in stage layer stack", a.c_str()); + return false; + } + if (itB == layerStrengthMap.end()) { + TF_WARN("Layer '%s' not found in stage layer stack", b.c_str()); + return true; + } + return itA->second < itB->second; + }); + + // Validate and collect all problems before modifying anything. + bool hasProblems = false; + + SdfLayerHandleVector layersByStrength; + layersByStrength.reserve(_layerIdentifiersByStrength.size()); + for (const auto& layerIdentifier : _layerIdentifiersByStrength) { + auto foundLayer = SdfLayer::FindOrOpen(layerIdentifier); + if (!foundLayer) { + TF_RUNTIME_ERROR("Cannot find layer: %s", layerIdentifier.c_str()); + return false; + } + layersByStrength.push_back(foundLayer); + } + + if (layersByStrength.empty()) { + TF_RUNTIME_ERROR("No valid layer found to stitch"); + return false; + } + + const SdfLayerHandle strongestLayer = layersByStrength[0]; + if (!strongestLayer->PermissionToEdit()) { + TF_WARN( + "Cannot merge into layer '%s' because it is locked.", + strongestLayer->GetDisplayName().c_str()); + hasProblems = true; + } + + std::map> parentInfoByLayer; + for (const auto& potentialParent : stageLayers) { + const std::vector& subLayerPaths = potentialParent->GetSubLayerPaths(); + for (size_t i = 0; i < subLayerPaths.size(); ++i) { + auto subLayer = SdfLayer::FindRelativeToLayer(potentialParent, subLayerPaths[i]); + if (subLayer) { + parentInfoByLayer[subLayer->GetIdentifier()] + = std::make_pair(potentialParent, subLayerPaths[i]); + } + } + } + + // Filter the layers to be merged, only keeping those that have unlocked parents. + // + // Keep a map of parent layers to their removed children because multiple + // selected weak layers may share the same parent, so batch removals by + // parent to avoid calling SetSubLayerPaths more than once per parent layer. + std::map> removalsByParent; + { + SdfLayerHandleVector layersToBeMergedAndRemoved; + for (size_t i = 1; i < layersByStrength.size(); ++i) { + const SdfLayerHandle& weakLayer = layersByStrength[i]; + const std::string weakLayerId = weakLayer->GetIdentifier(); + + const auto& it = parentInfoByLayer.find(weakLayerId); + if (it == parentInfoByLayer.end()) { + TF_WARN( + "Could not find parent for layer: %s", weakLayer->GetDisplayName().c_str()); + hasProblems = true; + continue; + } + + const SdfLayerHandle& parentLayer = it->second.first; + if (!parentLayer->PermissionToEdit()) { + TF_WARN( + "Cannot merge layer '%s' because its parent '%s' is locked.", + weakLayer->GetDisplayName().c_str(), + parentLayer->GetDisplayName().c_str()); + continue; + } + + layersToBeMergedAndRemoved.push_back(weakLayer); + removalsByParent[parentLayer->GetIdentifier()].push_back(it->second.second); + } + layersByStrength.swap(layersToBeMergedAndRemoved); + } + + if (hasProblems) + return false; + + if (layersByStrength.empty()) { + TF_RUNTIME_ERROR("No valid layer found to stitch"); + return false; + } + + holdOntoSubLayers(strongestLayer); + + // Keep a hold of references for all selected layers, needed for undo(). + for (auto& layer : layersByStrength) + _subLayersRefs.push_back(layer); + + UsdUfe::UsdUndoManager::instance().trackLayerStates(strongestLayer); + for (auto& layer : layersByStrength) + UsdUfe::UsdUndoManager::instance().trackLayerStates(layer); + for (const auto& entry : removalsByParent) { + auto parentLayer = SdfLayer::Find(entry.first); + if (parentLayer) { + UsdUfe::UsdUndoManager::instance().trackLayerStates(parentLayer); + } + } + + { + UsdUfe::UsdUndoBlock undoBlock(&_undoItem); + + std::vector> movedSubLayers; + movedSubLayers.reserve(layersByStrength.size()); + for (auto& weakLayer : layersByStrength) { + movedSubLayers.push_back(weakLayer->GetSubLayerPaths()); + UsdUtilsStitchLayers(strongestLayer, weakLayer); + } + + // Add all collected subLayers to the strongest layer, preventing duplicates. + // Non-selected weak subLayers are added to the end of the subPathList of the + // strongestLayer. Note: This means there are cases where relative strength is not fully + // preserved. + auto strongLayerSubLayers = strongestLayer->GetSubLayerPaths(); + + // Creates a set of the added subLayers, prevent duplicates. + std::set addedSublayerIds; + for (const auto path : strongLayerSubLayers) { + const auto existingLayer = SdfLayer::FindRelativeToLayer(strongestLayer, path); + if (existingLayer) { + addedSublayerIds.insert(existingLayer->GetIdentifier()); + } + } + + // Adds any moved sub layers to the strong layers sub layers to prevent layers from + // being lost when the weak layer is deleted. + for (const auto& subLayerList : movedSubLayers) { + for (const auto& subLayerPath : subLayerList) { + const auto subLayer + = SdfLayer::FindRelativeToLayer(strongestLayer, subLayerPath); + if (subLayer + && addedSublayerIds.find(subLayer->GetIdentifier()) + == addedSublayerIds.end()) { + strongLayerSubLayers.push_back(subLayerPath); + addedSublayerIds.insert(subLayer->GetIdentifier()); + } + } + } + + // Remove any merged weak layers from the sublayer list before setting, to prevent + // them from being both stitched (merged) and referenced as subLayers. + for (const auto& weakLayer : layersByStrength) { + const std::string weakLayerId = weakLayer->GetIdentifier(); + const auto it = std::find( + strongLayerSubLayers.begin(), strongLayerSubLayers.end(), weakLayerId); + if (it != strongLayerSubLayers.end()) + strongLayerSubLayers.erase(it); + } + + strongestLayer->SetSubLayerPaths(strongLayerSubLayers); + + // Removes the selected weak layers from their parents. + // All parents were validated as editable before reaching this point. + for (auto& entry : removalsByParent) { + const auto parentLayer = SdfLayer::Find(entry.first); + if (!parentLayer) + continue; + + auto subLayerPaths = parentLayer->GetSubLayerPaths(); + for (const auto& pathToRemove : entry.second) { + auto it = std::find(subLayerPaths.begin(), subLayerPaths.end(), pathToRemove); + if (it != subLayerPaths.end()) + subLayerPaths.erase(it); + } + parentLayer->SetSubLayerPaths(subLayerPaths); + } + } + + backupEditTargets(strongestLayer); + + return true; +} + +bool StitchLayersCmd::undoIt(const SdfLayerHandle& /*layer*/) +{ + _undoItem.undo(); + + restoreEditTargets(); + releaseSubLayers(); + + return true; +} + } // namespace UsdLayerEditor \ No newline at end of file diff --git a/lib/usdLayerEditor/lib/layerEditorDCCFunctions.cpp b/lib/usdLayerEditor/lib/layerEditorDCCFunctions.cpp new file mode 100644 index 0000000000..ab2aa7873c --- /dev/null +++ b/lib/usdLayerEditor/lib/layerEditorDCCFunctions.cpp @@ -0,0 +1,354 @@ +// +// 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 "layerEditorDCCFunctions.h" + +#include + +namespace UsdLayerEditor { + +namespace { +LayerEditorDCCFunctions& registry() +{ + static LayerEditorDCCFunctions sFunctions; + return sFunctions; +} +} // namespace + +void setComponentFns(const ComponentFns& fns) { registry().component = fns; } +void setEditForwardingFns(const EditForwardingFns& fns) { registry().editForwarding = fns; } +void setDccObjectFns(const DccObjectFns& fns) { registry().dccObject = fns; } +void setSaveOptionFns(const SaveOptionFns& fns) { registry().saveOption = fns; } +void setEnvironmentFns(const EnvironmentFns& fns) { registry().environment = fns; } +void setFileSystemFns(const FileSystemFns& fns) { registry().fileSystem = fns; } +void setSerializationFns(const SerializationFns& fns) { registry().serialization = fns; } +void setLayerEditorDCCFunctions(const LayerEditorDCCFunctions& fns) { registry() = fns; } +const LayerEditorDCCFunctions& layerEditorDCCFunctions() { return registry(); } + +// ---- Component ---- +void saveComponent(const PXR_NS::UsdStageRefPtr& stage, const std::string& dccObjectPath) +{ + if (registry().component.saveComponent) + registry().component.saveComponent(stage, dccObjectPath); +} +void reloadComponent(const std::string& dccObjectPath) +{ + if (registry().component.reloadComponent) + registry().component.reloadComponent(dccObjectPath); +} +bool isStageAComponent(const std::string& dccObjectPath) +{ + return registry().component.isStageAComponent + ? registry().component.isStageAComponent(dccObjectPath) + : false; +} +bool isUnsavedComponent(const PXR_NS::UsdStageRefPtr& stage) +{ + return registry().component.isUnsavedComponent + ? registry().component.isUnsavedComponent(stage) + : false; +} +bool shouldDisplayComponentInitialSaveDialog( + const PXR_NS::UsdStageRefPtr& stage, + const std::string& dccObjectPath) +{ + return registry().component.shouldDisplayComponentInitialSaveDialog + ? registry().component.shouldDisplayComponentInitialSaveDialog(stage, dccObjectPath) + : false; +} +std::string moveComponent( + const std::string& saveLocation, + const std::string& componentName, + const std::string& dccObjectPath) +{ + return registry().component.moveComponent + ? registry().component.moveComponent(saveLocation, componentName, dccObjectPath) + : std::string {}; +} +std::string previewComponentSave( + const std::string& saveLocation, + const std::string& componentName, + const std::string& dccObjectPath) +{ + return registry().component.previewComponentSave + ? registry().component.previewComponentSave(saveLocation, componentName, dccObjectPath) + : std::string {}; +} +std::vector getComponentLayersToSave(const std::string& dccObjectPath) +{ + return registry().component.getComponentLayersToSave + ? registry().component.getComponentLayersToSave(dccObjectPath) + : std::vector {}; +} + +// ---- Edit Forwarding ---- +bool supportsEditForwarding() +{ + return registry().editForwarding.supportsEditForwarding + ? registry().editForwarding.supportsEditForwarding() + : false; +} +bool echoEditForwarding() +{ + return registry().editForwarding.echoEditForwarding + ? registry().editForwarding.echoEditForwarding() + : false; +} +void setEchoEditForwarding(bool echo) +{ + if (registry().editForwarding.setEchoEditForwarding) + registry().editForwarding.setEchoEditForwarding(echo); +} +void openEditForwardDialog(const PXR_NS::UsdStageRefPtr& currentStage) +{ + if (registry().editForwarding.openEditForwardDialog) + registry().editForwarding.openEditForwardDialog(currentStage); +} +bool handleEFEditTargetUpdate(const PXR_NS::UsdStageRefPtr& stage) +{ + return registry().editForwarding.handleEFEditTargetUpdate + ? registry().editForwarding.handleEFEditTargetUpdate(stage) + : false; +} +bool isEditForwardDialogOpen() +{ + return registry().editForwarding.isEditForwardDialogOpen + ? registry().editForwarding.isEditForwardDialogOpen() + : false; +} + +// ---- DCC object/stage queries ---- +bool isDccObjectStageIncoming(const std::string& dccObjectPath) +{ + return registry().dccObject.isDccObjectStageIncoming + ? registry().dccObject.isDccObjectStageIncoming(dccObjectPath) + : false; +} +bool isDccObjectSharedStage(const std::string& dccObjectPath) +{ + return registry().dccObject.isDccObjectSharedStage + ? registry().dccObject.isDccObjectSharedStage(dccObjectPath) + : true; // matches the former AbstractCommandHook default +} +std::string renameObject(const std::string& oldDccObjectPath, const std::string& newName) +{ + return registry().dccObject.renameObject + ? registry().dccObject.renameObject(oldDccObjectPath, newName) + : std::string {}; +} + +// ---- SaveOption ---- +bool requireUsdPathsRelativeToSceneFile() +{ + return registry().saveOption.requireUsdPathsRelativeToSceneFile + ? registry().saveOption.requireUsdPathsRelativeToSceneFile() + : true; +} +bool requireUsdPathsRelativeToParentLayer() +{ + return registry().saveOption.requireUsdPathsRelativeToParentLayer + ? registry().saveOption.requireUsdPathsRelativeToParentLayer() + : true; +} +bool requireUsdPathsRelativeToEditTargetLayer() +{ + return registry().saveOption.requireUsdPathsRelativeToEditTargetLayer + ? registry().saveOption.requireUsdPathsRelativeToEditTargetLayer() + : true; +} +bool wantReferenceCompositionArc() +{ + return registry().saveOption.wantReferenceCompositionArc + ? registry().saveOption.wantReferenceCompositionArc() + : false; +} +bool wantPrependCompositionArc() +{ + return registry().saveOption.wantPrependCompositionArc + ? registry().saveOption.wantPrependCompositionArc() + : true; +} +bool wantPayloadLoaded() +{ + return registry().saveOption.wantPayloadLoaded + ? registry().saveOption.wantPayloadLoaded() + : true; +} +std::string getReferencedPrimPath() +{ + return registry().saveOption.getReferencedPrimPath + ? registry().saveOption.getReferencedPrimPath() + : std::string(); +} +void setRequireUsdPathsRelativeToSceneFile(bool value) +{ + if (registry().saveOption.setRequireUsdPathsRelativeToSceneFile) + registry().saveOption.setRequireUsdPathsRelativeToSceneFile(value); +} +void setRequireUsdPathsRelativeToParentLayer(bool value) +{ + if (registry().saveOption.setRequireUsdPathsRelativeToParentLayer) + registry().saveOption.setRequireUsdPathsRelativeToParentLayer(value); +} +bool confirmExistingFileSave() +{ + return registry().saveOption.confirmExistingFileSave + ? registry().saveOption.confirmExistingFileSave() + : true; +} +bool getSaveLayerFormatBinary() +{ + return registry().saveOption.getSaveLayerFormatBinary + ? registry().saveOption.getSaveLayerFormatBinary() + : true; +} +void setSaveLayerFormatBinary(bool value) +{ + if (registry().saveOption.setSaveLayerFormatBinary) + registry().saveOption.setSaveLayerFormatBinary(value); +} +int getSerializedUsdEditsLocation() +{ + return registry().saveOption.getSerializedUsdEditsLocation + ? registry().saveOption.getSerializedUsdEditsLocation() + : 1; // kSaveToUSDFiles +} +void setSerializedUsdEditsLocation(int value) +{ + if (registry().saveOption.setSerializedUsdEditsLocation) + registry().saveOption.setSerializedUsdEditsLocation(value); +} +// ---- Environment ---- +bool getPinLayerEditorStage() +{ + return registry().environment.getPinLayerEditorStage + ? registry().environment.getPinLayerEditorStage() + : false; +} +void setPinLayerEditorStage(bool value) +{ + if (registry().environment.setPinLayerEditorStage) + registry().environment.setPinLayerEditorStage(value); +} +bool isInteractiveDCCSession() +{ + return registry().environment.isInteractiveDCCSession + ? registry().environment.isInteractiveDCCSession() + : true; +} +bool shouldExpandOrCollapseAll() +{ + return registry().environment.shouldExpandOrCollapseAll + ? registry().environment.shouldExpandOrCollapseAll() + : false; +} +QWidget* mainWindowParent() +{ + return registry().environment.mainWindowParent ? registry().environment.mainWindowParent() + : nullptr; +} +int64_t layerContentsArraySizeLimit() +{ + return registry().environment.layerContentsArraySizeLimit + ? registry().environment.layerContentsArraySizeLimit() + : 8; +} +int64_t layerContentsTimeSamplesSizeLimit() +{ + return registry().environment.layerContentsTimeSamplesSizeLimit + ? registry().environment.layerContentsTimeSamplesSizeLimit() + : 8; +} +void displayError(const std::string& error) +{ + if (registry().environment.displayError) + registry().environment.displayError(error); +} + +// ---- FileSystem ---- +std::string getDCCSceneDir() +{ + return registry().fileSystem.getDCCSceneDir + ? registry().fileSystem.getDCCSceneDir() + : std::string {}; +} +std::string getDCCWorkspaceScenesDir() +{ + return registry().fileSystem.getDCCWorkspaceScenesDir + ? registry().fileSystem.getDCCWorkspaceScenesDir() + : std::string {}; +} +std::string sceneFolder() +{ + return registry().fileSystem.sceneFolder ? registry().fileSystem.sceneFolder() + : std::string {}; +} +bool prepareLayerSaveUILayer(const std::string& relativeAnchor) +{ + return registry().fileSystem.prepareLayerSaveUILayer + ? registry().fileSystem.prepareLayerSaveUILayer(relativeAnchor) + : true; +} +bool checkWriteAccess(const std::string& filePath) +{ + return registry().fileSystem.checkWriteAccess + ? registry().fileSystem.checkWriteAccess(filePath) + : false; +} + +// ---- Serialization ---- +std::vector getStageCaches() +{ + return registry().serialization.getStageCaches + ? registry().serialization.getStageCaches() + : std::vector { &PXR_NS::UsdUtilsStageCache::Get() }; +} +std::vector getAllStages() +{ + return registry().serialization.getAllStages + ? registry().serialization.getAllStages() + : PXR_NS::UsdUtilsStageCache::Get().GetAllStages(); +} +void setLayerUpAxisAndUnits(const PXR_NS::SdfLayerRefPtr& layer) +{ + if (registry().serialization.setLayerUpAxisAndUnits) + registry().serialization.setLayerUpAxisAndUnits(layer); +} +void updateDCCObjectRootLayer( + const std::string& dccObjectPath, + const std::string& layerPath, + const PXR_NS::SdfLayerRefPtr& layer, + bool wasTargetLayer, + DccObjectRootLayerPathMode pathMode) +{ + if (registry().serialization.updateDCCObjectRootLayer) + registry().serialization.updateDCCObjectRootLayer( + dccObjectPath, layerPath, layer, wasTargetLayer, pathMode); +} +PXR_NS::SdfLayerRefPtr captureSessionLayer(const std::string& dccObjectPath) +{ + return registry().serialization.captureSessionLayer + ? registry().serialization.captureSessionLayer(dccObjectPath) + : PXR_NS::SdfLayerRefPtr {}; +} +void transferSessionLayer( + const PXR_NS::SdfLayerRefPtr& sourceSessionLayer, + const std::string& dstDccObjectPath) +{ + if (registry().serialization.transferSessionLayer) + registry().serialization.transferSessionLayer(sourceSessionLayer, dstDccObjectPath); +} + +} // namespace UsdLayerEditor diff --git a/lib/usdLayerEditor/lib/layerEditorDCCFunctions.h b/lib/usdLayerEditor/lib/layerEditorDCCFunctions.h new file mode 100644 index 0000000000..e74b8af721 --- /dev/null +++ b/lib/usdLayerEditor/lib/layerEditorDCCFunctions.h @@ -0,0 +1,258 @@ +// +// 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 LAYER_EDITOR_DCC_FUNCTIONS_H +#define LAYER_EDITOR_DCC_FUNCTIONS_H + +#include "layerEditorAPI.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +class QWidget; + +namespace UsdLayerEditor { + +// std::function typedefs use the EXACT signatures of the former base-class overrides. +using SaveComponentFn = std::function; +using ReloadComponentFn = std::function; +using IsStageAComponentFn = std::function; +using IsUnsavedComponentFn = std::function; +using ShouldDisplayComponentInitialSaveDialogFn + = std::function; +using SceneFolderFn = std::function; +using MoveComponentFn + = std::function; +using PreviewComponentSaveFn + = std::function; +using GetComponentLayersToSaveFn = std::function(const std::string&)>; +using CaptureSessionLayerFn = std::function; +using TransferSessionLayerFn = std::function; + +// How updateDCCObjectRootLayer should resolve the root-layer path written back to the DCC object. +enum class DccObjectRootLayerPathMode +{ + FollowPreference, // honor the proxy / option-var relative-vs-absolute path-mode preference + ForceAbsolute, // ignore the preference and write an absolute root-layer path +}; + +using SupportsEditForwardingFn = std::function; +using EchoEditForwardingFn = std::function; +using SetEchoEditForwardingFn = std::function; +using OpenEditForwardDialogFn = std::function; +// Returns true if edit forwarding is active and has handled the stage's edit +// target (the caller then skips the normal auto-targeting); false otherwise. +using HandleEFEditTargetUpdateFn = std::function; +using IsEditForwardDialogOpenFn = std::function; + +using IsDccObjectStageIncomingFn = std::function; +using IsDccObjectSharedStageFn = std::function; +// Returns the new DCC object path of the renamed object (empty if no rename happened). +using RenameObjectFn = std::function; + +struct ComponentFns +{ + SaveComponentFn saveComponent; + ReloadComponentFn reloadComponent; + IsStageAComponentFn isStageAComponent; + IsUnsavedComponentFn isUnsavedComponent; + ShouldDisplayComponentInitialSaveDialogFn shouldDisplayComponentInitialSaveDialog; + MoveComponentFn moveComponent; + PreviewComponentSaveFn previewComponentSave; + GetComponentLayersToSaveFn getComponentLayersToSave; +}; + +struct EditForwardingFns +{ + SupportsEditForwardingFn supportsEditForwarding; + EchoEditForwardingFn echoEditForwarding; + SetEchoEditForwardingFn setEchoEditForwarding; + OpenEditForwardDialogFn openEditForwardDialog; + HandleEFEditTargetUpdateFn handleEFEditTargetUpdate; + IsEditForwardDialogOpenFn isEditForwardDialogOpen; // default false when unset +}; + +struct DccObjectFns +{ + IsDccObjectStageIncomingFn isDccObjectStageIncoming; + IsDccObjectSharedStageFn isDccObjectSharedStage; + RenameObjectFn renameObject; +}; + +struct SaveOptionFns +{ + std::function requireUsdPathsRelativeToSceneFile; + std::function requireUsdPathsRelativeToParentLayer; + std::function requireUsdPathsRelativeToEditTargetLayer; + std::function wantReferenceCompositionArc; + std::function wantPrependCompositionArc; + std::function wantPayloadLoaded; + std::function getReferencedPrimPath; + std::function setRequireUsdPathsRelativeToSceneFile; + std::function setRequireUsdPathsRelativeToParentLayer; + std::function confirmExistingFileSave; + std::function getSaveLayerFormatBinary; + std::function setSaveLayerFormatBinary; + std::function getSerializedUsdEditsLocation; + std::function setSerializedUsdEditsLocation; +}; + +struct EnvironmentFns +{ + std::function getPinLayerEditorStage; + std::function setPinLayerEditorStage; + std::function isInteractiveDCCSession; + std::function shouldExpandOrCollapseAll; + std::function mainWindowParent; + std::function layerContentsArraySizeLimit; + std::function layerContentsTimeSamplesSizeLimit; + std::function displayError; +}; + +struct FileSystemFns +{ + std::function getDCCSceneDir; + std::function getDCCWorkspaceScenesDir; + SceneFolderFn sceneFolder; + std::function prepareLayerSaveUILayer; + std::function checkWriteAccess; +}; + +struct SerializationFns +{ + std::function()> getStageCaches; + std::function()> getAllStages; + std::function setLayerUpAxisAndUnits; + std::function + updateDCCObjectRootLayer; + CaptureSessionLayerFn captureSessionLayer; + TransferSessionLayerFn transferSessionLayer; +}; + +struct LayerEditorDCCFunctions +{ + ComponentFns component; + EditForwardingFns editForwarding; + DccObjectFns dccObject; + SaveOptionFns saveOption; + EnvironmentFns environment; + FileSystemFns fileSystem; + SerializationFns serialization; +}; + +// Registration API — per-group setters (play cleanly with #ifdef guards), plus a +// full-struct setter and a getter used by the test RAII helper. +LayerEditorAPI void setComponentFns(const ComponentFns&); +LayerEditorAPI void setEditForwardingFns(const EditForwardingFns&); +LayerEditorAPI void setDccObjectFns(const DccObjectFns&); +LayerEditorAPI void setSaveOptionFns(const SaveOptionFns&); +LayerEditorAPI void setEnvironmentFns(const EnvironmentFns&); +LayerEditorAPI void setFileSystemFns(const FileSystemFns&); +LayerEditorAPI void setSerializationFns(const SerializationFns&); +LayerEditorAPI void setLayerEditorDCCFunctions(const LayerEditorDCCFunctions&); +LayerEditorAPI const LayerEditorDCCFunctions& layerEditorDCCFunctions(); + +// Accessor free functions — callers never null-check; an unset std::function +// yields the documented default (false / empty / no-op, except +// isDccObjectSharedStage which defaults to true). +LayerEditorAPI void saveComponent(const PXR_NS::UsdStageRefPtr&, const std::string&); +LayerEditorAPI void reloadComponent(const std::string&); +LayerEditorAPI bool isStageAComponent(const std::string&); +LayerEditorAPI bool isUnsavedComponent(const PXR_NS::UsdStageRefPtr&); +LayerEditorAPI bool shouldDisplayComponentInitialSaveDialog( + const PXR_NS::UsdStageRefPtr&, + const std::string&); +LayerEditorAPI std::string sceneFolder(); +LayerEditorAPI std::string +moveComponent(const std::string&, const std::string&, const std::string&); +LayerEditorAPI std::string +previewComponentSave(const std::string&, const std::string&, const std::string&); +LayerEditorAPI std::vector getComponentLayersToSave(const std::string&); + +LayerEditorAPI bool supportsEditForwarding(); +LayerEditorAPI bool echoEditForwarding(); +LayerEditorAPI void setEchoEditForwarding(bool); +LayerEditorAPI void openEditForwardDialog(const PXR_NS::UsdStageRefPtr&); +LayerEditorAPI bool handleEFEditTargetUpdate(const PXR_NS::UsdStageRefPtr&); +LayerEditorAPI bool isEditForwardDialogOpen(); + +LayerEditorAPI bool isDccObjectStageIncoming(const std::string&); +LayerEditorAPI bool isDccObjectSharedStage(const std::string&); +LayerEditorAPI std::string renameObject(const std::string&, const std::string&); + +// SaveOptionFns +LayerEditorAPI bool requireUsdPathsRelativeToSceneFile(); +LayerEditorAPI bool requireUsdPathsRelativeToParentLayer(); +LayerEditorAPI bool requireUsdPathsRelativeToEditTargetLayer(); +LayerEditorAPI bool wantReferenceCompositionArc(); +LayerEditorAPI bool wantPrependCompositionArc(); +LayerEditorAPI bool wantPayloadLoaded(); +LayerEditorAPI std::string getReferencedPrimPath(); +LayerEditorAPI void setRequireUsdPathsRelativeToSceneFile(bool); +LayerEditorAPI void setRequireUsdPathsRelativeToParentLayer(bool); +LayerEditorAPI bool confirmExistingFileSave(); +LayerEditorAPI bool getSaveLayerFormatBinary(); +LayerEditorAPI void setSaveLayerFormatBinary(bool); +LayerEditorAPI int getSerializedUsdEditsLocation(); +LayerEditorAPI void setSerializedUsdEditsLocation(int); +// EnvironmentFns +LayerEditorAPI bool getPinLayerEditorStage(); +LayerEditorAPI void setPinLayerEditorStage(bool); +LayerEditorAPI bool isInteractiveDCCSession(); +LayerEditorAPI bool shouldExpandOrCollapseAll(); +LayerEditorAPI QWidget* mainWindowParent(); +LayerEditorAPI int64_t layerContentsArraySizeLimit(); +LayerEditorAPI int64_t layerContentsTimeSamplesSizeLimit(); +LayerEditorAPI void displayError(const std::string&); + +// FileSystemFns +LayerEditorAPI std::string getDCCSceneDir(); +LayerEditorAPI std::string getDCCWorkspaceScenesDir(); +LayerEditorAPI bool prepareLayerSaveUILayer(const std::string& relativeAnchor); +LayerEditorAPI bool checkWriteAccess(const std::string& filePath); + +// SerializationFns +LayerEditorAPI std::vector getStageCaches(); +LayerEditorAPI std::vector getAllStages(); +LayerEditorAPI void setLayerUpAxisAndUnits(const PXR_NS::SdfLayerRefPtr& layer); +// Writes the saved root-layer path back onto the DCC object. pathMode selects whether to honor the +// proxy/option-var relative-vs-absolute preference (FollowPreference, the normal save flow) or to +// force an absolute path (ForceAbsolute, used after a component move repaths the root). +LayerEditorAPI void updateDCCObjectRootLayer( + const std::string& dccObjectPath, + const std::string& layerPath, + const PXR_NS::SdfLayerRefPtr& layer, + bool wasTargetLayer, + DccObjectRootLayerPathMode pathMode = DccObjectRootLayerPathMode::FollowPreference); +LayerEditorAPI PXR_NS::SdfLayerRefPtr captureSessionLayer(const std::string& dccObjectPath); +LayerEditorAPI void transferSessionLayer( + const PXR_NS::SdfLayerRefPtr& sourceSessionLayer, + const std::string& dstDccObjectPath); + +} // namespace UsdLayerEditor + +#endif // LAYER_EDITOR_DCC_FUNCTIONS_H diff --git a/lib/usdLayerEditor/lib/layerEditorWidget.cpp b/lib/usdLayerEditor/lib/layerEditorWidget.cpp index e59408f46a..8384dce2d1 100644 --- a/lib/usdLayerEditor/lib/layerEditorWidget.cpp +++ b/lib/usdLayerEditor/lib/layerEditorWidget.cpp @@ -1,435 +1,757 @@ -// -// Copyright 2020 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 "layerEditorWidget.h" -#include "layerEditorWidgetManager.h" - -#include "abstractCommandHook.h" -#include "dirtyLayersCountBadge.h" -#include "layerTreeModel.h" -#include "layerTreeView.h" -#include "stageSelectorWidget.h" -#include "stringResources.h" -#include "utilQT.h" - -#include -#include - -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) -#include -#else -#include -#endif - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -PXR_NAMESPACE_USING_DIRECTIVE - -namespace { - -using namespace UsdLayerEditor; -class LayerEditorWidgetManager; - -// create the default menus on the parent QMainWindow -void setupDefaultMenu(SessionState* in_sessionState, QMainWindow* in_parent) -{ - auto menuBar = in_parent->menuBar(); - // don't add menu twice -- this window is destroyed and re-created on new scene - if (menuBar->actions().length() == 0) { - auto createMenu = menuBar->addMenu(StringResources::getAsQString(StringResources::kCreate)); - auto aboutToShowCallback = [createMenu, in_sessionState]() { - if (createMenu->actions().length() == 0) { - in_sessionState->setupCreateMenu(createMenu); - } - }; - // we delay populating the create menu to first show, because in the python prototype, if - // the layer editor was docked, the menu would get populated before the runtime commands had - // the time to be created - QObject::connect(createMenu, &QMenu::aboutToShow, in_parent, aboutToShowCallback); - - // TODO LE-EXTRACT Maya-usd menus (auto-hide session layer / help menu) - /*auto optionMenu = menuBar->addMenu(StringResources::getAsQString(StringResources::kOption)); - auto action = optionMenu->addAction( - StringResources::getAsQString(StringResources::kAutoHideSessionLayer)); - QObject::connect( - action, &QAction::toggled, in_sessionState, &SessionState::setAutoHideSessionLayer); - action->setCheckable(true); - action->setChecked(in_sessionState->autoHideSessionLayer()); - - auto helpMenu = menuBar->addMenu(StringResources::getAsQString(StringResources::kHelp)); - helpMenu->addAction( - StringResources::getAsQString(StringResources::kHelpOnUSDLayerEditor), - [in_sessionState]() { in_sessionState->commandHook()->showLayerEditorHelp(); });*/ - } -} - -} // namespace - -/////////////////////////////////////////////////////////////////////////////// - -namespace UsdLayerEditor { -LayerEditorWidget::LayerEditorWidget(SessionState& in_sessionState, QMainWindow* in_parent) - : QWidget(in_parent) - , _sessionState(in_sessionState) -{ - // Force link the resources - needed to prevent the compiler to cull out the icons from the lib. - Q_INIT_RESOURCE(resources); - - setupLayout(); - ::setupDefaultMenu(&in_sessionState, in_parent); - auto layerEditorManager = LayerEditorWidgetManager::getInstance(); - layerEditorManager->setWidget(this); -} - -// helper for setupLayout -QLayout* LayerEditorWidget::setupLayout_toolbar() -{ - auto buttonSize = DPIScale(24); - auto toolbar = new QHBoxLayout(); - toolbar->setContentsMargins(0, 0, 0, 0); - auto buttonAlignment = Qt::AlignLeft | Qt::AlignRight; - - auto addHIGButton - = [buttonSize, toolbar, buttonAlignment](const QString& iconName, const QString& tooltip) { - auto higButtonYOffset = DPIScale(4); - auto higBtn = new QPushButton(); - higBtn->move(0, higButtonYOffset); - QtUtils::setupButtonWithHIGBitmaps(higBtn, iconName); - higBtn->setFixedSize(buttonSize, buttonSize); - higBtn->setToolTip(tooltip); - toolbar->addWidget(higBtn, 0, buttonAlignment); - return higBtn; - }; - - _buttons._newLayer = addHIGButton( - ":/UsdLayerEditor/LE_add_layer", - StringResources::getAsQString(StringResources::kAddNewLayer)); - // clicked callback - connect( - _buttons._newLayer, - &QAbstractButton::clicked, - this, - &LayerEditorWidget::onNewLayerButtonClicked); - // update layer button on stage change - connect( - _treeView->model(), - &QAbstractItemModel::modelReset, - this, - &LayerEditorWidget::updateNewLayerButton); - // update layer button if muted state changes - connect( - _treeView->model(), - &QAbstractItemModel::dataChanged, - this, - &LayerEditorWidget::updateNewLayerButton); - // update layer button on selection change - connect( - _treeView->selectionModel(), - &QItemSelectionModel::selectionChanged, - this, - &LayerEditorWidget::updateNewLayerButton); - - // send callback notification to usdufe when selection changes - connect( - _treeView->selectionModel(), - &QItemSelectionModel::selectionChanged, - this, - &LayerEditorWidget::onSelectionChanged); - - _buttons._loadLayer = addHIGButton( - ":/UsdLayerEditor/LE_import_layer", - StringResources::getAsQString(StringResources::kLoadExistingLayer)); - // clicked callback - connect( - _buttons._loadLayer, - &QAbstractButton::clicked, - this, - &LayerEditorWidget::onLoadLayersButtonClicked); - - toolbar->addStretch(); - - QWidget* saveContainer = new QWidget; - auto saveLayout = new QHBoxLayout(saveContainer); - saveLayout->setContentsMargins(0, 0, 0, 0); - saveLayout->setSpacing(0); - saveLayout->addStretch(); - { - auto badgeYOffset = DPIScale(4); - auto dirtyCountBadge = new DirtyLayersCountBadge(nullptr); - auto badgeSize = QSize(buttonSize + DPIScale(12), buttonSize + badgeYOffset); - dirtyCountBadge->setFixedSize(badgeSize); - - saveLayout->addWidget(dirtyCountBadge, 0, Qt::AlignRight); - _buttons._dirtyCountBadge = dirtyCountBadge; - } - - // save stage button: contains a push button and a "badge" widget - { - auto saveButtonYOffset = DPIScale(4); - auto saveButtonSize = QSize(buttonSize + DPIScale(12), buttonSize + saveButtonYOffset); - auto saveStageBtn = new QPushButton(); - saveStageBtn->setFixedSize(saveButtonSize); - QtUtils::setupButtonWithHIGBitmaps(saveStageBtn, ":/UsdLayerEditor/LE_save_all"); - saveStageBtn->setFixedSize(buttonSize, buttonSize); - - saveStageBtn->setToolTip( - StringResources::getAsQString(StringResources::kSaveAllEditsInLayerStack)); - connect( - saveStageBtn, - &QAbstractButton::clicked, - this, - &LayerEditorWidget::onSaveStageButtonClicked); - - saveLayout->addWidget(saveStageBtn, 0, buttonAlignment); - _buttons._saveStageButton = saveStageBtn; - } - - toolbar->addWidget(saveContainer, 0, buttonAlignment); - - // update buttons on stage change for example dirty count - connect( - _treeView->model(), - &QAbstractItemModel::modelReset, - this, - &LayerEditorWidget::updateButtonsOnIdle); - // update dirty count on dirty notfication - connect( - _treeView->model(), - &QAbstractItemModel::dataChanged, - this, - &LayerEditorWidget::updateButtonsOnIdle); - - return toolbar; -} - -void LayerEditorWidget::setupLayout() -{ - _treeView = new LayerTreeView(&_sessionState, this); - - auto mainVLayout = new QVBoxLayout(); - mainVLayout->setSpacing(DPIScale(4)); - - auto stageSelector = new StageSelectorWidget(&_sessionState, this); - mainVLayout->addWidget(stageSelector); - - auto toolbarLayout = setupLayout_toolbar(); - mainVLayout->addLayout(toolbarLayout); - - mainVLayout->addWidget(_treeView); - - setLayout(mainVLayout); - - updateNewLayerButton(); - updateButtons(); -} - -void LayerEditorWidget::updateButtonsOnIdle() -{ - if (!_updateButtonsOnIdle) { - _updateButtonsOnIdle = true; - QTimer::singleShot(0, this, &LayerEditorWidget::updateButtons); - } -} - -void LayerEditorWidget::updateNewLayerButton() -{ - bool disabled = _treeView->model()->rowCount() == 0; - if (!disabled) { - auto selectionModel = _treeView->selectionModel(); - // enabled if no or one layer selected - disabled = selectionModel->selectedRows().size() > 1; - - // also disable if the selected layer is muted or invalid - if (!disabled) { - auto selectedRows = selectionModel->selectedRows(); - LayerTreeItem* item = nullptr; - if (!selectedRows.empty()) { - item = _treeView->layerItemFromIndex(selectedRows[0]); - } - // When nothing is selected, the button adds a new layer to the root layer. - else { - auto treeModel = _treeView->layerTreeModel(); - item = treeModel->layerItemFromIndex(treeModel->rootLayerIndex()); - } - - if (item) { - disabled = item->isInvalidLayer() || item->appearsMuted() || item->isReadOnly() - || item->isLocked(); - } - } - } - _buttons._newLayer->setDisabled(disabled); - _buttons._loadLayer->setDisabled(disabled); -} - -void LayerEditorWidget::updateButtons() -{ - if (_sessionState.commandHook()->isDccObjectSharedStage( - _sessionState.stageEntry()._dccObjectPath)) { - if (_buttons._dirtyCountBadge) { - _buttons._dirtyCountBadge->setVisible(true); - } - if (_buttons._saveStageButton) { - _buttons._saveStageButton->setVisible(true); - } - const auto layers = _treeView->layerTreeModel()->getAllNeedsSavingLayers(); - int count = static_cast(layers.size()); - for (auto layer : layers) { - // The system locked layers do not count towards saving. - if (layer->isSystemLocked()) { - count--; - } - // Neither does any anonymous layer whose parent is locked or system-locked. - // This is because saving an anonymous layer will cause - // the parent layer to re-path the sub layer with a file name. - if (layer->isAnonymous() && (layer->appearsLocked() || layer->appearsSystemLocked())) { - count--; - } - } - _buttons._dirtyCountBadge->updateCount(count); - bool disable = count == 0; - QtUtils::disableHIGButton(_buttons._saveStageButton, disable); - } else { - if (_buttons._dirtyCountBadge) { - _buttons._dirtyCountBadge->setVisible(false); - } - if (_buttons._saveStageButton) { - _buttons._saveStageButton->setVisible(false); - } - } - _updateButtonsOnIdle = false; -} - -void LayerEditorWidget::onNewLayerButtonClicked() -{ - // if nothing or root selected, add new layer to top of root - auto model = _treeView->layerTreeModel(); - auto selectionModel = _treeView->selectionModel(); - auto selection = selectionModel->selectedRows(); - bool addToRoot = false; - - LayerTreeItem* layerTreeItem; - if (selection.size() == 0) { - addToRoot = true; - layerTreeItem = model->layerItemFromIndex(model->rootLayerIndex()); - } else { - layerTreeItem = model->layerItemFromIndex(selection[0]); - // this test catches both root layer and session layer - if (layerTreeItem->parent() == nullptr) { - addToRoot = true; - } - } - - if (addToRoot) { - layerTreeItem->addAnonymousSublayer(); - } else { - auto parentItem = layerTreeItem->parentLayerItem(); - int rowToInsert = layerTreeItem->row(); - // add a sibling to the selection - pxr::SdfLayerRefPtr newLayer = parentItem->addAnonymousSublayerAndReturn(); - - // move it to the right place, if it's not top - if (rowToInsert > 0 && newLayer) { - // create a UndoContext, which will create a composite command including - // the following 2 operations. - UndoContext context(_sessionState.commandHook(), "Reorder Newly Added Layer"); - context.hook()->removeSubLayerPath(parentItem->layer(), newLayer->GetIdentifier()); - context.hook()->insertSubLayerPath( - parentItem->layer(), newLayer->GetIdentifier(), rowToInsert); - model->selectUsdLayerOnIdle(newLayer); - } - } -} - -void LayerEditorWidget::onLoadLayersButtonClicked() -{ - const auto model = _treeView->layerTreeModel(); - const auto selectionModel = _treeView->selectionModel(); - const auto selection = selectionModel->selectedRows(); - LayerTreeItem* layerTreeItem; - if (selection.size() == 0) { - layerTreeItem = model->layerItemFromIndex(model->rootLayerIndex()); - } else { - layerTreeItem = model->layerItemFromIndex(selection[0]); - } - layerTreeItem->loadSubLayers(this); -} - -void LayerEditorWidget::onSaveStageButtonClicked() { _treeView->layerTreeModel()->saveStage(this); } - - -void LayerEditorWidget::onSelectionChanged( - const QItemSelection& selected, - const QItemSelection& deselected) -{ - if (!UsdUfe::isUICallbackRegistered(TfToken("onLayerEditorSelectionChanged"))) { - return; - } - - const std::vector selectedLayerIDs = getSelectedLayers(); - - PXR_NS::VtDictionary callbackContext; - callbackContext["objectPath"] = PXR_NS::VtValue(UsdUfe::stagePath(_sessionState.stageEntry()._stage).string().c_str()); - PXR_NS::VtDictionary callbackData; - - VtStringArray layerIds(selectedLayerIDs.begin(), selectedLayerIDs.end()); - callbackData["layerIds"] = layerIds; - - UsdUfe::triggerUICallback(TfToken("onLayerEditorSelectionChanged"), callbackContext, callbackData); -} - -std::vector LayerEditorWidget::getSelectedLayers() -{ - const auto model = _treeView->layerTreeModel(); - const auto selectionModel = _treeView->selectionModel(); - const auto selection = selectionModel->selectedRows(); - std::vector selectedLayerIDs; - for (int i = 0; i < selection.size(); ++i) { - selectedLayerIDs.emplace_back( - model->layerItemFromIndex(selection[i])->layer()->GetIdentifier()); - } - - return selectedLayerIDs; -} - -void LayerEditorWidget::selectLayers(const std::vector& layerIdentifiers) -{ - const auto model = _treeView->layerTreeModel(); - const auto selectionModel = _treeView->selectionModel(); - - // clear selection first - selectionModel->clearSelection(); - - // apply selection if layer exists in stage - for (const auto& layerId : layerIdentifiers) { - const auto sdfLayer = SdfLayer::Find(layerId); - if (sdfLayer) { - if (const auto item = model->findUSDLayerItem(sdfLayer)) { - selectionModel->select(item->index(), QItemSelectionModel::Select); - } - } - } -} - -} // namespace UsdLayerEditor +// +// Copyright 2020 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 "layerEditorWidget.h" +#include "layerEditorWidgetManager.h" + +#include "abstractCommandHook.h" +#include "dirtyLayersCountBadge.h" +#include "layerContentsWidget.h" +#include "layerEditorDCCFunctions.h" +#include "layerTreeModel.h" +#include "layerTreeView.h" +#include "sessionState.h" +#include "stageSelectorWidget.h" +#include "stringResources.h" +#include "utilQT.h" + +#include + +#include +#include +#include + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +#include +#else +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +PXR_NAMESPACE_USING_DIRECTIVE + +// Q_INIT_RESOURCE must be called from global scope; calling it inside a namespace +// makes the linker look for a namespaced symbol that Qt's rcc never generates. +static void initLayerEditorResources() { Q_INIT_RESOURCE(resources); } + +namespace UsdLayerEditor { +LayerEditorWidget::~LayerEditorWidget() +{ + disconnect( + qApp, &QApplication::focusChanged, this, &LayerEditorWidget::updateTreeContainerBorder); +} + +LayerEditorWidget::LayerEditorWidget(SessionState& in_sessionState, QMainWindow* in_parent) + : QWidget(in_parent) + , _sessionState(in_sessionState) +{ + // Force link the resources - needed to prevent the compiler to cull out the icons from the lib. + initLayerEditorResources(); + + setupLayout(); + setupDefaultMenu(in_parent); + auto layerEditorManager = LayerEditorWidgetManager::getInstance(); + layerEditorManager->setWidget(this); +} + +// create the default menus on the parent QMainWindow +void LayerEditorWidget::setupDefaultMenu(QMainWindow* in_parent) +{ + auto menuBar = in_parent->menuBar(); + SessionState* ss = &_sessionState; + // don't add menu twice -- this window is destroyed and re-created on new scene + if (menuBar->actions().length() == 0) { + auto createMenu = menuBar->addMenu(StringResources::getAsQString(StringResources::kCreate)); + auto aboutToShowCallback = [createMenu, ss]() { + if (createMenu->actions().length() == 0) { + ss->setupCreateMenu(createMenu); + } + }; + // we delay populating the create menu to first show, because in the python prototype, if + // the layer editor was docked, the menu would get populated before the runtime commands had + // the time to be created + QObject::connect(createMenu, &QMenu::aboutToShow, in_parent, aboutToShowCallback); + + auto optionMenu = menuBar->addMenu(StringResources::getAsQString(StringResources::kOption)); + + _actions._autoHide = optionMenu->addAction( + StringResources::getAsQString(StringResources::kAutoHideSessionLayer)); + QObject::connect( + _actions._autoHide, &QAction::toggled, ss, &SessionState::setAutoHideSessionLayer); + _actions._autoHide->setCheckable(true); + _actions._autoHide->setChecked(ss->autoHideSessionLayer()); + + optionMenu->addSeparator(); + + _actions._displayLayerContents = optionMenu->addAction( + StringResources::getAsQString(StringResources::kDisplayLayerContents)); + QObject::connect( + _actions._displayLayerContents, + &QAction::toggled, + ss, + &SessionState::setDisplayLayerContents); + _actions._displayLayerContents->setCheckable(true); + _actions._displayLayerContents->setChecked(ss->displayLayerContents()); + + _actions._displayLayerExpandAllValues = optionMenu->addAction( + StringResources::getAsQString(StringResources::kDisplayLayerExpandAllValues)); + _actions._displayLayerExpandAllValues->setStatusTip( + StringResources::getAsQString(StringResources::kDisplayLayerExpandAllValuesTooltip)); + QObject::connect( + _actions._displayLayerExpandAllValues, + &QAction::toggled, + ss, + &SessionState::setDisplayLayerExpandAllValues); + _actions._displayLayerExpandAllValues->setCheckable(true); + _actions._displayLayerExpandAllValues->setChecked(ss->displayLayerExpandAllValues()); + + // Echo Edit Forwarding menu item is only shown when the DCC integration + // actually supports EF (e.g. Maya builds with WANT_ADSK_USD_EDIT_FORWARD_BUILD). + // No EF symbols are referenced here — only the SessionState virtuals. + if (UsdLayerEditor::supportsEditForwarding()) { + optionMenu->addSeparator(); + _actions._echoEditForwarding = optionMenu->addAction( + StringResources::getAsQString(StringResources::kEchoEditForwarding)); + QObject::connect( + _actions._echoEditForwarding, + &QAction::toggled, + [](bool checked) { UsdLayerEditor::setEchoEditForwarding(checked); }); + _actions._echoEditForwarding->setCheckable(true); + _actions._echoEditForwarding->setChecked(UsdLayerEditor::echoEditForwarding()); + + auto configureEditForwardingAction = optionMenu->addAction( + StringResources::getAsQString(StringResources::kConfigureEditForwarding)); + QObject::connect( + configureEditForwardingAction, + &QAction::triggered, + this, + &LayerEditorWidget::openEditForwardDialog); + } + + auto helpMenu = menuBar->addMenu(StringResources::getAsQString(StringResources::kHelp)); + helpMenu->addAction( + StringResources::getAsQString(StringResources::kHelpOnUSDLayerEditor), + [ss]() { ss->commandHook()->showLayerEditorHelp(); }); + } +} + +// helper for setupLayout +QLayout* LayerEditorWidget::setupLayout_toolbar() +{ + auto buttonSize = DPIScale(24); + auto toolbar = new QHBoxLayout(); + toolbar->setContentsMargins(0, 0, 0, 0); + auto buttonAlignment = Qt::AlignLeft | Qt::AlignRight; + + auto addHIGButton = [buttonSize, toolbar, buttonAlignment]( + const QString& iconName, const QString& tooltip, const QString& uiName) { + auto higButtonYOffset = DPIScale(4); + auto higBtn = new QPushButton(); + higBtn->move(0, higButtonYOffset); + QtUtils::setupButtonWithHIGBitmaps(higBtn, iconName); + higBtn->setFixedSize(buttonSize, buttonSize); + higBtn->setToolTip(tooltip); + higBtn->setObjectName(uiName); + toolbar->addWidget(higBtn, 0, buttonAlignment); + return higBtn; + }; + + _buttons._newLayer = addHIGButton( + ":/UsdLayerEditor/LE_add_layer", + StringResources::getAsQString(StringResources::kAddNewLayer), + "LayerEditorAddLayerButton"); + // clicked callback + connect( + _buttons._newLayer, + &QAbstractButton::clicked, + this, + &LayerEditorWidget::onNewLayerButtonClicked); + // update layer button on stage change + connect( + _treeView->model(), + &QAbstractItemModel::modelReset, + this, + &LayerEditorWidget::updateNewLayerButton); + // update layer button if muted state changes + connect( + _treeView->model(), + &QAbstractItemModel::dataChanged, + this, + &LayerEditorWidget::updateNewLayerButton); + // update layer button on selection change + connect( + _treeView->selectionModel(), + &QItemSelectionModel::selectionChanged, + this, + &LayerEditorWidget::updateNewLayerButton); + + // send callback notification to usdufe when selection changes + connect( + _treeView->selectionModel(), + &QItemSelectionModel::selectionChanged, + this, + &LayerEditorWidget::onSelectionChanged); + + // update layer contents widget on selection change + connect( + _treeView->selectionModel(), + &QItemSelectionModel::selectionChanged, + this, + &LayerEditorWidget::onLazyUpdateLayerContents); + + // update layer contents widget on selected layer data change + connect( + _treeView->layerTreeModel(), + &LayerTreeModel::selectedLayerDataChangedSignal, + this, + &LayerEditorWidget::onLazyUpdateLayerContents); + + _buttons._loadLayer = addHIGButton( + ":/UsdLayerEditor/LE_import_layer", + StringResources::getAsQString(StringResources::kLoadExistingLayer), + "LayerEditorImportLayerButton"); + // clicked callback + connect( + _buttons._loadLayer, + &QAbstractButton::clicked, + this, + &LayerEditorWidget::onLoadLayersButtonClicked); + + if (UsdLayerEditor::supportsEditForwarding()) { + auto separator = new QFrame(); + separator->setFrameShape(QFrame::VLine); + separator->setFrameShadow(QFrame::Sunken); + separator->setFixedHeight(buttonSize); + toolbar->addWidget(separator); + + _buttons._toggleEFButton = new QPushButton(); + _buttons._toggleEFButton->setFlat(true); + _buttons._toggleEFButton->setFixedSize(buttonSize, buttonSize); + _buttons._toggleEFButton->setToolTip( + StringResources::getAsQString(StringResources::kToggleEditForwarding)); + _buttons._toggleEFButton->setObjectName("LayerEditorToggleEFButton"); + toolbar->addWidget(_buttons._toggleEFButton, 0, buttonAlignment); + connect( + _buttons._toggleEFButton, + &QAbstractButton::clicked, + this, + &LayerEditorWidget::openEditForwardDialog); + } + + toolbar->addStretch(); + + QWidget* saveContainer = new QWidget; + auto saveLayout = new QHBoxLayout(saveContainer); + saveLayout->setContentsMargins(0, 0, 0, 0); + saveLayout->setSpacing(0); + saveLayout->addStretch(); + { + auto badgeYOffset = DPIScale(4); + auto dirtyCountBadge = new DirtyLayersCountBadge(nullptr); + auto badgeSize = QSize(buttonSize + DPIScale(12), buttonSize + badgeYOffset); + dirtyCountBadge->setFixedSize(badgeSize); + + saveLayout->addWidget(dirtyCountBadge, 0, Qt::AlignRight); + _buttons._dirtyCountBadge = dirtyCountBadge; + } + + // save stage button: contains a push button and a "badge" widget + { + auto saveButtonYOffset = DPIScale(4); + auto saveButtonSize = QSize(buttonSize + DPIScale(12), buttonSize + saveButtonYOffset); + auto saveStageBtn = new QPushButton(); + saveStageBtn->setFixedSize(saveButtonSize); + QtUtils::setupButtonWithHIGBitmaps(saveStageBtn, ":/UsdLayerEditor/LE_save_all"); + saveStageBtn->setFixedSize(buttonSize, buttonSize); + + saveStageBtn->setToolTip( + StringResources::getAsQString(StringResources::kSaveAllEditsInLayerStack)); + saveStageBtn->setObjectName("LayerEditorSaveAllButton"); + connect( + saveStageBtn, + &QAbstractButton::clicked, + this, + &LayerEditorWidget::onSaveStageButtonClicked); + + saveLayout->addWidget(saveStageBtn, 0, buttonAlignment); + _buttons._saveStageButton = saveStageBtn; + } + + toolbar->addWidget(saveContainer, 0, buttonAlignment); + + // update buttons on stage change for example dirty count + connect( + _treeView->model(), + &QAbstractItemModel::modelReset, + this, + &LayerEditorWidget::updateButtonsOnIdle); + // update dirty count on dirty notfication + connect( + _treeView->model(), + &QAbstractItemModel::dataChanged, + this, + &LayerEditorWidget::updateButtonsOnIdle); + + return toolbar; +} + +void LayerEditorWidget::setupLayout() +{ + // Horizontal splitter that will contain the Layer Editor and Display Layer + // Contents Window. + auto mainHSplitter = new QSplitter(Qt::Horizontal); + + // Main LayerEditor widget that will contain the Stage Selector widget, toolbar + // and Layer Editor tree view. + auto mainVWidget = new QWidget(); + { + _treeView = new LayerTreeView(&_sessionState, mainVWidget); + + auto mainVLayout = new QVBoxLayout(); + mainVLayout->setSpacing(DPIScale(4)); + mainVLayout->setContentsMargins(0, 0, 0, 0); + + auto stageSelector = new StageSelectorWidget(&_sessionState, mainVWidget); + mainVLayout->addWidget(stageSelector); + + auto toolbarLayout = setupLayout_toolbar(); + mainVLayout->addLayout(toolbarLayout); + + // Wrap the banner and tree view together in a framed container so they + // share the same border. + _treeContainer = new QFrame(mainVWidget); + _treeContainer->setFrameShape(QFrame::NoFrame); + _treeContainer->setFocusPolicy(Qt::NoFocus); + _treeContainer->setObjectName("layerEditorTreeContainer"); + updateTreeContainerStyle(false); + + connect( + qApp, + &QApplication::focusChanged, + this, + &LayerEditorWidget::updateTreeContainerBorder); + + auto treeContainerLayout = new QVBoxLayout(_treeContainer); + treeContainerLayout->setSpacing(0); + treeContainerLayout->setContentsMargins(0, 0, 0, 0); + + _treeView->setFrameShape(QFrame::NoFrame); + treeContainerLayout->addWidget(_treeView); + + mainVLayout->addWidget(_treeContainer); + + mainVWidget->setLayout(mainVLayout); + mainHSplitter->addWidget(mainVWidget); + } + + _layerContents = new LayerContentsWidget(this); + mainHSplitter->addWidget(_layerContents); + _layerContents->setVisible(_sessionState.displayLayerContents()); + + connect( + &_sessionState, + &SessionState::showDisplayLayerContents, + this, + &LayerEditorWidget::showDisplayLayerContents); + + connect( + &_sessionState, + &SessionState::editForwardingChanged, + this, + &LayerEditorWidget::updateButtonsOnIdle); + + // Follow the Layer editor selection in the EF config dialog. + if (UsdLayerEditor::supportsEditForwarding()) { + connect(&_sessionState, &SessionState::currentStageChangedSignal, this, [this]() { + if (UsdLayerEditor::isEditForwardDialogOpen()) + UsdLayerEditor::openEditForwardDialog(_sessionState.stage()); + }); + } + + auto mainLayout = new QVBoxLayout(this); + mainLayout->setSpacing(0); + mainLayout->setContentsMargins(DPIScale(4), DPIScale(4), DPIScale(4), DPIScale(4)); + mainLayout->addWidget(mainHSplitter); + setLayout(mainLayout); + + connect(mainHSplitter, &QSplitter::splitterMoved, this, &LayerEditorWidget::onSplitterMoved); + + updateNewLayerButton(); + updateButtons(); +} + +void LayerEditorWidget::updateTreeContainerBorder(QWidget*, QWidget* now) +{ + if (!_treeContainer) + return; + + const bool focused = now && _treeContainer->isAncestorOf(now); + updateTreeContainerStyle(focused); +} + +void LayerEditorWidget::updateTreeContainerStyle(bool focused) +{ + // Also mimic the selection highlight of treeview around both the banner and tree. + static const QString baseStyle + = "QFrame#layerEditorTreeContainer { border: 2px solid rgb(55, 55, 55); }"; + static const QString focusStyle + = "QFrame#layerEditorTreeContainer { border: 1px solid palette(highlight); }"; + + if (!_treeContainer) + return; + + _treeContainer->setStyleSheet(focused ? focusStyle : baseStyle); + + // When highlighted we want the border to be a single pixel wide, so adjust the + // margin to avoid the content moving. + auto layout = _treeContainer->layout(); + if (!layout) + return; + + const int margin = focused ? 1 : 0; + layout->setContentsMargins(margin, margin, margin, margin); +} + +void LayerEditorWidget::openEditForwardDialog() +{ + // The DCC integration registers how to open its EF configuration dialog. + UsdLayerEditor::openEditForwardDialog(_sessionState.stage()); +} + +void LayerEditorWidget::updateButtonsOnIdle() +{ + if (!_updateButtonsOnIdle) { + _updateButtonsOnIdle = true; + QTimer::singleShot(0, this, &LayerEditorWidget::updateButtons); + } +} + +void LayerEditorWidget::updateNewLayerButton() +{ + bool disabled = _treeView->model()->rowCount() == 0; + if (!disabled) { + auto selectionModel = _treeView->selectionModel(); + // enabled if no or one layer selected + disabled = selectionModel->selectedRows().size() > 1; + + // also disable if the selected layer is muted or invalid + if (!disabled) { + auto selectedRows = selectionModel->selectedRows(); + LayerTreeItem* item = nullptr; + if (!selectedRows.empty()) { + item = _treeView->layerItemFromIndex(selectedRows[0]); + } + // When nothing is selected, the button adds a new layer to the root layer. + else { + auto treeModel = _treeView->layerTreeModel(); + item = treeModel->layerItemFromIndex(treeModel->rootLayerIndex()); + } + + if (item) { + disabled = item->isInvalidLayer() || item->appearsMuted() || item->isReadOnly() + || item->isLocked(); + } else { + // if for whatever reason we get an invalid item + // we should disable the buttons as well + disabled = true; + } + } + } + _buttons._newLayer->setDisabled(disabled); + _buttons._loadLayer->setDisabled(disabled); +} + +void LayerEditorWidget::updateButtons() +{ + if (UsdLayerEditor::isDccObjectSharedStage( + _sessionState.stageEntry()._dccObjectPath)) { + if (_buttons._dirtyCountBadge) { + _buttons._dirtyCountBadge->setVisible(true); + } + if (_buttons._saveStageButton) { + _buttons._saveStageButton->setVisible(true); + } + + int count = 0; + + // Special case for components created by the component creator. Non-local layers, + // non-active layers, and non-dirty but to be renamed layers, can be impacted when + // saving a component. Only the component creator knows how to save a component + // properly, we need to ask it what layers will be impacted. + if (UsdLayerEditor::isStageAComponent(_sessionState.stageEntry()._dccObjectPath)) { + const auto layerIds = UsdLayerEditor::getComponentLayersToSave( + _sessionState.stageEntry()._dccObjectPath); + count = static_cast(layerIds.size()); + } else { + const auto layers = _treeView->layerTreeModel()->getAllNeedsSavingLayers(); + count = static_cast(layers.size()); + for (auto layer : layers) { + // The system locked layers do not count towards saving. + if (layer->isSystemLocked()) { + count--; + } + // Neither does any anonymous layer whose parent is locked or system-locked. + // This is because saving an anonymous layer will cause + // the parent layer to re-path the sub layer with a file name. + if (layer->isAnonymous() + && (layer->appearsLocked() || layer->appearsSystemLocked())) { + count--; + } + } + } + _buttons._dirtyCountBadge->updateCount(count); + bool disable = count == 0; + QtUtils::disableHIGButton(_buttons._saveStageButton, disable); + } else { + if (_buttons._dirtyCountBadge) { + _buttons._dirtyCountBadge->setVisible(false); + } + if (_buttons._saveStageButton) { + _buttons._saveStageButton->setVisible(false); + } + } + if (_buttons._toggleEFButton) { + const bool efActive = _sessionState.isEditForwardMode(); + const auto baseName = efActive ? ":/UsdLayerEditor/ef_on" : ":/UsdLayerEditor/ef_default"; + _buttons._toggleEFButton->setStyleSheet( + QString("QPushButton { padding: %1px; background-image: url(%2); " + "background-position: center center; background-repeat: no-repeat; " + "border: 0px; background-origin: content; }") + .arg(DPIScale(4)) + .arg(QtUtils::getDPIPixmapName(baseName))); + } + + _updateButtonsOnIdle = false; +} + +void LayerEditorWidget::onNewLayerButtonClicked() +{ + // if nothing or root selected, add new layer to top of root + auto model = _treeView->layerTreeModel(); + auto selectionModel = _treeView->selectionModel(); + auto selection = selectionModel->selectedRows(); + bool addToRoot = false; + + LayerTreeItem* layerTreeItem; + if (selection.size() == 0) { + addToRoot = true; + layerTreeItem = model->layerItemFromIndex(model->rootLayerIndex()); + } else { + layerTreeItem = model->layerItemFromIndex(selection[0]); + // this test catches both root layer and session layer + if (layerTreeItem->parent() == nullptr) { + addToRoot = true; + } + } + + if (addToRoot) { + layerTreeItem->addAnonymousSublayer(_treeView); + } else { + // Single undo bracket spanning the add + reorder. + UndoContext context(_sessionState.commandHook(), "Add Anonymous Layer"); + auto parentItem = layerTreeItem->parentLayerItem(); + int rowToInsert = layerTreeItem->row(); + pxr::SdfLayerRefPtr newLayer = context.hook()->addAnonymousSubLayer( + parentItem->layer(), model->findNameForNewAnonymousLayer()); + + // move it to the right place, if it's not top + if (rowToInsert > 0 && newLayer) { + context.hook()->removeSubLayerPath(parentItem->layer(), newLayer->GetIdentifier()); + context.hook()->insertSubLayerPath( + parentItem->layer(), newLayer->GetIdentifier(), rowToInsert); + } + if (newLayer) + model->selectUsdLayerOnIdle(newLayer); + } +} + +void LayerEditorWidget::onLoadLayersButtonClicked() +{ + const auto model = _treeView->layerTreeModel(); + const auto selectionModel = _treeView->selectionModel(); + const auto selection = selectionModel->selectedRows(); + LayerTreeItem* layerTreeItem; + if (selection.size() == 0) { + layerTreeItem = model->layerItemFromIndex(model->rootLayerIndex()); + } else { + layerTreeItem = model->layerItemFromIndex(selection[0]); + } + layerTreeItem->loadSubLayers(this); +} + +void LayerEditorWidget::onSaveStageButtonClicked() { _treeView->layerTreeModel()->saveStage(this); } + + +void LayerEditorWidget::onSelectionChanged( + const QItemSelection& selected, + const QItemSelection& deselected) +{ + if (!UsdUfe::isUICallbackRegistered(TfToken("onLayerEditorSelectionChanged"))) { + return; + } + + const std::vector selectedLayerIDs = getSelectedLayers(); + + PXR_NS::VtDictionary callbackContext; + callbackContext["objectPath"] = PXR_NS::VtValue(UsdUfe::stagePath(_sessionState.stageEntry()._stage).string().c_str()); + PXR_NS::VtDictionary callbackData; + + VtStringArray layerIds(selectedLayerIDs.begin(), selectedLayerIDs.end()); + callbackData["layerIds"] = layerIds; + + UsdUfe::triggerUICallback(TfToken("onLayerEditorSelectionChanged"), callbackContext, callbackData); +} + +std::vector LayerEditorWidget::getSelectedLayers() +{ + auto selectedLayerItems = _treeView->getSelectedLayerItems(); + std::vector selectedLayerIDs; + selectedLayerIDs.reserve(selectedLayerItems.size()); + for (const auto& item : selectedLayerItems) { + if (item) { + selectedLayerIDs.emplace_back(item->layer()->GetIdentifier()); + } + } + return selectedLayerIDs; +} + +void LayerEditorWidget::selectLayers(const std::vector& layerIdentifiers) +{ + const auto model = _treeView->layerTreeModel(); + const auto selectionModel = _treeView->selectionModel(); + + // clear selection first + selectionModel->clearSelection(); + + if (layerIdentifiers.empty()) { + // Also clear the current index so getSelectedLayerItems() doesn't + // return it via the "currentIndex not in selection" fallback path. + selectionModel->setCurrentIndex(QModelIndex(), QItemSelectionModel::NoUpdate); + return; + } + + // apply selection if layer exists in stage + QItemSelection* selection = nullptr; + for (const auto& layerId : layerIdentifiers) { + const auto sdfLayer = SdfLayer::Find(layerId); + if (sdfLayer) { + if (const auto item = model->findUSDLayerItem(sdfLayer)) { + auto index = item->index(); + if (nullptr == selection) { + selection = new QItemSelection(); + + // Set the current index to the first item in the selection. + // This is necessary since the other command flags (like isSessionLayer) act + // on the current index. + selectionModel->setCurrentIndex(index, QItemSelectionModel::NoUpdate); + } + selection->select(index, index); + } + } + } + if (selection != nullptr) { + selectionModel->select( + *selection, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); + delete selection; + } +} + +void LayerEditorWidget::showDisplayLayerContents(bool showIt) +{ + // Update the menu action to reflect the current state. + // But don't send any Signal. + if (nullptr != _actions._displayLayerContents) { + QSignalBlocker blocker(_actions._displayLayerContents); + _actions._displayLayerContents->setChecked(showIt); + } + + if (_layerContents) { + _layerContents->setVisible(showIt); + } + if (showIt) { + // Use lazy method in order to allow the window to become visible first. + onLazyUpdateLayerContents(); + } +} + +void LayerEditorWidget::onLazyUpdateLayerContents() +{ + // Start (or restart) the timer to update the layer contents widget. + _layerContentsTimer.start(500, this); +} + +void LayerEditorWidget::timerEvent(QTimerEvent* event) +{ + if (event->timerId() == _layerContentsTimer.timerId()) { + _layerContentsTimer.stop(); + updateLayerContentsWidget(); + } else { + QWidget::timerEvent(event); + } +} + +void LayerEditorWidget::updateLayerContentsWidget() +{ + if (!_layerContents) { + return; + } + // If the layer contents widget is not visible, we don't need to update it. + if (_layerContents->isVisible() && (_layerContents->width() > 0)) { + // Update the layer contents widget with the current selection + // if there is one selected item. + const auto selection = _treeView->selectionModel()->selectedRows(); + if (selection.size() == 1) { + const auto model = _treeView->layerTreeModel(); + auto layerTreeItem = model->layerItemFromIndex(selection[0]); + _layerContents->setLayer( + layerTreeItem->layer(), _sessionState.displayLayerExpandAllValues()); + } else { + // If there is no selection or multiple items selected, clear the contents. + _layerContents->setLayer(nullptr); + } + } +} + +void LayerEditorWidget::onSplitterMoved(int pos, int index) +{ + if (index == 1 && _layerContents) { + // If the user collapsed the layer contents pane, we disable the contents. + auto w = _layerContents->width(); + if (w == 0) { + _layerContents->clear(); + } + // If the user expanded the layer contents pane, and it is empty, we update it. + else if ((w > 0) && _layerContents->isEmpty()) { + // Lazy update to allow the user to continue resizing panel. + onLazyUpdateLayerContents(); + } + } +} + +} // namespace UsdLayerEditor diff --git a/lib/usdLayerEditor/lib/layerEditorWidget.h b/lib/usdLayerEditor/lib/layerEditorWidget.h index 94bb78f67e..f6d1f931c9 100644 --- a/lib/usdLayerEditor/lib/layerEditorWidget.h +++ b/lib/usdLayerEditor/lib/layerEditorWidget.h @@ -20,17 +20,23 @@ #include "layerTreeItem.h" #include "layerTreeView.h" + +#include #include #include +class QFrame; +class QLabel; class QMainWindow; class QLayout; class QPushButton; +class QAction; namespace UsdLayerEditor { class DirtyLayersCountBadge; class LayerTreeView; class SessionState; +class LayerContentsWidget; /** * @brief Widget that manages a menu, a combo box to select a USD stage, and USD Layer Tree view @@ -38,11 +44,14 @@ class SessionState; * This widget is meant to be hosted by a parent QMainWindow, where the menu will be created **/ -class LayerEditorAPI LayerEditorWidget : public QWidget +class LayerEditorAPI LayerEditorWidget + : public QWidget + , public PXR_NS::TfWeakBase { Q_OBJECT public: explicit LayerEditorWidget(SessionState& in_sessionState, QMainWindow* in_parent = nullptr); + ~LayerEditorWidget() override; Q_SIGNALS: @@ -51,6 +60,9 @@ public Q_SLOTS: void onLoadLayersButtonClicked(); void onSaveStageButtonClicked(); void updateButtonsOnIdle(); + void showDisplayLayerContents(bool show); + void onSplitterMoved(int pos, int index); + void onLazyUpdateLayerContents(); public: LayerTreeView* layerTree() { return _treeView.data(); } @@ -67,7 +79,17 @@ public Q_SLOTS: QPushButton* _loadLayer; QPushButton* _saveStageButton; DirtyLayersCountBadge* _dirtyCountBadge; + QPushButton* _toggleEFButton { nullptr }; } _buttons; + + void setupDefaultMenu(QMainWindow* in_parent); + struct + { + QAction* _autoHide { nullptr }; + QAction* _displayLayerContents { nullptr }; + QAction* _displayLayerExpandAllValues { nullptr }; + QAction* _echoEditForwarding { nullptr }; + } _actions; void updateNewLayerButton(); void updateButtons(); @@ -75,9 +97,20 @@ public Q_SLOTS: const QItemSelection& selected, const QItemSelection& deselected); - QPointer _treeView; + void timerEvent(QTimerEvent* event) override; + void updateLayerContentsWidget(); + void updateTreeContainerStyle(bool focused); + void updateTreeContainerBorder(QWidget* previous, QWidget* now); + + QPointer _treeContainer; + QPointer _treeView; + QPointer _layerContents; + QBasicTimer _layerContentsTimer; bool _updateButtonsOnIdle = false; // true if request to update on idle is pending + +private: + void openEditForwardDialog(); }; } // namespace UsdLayerEditor diff --git a/lib/usdLayerEditor/lib/layerEditorWidgetManager.h b/lib/usdLayerEditor/lib/layerEditorWidgetManager.h index 4a3a1257a0..5c1726c292 100644 --- a/lib/usdLayerEditor/lib/layerEditorWidgetManager.h +++ b/lib/usdLayerEditor/lib/layerEditorWidgetManager.h @@ -13,6 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. // +#ifndef LAYER_EDITOR_WIDGETMANAGER_H +#define LAYER_EDITOR_WIDGETMANAGER_H #include "layerEditorAPI.h" @@ -44,4 +46,6 @@ class LayerEditorAPI LayerEditorWidgetManager static std::unique_ptr instance; }; -} // namespace UsdLayerEditor \ No newline at end of file +} // namespace UsdLayerEditor + +#endif // LAYER_EDITOR_WIDGETMANAGER_H \ No newline at end of file diff --git a/lib/usdLayerEditor/lib/layerEditorWindow.cpp b/lib/usdLayerEditor/lib/layerEditorWindow.cpp index 83fad64f6b..f7393dbae9 100644 --- a/lib/usdLayerEditor/lib/layerEditorWindow.cpp +++ b/lib/usdLayerEditor/lib/layerEditorWindow.cpp @@ -22,6 +22,7 @@ #include "stringResources.h" #include +#include #include namespace UsdLayerEditor { @@ -232,24 +233,27 @@ void LayerEditorWindow::selectPrimsWithSpec() void LayerEditorWindow::buildContextMenu(const QPoint& pos) { const bool singleSelect = treeView()->selectionModel()->selectedRows().size() == 1; - const auto isInvalid = isInvalidLayer(); + const bool multiSelect = treeView()->selectionModel()->selectedRows().size() > 1; + const auto isInvalid = isInvalidLayer(); + const auto hasSublayers = layerHasSubLayers(); const auto menu = new QMenu(); - const auto needsSaving = layerNeedsSaving(); - const auto isReadOnly = layerIsReadOnly(); - const auto isLocked = layerIsLocked(); - const auto isSystemLocked = layerIsSystemLocked(); - const auto isSublayer = isSubLayer(); - const auto appearsLocked = layerAppearsLocked(); + const auto needsSaving = layerNeedsSaving(); + const auto isReadOnly = layerIsReadOnly(); + const auto isLocked = layerIsLocked(); + const auto isSystemLocked = layerIsSystemLocked(); + const auto isSublayer = isSubLayer(); + const auto isSession = isSessionLayer(); + const auto isAnonymous = isAnonymousLayer(); + const auto appearsLocked = layerAppearsLocked(); const auto appearsSystemLocked = layerAppearsSystemLocked(); - const auto appearsMuted = layerAppearsMuted(); + const auto appearsMuted = layerAppearsMuted(); auto addRemoveLayerAction = [this, &menu, &isReadOnly, &appearsLocked, &appearsSystemLocked] { - QString label = QObject::tr("Remove"); + QString label = QObject::tr("Remove"); auto action = menu->addAction(label); - bool enabled = !isReadOnly && !appearsLocked && !appearsSystemLocked; - action->setEnabled(enabled); + action->setEnabled(!isReadOnly && !appearsLocked && !appearsSystemLocked); QObject::connect(action, &QAction::triggered, [this]() { removeSubLayer(); }); }; @@ -259,103 +263,117 @@ void LayerEditorWindow::buildContextMenu(const QPoint& pos) return; } - { - QString label; - label = isAnonymousLayer() ? QObject::tr("Save As...") : QObject::tr("Save Edits"); - bool enable = singleSelect && needsSaving && !isSystemLocked; + // Save / Reload — not shown for session layer + if (!isSession) { + QString label = isAnonymous ? QObject::tr("Save As...") : QObject::tr("Save Edits"); + bool enable = singleSelect && needsSaving && !isSystemLocked; + if (isAnonymous) + enable = enable && !appearsLocked && !appearsSystemLocked; auto action = menu->addAction(label); action->setEnabled(enable); QObject::connect(action, &QAction::triggered, [this]() { saveEdits(); }); } - if (!isAnonymousLayer()) { - QString label = QObject::tr("Reload"); - auto action = menu->addAction(label); + if (!isAnonymous) { + auto action = menu->addAction(QObject::tr("Reload")); QObject::connect(action, &QAction::triggered, [this]() { discardEdits(); }); } + menu->addSeparator(); + + // Sublayer management { - QString label = QObject::tr("Load Sublayers..."); - auto action = menu->addAction(label); - const auto enabled - = singleSelect && !appearsMuted && !isReadOnly && !isLocked && !isSystemLocked; - action->setEnabled(enabled); - QObject::connect(action, &QAction::triggered, [this]() { loadSubLayers(); }); - } - - if (layerHasSubLayers()) { - QString label = QObject::tr("Merge with Sublayers"); - auto action = menu->addAction(label); + auto action = menu->addAction(QObject::tr("Add Sublayer")); const bool enabled = !appearsMuted && !isReadOnly && !isLocked && !isSystemLocked; action->setEnabled(enabled); - QObject::connect(action, &QAction::triggered, [this]() { mergeWithSublayers(); }); + QObject::connect(action, &QAction::triggered, [this]() { addAnonymousSublayer(); }); } { - const bool multiSelect = treeView()->selectionModel()->selectedRows().size() > 1; - if (multiSelect && !singleSelect) { - QString label = StringResources::getAsQString(StringResources::kMenuStitchLayers); - auto action = menu->addAction(label); - bool enabled = !isReadOnly && !appearsLocked && !appearsSystemLocked && !appearsMuted; - action->setEnabled(enabled); - QObject::connect(action, &QAction::triggered, [this]() { stitchLayers(); }); - } + auto action = menu->addAction(QObject::tr("Add Parent Layer")); + const bool enabled = isSublayer && !appearsMuted && !isReadOnly && !appearsLocked && !appearsSystemLocked; + action->setEnabled(enabled); + QObject::connect(action, &QAction::triggered, [this]() { addParentLayer(); }); } - - // TODO LE-EXACT Context menus to add parent layer { - QString label = QObject::tr("Add Sublayer"); - auto action = menu->addAction(label); - const auto enabled - = singleSelect && !appearsMuted && !isReadOnly && !isLocked && !isSystemLocked; + auto action = menu->addAction(QObject::tr("Load Sublayers...")); + const bool enabled = singleSelect && !appearsMuted && !isReadOnly && !isLocked && !isSystemLocked; action->setEnabled(enabled); - QObject::connect(action, &QAction::triggered, [this]() { addAnonymousSublayer(); }); + QObject::connect(action, &QAction::triggered, [this]() { loadSubLayers(); }); + } + + if (multiSelect && !singleSelect) { + QString label = StringResources::getAsQString(StringResources::kMenuStitchLayers); + auto action = menu->addAction(label); + bool enabled = !isReadOnly && !isLocked && !appearsSystemLocked && !appearsMuted; + action->setEnabled(enabled); + QObject::connect(action, &QAction::triggered, [this]() { stitchLayers(); }); } + if (hasSublayers) { + auto action = menu->addAction(QObject::tr("Merge with Sublayers")); + const bool enabled = !appearsMuted && !isReadOnly && !isLocked && !isSystemLocked; + action->setEnabled(enabled); + QObject::connect(action, &QAction::triggered, [this]() { mergeWithSublayers(); }); + } + + menu->addSeparator(); + + // Mute / Lock if (isSublayer) { - QString label; - if (layerIsMuted()) { - label = QObject::tr("Unmute"); - } else { - label = QObject::tr("Mute"); - } - auto action = menu->addAction(label); + QString label = layerIsMuted() ? QObject::tr("Unmute") : QObject::tr("Mute"); + auto action = menu->addAction(label); QObject::connect(action, &QAction::triggered, [this]() { muteLayer(); }); } - if (!isSessionLayer()) { - QString label; - if (isLocked) { - label = QObject::tr("Unlock"); - } else { - label = QObject::tr("Lock"); + if (!isSession) { + { + QString label = isLocked ? QObject::tr("Unlock") : QObject::tr("Lock"); + auto action = menu->addAction(label); + action->setEnabled(!isSystemLocked); + QObject::connect(action, &QAction::triggered, [this]() { lockLayer(); }); } - auto action = menu->addAction(label); - action->setEnabled(!isSystemLocked); - QObject::connect(action, &QAction::triggered, [this]() { lockLayer(); }); - } - // TODO LE-EXACT Context menu lock layers AND sublayers. + if (hasSublayers) { + QString label = isLocked ? QObject::tr("Unlock Layer and Sublayers") + : QObject::tr("Lock Layer and Sublayers"); + auto action = menu->addAction(label); + action->setEnabled(!isSystemLocked); + QObject::connect(action, &QAction::triggered, [this]() { lockLayerAndSubLayers(); }); + } + } { - QString label = QObject::tr("Print to Listener"); - auto action = menu->addAction(label); + auto action = menu->addAction(QObject::tr("Print to Listener")); QObject::connect(action, &QAction::triggered, [this]() { printLayer(); }); } + menu->addSeparator(); + { - QString label = QObject::tr("Clear"); - auto action = menu->addAction(label); - bool enabled = !isReadOnly && !isLocked && !isSystemLocked; - action->setEnabled(enabled); - QObject::connect(action, &QAction::triggered, [this]() { clearLayer(); }); + auto action = menu->addAction(QObject::tr("Select Prims with Spec")); + QObject::connect(action, &QAction::triggered, [this]() { selectPrimsWithSpec(); }); + } + + // DCC-specific extensions (e.g. "Select Incoming Node" in Maya) + addDCCContextMenuItems(menu); + + if (isSublayer || !isAnonymous) { + menu->addSeparator(); } if (isSublayer) { addRemoveLayerAction(); } + { + auto action = menu->addAction(QObject::tr("Clear")); + bool enabled = !isReadOnly && !isLocked && !isSystemLocked; + action->setEnabled(enabled); + QObject::connect(action, &QAction::triggered, [this]() { clearLayer(); }); + } + menu->exec(_layerEditor->layerTree()->viewport()->mapToGlobal(pos)); } diff --git a/lib/usdLayerEditor/lib/layerEditorWindow.h b/lib/usdLayerEditor/lib/layerEditorWindow.h index f20490d1b3..a2b591d05d 100644 --- a/lib/usdLayerEditor/lib/layerEditorWindow.h +++ b/lib/usdLayerEditor/lib/layerEditorWindow.h @@ -23,6 +23,8 @@ #include +class QMenu; + namespace UsdLayerEditor { class LayerEditorWidget; @@ -75,6 +77,10 @@ class LayerEditorAPI LayerEditorWindow : public AbstractLayerEditorWindow protected: + // Override to append DCC-specific items after "Select Prims with Spec". + // Default implementation does nothing. + virtual void addDCCContextMenuItems(QMenu* /*menu*/) { } + QPointer _layerEditor; std::string _panelName; diff --git a/lib/usdLayerEditor/lib/layerMuting.cpp b/lib/usdLayerEditor/lib/layerMuting.cpp index 901bf8fb25..1b2ff0f4aa 100644 --- a/lib/usdLayerEditor/lib/layerMuting.cpp +++ b/lib/usdLayerEditor/lib/layerMuting.cpp @@ -18,23 +18,41 @@ #include +#include + PXR_NAMESPACE_USING_DIRECTIVE namespace UsdLayerEditor { namespace { -// The set of muted layers. -// // Kept in a function to avoid problem with the order of construction // of global variables in C++. -using MutedLayers = std::set; -MutedLayers& getMutedLayers() +using MutedLayers = std::unordered_map; +MutedLayers& getMutedLayersMap() { // Note: C++ guarantees correct multi-thread protection for static // variables initialization in functions. static MutedLayers layers; return layers; } + +void holdMutedLayers(const PXR_NS::SdfLayerRefPtr& layer, LayerRefSet& heldLayers) +{ + // Non-dirty, non-anonymous layers can be reloaded, so we + // won't hold onto them. + const bool needHolding = (layer->IsDirty() || layer->IsAnonymous()); + if (needHolding) + heldLayers.insert(layer); + + // Hold onto sub-layers as well, in case they are dirty or anonymous. + // Note: the GetSubLayerPaths function returns proxies, so we have to + // hold the std::string by value, not reference. + for (const std::string subLayerPath : layer->GetSubLayerPaths()) { + auto subLayer = SdfLayer::FindRelativeToLayer(layer, subLayerPath); + if (subLayer) + holdMutedLayers(subLayer, heldLayers); + } +} } // namespace void loadLayerMuteState( @@ -71,56 +89,49 @@ void loadLayerMuteState( stage.MuteAndUnmuteLayers(remapped, unmuted); } -void addMutedLayer(const PXR_NS::SdfLayerRefPtr& layer) +bool addMutedLayer(const PXR_NS::SdfLayerRefPtr& layer) { if (!layer) - return; + return false; - // Non-dirty, non-anonymous layers can be reloaded, so we - // won't hold onto them. - const bool needHolding = (layer->IsDirty() || layer->IsAnonymous()); - if (!needHolding) - return; + MutedLayers& mutedLayers = getMutedLayersMap(); - MutedLayers& layers = getMutedLayers(); - layers.insert(layer); + // Hold the layer dirty graph, only the first time we see this mute ancestor. + auto ret = mutedLayers.emplace(layer->GetIdentifier(), LayerRefSet {}); + if (ret.second) + holdMutedLayers(layer, ret.first->second); - // Hold onto sub-layers as well, in case they are dirty or anonymous. - // Note: the GetSubLayerPaths function returns proxies, so we have to - // hold the std::string by value, not reference. - for (const std::string subLayerPath : layer->GetSubLayerPaths()) { - auto subLayer = SdfLayer::FindRelativeToLayer(layer, subLayerPath); - addMutedLayer(subLayer); - } + return ret.second; } -void removeMutedLayer(const PXR_NS::SdfLayerRefPtr& layer) +bool removeMutedLayer(const PXR_NS::SdfLayerRefPtr& layer) { if (!layer) - return; + return false; - MutedLayers& layers = getMutedLayers(); - layers.erase(layer); + MutedLayers& layers = getMutedLayersMap(); - // Stop holding onto sub-layers as well, in case they were previously - // dirty or anonymous. - // - // Note: we don't check the dirty or anonymous status while removing - // in case the status changed. Trying to remove a layer that - // was not held has no consequences. - // - // Note: the GetSubLayerPaths function returns proxies, so we have to - // hold the std::string by value, not reference. - for (const std::string subLayerPath : layer->GetSubLayerPaths()) { - auto subLayer = SdfLayer::FindRelativeToLayer(layer, subLayerPath); - removeMutedLayer(subLayer); - } + // Stop holding the layers rooted at this layer. + return (layers.erase(layer->GetIdentifier()) > 0); } void forgetMutedLayers() { - MutedLayers& layers = getMutedLayers(); + MutedLayers& layers = getMutedLayersMap(); layers.clear(); } +const LayerRefSet& getMutedLayers(const std::string& mutedIdentifier) +{ + const MutedLayers& mutedLayers = getMutedLayersMap(); + + const auto foundSet = mutedLayers.find(mutedIdentifier); + if (foundSet == mutedLayers.end()) { + static const LayerRefSet kEmpty; + return kEmpty; + } + + return foundSet->second; +} + } // namespace UsdLayerEditor diff --git a/lib/usdLayerEditor/lib/layerMuting.h b/lib/usdLayerEditor/lib/layerMuting.h index 3765611361..b6a6167e6d 100644 --- a/lib/usdLayerEditor/lib/layerMuting.h +++ b/lib/usdLayerEditor/lib/layerMuting.h @@ -21,6 +21,9 @@ #include #include +#include +#include + namespace UsdLayerEditor { using LayerNameMap = std::map; @@ -52,12 +55,22 @@ LayerEditorAPI void loadLayerMuteState( // So we need to hold on to muted layers. We do this in a private global list // of muted layers. That list gets cleared when a new Maya scene is created. -LayerEditorAPI void addMutedLayer(const PXR_NS::SdfLayerRefPtr& layer); +LayerEditorAPI bool addMutedLayer(const PXR_NS::SdfLayerRefPtr& layer); -LayerEditorAPI void removeMutedLayer(const PXR_NS::SdfLayerRefPtr& layer); +LayerEditorAPI bool removeMutedLayer(const PXR_NS::SdfLayerRefPtr& layer); LayerEditorAPI void forgetMutedLayers(); +/*! Set of layer reference pointers. + */ +using LayerRefSet = std::set; + +/*! Returns layers held due to muting layer \p mutedIdentifier in a USD stage, + * includes the muted root (if dirty/anonymous) and all recorded descendants + * in its sublayer hierarchy. + */ +LayerEditorAPI const LayerRefSet& getMutedLayers(const std::string& mutedIdentifier); + } // namespace UsdLayerEditor #endif diff --git a/lib/usdLayerEditor/lib/layerTreeItem.cpp b/lib/usdLayerEditor/lib/layerTreeItem.cpp index 2e13dfea1e..84d5aaffee 100644 --- a/lib/usdLayerEditor/lib/layerTreeItem.cpp +++ b/lib/usdLayerEditor/lib/layerTreeItem.cpp @@ -17,6 +17,7 @@ #include "layerTreeItem.h" #include "abstractCommandHook.h" +#include "layerEditorDCCFunctions.h" #include "layerLocking.h" #include "layerTreeModel.h" #include "loadLayersDialog.h" @@ -27,7 +28,6 @@ #include "tokens.h" #include "utilUI.h" #include "utilFileSystem.h" -#include "utilOptions.h" #include "utilQT.h" #include "utilSerialization.h" #include "warningDialogs.h" @@ -37,7 +37,11 @@ #include #if PXR_VERSION >= 2308 +#include +#include +#include #include +#include #endif #include @@ -161,32 +165,26 @@ void LayerTreeItem::populateChildren(RecursionDetector* recursionDetector) for (auto const path : subPaths) { #if PXR_VERSION >= 2308 - // Resolve any variable expressions in the path using the stage's expression variables + // Resolve any variable expressions in the path using the stage's expression variables, + // composed from root and session layer variables. std::string resolvedPath = path; if (_stage && SdfVariableExpression::IsExpression(path)) { - auto resolveExprVarsFromLayer = [](SdfVariableExpression& varExpr, SdfLayerRefPtr fromLayer, std::string& outPath) { - if (fromLayer && fromLayer->HasExpressionVariables()) { - auto expressionVars = fromLayer->GetExpressionVariables(); - auto result = varExpr.Evaluate(expressionVars); - if (result.errors.empty() && !result.value.IsEmpty()) { - outPath = result.value.UncheckedGet(); - } + const auto stageRootLayerStack + = _stage->GetPseudoRoot().GetPrimIndex().GetRootNode().GetLayerStack(); + + if (stageRootLayerStack) { + const auto& expressionVars + = stageRootLayerStack->GetExpressionVariables().GetVariables(); + + const auto result + = SdfVariableExpression(path).EvaluateTyped(expressionVars); + + if (result.errors.empty() && !result.value.IsEmpty()) { + resolvedPath = result.value.UncheckedGet(); } - }; - - SdfVariableExpression varExpr(path); - // Get the root layer's expression variables for resolution context - auto rootLayer = _stage->GetRootLayer(); - resolveExprVarsFromLayer(varExpr, rootLayer, resolvedPath); - - // Expression variables are composed across session layer and root - // layer of a stage. So we do another pass with the session layer - // to override/set the resolvedPath in case it is present in the - // session layer - auto sessionLayer = _stage->GetSessionLayer(); - resolveExprVarsFromLayer(varExpr, sessionLayer, resolvedPath); + } } - + std::string actualPath = SdfComputeAssetPathRelativeToLayer(_layer, resolvedPath); auto subLayer = SdfLayer::FindOrOpen(actualPath); #else @@ -220,6 +218,39 @@ LayerItemVector LayerTreeItem::childrenVector() const return result; } +bool LayerTreeItem::isIdenticalItem(const LayerTreeItem* other) const +{ + if (!other) { + return false; + } + + if (this == other) { + return true; + } + + if (layer() != other->layer()) { + return false; + } + + if (_isSharedStage != other->_isSharedStage) { + return false; + } + + auto myChildren = childrenVector(); + auto otherChildren = other->childrenVector(); + if (myChildren.size() != otherChildren.size()) { + return false; + } + + for (size_t i = 0; i < myChildren.size(); i++) { + if (!myChildren[i]->isIdenticalItem(otherChildren[i])) { + return false; + } + } + + return true; +} + // recursively update the target layer data member. Meant to be called from invisibleRoot void LayerTreeItem::updateTargetLayerRecursive(const PXR_NS::SdfLayerRefPtr& newTargetLayer) { @@ -261,7 +292,7 @@ void LayerTreeItem::fetchData(RebuildChildren in_rebuild, RecursionDetector* in_ QVariant LayerTreeItem::data(int role) const { switch (role) { -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +#if QT_DISABLE_DEPRECATED_BEFORE || QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) case Qt::ForegroundRole: return QApplication::palette().color(QPalette::ButtonText); #else case Qt::TextColorRole: return QApplication::palette().color(QPalette::ButtonText); @@ -289,6 +320,18 @@ bool LayerTreeItem::isMuted() const return isInvalidLayer() || !_stage ? false : _stage->IsLayerMuted(_layer->GetIdentifier()); } +bool LayerTreeItem::isAnonymous() const +{ + if (LayerTreeModel* model = parentModel()) { + if (SessionState* ss = model->sessionState()) { + if (UsdLayerEditor::isStageAComponent(ss->stageEntry()._dccObjectPath)) { + return UsdLayerEditor::isUnsavedComponent(ss->stage()); + } + } + } + return _layer ? _layer->IsAnonymous() : false; +} + bool LayerTreeItem::appearsMuted() const { if (isMuted()) { @@ -399,15 +442,24 @@ void LayerTreeItem::getActionButton(LayerActionType actionType, LayerActionInfo& } } -void LayerTreeItem::removeSubLayer() +void LayerTreeItem::removeSubLayer(QWidget* /*in_parent*/) { if (isSublayer()) { // can't remove session or root layer commandHook()->removeSubLayerPath(parentLayerItem()->layer(), subLayerPath()); } } -void LayerTreeItem::saveEdits() +void LayerTreeItem::saveEdits(QWidget* in_parent) { + if (LayerTreeModel* model = parentModel()) { + if (SessionState* ss = model->sessionState()) { + if (UsdLayerEditor::isStageAComponent(ss->stageEntry()._dccObjectPath)) { + model->saveStage(in_parent); + return; + } + } + } + bool shouldSaveEdits = true; // if the current layer contains anonymous layer(s), @@ -429,17 +481,18 @@ void LayerTreeItem::saveEdits() } warningDialog( - QString::fromStdString(title), QString::fromStdString(msg), &anonymLayerNames); + QString::fromStdString(title), + QString::fromStdString(msg), + &anonymLayerNames, + QMessageBox::Icon::NoIcon, + in_parent); return; } // the layer is already saved on disk. // ask the user a confirmation before overwrite it. - static const std::string kConfirmExistingFileSave - = UsdLayerEditorOptionVars->ConfirmExistingFileSave.GetText(); - const bool showConfirmDgl = Options::optionVarExists(kConfirmExistingFileSave) - && Options::optionVarExists(kConfirmExistingFileSave) != 0; + const bool showConfirmDgl = confirmExistingFileSave(); if (showConfirmDgl && !isAnonymous()) { const std::string titleFormat = StringResources::kSaveLayerWarnTitle.value; const std::string msgFormat = StringResources::kSaveLayerWarnMsg.value; @@ -453,19 +506,21 @@ void LayerTreeItem::saveEdits() QString::fromStdString(title), QString::fromStdString(msg), nullptr /*bulletList*/, - &okButtonText); + &okButtonText, + QMessageBox::Icon::NoIcon, + in_parent); } if (shouldSaveEdits) { - saveEditsNoPrompt(); + saveEditsNoPrompt(in_parent); } } -void LayerTreeItem::saveEditsNoPrompt() +void LayerTreeItem::saveEditsNoPrompt(QWidget* in_parent) { if (isAnonymous()) { if (!isSessionLayer()) - saveAnonymousLayer(); + saveAnonymousLayer(in_parent); } else { if (!Serialization::saveLayerWithFormat(layer())) { std::string layerName(layer()->GetDisplayName().c_str()); @@ -476,20 +531,30 @@ void LayerTreeItem::saveEditsNoPrompt() } // helper to save anon layers called by saveEdits() -void LayerTreeItem::saveAnonymousLayer() -{ +void LayerTreeItem::saveAnonymousLayer(QWidget* in_parent) +{ + // Special case for components created by the component creator. Only the + // component creator knows how to save a component properly; delegate to the + // stage save. The predicate is false for DCCs without component support. + if (SessionState* ss = parentModel()->sessionState()) { + if (UsdLayerEditor::isStageAComponent(ss->stageEntry()._dccObjectPath)) { + parentModel()->saveStage(in_parent); + return; + } + } + SessionState* sessionState = parentModel()->sessionState(); // the path we have is an absolute path std::string fileName; - if (!sessionState->saveLayerUI(nullptr, &fileName, parentLayer())) + if (!sessionState->saveLayerUI(in_parent, &fileName, parentLayer())) return; Serialization::ensureUSDFileExtension(fileName); const QString dialogTitle = StringResources::getAsQString(StringResources::kSaveLayer); - if (!checkIfPathIsSafeToAdd(dialogTitle, parentLayerItem(), fileName)) + if (!checkIfPathIsSafeToAdd(in_parent, dialogTitle, parentLayerItem(), fileName)) return; Serialization::PathInfo pathInfo; @@ -508,7 +573,7 @@ void LayerTreeItem::saveAnonymousLayer() SdfLayerRefPtr newLayer = Serialization::saveAnonymousLayer( sessionState->stage(), layer(), pathInfo, layerParent, formatTag, &errMsg); if (!newLayer) { - warningDialog(dialogTitle, errMsg.c_str()); + warningDialog(dialogTitle, errMsg.c_str(), nullptr, QMessageBox::Icon::NoIcon, in_parent); return; } @@ -522,12 +587,14 @@ void LayerTreeItem::saveAnonymousLayer() model->selectUsdLayerOnIdle(newLayer); } -void LayerTreeItem::discardEdits() +void LayerTreeItem::discardEdits(QWidget* in_parent) { + bool confirmed = false; + if (isAnonymous() || !isDirty()) { // according to MAYA-104336, we don't prompt for confirmation for anonymous layers // according to EMSUSD-964, we don't prompt for confirmation if the layer is not dirty - commandHook()->discardEdits(layer()); + confirmed = true; } else { std::string title = String::format(StringResources::kReloadTitle.value, text().toStdString()); @@ -537,23 +604,39 @@ void LayerTreeItem::discardEdits() const QString buttonText = QString::fromStdString(StringResources::kReloadButtonText.value); - if (confirmDialog( - QString::fromStdString(title), - QString::fromStdString(desc), - nullptr, - &buttonText - )) { - commandHook()->discardEdits(layer()); + confirmed = confirmDialog( + QString::fromStdString(title), + QString::fromStdString(desc), + nullptr, + &buttonText, + QMessageBox::Icon::NoIcon, + in_parent); + } + + if (!confirmed) + return; + + // Special case for components created by the component creator. Only the component + // creator knows how to reload a component properly. The revert confirmation above + // is shown first, then the component is reloaded as a unit. + if (LayerTreeModel* model = parentModel()) { + if (SessionState* ss = model->sessionState()) { + if (UsdLayerEditor::isStageAComponent(ss->stageEntry()._dccObjectPath)) { + model->reloadComponent(in_parent); + return; + } } } + + commandHook()->discardEdits(layer()); } -void LayerTreeItem::addAnonymousSublayer() +void LayerTreeItem::addAnonymousSublayer(QWidget* in_parent) { - addAnonymousSublayerAndReturn(); + addAnonymousSublayerAndReturn(in_parent); } -PXR_NS::SdfLayerRefPtr LayerTreeItem::addAnonymousSublayerAndReturn() +PXR_NS::SdfLayerRefPtr LayerTreeItem::addAnonymousSublayerAndReturn(QWidget* /*in_parent*/) { UndoContext context(commandHook(), "Add Anonymous Layer"); auto model = parentModel(); @@ -585,16 +668,16 @@ void LayerTreeItem::loadSubLayers(QWidget* in_parent) } } -void LayerTreeItem::printLayer() +void LayerTreeItem::printLayer(QWidget* /*in_parent*/) { if (!isInvalidLayer()) { parentModel()->sessionState()->printLayer(layer()); } } -void LayerTreeItem::clearLayer() { commandHook()->clearLayer(layer()); } +void LayerTreeItem::clearLayer(QWidget* /*in_parent*/) { commandHook()->clearLayer(layer()); } -void LayerTreeItem::mergeWithSublayers() +void LayerTreeItem::mergeWithSublayers(QWidget* /*in_parent*/) { if (!_layer || isInvalidLayer() || !hasSubLayers() || isLocked()) return; diff --git a/lib/usdLayerEditor/lib/layerTreeItem.h b/lib/usdLayerEditor/lib/layerTreeItem.h index cccbf7dc6e..22a14899e0 100644 --- a/lib/usdLayerEditor/lib/layerTreeItem.h +++ b/lib/usdLayerEditor/lib/layerTreeItem.h @@ -84,7 +84,7 @@ struct LayerActionInfo int _order = 0; }; -bool IsLayerActionAllowed(const LayerActionInfo& actionInfo, LayerMasks layerMaskFlag); +LayerEditorAPI bool IsLayerActionAllowed(const LayerActionInfo& actionInfo, LayerMasks layerMaskFlag); using recursionDetection = std::vector; using LayerItemVector = std::vector; @@ -171,7 +171,7 @@ class LayerEditorAPI LayerTreeItem : public QStandardItem bool isSessionLayer() const { return _layerType == LayerType::SessionLayer; } bool isSublayer() const { return _layerType == LayerType::SubLayer; } bool isTargetLayer() const { return _isTargetLayer; } - bool isAnonymous() const { return _layer ? _layer->IsAnonymous() : false; } + bool isAnonymous() const; bool isRootLayer() const { return _layerType == LayerType::RootLayer; } PXR_NS::SdfLayerRefPtr layer() const { return _layer; } PXR_NS::SdfLayerRefPtr parentLayer() const @@ -183,19 +183,22 @@ class LayerEditorAPI LayerTreeItem : public QStandardItem // allows c++ iteration of children LayerItemVector childrenVector() const; + // Check if this item and its children are identical. + bool isIdenticalItem(const LayerTreeItem* other) const; + // menu callbacks - void removeSubLayer(); - void saveEdits(); - void saveEditsNoPrompt(); - void discardEdits(); - void mergeWithSublayers(); + void removeSubLayer(QWidget* in_parent); + void saveEdits(QWidget* in_parent); + void saveEditsNoPrompt(QWidget* in_parent); + void discardEdits(QWidget* in_parent); + void mergeWithSublayers(QWidget* in_parent); // there are two addAnonymousSubLayer , because the menu needs all method to be void - void addAnonymousSublayer(); - PXR_NS::SdfLayerRefPtr addAnonymousSublayerAndReturn(); + void addAnonymousSublayer(QWidget* in_parent); + PXR_NS::SdfLayerRefPtr addAnonymousSublayerAndReturn(QWidget* in_parent); void loadSubLayers(QWidget* in_parent); - void printLayer(); - void clearLayer(); + void printLayer(QWidget* in_parent); + void clearLayer(QWidget* in_parent); // delegate Action API for command buttons void getActionButton(LayerActionType actionType, LayerActionInfo& out_info) const; @@ -222,7 +225,7 @@ class LayerEditorAPI LayerTreeItem : public QStandardItem protected: void populateChildren(RecursionDetector* in_recursionDetector); // helper to save anon layers called by saveEdits() - void saveAnonymousLayer(); + void saveAnonymousLayer(QWidget* in_parent); private: bool sublayerOfShared() const; diff --git a/lib/usdLayerEditor/lib/layerTreeItemDelegate.cpp b/lib/usdLayerEditor/lib/layerTreeItemDelegate.cpp index edf7cdff74..48446cb7f3 100644 --- a/lib/usdLayerEditor/lib/layerTreeItemDelegate.cpp +++ b/lib/usdLayerEditor/lib/layerTreeItemDelegate.cpp @@ -265,7 +265,7 @@ void LayerTreeItemDelegate::paint_drawArrow(QPainter* painter, const ItemPaintCo void LayerTreeItemDelegate::paint_drawText(QPainter* painter, const ItemPaintContext& ctx) const { -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +#if QT_DISABLE_DEPRECATED_BEFORE || QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QColor penColor = ctx.item->data(Qt::ForegroundRole).value(); #else QColor penColor = ctx.item->data(Qt::TextColorRole).value(); diff --git a/lib/usdLayerEditor/lib/layerTreeModel.cpp b/lib/usdLayerEditor/lib/layerTreeModel.cpp index b1087d0d49..b2b14962ef 100644 --- a/lib/usdLayerEditor/lib/layerTreeModel.cpp +++ b/lib/usdLayerEditor/lib/layerTreeModel.cpp @@ -17,13 +17,13 @@ #include "layerTreeModel.h" #include "customLayerData.h" +#include "layerEditorDCCFunctions.h" #include "layerEditorWidget.h" #include "layerTreeItem.h" #include "layers.h" #include "saveLayersDialog.h" #include "stringResources.h" #include "tokens.h" -#include "utilOptions.h" #include "utilSerialization.h" #include "utilString.h" #include "utilUI.h" @@ -33,6 +33,7 @@ #include #include +#include #include PXR_NAMESPACE_USING_DIRECTIVE @@ -90,12 +91,7 @@ Qt::ItemFlags LayerTreeModel::flags(const QModelIndex& index) const return Qt::ItemIsSelectable | Qt::ItemIsEnabled; } - // TODO LE-EXTRACT Move sublayers - reenable drag&drop. Qt::ItemFlags defaultFlags = QStandardItemModel::flags(index); - defaultFlags &= ~Qt::ItemIsDragEnabled; - defaultFlags &= ~Qt::ItemIsDropEnabled; - return defaultFlags; - if (index.isValid() && item->isMovable()) { return Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags; } else { @@ -106,10 +102,8 @@ Qt::ItemFlags LayerTreeModel::flags(const QModelIndex& index) const Qt::DropActions LayerTreeModel::supportedDropActions() const { - // TODO LE-EXTRACT Move sublayers - reenable drag&drop. - return Qt::IgnoreAction; // We support only moving layers around to reorder or re-parent. - // return Qt::MoveAction; + return Qt::MoveAction; } QStringList LayerTreeModel::mimeTypes() const @@ -285,11 +279,18 @@ void LayerTreeModel::setSessionState(SessionState* in_sessionState) this, &LayerTreeModel::autoHideSessionLayerChanged); - rebuildModelOnIdle(); + connect( + in_sessionState, + &SessionState::editForwardingFallbackTargetChanged, + this, + &LayerTreeModel::onEFFallbackTargetChanged); + + rebuildModelOnIdle(true); } -void LayerTreeModel::rebuildModelOnIdle() +void LayerTreeModel::rebuildModelOnIdle(bool dataChanged) { + _selectedLayerDataChanged |= dataChanged; if (!_rebuildOnIdlePending) { _rebuildOnIdlePending = true; @@ -313,61 +314,118 @@ void LayerTreeModel::rebuildModel(bool refreshLockState /*= false*/) _rebuildOnIdlePending = false; _lastAskedAnonLayerNameSinceRebuild = 0; - beginResetModel(); - clear(); - - if (_sessionState->isValid()) { - auto rootLayer = _sessionState->stage()->GetRootLayer(); - bool showSessionLayer = true; - auto sessionLayer = _sessionState->stage()->GetSessionLayer(); - if (_sessionState->autoHideSessionLayer()) { - showSessionLayer - = sessionLayer->IsDirty() || sessionLayer == _sessionState->targetLayer(); + if (_selectedLayerDataChanged) { + Q_EMIT selectedLayerDataChangedSignal(); + _selectedLayerDataChanged = false; + } + + if (!_sessionState->isValid()) { + if (rowCount() > 0) { + // Note: clear() calls beginResetModel and endResetModel for us. + clear(); } + return; + } - std::set sharedLayers; - auto sharedStage = _sessionState->commandHook()->isDccObjectSharedStage( - _sessionState->stageEntry()._dccObjectPath); + auto rootLayer = _sessionState->stage()->GetRootLayer(); + auto sessionLayer = _sessionState->stage()->GetSessionLayer(); + bool showSessionLayer = true; + if (_sessionState->autoHideSessionLayer()) { + // Use the effective target so that in EF mode the decision follows the fallback + // target rather than the session layer (which is always the stage edit target there). + showSessionLayer + = sessionLayer->IsDirty() || sessionLayer == _sessionState->effectiveTargetLayer(); + } + + std::set sharedLayers; + auto sharedStage = UsdLayerEditor::isDccObjectSharedStage( + _sessionState->stageEntry()._dccObjectPath); + if (!sharedStage) { + auto layers = CustomLayerData::getStringArray( + rootLayer, UsdLayerEditorMetadata->ReferencedLayers); + // Also read the legacy Maya-specific token written by proxyShapeBase. + auto mayaLayers = CustomLayerData::getStringArray( + rootLayer, UsdLayerEditorMetadata->MayaReferencedLayers); + for (const auto& l : mayaLayers) + layers.push_back(l); + std::vector layerIds; + std::move(layers.begin(), layers.end(), inserter(layerIds, layerIds.begin())); + sharedLayers = Layers::getAllSublayers(layerIds, true); + } + + std::set incomingLayers; + if (UsdLayerEditor::isDccObjectStageIncoming( + _sessionState->stageEntry()._dccObjectPath)) { if (!sharedStage) { - auto layers = CustomLayerData::getStringArray( - rootLayer, UsdLayerEditorMetadata->ReferencedLayers); + incomingLayers = sharedLayers; + } else { std::vector layerIds; - std::move(layers.begin(), layers.end(), inserter(layerIds, layerIds.begin())); - sharedLayers = Layers::getAllSublayers(layerIds, true); + layerIds.push_back(rootLayer->GetIdentifier()); + incomingLayers = Layers::getAllSublayers(layerIds, true); } + } - std::set incomingLayers; - if (_sessionState->commandHook()->isDccObjectStageIncoming( - _sessionState->stageEntry()._dccObjectPath)) { - if (!sharedStage) { - incomingLayers = sharedLayers; - } else { - std::vector layerIds; - layerIds.push_back(rootLayer->GetIdentifier()); - incomingLayers = Layers::getAllSublayers(layerIds, true); - } - } + LayerTreeItem* oldSessionItem = nullptr; + LayerTreeItem* oldRootItem = nullptr; - if (showSessionLayer) { - appendRow(new LayerTreeItem( - sessionLayer, - _sessionState->stage(), - LayerType::SessionLayer, - "", - &incomingLayers, - sharedStage, - &sharedLayers)); + if (rowCount() > 0) { + if (rowCount() > 1) { + oldSessionItem = dynamic_cast(invisibleRootItem()->child(0)); + oldRootItem = dynamic_cast(invisibleRootItem()->child(1)); + } else { + oldRootItem = dynamic_cast(invisibleRootItem()->child(0)); } + } - appendRow(new LayerTreeItem( - rootLayer, _sessionState->stage(), LayerType::RootLayer, "", &incomingLayers, sharedStage, &sharedLayers)); + std::unique_ptr newSessionItem; + if (showSessionLayer) { + newSessionItem = std::make_unique( + sessionLayer, + _sessionState->stage(), + LayerType::SessionLayer, + "", + &incomingLayers, + sharedStage, + &sharedLayers); + } + + std::unique_ptr newRootItem = std::make_unique( + rootLayer, + _sessionState->stage(), + LayerType::RootLayer, + "", + &incomingLayers, + sharedStage, + &sharedLayers); + + const bool rootIdentical = newRootItem->isIdenticalItem(oldRootItem); + const bool sessionIdentical = (!newSessionItem && !oldSessionItem) + || (newSessionItem && newSessionItem->isIdenticalItem(oldSessionItem)); + + if (!refreshLockState && rootIdentical && sessionIdentical) { + return; + } - updateTargetLayer(InRebuildModel::Yes); + beginResetModel(); - if (refreshLockState) { - bool refreshSubLayers = true; - _sessionState->commandHook()->refreshLayerSystemLock(rootLayer, refreshSubLayers); - } + // Note: do *not* call clear() here! Unfortunately, clear() itself calls, + // beginResetModel() and endResetModel(). Qt does not detect the nested + // begin/end/ So calling clear() would make the layer manager flicker + // to be empty for a brief time. + if (rowCount() > 0) + removeRows(0, rowCount()); + + if (newSessionItem) { + appendRow(newSessionItem.release()); + } + + appendRow(newRootItem.release()); + + updateTargetLayer(InRebuildModel::Yes); + + if (refreshLockState) { + bool refreshSubLayers = true; + _sessionState->commandHook()->refreshLayerSystemLock(rootLayer, refreshSubLayers); } endResetModel(); @@ -389,7 +447,10 @@ void LayerTreeModel::updateTargetLayer(InRebuildModel inRebuild) return; } - auto editTarget = _sessionState->targetLayer(); + // In Edit Forwarding mode the stage edit target is pinned to the session layer; + // effectiveTargetLayer() returns the fallback target in that case, and the stage edit + // target otherwise. The auto-hide logic below is target-source agnostic and works for both. + auto editTarget = _sessionState->effectiveTargetLayer(); auto root = invisibleRootItem(); // if session layer is in auto-hide handle case where it is the target @@ -422,7 +483,7 @@ void LayerTreeModel::usd_layerChanged(SdfNotice::LayersDidChangeSentPerLayer con { // experienced crashes in python prototype For now, rebuild everything if (!_blockUsdNotices) - rebuildModelOnIdle(); + rebuildModelOnIdle(true); } // notification from USD @@ -443,6 +504,11 @@ void LayerTreeModel::usd_layerDirtinessChanged( auto layerItem = findUSDLayerItem(layer); if (layerItem) { layerItem->fetchData(RebuildChildren::No); + } else if (rowCount() > 0) { + // A non-local layer would not be visible in the tree - but in some cases + // (components) we still want to signal a data change - so that the save button + // refreshes. + Q_EMIT dataChanged(index(0, 0), index(0, 0)); } } } @@ -457,6 +523,17 @@ void LayerTreeModel::sessionStageChanged() // called from SessionState::autoHideSessionLayerSignal void LayerTreeModel::autoHideSessionLayerChanged() { rebuildModelOnIdle(); } +void LayerTreeModel::onEFFallbackTargetChanged() +{ + // Dispatch asynchronously to avoid re-entrant model updates while the + // edit-forwarding fallback-target change is still settling. + QTimer::singleShot(0, this, [this]() { + // Emit dataChanged so the EF toggle button repaints when the EF rule changes. + Q_EMIT dataChanged(index(0, 0), index(0, 0)); + updateTargetLayer(InRebuildModel::No); + }); +} + LayerTreeItem* LayerTreeModel::layerItemFromIndex(const QModelIndex& index) const { return dynamic_cast(itemFromIndex(index)); @@ -506,32 +583,46 @@ LayerTreeModel::getAllAnonymousLayers(const LayerTreeItem* item /* = nullptr*/) void LayerTreeModel::saveStage(QWidget* in_parent) { - auto saveAllLayers = [this]() { + auto saveAllLayers = [this, in_parent]() { + // Special case for components created by the component creator. Only + // the component creator knows how to save a component properly. The + // hook is a no-op for DCCs without component support. + if (_sessionState + && UsdLayerEditor::isStageAComponent(_sessionState->stageEntry()._dccObjectPath)) { + UsdLayerEditor::saveComponent( + _sessionState->stageEntry()._stage, _sessionState->stageEntry()._dccObjectPath); + return; + } + const auto layers = getAllNeedsSavingLayers(); for (auto layer : layers) { if (!layer->isSystemLocked()) { if (!layer->isAnonymous()) { - layer->saveEditsNoPrompt(); + layer->saveEditsNoPrompt(in_parent); } } } }; - static const std::string kConfirmExistingFileSave - = UsdLayerEditorOptionVars->ConfirmExistingFileSave.GetText(); - bool showConfirmDgl = Options::optionVarExists(kConfirmExistingFileSave) - && Options::optionVarExists(kConfirmExistingFileSave) != 0; + bool showConfirmDgl = confirmExistingFileSave(); - // if the stage contains anonymous layers, you need to show the comfirm dialog + // if the stage contains anonymous layers, you need to show the confirm dialog // so the user can choose where to save the anonymous layers. if (!showConfirmDgl) { - // Get the layers to save for this stage. Serialization::StageLayersToSave StageLayersToSave; - auto& stageEntry = _sessionState->stageEntry(); - Serialization::getLayersToSaveFromDCCObject(stageEntry._dccObjectPath, StageLayersToSave); + auto& stageEntry = _sessionState->stageEntry(); + Serialization::getLayersToSaveFromStage( + stageEntry._stage, stageEntry._dccObjectPath, StageLayersToSave); showConfirmDgl = !StageLayersToSave._anonLayers.empty(); } + // Show the save dialog for component stages (initial save) or if confirmation is needed + if (_sessionState + && UsdLayerEditor::shouldDisplayComponentInitialSaveDialog( + _sessionState->stageEntry()._stage, _sessionState->stageEntry()._dccObjectPath)) { + showConfirmDgl = true; + } + if (showConfirmDgl) { bool isExporting = false; @@ -565,6 +656,17 @@ void LayerTreeModel::saveStage(QWidget* in_parent) } } +void LayerTreeModel::reloadComponent(QWidget* /*in_parent*/) +{ + if (!_sessionState) { + return; + } + if (UsdLayerEditor::isUnsavedComponent(_sessionState->stage())) { + return; + } + UsdLayerEditor::reloadComponent(_sessionState->stageEntry()._dccObjectPath); +} + std::string LayerTreeModel::findNameForNewAnonymousLayer() const { const std::string prefix = "anonymousLayer"; diff --git a/lib/usdLayerEditor/lib/layerTreeModel.h b/lib/usdLayerEditor/lib/layerTreeModel.h index a67dc6101b..235289c6ce 100644 --- a/lib/usdLayerEditor/lib/layerTreeModel.h +++ b/lib/usdLayerEditor/lib/layerTreeModel.h @@ -77,6 +77,11 @@ class LayerEditorAPI LayerTreeModel // save stage UI void saveStage(QWidget* in_parent); + // reload a component stage (component-creator). Default behavior is to + // route to AbstractCommandHook::reloadComponent() which is a no-op for + // DCCs without component support. + void reloadComponent(QWidget* in_parent); + // return the index of the root layer QModelIndex rootLayerIndex(); @@ -116,11 +121,13 @@ class LayerEditorAPI LayerTreeModel Q_SIGNALS: void selectLayerSignal(const QModelIndex&); + void selectedLayerDataChangedSignal(); protected: // slots void sessionStageChanged(); void autoHideSessionLayerChanged(); + void onEFFallbackTargetChanged(); void setSessionState(SessionState* in_sessionState); SessionState* _sessionState = nullptr; @@ -137,8 +144,9 @@ class LayerEditorAPI LayerTreeModel mutable int _lastAskedAnonLayerNameSinceRebuild = 0; - void rebuildModelOnIdle(); + void rebuildModelOnIdle(bool dataChanged = false); bool _rebuildOnIdlePending = false; + bool _selectedLayerDataChanged = false; void rebuildModel(bool refreshLockState = false); void updateTargetLayer(InRebuildModel inRebuild); diff --git a/lib/usdLayerEditor/lib/layerTreeView.cpp b/lib/usdLayerEditor/lib/layerTreeView.cpp index 847266383f..1916ae46ee 100644 --- a/lib/usdLayerEditor/lib/layerTreeView.cpp +++ b/lib/usdLayerEditor/lib/layerTreeView.cpp @@ -17,6 +17,7 @@ #include "layerTreeView.h" #include "abstractCommandHook.h" +#include "layerEditorDCCFunctions.h" #include "layerTreeItem.h" #include "layerTreeItemDelegate.h" #include "layerTreeModel.h" @@ -40,15 +41,18 @@ struct CallMethodParams namespace { -typedef void (LayerTreeItem::*simpleLayerMethod)(); +typedef void (LayerTreeItem::*simpleLayerMethod)(QWidget* in_parent); -void doCallMethodOnSelection(const CallMethodParams& params, simpleLayerMethod method) +void doCallMethodOnSelection( + const CallMethodParams& params, + simpleLayerMethod method, + QWidget* in_parent) { if (params.selection->size() > 0) { UndoContext context(params.commandHook, params.name); for (auto item : *params.selection) { - (item->*method)(); + (item->*method)(in_parent); } } } @@ -87,10 +91,12 @@ LayerTreeView::LayerTreeView(SessionState* in_sessionState, QWidget* in_parent) connect(_model, &LayerTreeModel::selectLayerSignal, this, &LayerTreeView::selectLayerRequest); // clang-format off - QString styleSheet = + QString styleSheet = "QTreeView { " "background: " + QApplication::palette().color(QPalette::Dark).name() + ";" "show-decoration-selected: 0;" + "outline: none;" + "border: none;" "}"; // clang-format on setStyleSheet(styleSheet); @@ -149,7 +155,6 @@ LayerTreeView::LayerTreeView(SessionState* in_sessionState, QWidget* in_parent) _actionButtons._staticActions.push_back(lockAction); } - // TODO LE-EXTRACT Refresh layer treeview on new system lock. _refreshCallback = std::make_shared(this); UsdUfe::registerUICallback(PXR_NS::TfToken("onRefreshSystemLock"), _refreshCallback); @@ -198,26 +203,12 @@ void LayerTreeView::onItemDoubleClicked(const QModelIndex& index) if (layerTreeItem->isSystemLocked() || layerTreeItem->appearsSystemLocked()) return; - layerTreeItem->saveEdits(); + layerTreeItem->saveEdits(this); } bool LayerTreeView::shouldExpandOrCollapseAll() const { - // TODO LE-EXTRACT Treeview expand/collapse all key modifier. - return false; - - //// Internal private function to check if the expand and collapse of - //// items should be recursive. Currently, this is controlled by the - //// fact the user is pressing the SHIFT key on the keyboard. - // int modifiers = 0; - // MGlobal::executeCommand("getModifiers", modifiers); - - //// Magic constant 2 is how the getModifiers reports the SHIFT key. - //// This is a public command and the shift value is only declared in - //// its documentation. Being a public command, it is practically - //// guaranteed to never change, so hard-coding the value is not a problem. - // const bool shiftHeld = ((modifiers % 2) != 0); - // return shiftHeld; + return UsdLayerEditor::shouldExpandOrCollapseAll(); } void LayerTreeView::onExpanded(const QModelIndex& index) @@ -260,10 +251,20 @@ LayerViewMemento::LayerViewMemento(const LayerTreeView& view, const LayerTreeMod void LayerViewMemento::preserve(const LayerTreeView& view, const LayerTreeModel& model) { + if (QScrollBar* hsb = view.horizontalScrollBar()) { + _horizontalScrollbarPosition = hsb->value(); + } + if (QScrollBar* vsb = view.verticalScrollBar()) { + _verticalScrollbarPosition = vsb->value(); + } + + auto selectionModel = view.selectionModel(); const LayerItemVector items = model.getAllItems(); if (items.size() == 0) return; + auto currentIndex = selectionModel->currentIndex(); + for (const LayerTreeItem* item : items) { if (!item) continue; @@ -273,7 +274,9 @@ void LayerViewMemento::preserve(const LayerTreeView& view, const LayerTreeModel& continue; const ItemId id = layer->GetIdentifier(); - const ItemState state = { view.isExpanded(item->index()) }; + const ItemState state = { view.isExpanded(item->index()), + selectionModel->isSelected(item->index()), + item->index() == currentIndex }; _itemsState[id] = state; } @@ -285,6 +288,9 @@ void LayerViewMemento::restore(LayerTreeView& view, LayerTreeModel& model) QtDisableRepaintUpdates disableUpdates(view); + QItemSelection* selection = nullptr; + const auto selectionModel = view.selectionModel(); + for (const LayerTreeItem* item : items) { if (!item) continue; @@ -297,11 +303,39 @@ void LayerViewMemento::restore(LayerTreeView& view, LayerTreeModel& model) const auto state = _itemsState.find(id); if (state != _itemsState.end()) { expanded = state->second._expanded; + if (state->second._selected) { + if (!selection) { + selection = new QItemSelection(); + } + selection->select(item->index(), item->index()); + } + if (state->second._current) { + selectionModel->setCurrentIndex(item->index(), QItemSelectionModel::NoUpdate); + } } } view.setExpanded(item->index(), expanded); } + + if (selection != nullptr) { + selectionModel->select( + *selection, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); + delete selection; + } + + if (QScrollBar* hsb = view.horizontalScrollBar()) { + if (hsb->value() != _horizontalScrollbarPosition) { + hsb->setValue(_horizontalScrollbarPosition); + hsb->valueChanged(_horizontalScrollbarPosition); + } + } + if (QScrollBar* vsb = view.verticalScrollBar()) { + if (vsb->value() != _verticalScrollbarPosition) { + vsb->setValue(_verticalScrollbarPosition); + vsb->valueChanged(_verticalScrollbarPosition); + } + } } void LayerTreeView::updateFromSessionState() @@ -316,7 +350,7 @@ void LayerTreeView::updateFromSessionState() // Only keep the state of stages that still exist for (auto const& stageEntry : allStages) { auto stageLayers = stageEntry._stage->GetLayerStack(); - for (const auto stageLayer : stageLayers) { + for (const auto& stageLayer : stageLayers) { newState[stageLayer->GetIdentifier()] = oldState[stageLayer->GetIdentifier()]; } } @@ -457,7 +491,7 @@ void LayerTreeView::callMethodOnSelection(const QString& undoName, simpleLayerMe params.selection = &selection; params.commandHook = _model->sessionState()->commandHook(); params.name = undoName; - doCallMethodOnSelection(params, method); + doCallMethodOnSelection(params, method, this); } void LayerTreeView::setCustomLayerItemDelegate(LayerTreeItemDelegate* itemDelegate) @@ -516,7 +550,6 @@ void LayerTreeView::mousePressEvent(QMouseEvent* event) void LayerTreeView::updateMouseCursor() { - // TODO LE-EXTRACT Update mouse cursor over layer tree. // Note: special mouse cursor taken from Maya resources. QString pixmapName = QtUtils::getDPIPixmapName(":/rmbMenu"); // Note: in Maya, the normal-sized pixmap name does not ends with _100, @@ -578,7 +611,7 @@ void LayerTreeView::keyPressEvent(QKeyEvent* event) params.selection = &selection; params.commandHook = _model->sessionState()->commandHook(); params.name = "Remove"; - doCallMethodOnSelection(params, &LayerTreeItem::removeSubLayer); + doCallMethodOnSelection(params, &LayerTreeItem::removeSubLayer, this); return; } else if (event->key() == Qt::Key_R) { _model->forceRefresh(); diff --git a/lib/usdLayerEditor/lib/layerTreeView.h b/lib/usdLayerEditor/lib/layerTreeView.h index 6ac11b3e06..ec01e83655 100644 --- a/lib/usdLayerEditor/lib/layerTreeView.h +++ b/lib/usdLayerEditor/lib/layerTreeView.h @@ -41,14 +41,14 @@ class LayerTreeView; class SessionState; typedef std::vector LayerItemVector; -typedef void (LayerTreeItem::*simpleLayerMethod)(); +typedef void (LayerTreeItem::*simpleLayerMethod)(QWidget* in_parent); /** * @brief State of the layer tree view and layer model. * Used to save and restore the state when the model is rebuilt. * */ -class LayerViewMemento +class LayerEditorAPI LayerViewMemento { public: LayerViewMemento(const LayerTreeView&, const LayerTreeModel&); @@ -62,6 +62,8 @@ class LayerViewMemento struct ItemState { bool _expanded = false; + bool _selected = false; + bool _current = false; }; std::map getItemsState() { return _itemsState; } @@ -69,51 +71,52 @@ class LayerViewMemento private: std::map _itemsState; + int _horizontalScrollbarPosition { 0 }; + int _verticalScrollbarPosition { 0 }; }; /** * @brief Implements the Qt TreeView for USD layers. This widget is owned by the LayerEditorWidget. * */ -class LayerTreeView +class LayerEditorAPI LayerTreeView : public QTreeView , public PXR_NS::TfWeakBase { Q_OBJECT public: typedef QTreeView PARENT_CLASS; - LayerEditorAPI LayerTreeView(SessionState* in_sessionState, QWidget* in_parent); - LayerEditorAPI ~LayerTreeView() override; + LayerTreeView(SessionState* in_sessionState, QWidget* in_parent); + ~LayerTreeView() override; // get properly typed item - LayerEditorAPI LayerTreeItem* layerItemFromIndex(const QModelIndex& index) const; + LayerTreeItem* layerItemFromIndex(const QModelIndex& index) const; // QTreeWidget-like method that returns the current item when one is selected - LayerEditorAPI LayerTreeItem* currentLayerItem() const + LayerTreeItem* currentLayerItem() const { auto index = currentIndex(); return index.isValid() ? layerItemFromIndex(index) : nullptr; } // returns array of selected item, including the current item - LayerEditorAPI LayerItemVector getSelectedLayerItems() const; + LayerItemVector getSelectedLayerItems() const; // return property typed model - LayerEditorAPI LayerTreeModel* layerTreeModel() const; + LayerTreeModel* layerTreeModel() const; // calls a given method on all items in the selection, with the given string as the undo chunk // name - LayerEditorAPI void callMethodOnSelection(const QString& undoName, simpleLayerMethod method); + void callMethodOnSelection(const QString& undoName, simpleLayerMethod method); // Override the layer tree styled item delegate. - LayerEditorAPI void setCustomLayerItemDelegate(LayerTreeItemDelegate*); + void setCustomLayerItemDelegate(LayerTreeItemDelegate*); // menu callbacks - LayerEditorAPI void onAddParentLayer(const QString& undoName) const; - LayerEditorAPI void onMuteLayer(const QString& undoName) const; - LayerEditorAPI void onLockLayer(const QString& undoName) const; - LayerEditorAPI void - onLockLayerAndSublayers(const QString& undoName, bool includeSublayers) const; + void onAddParentLayer(const QString& undoName) const; + void onMuteLayer(const QString& undoName) const; + void onLockLayer(const QString& undoName) const; + void onLockLayerAndSublayers(const QString& undoName, bool includeSublayers) const; // QWidgets overrides void paintEvent(QPaintEvent* event) override; @@ -124,26 +127,26 @@ class LayerTreeView void leaveEvent(QEvent* event) override; protected: - LayerEditorAPI void updateMouseCursor(); + void updateMouseCursor(); // slot: - LayerEditorAPI void onModelAboutToBeReset(); - LayerEditorAPI void onModelReset(); - LayerEditorAPI void onItemDoubleClicked(const QModelIndex& index); - LayerEditorAPI void onExpanded(const QModelIndex& index); - LayerEditorAPI void onCollapsed(const QModelIndex& index); - LayerEditorAPI void onMuteLayerButtonPushed(); - LayerEditorAPI void onLockLayerButtonPushed(); + void onModelAboutToBeReset(); + void onModelReset(); + void onItemDoubleClicked(const QModelIndex& index); + void onExpanded(const QModelIndex& index); + void onCollapsed(const QModelIndex& index); + void onMuteLayerButtonPushed(); + void onLockLayerButtonPushed(); // Notice listener method for layer muting changes. - LayerEditorAPI void onLayerMutingChanged(const pxr::UsdNotice::LayerMutingChanged& notice); + void onLayerMutingChanged(const pxr::UsdNotice::LayerMutingChanged& notice); - LayerEditorAPI bool shouldExpandOrCollapseAll() const; - LayerEditorAPI void expandChildren(const QModelIndex& index); - LayerEditorAPI void collapseChildren(const QModelIndex& index); + bool shouldExpandOrCollapseAll() const; + void expandChildren(const QModelIndex& index); + void collapseChildren(const QModelIndex& index); // delayed signal to select a layer on idle - LayerEditorAPI void selectLayerRequest(const QModelIndex& index); + void selectLayerRequest(const QModelIndex& index); // Updates the _cachedModelState using stage data from the session void updateFromSessionState(); diff --git a/lib/usdLayerEditor/lib/layerTreeViewStyle.h b/lib/usdLayerEditor/lib/layerTreeViewStyle.h index 8fbaf105a8..4e4d82ce51 100644 --- a/lib/usdLayerEditor/lib/layerTreeViewStyle.h +++ b/lib/usdLayerEditor/lib/layerTreeViewStyle.h @@ -157,7 +157,7 @@ class LayerTreeViewStyle : public QCommonStyle const override { if (hint == QStyle::SH_Slider_AbsoluteSetButtons) { -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +#if QT_DISABLE_DEPRECATED_BEFORE || QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) return Qt::LeftButton | Qt::MiddleButton | Qt::RightButton; #else return Qt::LeftButton | Qt::MidButton | Qt::RightButton; diff --git a/lib/usdLayerEditor/lib/layers.h b/lib/usdLayerEditor/lib/layers.h index 6f1f3a8ca2..4985dc8dd5 100644 --- a/lib/usdLayerEditor/lib/layers.h +++ b/lib/usdLayerEditor/lib/layers.h @@ -15,9 +15,9 @@ // #ifndef USDLAYEREDITOR_LAYERS_H -#define USDLAYEREDITOR__LAYERS_H +#define USDLAYEREDITOR_LAYERS_H -#include "LayerEditorAPI.h" +#include "layerEditorAPI.h" #include diff --git a/lib/usdLayerEditor/lib/loadLayersDialog.cpp b/lib/usdLayerEditor/lib/loadLayersDialog.cpp index 807ef4d587..1533e4aff1 100644 --- a/lib/usdLayerEditor/lib/loadLayersDialog.cpp +++ b/lib/usdLayerEditor/lib/loadLayersDialog.cpp @@ -191,6 +191,7 @@ void LoadLayersDialog::onOk() auto row = dynamic_cast(_rowsLayout->itemAt(i)->widget()); if (!row->pathToUse().empty()) { if (!checkIfPathIsSafeToAdd( + this, StringResources::getAsQString(StringResources::kLoadSublayersError), _treeItem, row->pathToUse())) { diff --git a/lib/usdLayerEditor/lib/loadLayersDialog.h b/lib/usdLayerEditor/lib/loadLayersDialog.h index eb693fc77f..cdaac4aabc 100644 --- a/lib/usdLayerEditor/lib/loadLayersDialog.h +++ b/lib/usdLayerEditor/lib/loadLayersDialog.h @@ -17,6 +17,8 @@ #ifndef USDLAYEREDITOR_LOADLAYERSDIALOG_H #define USDLAYEREDITOR_LOADLAYERSDIALOG_H +#include "layerEditorAPI.h" + #include #include #include @@ -38,7 +40,7 @@ class LayerPathRow; * @brief Dialog to load multiple USD sublayers at once onto a parent layer. * */ -class LoadLayersDialog : public QDialog +class LayerEditorAPI LoadLayersDialog : public QDialog { public: typedef std::list PathList; diff --git a/lib/usdLayerEditor/lib/pathChecker.cpp b/lib/usdLayerEditor/lib/pathChecker.cpp index d9a6ac4c16..edd41e20e5 100644 --- a/lib/usdLayerEditor/lib/pathChecker.cpp +++ b/lib/usdLayerEditor/lib/pathChecker.cpp @@ -49,6 +49,7 @@ UsdLayerVector getAllParentHandles(LayerTreeItem* parentItem) // logic: we've loaded the layer, now check if it's also somewhere else in the hierachy // the path parameter are just for UI display. it's testLayer that's important bool checkPathRecursive( + QWidget* in_parent, const QString& in_errorTitle, UsdLayer parentLayer, UsdLayerVector parentHandles, @@ -70,7 +71,7 @@ bool checkPathRecursive( StringResources::kErrorCannotAddPathInHierarchy.value, pathToCheck.c_str()); message = QString::fromStdString(tmp); } - warningDialog(in_errorTitle, message); + warningDialog(in_errorTitle, message, nullptr, QMessageBox::Icon::NoIcon, in_parent); return false; } @@ -84,6 +85,7 @@ bool checkPathRecursive( auto childLayer = PXR_NS::SdfLayer::FindOrOpen(actualpath); if (childLayer != nullptr) { if (!checkPathRecursive( + in_parent, in_errorTitle, testLayer, parentHandles, @@ -104,6 +106,7 @@ bool checkPathRecursive( namespace UsdLayerEditor { bool checkIfPathIsSafeToAdd( + QWidget* in_parent, const QString& in_errorTitle, LayerTreeItem* in_parentItem, const std::string& in_pathToAdd) @@ -145,13 +148,20 @@ bool checkIfPathIsSafeToAdd( if (!foundLayerInStack) { UsdLayerVector parentHandles = getAllParentHandles(in_parentItem); return checkPathRecursive( - in_errorTitle, parentLayer, parentHandles, subLayer, pathToAdd, pathToAdd); + in_parent, + in_errorTitle, + parentLayer, + parentHandles, + subLayer, + pathToAdd, + pathToAdd); } } std::string msg = String::format(StringResources::kErrorCannotAddPathTwice.value, in_pathToAdd.c_str()); - warningDialog(in_errorTitle, QString::fromStdString(msg)); + warningDialog( + in_errorTitle, QString::fromStdString(msg), nullptr, QMessageBox::Icon::NoIcon, in_parent); return false; } diff --git a/lib/usdLayerEditor/lib/pathChecker.h b/lib/usdLayerEditor/lib/pathChecker.h index d2594236a5..aaa1772262 100644 --- a/lib/usdLayerEditor/lib/pathChecker.h +++ b/lib/usdLayerEditor/lib/pathChecker.h @@ -16,11 +16,14 @@ #ifndef USDLAYEREDITOR_PATHCHECKER_H #define USDLAYEREDITOR_PATHCHECKER_H +#include "layerEditorAPI.h" + #include #include class QString; +class QWidget; namespace UsdLayerEditor { class LayerTreeItem; @@ -30,7 +33,8 @@ class LayerTreeItem; // bad paths are always allowed, because they could be custom URIs or future paths // used for Load Layers // in corner cases the parent layer is null, we assume its safe to add -bool checkIfPathIsSafeToAdd( +LayerEditorAPI bool checkIfPathIsSafeToAdd( + QWidget* in_parent, const QString& in_errorTitle, LayerTreeItem* in_parentItem, const std::string& in_pathToAdd); diff --git a/lib/usdLayerEditor/lib/resources.qrc b/lib/usdLayerEditor/lib/resources.qrc index a2464a3567..4b36ef3b95 100644 --- a/lib/usdLayerEditor/lib/resources.qrc +++ b/lib/usdLayerEditor/lib/resources.qrc @@ -1,6 +1,31 @@ + resources/ef_default_100.png + resources/ef_default_150.png + resources/ef_default_200.png + resources/ef_on_100.png + resources/ef_on_150.png + resources/ef_on_200.png + resources/contents_off_100.png + resources/contents_off_150.png + resources/contents_off_200.png + resources/contents_off_hover_100.png + resources/contents_off_hover_150.png + resources/contents_off_hover_200.png + resources/contents_on_hover_100.png + resources/contents_on_hover_150.png + resources/contents_on_hover_200.png + resources/contents_on_100.png + resources/contents_on_150.png + resources/contents_on_200.png + resources/contents_on_hover_100.png + resources/contents_on_hover_150.png + resources/contents_on_hover_200.png + resources/contents_off_hover_100.png + resources/contents_off_hover_150.png + resources/contents_off_hover_200.png + resources/addCreateGeneric_100.png resources/addCreateGeneric_100.png resources/addCreateGeneric_150.png diff --git a/lib/usdLayerEditor/lib/resources/contents_off_100.png b/lib/usdLayerEditor/lib/resources/contents_off_100.png new file mode 100644 index 0000000000..cb2768be9d Binary files /dev/null and b/lib/usdLayerEditor/lib/resources/contents_off_100.png differ diff --git a/lib/usdLayerEditor/lib/resources/contents_off_150.png b/lib/usdLayerEditor/lib/resources/contents_off_150.png new file mode 100644 index 0000000000..abd3ad67a8 Binary files /dev/null and b/lib/usdLayerEditor/lib/resources/contents_off_150.png differ diff --git a/lib/usdLayerEditor/lib/resources/contents_off_200.png b/lib/usdLayerEditor/lib/resources/contents_off_200.png new file mode 100644 index 0000000000..9d05707214 Binary files /dev/null and b/lib/usdLayerEditor/lib/resources/contents_off_200.png differ diff --git a/lib/usdLayerEditor/lib/resources/contents_off_hover_100.png b/lib/usdLayerEditor/lib/resources/contents_off_hover_100.png new file mode 100644 index 0000000000..c160632ace Binary files /dev/null and b/lib/usdLayerEditor/lib/resources/contents_off_hover_100.png differ diff --git a/lib/usdLayerEditor/lib/resources/contents_off_hover_150.png b/lib/usdLayerEditor/lib/resources/contents_off_hover_150.png new file mode 100644 index 0000000000..0ed1ac7677 Binary files /dev/null and b/lib/usdLayerEditor/lib/resources/contents_off_hover_150.png differ diff --git a/lib/usdLayerEditor/lib/resources/contents_off_hover_200.png b/lib/usdLayerEditor/lib/resources/contents_off_hover_200.png new file mode 100644 index 0000000000..18dd98be47 Binary files /dev/null and b/lib/usdLayerEditor/lib/resources/contents_off_hover_200.png differ diff --git a/lib/usdLayerEditor/lib/resources/contents_on_100.png b/lib/usdLayerEditor/lib/resources/contents_on_100.png new file mode 100644 index 0000000000..6d0379ac4b Binary files /dev/null and b/lib/usdLayerEditor/lib/resources/contents_on_100.png differ diff --git a/lib/usdLayerEditor/lib/resources/contents_on_150.png b/lib/usdLayerEditor/lib/resources/contents_on_150.png new file mode 100644 index 0000000000..14a2a3c4d8 Binary files /dev/null and b/lib/usdLayerEditor/lib/resources/contents_on_150.png differ diff --git a/lib/usdLayerEditor/lib/resources/contents_on_200.png b/lib/usdLayerEditor/lib/resources/contents_on_200.png new file mode 100644 index 0000000000..6663b77ce8 Binary files /dev/null and b/lib/usdLayerEditor/lib/resources/contents_on_200.png differ diff --git a/lib/usdLayerEditor/lib/resources/contents_on_hover_100.png b/lib/usdLayerEditor/lib/resources/contents_on_hover_100.png new file mode 100644 index 0000000000..76838e39ba Binary files /dev/null and b/lib/usdLayerEditor/lib/resources/contents_on_hover_100.png differ diff --git a/lib/usdLayerEditor/lib/resources/contents_on_hover_150.png b/lib/usdLayerEditor/lib/resources/contents_on_hover_150.png new file mode 100644 index 0000000000..b075c57a7e Binary files /dev/null and b/lib/usdLayerEditor/lib/resources/contents_on_hover_150.png differ diff --git a/lib/usdLayerEditor/lib/resources/contents_on_hover_200.png b/lib/usdLayerEditor/lib/resources/contents_on_hover_200.png new file mode 100644 index 0000000000..499821e008 Binary files /dev/null and b/lib/usdLayerEditor/lib/resources/contents_on_hover_200.png differ diff --git a/lib/usdLayerEditor/lib/resources/ef_default_100.png b/lib/usdLayerEditor/lib/resources/ef_default_100.png new file mode 100644 index 0000000000..31dc535df9 Binary files /dev/null and b/lib/usdLayerEditor/lib/resources/ef_default_100.png differ diff --git a/lib/usdLayerEditor/lib/resources/ef_default_150.png b/lib/usdLayerEditor/lib/resources/ef_default_150.png new file mode 100644 index 0000000000..401ad9040f Binary files /dev/null and b/lib/usdLayerEditor/lib/resources/ef_default_150.png differ diff --git a/lib/usdLayerEditor/lib/resources/ef_default_200.png b/lib/usdLayerEditor/lib/resources/ef_default_200.png new file mode 100644 index 0000000000..f9147eeb26 Binary files /dev/null and b/lib/usdLayerEditor/lib/resources/ef_default_200.png differ diff --git a/lib/usdLayerEditor/lib/resources/ef_on_100.png b/lib/usdLayerEditor/lib/resources/ef_on_100.png new file mode 100644 index 0000000000..77bf94a2fb Binary files /dev/null and b/lib/usdLayerEditor/lib/resources/ef_on_100.png differ diff --git a/lib/usdLayerEditor/lib/resources/ef_on_150.png b/lib/usdLayerEditor/lib/resources/ef_on_150.png new file mode 100644 index 0000000000..fc43d73c84 Binary files /dev/null and b/lib/usdLayerEditor/lib/resources/ef_on_150.png differ diff --git a/lib/usdLayerEditor/lib/resources/ef_on_200.png b/lib/usdLayerEditor/lib/resources/ef_on_200.png new file mode 100644 index 0000000000..10255c7d29 Binary files /dev/null and b/lib/usdLayerEditor/lib/resources/ef_on_200.png differ diff --git a/lib/usdLayerEditor/lib/saveLayersDialog.cpp b/lib/usdLayerEditor/lib/saveLayersDialog.cpp index 42bcd8547a..caf7059ea4 100644 --- a/lib/usdLayerEditor/lib/saveLayersDialog.cpp +++ b/lib/usdLayerEditor/lib/saveLayersDialog.cpp @@ -16,7 +16,9 @@ #include "saveLayersDialog.h" +#include "componentSaveWidget.h" #include "generatedIconButton.h" +#include "layerEditorDCCFunctions.h" #include "layerLocking.h" #include "layerTreeItem.h" #include "layerTreeModel.h" @@ -24,7 +26,6 @@ #include "sessionState.h" #include "stringResources.h" #include "utilFileSystem.h" -#include "utilOptions.h" #include "utilQT.h" #include "utilSerialization.h" #include "utilString.h" @@ -32,6 +33,7 @@ #include +#include #include #include @@ -41,8 +43,8 @@ #include #include #include +#include #include -#include PXR_NAMESPACE_USING_DIRECTIVE @@ -66,6 +68,7 @@ void getDialogMessages( const int nbAnonLayers, QString& msg1, QString& msg2, + QString& msg3, bool isExporting) { std::string strNbStages = std::to_string(nbStages); @@ -80,6 +83,11 @@ void getDialogMessages( : StringResources::kToSaveTheStageSaveFiles; msg = String::format(msgRes.value, strNbStages); msg2 = QString::fromStdString(msg); + + msgRes = isExporting ? StringResources::kToExportTheStageSaveComponents + : StringResources::kToSaveTheStageSaveComponents; + msg = String::format(msgRes.value, strNbStages); + msg3 = QString::fromStdString(msg); } class AnonLayerPathEdit : public QLineEdit @@ -139,8 +147,8 @@ class SaveLayerPathRow : public QWidget void postUpdate(); public: - std::filesystem::path _absolutePath; - std::filesystem::path _relativeAnchor; + fs::filesystem::path _absolutePath; + fs::filesystem::path _relativeAnchor; SaveLayersDialog* _parent { nullptr }; LayerInfo _layerInfo; @@ -244,7 +252,7 @@ void SaveLayerPathRow::setPathToSaveAs(const std::string& absolutePath, bool sav relativeAnchor = FileSystem::getDCCSceneFileDir(); if (relativeAnchor.empty()) { relativeAnchor - = std::filesystem::path(absolutePath).remove_filename().generic_string(); + = fs::filesystem::path(absolutePath).remove_filename().generic_string(); } } } @@ -292,31 +300,28 @@ std::string SaveLayerPathRow::calculateParentLayerDir() const void SaveLayerPathRow::onOpenBrowser() { - // Calculate parent layer path auto& parentLayer = _layerInfo.parent._layerParent; std::string parentLayerPath = calculateParentLayerDir(); - // Update appropriate option var - const char* optionVarName = parentLayer ? "mayaUsd_MakePathRelativeToParentLayer" - : "mayaUsd_MakePathRelativeToSceneFile"; - const bool optionvarExists = Options::optionVarExists(optionVarName); - int optionVarValue = 0; - if (optionvarExists) { - optionVarValue = Options::optionVarExists(optionVarName); - Options::setOptionVarValue(optionVarName, needToSaveAsRelative() ? 1 : 0); - } + const bool isParent = (parentLayer != nullptr); + const bool prev = isParent ? FileSystem::requireUsdPathsRelativeToParentLayer() + : FileSystem::requireUsdPathsRelativeToDCCSceneFile(); + if (isParent) + FileSystem::setRequireUsdPathsRelativeToParentLayer(needToSaveAsRelative()); + else + FileSystem::setRequireUsdPathsRelativeToDCCSceneFile(needToSaveAsRelative()); - // Run the UI and set the resulting path std::string absolutePath; if (SaveLayersDialog::saveLayerFilePathUI(absolutePath, parentLayerPath)) { - bool saveAsRelative = (optionvarExists && Options::optionVarExists(optionVarName) != 0); + const bool saveAsRelative = isParent ? FileSystem::requireUsdPathsRelativeToParentLayer() + : FileSystem::requireUsdPathsRelativeToDCCSceneFile(); setPathToSaveAs(absolutePath, saveAsRelative); } - // Restore original option var value - if (optionvarExists) { - Options::setOptionVarValue(optionVarName, optionVarValue); - } + if (isParent) + FileSystem::setRequireUsdPathsRelativeToParentLayer(prev); + else + FileSystem::setRequireUsdPathsRelativeToDCCSceneFile(prev); } void SaveLayerPathRow::onTextChanged(const QString& text) @@ -325,7 +330,7 @@ void SaveLayerPathRow::onTextChanged(const QString& text) return; } - std::filesystem::path inputPath(text.toStdString()); + fs::filesystem::path inputPath(text.toStdString()); if (inputPath.is_absolute()) { _relativeAnchor.clear(); _absolutePath = inputPath; @@ -361,10 +366,10 @@ void SaveLayerPathRow::postUpdate() auto entry = dynamic_cast(w); if (entry && (entry->_layerInfo.parent._layerParent == _layerInfo.layer) && entry->needToSaveAsRelative()) { - std::filesystem::path relativeAnchor + fs::filesystem::path relativeAnchor = FileSystem::getDir(getAbsolutePath().toStdString()); - std::filesystem::path relatievPath = entry->_pathEdit->text().toStdString(); - std::filesystem::path absolutePath = (relativeAnchor / relatievPath).lexically_normal(); + fs::filesystem::path relatievPath = entry->_pathEdit->text().toStdString(); + fs::filesystem::path absolutePath = (relativeAnchor / relatievPath).lexically_normal(); entry->setPathToSaveAs(absolutePath.generic_string(), true); } }); @@ -441,7 +446,8 @@ namespace UsdLayerEditor { SaveLayersDialog::SaveLayersDialog( QWidget* in_parent, const std::vector& infos, - bool isExporting) + bool isExporting, + bool componentsOnly) : QDialog(in_parent) , _sessionState(nullptr) , _isExporting(isExporting) @@ -449,24 +455,41 @@ SaveLayersDialog::SaveLayersDialog( std::string msg = String::format(StringResources::kSaveXStages.value, std::to_string(infos.size())); setWindowTitle(QString::fromStdString(msg)); - // For each stage collect the layers to save. + // For each stage collect the layers to save and identify component stages. for (const auto& info : infos) { + std::string dccObjectPath = info.dccObjectPath; + if (dccObjectPath.empty()) { + dccObjectPath = UsdUfe::stagePath(info.stage).string(); + } + + // Check if this stage is a component stage + const bool isComponent + = UsdLayerEditor::shouldDisplayComponentInitialSaveDialog(info.stage, dccObjectPath); + if (isComponent) { + StageSavingInfo componentInfo = info; + componentInfo.dccObjectPath = dccObjectPath; + _componentStageInfos.push_back(componentInfo); + // Component stages are saved via the component system, skip layer collection + continue; + } - const auto objectPath = UsdUfe::stagePath(info.stage); - getLayersToSave( - info.stage, - objectPath.string(), - info.stageName); + // If componentsOnly mode, skip non-component stages entirely + if (componentsOnly) { + continue; + } + + getLayersToSave(info.stage, dccObjectPath, info.stageName); } - QString msg1, msg2; + QString msg1, msg2, msg3; getDialogMessages( static_cast(infos.size()), static_cast(_anonLayerInfos.size()), msg1, msg2, + msg3, isExporting); - buildDialog(msg1, msg2); + buildDialog(msg1, msg2, msg3); } SaveLayersDialog::SaveLayersDialog( @@ -484,25 +507,65 @@ SaveLayersDialog::SaveLayersDialog( std::string stageName = stageEntry._displayName; msg = String::format(StringResources::kSaveName.value, stageName.c_str()); dialogTitle = QString::fromStdString(msg); - getLayersToSave(stageEntry._stage, stageEntry._dccObjectPath, stageName); + + // Check if this stage is an unsaved component stage. + if (UsdLayerEditor::shouldDisplayComponentInitialSaveDialog( + stageEntry._stage, stageEntry._dccObjectPath)) { + StageSavingInfo info; + info.stage = stageEntry._stage; + info.stageName = stageName; + info.dccObjectPath = stageEntry._dccObjectPath; + _componentStageInfos.push_back(info); + } else { + getLayersToSave(stageEntry._stage, stageEntry._dccObjectPath, stageName); + } } setWindowTitle(dialogTitle); - QString msg1, msg2; - getDialogMessages(1, static_cast(_anonLayerInfos.size()), msg1, msg2, isExporting); - buildDialog(msg1, msg2); + QString msg1, msg2, msg3; + getDialogMessages(1, static_cast(_anonLayerInfos.size()), msg1, msg2, msg3, isExporting); + buildDialog(msg1, msg2, msg3); } SaveLayersDialog ::~SaveLayersDialog() { QApplication::restoreOverrideCursor(); } +namespace { +// Test-only override; unset in production. See setExecTestHandler. +SaveLayersDialog::ExecTestHandler& execTestHandler() +{ + static SaveLayersDialog::ExecTestHandler handler; + return handler; +} +} // namespace + +SaveLayersDialog::ExecTestHandler SaveLayersDialog::setExecTestHandler(ExecTestHandler handler) +{ + auto previous = execTestHandler(); + execTestHandler() = std::move(handler); + return previous; +} + +int SaveLayersDialog::exec() +{ + if (auto& handler = execTestHandler()) + return handler(); + return QDialog::exec(); +} + void SaveLayersDialog::getLayersToSave( const PXR_NS::UsdStageRefPtr& stage, const std::string& objectPath, const std::string& stageName) { - // Get the layers to save for this stage. + // Get the layers to save for this stage. Use the stage directly when available + // to avoid UFE path-format mismatches (e.g. Maya DAG paths vs |world-prefixed + // UFE keys) that cause getLayersToSaveFromDCCObject to silently return nothing. Serialization::StageLayersToSave StageLayersToSave; - Serialization::getLayersToSaveFromDCCObject(objectPath, StageLayersToSave); + if (stage) { + Serialization::getLayersToSaveFromStage(stage, objectPath, StageLayersToSave); + } else { + Serialization::getLayersToSaveFromDCCObject(objectPath, StageLayersToSave); + } // Keep track of all the layers for this particular stage. for (const auto& layerInfo : StageLayersToSave._anonLayers) { @@ -546,7 +609,7 @@ void SaveLayersDialog::getLayersToSave( std::begin(dirtyFileBackedLayersToDisplay), std::end(dirtyFileBackedLayersToDisplay)); } -void SaveLayersDialog::buildDialog(const QString& msg1, const QString& msg2) +void SaveLayersDialog::buildDialog(const QString& msg1, const QString& msg2, const QString& msg3) { const int mainMargin = DPIScale(20); @@ -567,8 +630,10 @@ void SaveLayersDialog::buildDialog(const QString& msg1, const QString& msg2) const bool haveAnonLayers { !_anonLayerInfos.empty() }; const bool haveFileBackedLayers { !_dirtyFileBackedLayers.empty() }; + const bool haveComponentStages { !_componentStageInfos.empty() }; SaveLayerPathRowArea* anonScrollArea { nullptr }; SaveLayerPathRowArea* fileScrollArea { nullptr }; + SaveLayerPathRowArea* componentScrollArea { nullptr }; const int margin { DPIScale(10) }; // Anonymous layers. @@ -602,10 +667,7 @@ void SaveLayersDialog::buildDialog(const QString& msg1, const QString& msg2) } // File backed layers - static const std::string kConfirmExistingFileSave - = UsdLayerEditorOptionVars->ConfirmExistingFileSave.GetText(); - const bool showFileOverrideSection = Options::optionVarExists(kConfirmExistingFileSave) - && Options::optionVarExists(kConfirmExistingFileSave) != 0; + const bool showFileOverrideSection = confirmExistingFileSave(); if (showFileOverrideSection && haveFileBackedLayers) { auto fileLayout = new QVBoxLayout(); fileLayout->setContentsMargins(margin, margin, margin, 0); @@ -635,6 +697,62 @@ void SaveLayersDialog::buildDialog(const QString& msg1, const QString& msg2) QtUtils::initLayoutMargins(topLayout, mainMargin); topLayout->setSpacing(DPIScale(8)); + // Component stages section - create ComponentSaveWidget for each component stage + if (haveComponentStages) { + auto componentLayout = new QVBoxLayout(); + componentLayout->setContentsMargins(margin, margin, margin, 0); + componentLayout->setSpacing(DPIScale(8)); + componentLayout->setAlignment(Qt::AlignTop); + + for (size_t i = 0; i < _componentStageInfos.size(); ++i) { + const auto& componentInfo = _componentStageInfos[i]; + auto componentWidget + = new ComponentSaveWidget(this, _sessionState, componentInfo.dccObjectPath); + componentWidget->setComponentName(QString::fromStdString(componentInfo.stageName)); + // Make compact if not the first component widget + if (i > 0) { + componentWidget->setCompactMode(true); + } + componentLayout->addWidget(componentWidget); + _componentSaveWidgets.push_back(componentWidget); + } + + _componentStagesWidget = new QWidget(); + _componentStagesWidget->setLayout(componentLayout); + + // Setup the scroll area for component stages. + componentScrollArea = new SaveLayerPathRowArea(); + componentScrollArea->setFrameShape(QFrame::NoFrame); + componentScrollArea->setBackgroundRole(QPalette::AlternateBase); + componentScrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); + componentScrollArea->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + componentScrollArea->setWidget(_componentStagesWidget); + componentScrollArea->setWidgetResizable(true); + } + + if (nullptr != componentScrollArea) { + // Add message above component save section + if (!msg3.isEmpty()) { + auto componentMessage = new QLabel(msg3, this); + topLayout->addWidget(componentMessage); + } + + // Add the component scroll area + topLayout->addWidget(componentScrollArea); + + // Add a separator if we also have anonymous layers or file backed layers + if (haveAnonLayers || haveFileBackedLayers) { + auto lineSep = new QFrame(); + lineSep->setFrameShape(QFrame::HLine); + lineSep->setLineWidth(DPIScale(1)); + QPalette pal(lineSep->palette()); + pal.setColor(QPalette::Base, QColor("#575757")); + lineSep->setPalette(pal); + lineSep->setBackgroundRole(QPalette::Base); + topLayout->addWidget(lineSep); + } + } + if (nullptr != anonScrollArea) { // Add the first message. if (!msg1.isEmpty()) { @@ -695,6 +813,7 @@ void SaveLayersDialog::buildDialog(const QString& msg1, const QString& msg2) setLayout(topLayout); setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); + resize(DPIScale(700), sizeHint().height()); setSizeGripEnabled(true); QApplication::setOverrideCursor(QCursor(Qt::ArrowCursor)); } @@ -747,6 +866,68 @@ void SaveLayersDialog::onSaveAll() _problemLayers.clear(); _emptyLayers.clear(); + // Save component stages first + for (auto* componentWidget : _componentSaveWidgets) { + std::string saveLocation = componentWidget->folderLocation().toStdString(); + std::string componentName = componentWidget->componentName().toStdString(); + std::string dccObjectPath = componentWidget->dccObjectPath(); + + // Move/save the component unconditionally; bulk save has no session state but + // must still save. + std::string newRootPath + = UsdLayerEditor::moveComponent(saveLocation, componentName, dccObjectPath); + + if (!newRootPath.empty()) { + _newPaths.append(QString::fromStdString(componentName)); + _newPaths.append(QString::fromStdString(newRootPath)); + + auto newRootLayer = SdfLayer::FindOrOpen(newRootPath); + if (newRootLayer) { + // Capture the component's session layer BEFORE the rename/repath, which + // recreates the stage with an empty session layer. + auto oldSessionLayer = UsdLayerEditor::captureSessionLayer(dccObjectPath); + + // Rename the DCC-side proxy to match the component's new name. + std::string newDccObjectPath + = UsdLayerEditor::renameObject(dccObjectPath, componentName); + const std::string effectivePath + = newDccObjectPath.empty() ? dccObjectPath : newDccObjectPath; + + // Rewrite the proxy's root .filePath to the new root layer + // (renameObject only renames the DAG node, not .filePath). The move repaths the + // root, so force an absolute path; the new root is not the edit target. + UsdLayerEditor::updateDCCObjectRootLayer( + effectivePath, + newRootPath, + newRootLayer, + /*wasTargetLayer=*/false, + DccObjectRootLayerPathMode::ForceAbsolute); + + // Transfer the captured session-layer opinions onto the new stage. + UsdLayerEditor::transferSessionLayer(oldSessionLayer, effectivePath); + + // Relocate the stage entry + lock the new root (needs session state). + if (_sessionState) { + auto entries = _sessionState->allStages(); + for (const auto& entry : entries) { + if (entry._dccObjectPath == effectivePath) { + _sessionState->setStageEntry(entry); + break; + } + } + lockLayer( + _sessionState->stageEntry()._dccObjectPath, + newRootLayer, + LayerLockType::LayerLock_Locked, + true); + } + } + } else { + _problemLayers.append(QString::fromStdString(componentName)); + _problemLayers.append(QString::fromStdString(saveLocation + "/" + componentName)); + } + } + // Note: must start from the end so that sub-layers are saved before their parent. for (int count = _saveLayerPathRows.size(), i = count - 1; i >= 0; --i) { auto row = dynamic_cast(_saveLayerPathRows[i]); @@ -787,6 +968,23 @@ void SaveLayersDialog::onCancel() { reject(); } bool SaveLayersDialog::okToSave() { + // Block overwriting of components. The target folder must be empty. + // Otherwise, log an error and abort. + for (auto* componentWidget : _componentSaveWidgets) { + fs::filesystem::path location { componentWidget->folderLocation().toStdString() }; + location.append(componentWidget->componentName().toStdString()); + + if (fs::filesystem::exists(location) && !fs::filesystem::is_empty(location)) { + TF_RUNTIME_ERROR( + "Cannot save %s with the given name since a non-empty folder with the same " + "name is already in that location. Use a unique name or save to a different " + "location and try the save again. Folder path: %s", + componentWidget->componentName().toStdString().c_str(), + location.generic_string().c_str()); + return false; + } + } + // Files can have the same file names in complicated ways, with one file having two copies, // another three, so we keep the exact number of copies per file path. QMap alreadySeenPaths; @@ -829,7 +1027,8 @@ bool SaveLayersDialog::okToSave() StringResources::getAsQString(StringResources::kSaveAnonymousIdenticalFilesTitle), QString::fromStdString(errorMsg), &identicalFiles, - QMessageBox::Icon::Critical); + QMessageBox::Icon::Critical, + this); return false; } @@ -842,7 +1041,8 @@ bool SaveLayersDialog::okToSave() QString::fromStdString(confirmMsg), &existingFiles, nullptr, - QMessageBox::Icon::Warning)); + QMessageBox::Icon::Warning, + this)); } return true; @@ -895,4 +1095,18 @@ bool SaveLayersDialog::saveLayerFilePathUI( return true; } +/*static*/ +bool SaveLayersDialog::saveLayerFilePathUI( + std::string& out_filePath, + const SdfLayerRefPtr& parentLayer) +{ + std::string parentLayerPath; + if (parentLayer) { + parentLayerPath = parentLayer->GetRealPath(); + if (parentLayerPath.empty()) + parentLayerPath = parentLayer->GetIdentifier(); + } + return saveLayerFilePathUI(out_filePath, parentLayerPath); +} + } // namespace UsdLayerEditor diff --git a/lib/usdLayerEditor/lib/saveLayersDialog.h b/lib/usdLayerEditor/lib/saveLayersDialog.h index 7c53f0df70..1c3d1cff75 100644 --- a/lib/usdLayerEditor/lib/saveLayersDialog.h +++ b/lib/usdLayerEditor/lib/saveLayersDialog.h @@ -37,6 +37,7 @@ namespace UsdLayerEditor { + class ComponentSaveWidget; class SessionState; @@ -49,10 +50,13 @@ SaveLayersDialog(SessionState* in_sessionState, QWidget* in_parent, bool isExporting); // Create dialog for bulk save using all provided proxy shapes and their owned stages. + // If componentsOnly is true, only component stages are shown (no anonymous/file-backed + // layers). SaveLayersDialog( QWidget* in_parent, const std::vector& infos, - bool isExporting); + bool isExporting, + bool componentsOnly = false); ~SaveLayersDialog(); @@ -60,6 +64,8 @@ // As output returns the path. static bool saveLayerFilePathUI(std::string& out_filePath, const std::string& parentLayer); + static bool + saveLayerFilePathUI(std::string& out_filePath, const SdfLayerRefPtr& parentLayer); QWidget* findEntry(SdfLayerRefPtr key); @@ -67,6 +73,13 @@ void quietlyUncheckAllAsRelative(); + // Test seam: when a handler is installed, exec() returns its result without + // showing the (blocking) dialog. Production never installs one. Returns the + // previously-installed handler. + using ExecTestHandler = std::function; + static ExecTestHandler setExecTestHandler(ExecTestHandler handler); + int exec() override; + protected: void onSaveAll(); void onCancel(); @@ -83,7 +96,7 @@ QString buildTooltipForLayer(SdfLayerRefPtr layer); private: - void buildDialog(const QString& msg1, const QString& msg2); + void buildDialog(const QString& msg1, const QString& msg2, const QString& msg3); void getLayersToSave( const UsdStageRefPtr& stage, const std::string& proxyPath, @@ -93,18 +106,21 @@ typedef std::unordered_set layerSet; using LayerInfos = UsdLayerEditor::Serialization::LayerInfos; - QStringList _newPaths; - QStringList _problemLayers; - QStringList _emptyLayers; - QWidget* _anonLayersWidget { nullptr }; - QWidget* _fileLayersWidget { nullptr }; - QCheckBox* _allAsRelative { nullptr }; - LayerInfos _anonLayerInfos; - layerSet _dirtyFileBackedLayers; - stageLayerMap _stageLayerMap; - SessionState* _sessionState; - std::vector _saveLayerPathRows; - bool _isExporting { false }; + QStringList _newPaths; + QStringList _problemLayers; + QStringList _emptyLayers; + QWidget* _anonLayersWidget { nullptr }; + QWidget* _fileLayersWidget { nullptr }; + QWidget* _componentStagesWidget { nullptr }; + QCheckBox* _allAsRelative { nullptr }; + LayerInfos _anonLayerInfos; + layerSet _dirtyFileBackedLayers; + stageLayerMap _stageLayerMap; + SessionState* _sessionState; + std::vector _saveLayerPathRows; + std::vector _componentStageInfos; + std::vector _componentSaveWidgets; + bool _isExporting { false }; }; }; // namespace UsdLayerEditor diff --git a/lib/usdLayerEditor/lib/sessionState.cpp b/lib/usdLayerEditor/lib/sessionState.cpp index 31cdbdccd0..bce2a47062 100644 --- a/lib/usdLayerEditor/lib/sessionState.cpp +++ b/lib/usdLayerEditor/lib/sessionState.cpp @@ -23,6 +23,24 @@ void SessionState::setAutoHideSessionLayer(bool hideIt) Q_EMIT autoHideSessionLayerSignal(_autoHideSessionLayer); } +void SessionState::setDisplayLayerContents(bool showIt) +{ + _displayLayerContents = showIt; + Q_EMIT showDisplayLayerContents(showIt); +} + +void SessionState::setDisplayLayerExpandAllValues(bool expand) +{ + _displayLayerExpandAllValues = expand; + Q_EMIT showDisplayLayerContents(_displayLayerContents); +} + +void SessionState::setDisplayLayerHideIndices(bool hide) +{ + _displayLayerHideIndices = hide; + Q_EMIT showDisplayLayerContents(_displayLayerContents); +} + void SessionState::setStageEntry(StageEntry const& in_entry) { if (_currentStageEntry != in_entry) { @@ -42,4 +60,6 @@ PXR_NS::SdfLayerRefPtr SessionState::targetLayer() const } } +PXR_NS::SdfLayerRefPtr SessionState::effectiveTargetLayer() const { return targetLayer(); } + } // namespace UsdLayerEditor \ No newline at end of file diff --git a/lib/usdLayerEditor/lib/sessionState.h b/lib/usdLayerEditor/lib/sessionState.h index 6db7641d73..70b5322ffa 100644 --- a/lib/usdLayerEditor/lib/sessionState.h +++ b/lib/usdLayerEditor/lib/sessionState.h @@ -79,6 +79,21 @@ class LayerEditorAPI SessionState : public QObject virtual bool autoHideSessionLayer() const { return _autoHideSessionLayer; } virtual void setAutoHideSessionLayer(bool hide); virtual bool autoObserveUfeSelection() const { return true; } + + virtual bool displayLayerContents() const { return _displayLayerContents; } + virtual void setDisplayLayerContents(bool show); + virtual bool displayLayerExpandAllValues() const { return _displayLayerExpandAllValues; } + virtual void setDisplayLayerExpandAllValues(bool expand); + virtual bool displayLayerHideIndices() const { return _displayLayerHideIndices; } + virtual void setDisplayLayerHideIndices(bool hide); + + // Edit-forwarding state lives here, on the SessionState, rather than in the + // layerEditorDCCFunctions registry because it depends on this instance's current stage. The + // rule: stage/instance-dependent state is a SessionState virtual; stateless DCC capabilities + // (e.g. supportsEditForwarding, handleEFEditTargetUpdate) live in the registry. + virtual bool isEditForwardMode() const { return false; } + virtual PXR_NS::SdfLayerRefPtr effectiveTargetLayer() const; + PXR_NS::UsdStageRefPtr const& stage() const { return _currentStageEntry._stage; } StageEntry const& stageEntry() const { return _currentStageEntry; } PXR_NS::SdfLayerRefPtr targetLayer() const; @@ -121,12 +136,20 @@ class LayerEditorAPI SessionState : public QObject void autoHideSessionLayerSignal(bool hideIt); void stageResetSignal(StageEntry const& entry); void dccSelectionChangedSignal(); + void showDisplayLayerContents(bool showIt); + // Emitted by DCC integrations when the EF state of the current stage changes. + void editForwardingChanged(); + // Emitted by DCC integrations when the EF fallback target layer changes. + void editForwardingFallbackTargetChanged(); protected: StageEntry _currentStageEntry; bool _autoHideSessionLayer = true; + bool _displayLayerContents { true }; + bool _displayLayerExpandAllValues { false }; + bool _displayLayerHideIndices { false }; }; } // namespace UsdLayerEditor -#endif // SESSIONSTATE_H +#endif // USDLAYEREDITOR_SESSIONSTATE_H diff --git a/lib/usdLayerEditor/lib/stageSelectorWidget.cpp b/lib/usdLayerEditor/lib/stageSelectorWidget.cpp index 20a87a4e60..604d729188 100644 --- a/lib/usdLayerEditor/lib/stageSelectorWidget.cpp +++ b/lib/usdLayerEditor/lib/stageSelectorWidget.cpp @@ -15,9 +15,9 @@ // #include "stageSelectorWidget.h" +#include "layerEditorDCCFunctions.h" #include "sessionState.h" #include "stringResources.h" -#include "utilOptions.h" #include "utilQT.h" #include @@ -60,21 +60,9 @@ int getEntryIndexById( return getEntryIndexById(entry._id, stages); } -bool loadStagePinnedOption() -{ - // TODO LE-EXTRACT Save/load pinned stage option. - return false; - // const std::wstring optionName = PXR_NS::MayaUsdOptionVars->PinLayerEditorStage.GetText(); - // return OptionsUtils::optionVarExists(optionName) - // && OptionsUtils::optionVarExists(optionName) != 0; -} +bool loadStagePinnedOption() { return UsdLayerEditor::getPinLayerEditorStage(); } -void saveStagePinnedOption(bool isPinned) -{ - // TODO LE-EXTRACT Save/load pinned stage option. - // const std::wstring optionName = PXR_NS::MayaUsdOptionVars->PinLayerEditorStage.GetText(); - // OptionsUtils::setOptionVarValue(optionName, isPinned ? 1 : 0); -} +void saveStagePinnedOption(bool isPinned) { UsdLayerEditor::setPinLayerEditorStage(isPinned); } //---------------------------------------------------------------------------------------------------------------------- @@ -182,11 +170,11 @@ StageSelectorWidget::StageSelectorWidget(SessionState* in_sessionState, QWidget* _pinStageSelection = loadStagePinnedOption(); updatePinnedStage(); + updateContentButton(); } StageSelectorWidget::~StageSelectorWidget() { - // Remove observer - if not observing this is a no-op. StageSelectorSelectionObserver::instance()->removeStageSelector(*this); } @@ -219,6 +207,20 @@ void StageSelectorWidget::createUI() mainHLayout->addWidget(_pinStage, 0, Qt::AlignLeft | Qt::AlignRight); connect(_pinStage, &QAbstractButton::clicked, this, &StageSelectorWidget::stagePinClicked); + _collapseContent = new QPushButton(); + _collapseContent->setObjectName("collapseContentButton"); + _collapseContent->move(0, higButtonYOffset); + QtUtils::setupButtonWithHIGBitmaps(_collapseContent, ":/UsdLayerEditor/contents_on"); + _collapseContent->setFixedSize(buttonSize, buttonSize); + _collapseContent->setToolTip( + StringResources::getAsQString(StringResources::kDisplayLayerContents)); + mainHLayout->addWidget(_collapseContent, 0, Qt::AlignLeft | Qt::AlignRight); + connect( + _collapseContent, + &QAbstractButton::clicked, + this, + &StageSelectorWidget::collapseContentClicked); + setLayout(mainHLayout); } @@ -245,6 +247,12 @@ void StageSelectorWidget::setSessionState(SessionState* in_sessionState) this, &StageSelectorWidget::selectionChanged); + connect( + _sessionState, + &SessionState::showDisplayLayerContents, + this, + &StageSelectorWidget::updateContentButton); + updateFromSessionState(_sessionState->stageEntry()); } @@ -307,24 +315,6 @@ void StageSelectorWidget::selectedIndexChanged(int index) _internalChange = false; } -// TODO LE-EXTRACT Handle maya proxy shape selection changed. -// static MayaUsdProxyShapeBase* getChildProxyShape(const Ufe::SceneItem::Ptr& item) -//{ -// Ufe::Hierarchy::Ptr hierarchy = Ufe::Hierarchy::hierarchy(item); -// if (!hierarchy) -// return nullptr; -// -// for (const auto& subItem : hierarchy->children()) { -// auto proxyShapePtr = MayaUsd::ufe::getProxyShape(subItem->path()); -// if (!proxyShapePtr) -// continue; -// -// return proxyShapePtr; -// } -// -// return nullptr; -// } - void StageSelectorWidget::selectionChanged() { // When the stage selection is pinned, don't follow the selection. @@ -344,27 +334,6 @@ void StageSelectorWidget::selectionChanged() return; } _dropDown->setCurrentIndex(index); - - // TODO LE-EXTRACT Handle maya proxy shape selection changed. - // const Ufe::Selection& ufeSelection = *ufeGlobalSelection; - // for (const auto& item : ufeSelection) { - // auto proxyShapePtr = MayaUsd::ufe::getProxyShape(item->path()); - // if (!proxyShapePtr) { - // proxyShapePtr = getChildProxyShape(item); - // if (!proxyShapePtr) { - // continue; - // } - // } - - // MFnDagNode dagNode(proxyShapePtr->thisMObject()); - // const std::string id = dagNode.uuid().asString().asChar(); - // const int index = getEntryIndexById(id, _sessionState->allStages()); - // if (index == -1) - // continue; - - // _dropDown->setCurrentIndex(index); - // break; - //} } void StageSelectorWidget::stagePinClicked() @@ -374,6 +343,23 @@ void StageSelectorWidget::stagePinClicked() updatePinnedStage(); } +void StageSelectorWidget::collapseContentClicked() +{ + if (_sessionState) { + _sessionState->setDisplayLayerContents(!_sessionState->displayLayerContents()); + } +} + +void StageSelectorWidget::updateContentButton() +{ + if (!_collapseContent) + return; + const bool showIt = _sessionState && _sessionState->displayLayerContents(); + QtUtils::setupButtonWithHIGBitmaps( + _collapseContent, + showIt ? ":/UsdLayerEditor/contents_on" : ":/UsdLayerEditor/contents_off"); +} + void StageSelectorWidget::updatePinnedStage() { QtUtils::setupButtonWithHIGBitmaps( diff --git a/lib/usdLayerEditor/lib/stageSelectorWidget.h b/lib/usdLayerEditor/lib/stageSelectorWidget.h index 5157deec4e..e3fe6cb973 100644 --- a/lib/usdLayerEditor/lib/stageSelectorWidget.h +++ b/lib/usdLayerEditor/lib/stageSelectorWidget.h @@ -30,7 +30,7 @@ namespace UsdLayerEditor { * @brief Drop down list that allows selecting a stage. Owned by the LayerEditorWidget * */ -class StageSelectorWidget : public QWidget +class LayerEditorAPI StageSelectorWidget : public QWidget { Q_OBJECT public: @@ -54,11 +54,14 @@ class StageSelectorWidget : public QWidget void sessionStageChanged(); void selectedIndexChanged(int index); void stagePinClicked(); + void collapseContentClicked(); + void updateContentButton(); private: SessionState* _sessionState = nullptr; QComboBox* _dropDown = nullptr; QPushButton* _pinStage = nullptr; + QPushButton* _collapseContent = nullptr; bool _internalChange = false; // for notifications bool _pinStageSelection = true; }; diff --git a/lib/usdLayerEditor/lib/stringResources.cpp b/lib/usdLayerEditor/lib/stringResources.cpp index 0e580599f3..ccb90b55ca 100644 --- a/lib/usdLayerEditor/lib/stringResources.cpp +++ b/lib/usdLayerEditor/lib/stringResources.cpp @@ -18,9 +18,6 @@ #include -// TODO LE-EXTRACT : String resources - Is this enough for maya-usd? Does the maya registration -// matter? - namespace UsdLayerEditor { namespace StringResources { diff --git a/lib/usdLayerEditor/lib/stringResources.h b/lib/usdLayerEditor/lib/stringResources.h index c929a49c00..dc38f91991 100644 --- a/lib/usdLayerEditor/lib/stringResources.h +++ b/lib/usdLayerEditor/lib/stringResources.h @@ -23,9 +23,6 @@ class QString; -// TODO LE-EXTRACT : String resources - Is this enough for maya-usd? Does the maya registration -// matter? - namespace UsdLayerEditor { namespace StringResources { @@ -37,10 +34,10 @@ struct Resource std::string value; }; -// Retreive a string resource from the given std::wstringResourceId. +// Retrieve a string resource from the given Resource. LayerEditorAPI QString getAsQString(const Resource& strResID); -// create a std::wstringResourceId, must be called before registerAll() +// Create a Resource; must be called before registerAll() LayerEditorAPI Resource create(const char* key, const char* value); // ------------------------------------------------------------- @@ -53,6 +50,15 @@ LayerEditorAPI Resource create(const char* key, const char* value); const auto kAddNewLayer { create("kAddNewLayer", "Add a New Layer") }; const auto kAddSublayer { create("kAddSublayer", "Add sublayer") }; const auto kAutoHideSessionLayer { create("kAutoHideSessionLayer", "Auto-Hide Session Layer") }; +const auto kDisplayLayerContents { create("kDisplayLayerContents", "Display Layer Content") }; +const auto kDisplayLayerContentsEmpty { create("kDisplayLayerContentsEmpty", "Select a single layer to display the contents.\n\nLarge layers may take longer to load.") }; +const auto kToggleEditForwarding { create("kToggleEditForwarding", "Toggle Edit Forwarding") }; +const auto kEchoEditForwarding { create("kEchoEditForwarding", "Echo Edit Forwarding in Script Editor") }; +const auto kConfigureEditForwarding { create("kConfigureEditForwarding", "Configure Edit Forwarding...") }; +const auto kConfigureEditForwardingTitle { create("kConfigureEditForwardingTitle", "Configure Edit Forwarding") }; +const auto kDisplayLayerExpandAllValues { create("kDisplayLayerExpandAllValues", "Expand All Values") }; +const auto kDisplayLayerExpandAllValuesTooltip { create("kDisplayLayerExpandAllValuesTooltip", + "Enable to display all array values and timeSamples in the layer content") }; const auto kConvertToRelativePath { create("kConvertToRelativePath", "Convert to Relative Path") }; const auto kCancel { create("kCancel", "Cancel") }; const auto kCreate { create("kCreate", "Create") }; @@ -90,11 +96,11 @@ const auto kSaveStages { create("kSaveStages", "Save Stage(s)" const auto kSaveStagesAndExport { create("kSaveStagesAndExport", "Save Stage(s) and Export") }; const auto kSaveXStages { create("kSaveXStages", "Save ^1s Stage(s)") }; const auto kToSaveTheStageSaveAnonym { create("kToSaveTheStageSaveAnonym", "To save the ^1s stage(s), save the following ^2s anonymous layer(s).") }; -// TODO LE-EXTRACT Temporary confirmation dialog string. To be removed. -const auto kToSaveStageFilesConfirm { create("kToSaveStageFilesConfirm", "To save the stage, all its modified layers will be overwritten. Are you sure you want to proceed?") }; const auto kToSaveTheStageSaveFiles { create("kToSaveTheStageSaveFiles", "To save the ^1s stage(s), the following existing file(s) will be overwritten.") }; const auto kToExportTheStageSaveAnonym { create("kToExportTheStageSaveAnonym", "To export the ^1s stage(s), save the following ^2s anonymous layer(s).") }; const auto kToExportTheStageSaveFiles { create("kToExportTheStageSaveFiles", "To export the ^1s stage(s), the following existing file(s) will be overwritten.") }; +const auto kToSaveTheStageSaveComponents { create("kToSaveTheStageSaveComponents", "To save the ^1s stage(s), save the following component(s).") }; +const auto kToExportTheStageSaveComponents { create("kToExportTheStageSaveComponents", "To export the ^1s stage(s), save the following component(s).") }; const auto kUsedInStagesTooltip { create("kUsedInStagesTooltip", "Used in Stages: ") }; const auto kSetLayerAsTargetLayerTooltip { create("kSetLayerAsTargetLayerTooltip", "Set layer as target layer. Edits are added to the target layer.") }; diff --git a/lib/usdLayerEditor/lib/tokens.h b/lib/usdLayerEditor/lib/tokens.h index 4d97fa1c9e..0769bb634b 100644 --- a/lib/usdLayerEditor/lib/tokens.h +++ b/lib/usdLayerEditor/lib/tokens.h @@ -53,9 +53,10 @@ TF_DECLARE_PUBLIC_TOKENS(UsdLayerEditorOptionVars, LayerEditorAPI, USDLAYEREDITO // // clang-format off #define USDLAYEREDITOR_METADATA_TOKENS \ - /* Referenced layers. */ \ - /* TODO LE-EXTRACT : mayaSharedLayers -> dccSharedLayers, do we need to support cross DCC metadata? */ \ - ((ReferencedLayers, "adskSharedLayers")) + /* DCC-agnostic token for layers referenced from a shared asset (read-only in this context). */ \ + ((ReferencedLayers, "adskSharedLayers")) \ + /* Legacy Maya-specific token written by MayaUSD proxyShapeBase — read for backward compat. */ \ + ((MayaReferencedLayers, "mayaSharedLayers")) // clang-format on TF_DECLARE_PUBLIC_TOKENS(UsdLayerEditorMetadata, LayerEditorAPI, USDLAYEREDITOR_METADATA_TOKENS); diff --git a/lib/usdLayerEditor/lib/ufeCommandHook.cpp b/lib/usdLayerEditor/lib/ufeCommandHook.cpp index e99dcb7837..f4cb9d695c 100644 --- a/lib/usdLayerEditor/lib/ufeCommandHook.cpp +++ b/lib/usdLayerEditor/lib/ufeCommandHook.cpp @@ -17,7 +17,7 @@ #include "ufeCommandHook.h" #include "abstractCommandHook.h" -#include "layerEditorCommands.h" +#include "LayerEditorCommands.h" #include "sessionState.h" #include @@ -80,12 +80,19 @@ void UfeCommandHook::moveSubLayerPath( UsdLayer newParentUsdLayer, int index) { - // TODO LE-EXTRACT Move sub-layer path. + auto removeCmd = ::std::make_shared( + _sessionState->stage(), oldParentUsdLayer, path); + AppendOrExecuteCommand(removeCmd); + + auto insertCmd = ::std::make_shared( + _sessionState->stage(), newParentUsdLayer, path, index); + AppendOrExecuteCommand(insertCmd); } void UfeCommandHook::replaceSubLayerPath(UsdLayer usdLayer, Path oldPath, Path newPath) { - // TODO LE-EXTRACT replace sub-layer path. + auto cmd = ::std::make_shared(usdLayer, oldPath, newPath); + AppendOrExecuteCommand(cmd); } void UfeCommandHook::discardEdits(UsdLayer usdLayer) diff --git a/lib/usdLayerEditor/lib/usdSyntaxConfig.json b/lib/usdLayerEditor/lib/usdSyntaxConfig.json new file mode 100644 index 0000000000..9cc783a903 --- /dev/null +++ b/lib/usdLayerEditor/lib/usdSyntaxConfig.json @@ -0,0 +1,389 @@ +{ + "syntaxHighlighting": { + "specifiers": { + "color": "#E98FE6", + "fontWeight": "normal", + "wordPatterns": [ + "def", + "class", + "over", + "variantSet" + ] + }, + + "storageModifier": { + "color": "#5DA5E2", + "fontWeight": "normal", + "wordPatterns": [ + "connect", + "custom", + "uniform", + "varying" + ] + }, + "geomTokens": { + "color": "#9CDCFE", + "fontWeight": "normal", + "wordPatterns": [ + "accelerations", + "all", + "angularVelocities", + "apiSchemas", + "assetInfo", + "axis", + "basis", + "bezier", + "bilinear", + "boundaries", + "bounds", + "box", + "bspline", + "cards", + "catmullClark", + "catmullRom", + "clippingPlanes", + "clippingRange", + "closed", + "constant", + "cornerIndices", + "cornerSharpnesses", + "cornersOnly", + "cornersPlus1", + "cornersPlus2", + "creaseIndices", + "creaseLengths", + "creaseSharpnesses", + "cross", + "cubic", + "curveVertexCounts", + "default", + "defaultPrim", + "doubleSided", + "edge", + "edgeAndCorner", + "edgeOnly", + "elementSize", + "elementType", + "exposure", + "exposure\\:fStop", + "exposure\\:iso", + "exposure\\:responsivity", + "exposure\\:time", + "extent", + "extentsHint", + "face", + "faceVarying", + "faceVaryingLinearInterpolation", + "faceVertexCounts", + "faceVertexIndices", + "familyName", + "focalLength", + "focusDistance", + "fromTexture", + "fStop", + "generated", + "guide", + "guideVisibility", + "height", + "hermite", + "holeIndices", + "horizontalAperture", + "horizontalApertureOffset", + "ids", + "inactiveIds", + "indices", + "info\\:id", + "inherited", + "interpolateBoundary", + "interpolation", + "invisible", + "invisibleIds", + "knots", + "left", + "leftHanded", + "length", + "linear", + "loop", + "material\\:binding", + "metersPerUnit", + "model\\:applyDrawMode", + "model\\:cardGeometry", + "model\\:cardTextureXNeg", + "model\\:cardTextureXPos", + "model\\:cardTextureYNeg", + "model\\:cardTextureYPos", + "model\\:cardTextureZNeg", + "model\\:cardTextureZPos", + "model\\:drawMode", + "model\\:drawModeColor", + "mono", + "motion\\:blurScale", + "motion\\:nonlinearSampleCount", + "motion\\:velocityScale", + "none", + "nonOverlapping", + "nonperiodic", + "normals", + "open", + "order", + "orientation", + "orientations", + "orientationsf", + "origin", + "orthographic", + "outputs\\:surface", + "partition", + "periodic", + "perspective", + "pinned", + "pivot", + "point", + "points", + "pointWeights", + "positions", + "power", + "primvars\\:displayColor", + "primvars\\:displayOpacity", + "primvars\\:st\\:", + "projection", + "protoIndices", + "prototypes", + "proxy", + "proxyPrim", + "proxyVisibility", + "purpose", + "radius", + "radiusBottom", + "radiusTop", + "ranges", + "render", + "renderVisibility", + "right", + "rightHanded", + "scales", + "segment", + "shutter\\:close", + "shutter\\:open", + "size", + "smooth", + "stereoRole", + "subdivisionScheme", + "surface", + "surfaceFaceVertexIndices", + "tangents", + "tetrahedron", + "tetVertexIndices", + "triangleSubdivisionRule", + "trimCurve\\:counts", + "trimCurve\\:knots", + "trimCurve\\:orders", + "trimCurve\\:points", + "trimCurve\\:ranges", + "trimCurve\\:vertexCounts", + "type", + "uForm", + "uKnots", + "unauthoredValuesIndex", + "unrestricted", + "uOrder", + "upAxis", + "uRange", + "uVertexCount", + "velocities", + "vertex", + "verticalAperture", + "verticalApertureOffset", + "vForm", + "visibility", + "visible", + "vKnots", + "vOrder", + "vRange", + "vVertexCount", + "width", + "widths", + "wrap", + "xformOpOrder", + "xformOp:rotateXYZ", + "xformOp:scale", + "xformOp:translate", + "xformOp:" + ] + }, + "keywords": { + "color": "#5DA5E2", + "fontWeight": "normal", + "wordPatterns": [ + "active", + "add", + "color", + "curvature", + "customData", + "delete", + "detail", + "dictionary", + "displayOpactity", + "doc", + "endFrame", + "floatAttribute", + "framePrecision", + "framesPerSecond", + "grimID", + "group", + "hidden", + "inherits", + "kind", + "numVerts", + "occulusion", + "payload", + "pivotPosition", + "prop", + "references", + "rel", + "reorder", + "set", + "shadingComplexity", + "slimSurfID", + "specializes", + "startFrame", + "subLayers", + "timeSamples", + "transform", + "twoSided", + "u_map1", + "v_map1", + "variants", + "variantSets", + "verts" + ] + }, + "sdfTypes": { + "color": "#5DA5E2", + "fontWeight": "normal", + "wordPatterns": [ + "asset", + "bool", + "color3d", + "color3f", + "color3h", + "color4d", + "color4f", + "color4h", + "double", + "double2", + "double3", + "double4", + "float", + "float2", + "float3", + "float4", + "frame4d", + "group", + "half", + "half2", + "half3", + "half4", + "int", + "int2", + "int3", + "int4", + "int64", + "matrix2d", + "matrix3d", + "matrix4d", + "normal3d", + "normal3f", + "normal3h", + "opaque", + "pathExpression", + "point3d", + "point3f", + "point3h", + "quatd", + "quatf", + "quath", + "string", + "texCoord2d", + "texCoord2f", + "texCoord2h", + "texCoord3d", + "texCoord3f", + "texCoord3h", + "timecode", + "token", + "uchar", + "uint", + "uint64", + "vector3d", + "vector3f", + "vector3h" + ] + }, + "primitiveTypes": { + "color": "#9CDCFE", + "fontWeight": "normal", + "__comment": "Primitive types are dynamically read, but you can add any extra here.", + "wordPatterns": [ + ] + }, + "operators": { + "color": "#9CDCFE", + "fontWeight": "normal", + "wordPatterns": [ + "NULL", + "false", + "off", + "on", + "true" + ] + }, + "numbers": { + "color": "#C3DCB5", + "fontWeight": "normal", + "patterns": [ + "\\b\\d+(\\.\\d+)?([eE][+-]?\\d+)?\\b" + ] + }, + "strings": { + "color": "#DFA790", + "fontWeight": "normal", + "patterns": [ + "\".*\"", + "@.*@" + ] + }, + "comments": { + "color": "#6A9955", + "fontWeight": "normal", + "patterns": [ + "#[^\r\n]*" + ] + }, + "brackets": { + "color": "#FFD853", + "fontWeight": "normal", + "patterns": [ + "\\[", + "\\]", + "\\{", + "\\}", + "\\(", + "\\)" + ] + }, + "delimiters": { + "color": "#FFFFFF", + "fontWeight": "normal", + "patterns": [ + "," + ] + }, + "angleBrackets": { + "color": "#DFA790", + "fontWeight": "normal", + "patterns": [ + "<.*>" + ] + } + } +} \ No newline at end of file diff --git a/lib/usdLayerEditor/lib/usdSyntaxHighlighter.cpp b/lib/usdLayerEditor/lib/usdSyntaxHighlighter.cpp new file mode 100644 index 0000000000..7715ba68aa --- /dev/null +++ b/lib/usdLayerEditor/lib/usdSyntaxHighlighter.cpp @@ -0,0 +1,234 @@ +// +// Copyright 2025 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 "usdSyntaxHighlighter.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +// Helper function to dynamically read all the concrete prim types. +PXR_NS::TfTokenVector getConcretePrimTypes() +{ + PXR_NS::TfTokenVector primTypes; + + // Query all the available types + PXR_NS::PlugRegistry& plugReg = PXR_NS::PlugRegistry::GetInstance(); + std::set schemaTypes; + plugReg.GetAllDerivedTypes(&schemaTypes); + + PXR_NS::UsdSchemaRegistry& schemaReg = PXR_NS::UsdSchemaRegistry::GetInstance(); + for (auto t : schemaTypes) { + if (!schemaReg.IsConcrete(t)) { + continue; + } + + auto plugin = plugReg.GetPluginForType(t); + if (!plugin) { + continue; + } + + auto type_name = PXR_NS::UsdSchemaRegistry::GetConcreteSchemaTypeName(t); + primTypes.push_back(type_name); + } + return primTypes; +} + +} // namespace + +namespace UsdLayerEditor { + +UsdSyntaxHighlighter::UsdSyntaxHighlighter(QTextDocument* parent) + : QSyntaxHighlighter(parent) +{ + setupHighlightingRules(); +} + +void UsdSyntaxHighlighter::setupHighlightingRules() +{ + // Clear existing rules + _highlightingRules.clear(); + + loadConfigFromJson(); +} + +QTextCharFormat UsdSyntaxHighlighter::createFormat(const QString& color, int fontWeight) +{ + QTextCharFormat format; + format.setForeground(QColor(color)); + format.setFontWeight(fontWeight); + return format; +} + +void UsdSyntaxHighlighter::addRule(const QString& pattern, const QTextCharFormat& format) +{ + HighlightingRule rule; + rule._pattern = QRegularExpression(pattern); + rule._format = format; + _highlightingRules.append(rule); +} + +bool UsdSyntaxHighlighter::loadConfigFromJson(const QString& configPath) +{ + PXR_NAMESPACE_USING_DIRECTIVE + + QString jsonPath = configPath; + if (jsonPath.isEmpty()) { + // First check if the user is providing a custom config file. + const std::string customConfigPath + = PXR_NS::TfGetenv("MAYAUSD_USD_SYNTAX_HIGHLIGHTING_CONFIG"); + if (!customConfigPath.empty()) { + jsonPath = QString::fromStdString(customConfigPath); + if (!QFile::exists(jsonPath)) { + TF_WARN( + "Custom USD syntax highlighting config file does not exist: [%s].", + qPrintable(jsonPath)); + jsonPath.clear(); + } + } + } + + if (jsonPath.isEmpty()) { + // Default to the config file installed in MayaUsd library location. + const std::string libLoc = PXR_NS::TfGetenv("MAYAUSD_LIB_LOCATION"); + if (!libLoc.empty()) { + jsonPath = QString::fromStdString(libLoc) + "/syntaxHighlight/usdSyntaxConfig.json"; + } + } + + QFile file(jsonPath); + if (!file.open(QIODevice::ReadOnly)) { + TF_WARN("Could not open USD syntax highlighting config file: [%s].", qPrintable(jsonPath)); + return false; + } + + QByteArray jsonData = file.readAll(); + file.close(); + + QJsonParseError parseError; + QJsonDocument doc = QJsonDocument::fromJson(jsonData, &parseError); + if (parseError.error != QJsonParseError::NoError) { + TF_WARN( + "Error during USD syntax highlighting JSON config parsing: %s", + qPrintable(parseError.errorString())); + return false; + } + + QJsonObject root = doc.object(); + QJsonObject syntaxHighlighting = root.value("syntaxHighlighting").toObject(); + + // Load each category from JSON + auto loadCategory = [this](const QString& categoryName, const QJsonObject& category) { + QString color = category.value("color").toString(); + QString fontWeight = category.value("fontWeight").toString(); + int weight = getFontWeight(fontWeight); + QTextCharFormat format = createFormat(color, weight); + + // Handle word patterns array + if (category.contains("wordPatterns")) { + QJsonArray patterns = category.value("wordPatterns").toArray(); + for (const auto patternValue : patterns) { + // Add in the word boundary to the pattern. + QString pattern = QString("\\b%1\\b").arg(patternValue.toString()); + addRule(pattern, format); + } + } + // Handle patterns array + else if (category.contains("patterns")) { + QJsonArray patterns = category.value("patterns").toArray(); + for (const auto patternValue : patterns) { + QString pattern = patternValue.toString(); + addRule(pattern, format); + } + } else { + TF_WARN( + "Category '%s' does not contain 'wordPatterns' or 'patterns'.", + qPrintable(categoryName)); + } + }; + + // Dynamically load concrete prim types + auto loadPrimTypes = [this](const QJsonObject& category) { + QString color = category.value("color").toString(); + QString fontWeight = category.value("fontWeight").toString(); + int weight = getFontWeight(fontWeight); + QTextCharFormat format = createFormat(color, weight); + + PXR_NS::TfTokenVector primTypes = getConcretePrimTypes(); + for (const auto& type : primTypes) { + QString pattern = QString("\\b%1\\b").arg(QString::fromStdString(type.GetString())); + addRule(pattern, format); + } + }; + + // Load all categories + const QStringList categories + = { "specifiers", "storageModifier", "geomTokens", "keywords", "sdfTypes", + "primitiveTypes", "operators", "numbers", "strings", "comments", + "brackets", "delimiters", "angleBrackets" }; + + for (const QString& categoryName : categories) { + if (syntaxHighlighting.contains(categoryName)) { + QJsonObject category = syntaxHighlighting.value(categoryName).toObject(); + loadCategory(categoryName, category); + + // Special case for primitive types. + // Any extra types defined in the JSON will also be added. + if (categoryName == "primitiveTypes") { + loadPrimTypes(category); + } + } + } + + return true; +} + +int UsdSyntaxHighlighter::getFontWeight(const QString& fontWeight) const +{ + if (fontWeight.toLower() == "bold") { + return QFont::Bold; + } + return QFont::Normal; +} + +void UsdSyntaxHighlighter::highlightBlock(const QString& text) +{ + // Apply all highlighting rules + for (const HighlightingRule& rule : qAsConst(_highlightingRules)) { + QRegularExpressionMatchIterator matchIterator = rule._pattern.globalMatch(text); + while (matchIterator.hasNext()) { + QRegularExpressionMatch match = matchIterator.next(); + setFormat(match.capturedStart(), match.capturedLength(), rule._format); + } + } +} + +} // namespace UsdLayerEditor diff --git a/lib/usdLayerEditor/lib/usdSyntaxHighlighter.h b/lib/usdLayerEditor/lib/usdSyntaxHighlighter.h new file mode 100644 index 0000000000..37a022cc35 --- /dev/null +++ b/lib/usdLayerEditor/lib/usdSyntaxHighlighter.h @@ -0,0 +1,91 @@ +// +// Copyright 2025 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 USDLAYEREDITOR_USDSYNTAXHIGHLIGHTER_H +#define USDLAYEREDITOR_USDSYNTAXHIGHLIGHTER_H + +#include "layerEditorAPI.h" + +#include +#include +#include +#include + +class QTextDocument; + +namespace UsdLayerEditor { + +/** + * @brief Syntax highlighter for USD (Universal Scene Description) files. + * + * This class provides syntax highlighting for USD layers based on + * the USD specification. It highlights keywords, data types, primitives, comments, + * strings, and numbers using appropriate colors. + */ +class LayerEditorAPI UsdSyntaxHighlighter : public QSyntaxHighlighter +{ + Q_OBJECT + +public: + /** + * @brief Constructor + * @param parent The parent QTextDocument to apply highlighting to + */ + explicit UsdSyntaxHighlighter(QTextDocument* parent = nullptr); + +protected: + void highlightBlock(const QString& text) override; + +private: + // Structure to hold highlighting rules + struct HighlightingRule + { + QRegularExpression _pattern; + QTextCharFormat _format; + }; + + // Initialize highlighting rules and formats + void setupHighlightingRules(); + + /** + * @brief Load syntax highlighting configuration from JSON file + * @param configPath Path to the JSON configuration file + * @return true if configuration was loaded successfully, false otherwise + */ + bool loadConfigFromJson(const QString& configPath = QString()); + + /** + * @brief Create text format with specified color + * @param color The color in hex format (e.g., "#93C763") + * @param fontWeight The font weight (default is QFont::Normal) + * @return QTextCharFormat with the specified color + */ + QTextCharFormat createFormat(const QString& color, int fontWeight = QFont::Normal); + + /** + * @brief Get font weight from string value + * @param fontWeight String representation of font weight ("normal", "bold") + * @return QFont weight value + */ + int getFontWeight(const QString& fontWeight) const; + + void addRule(const QString& pattern, const QTextCharFormat& format); + + QVector _highlightingRules; +}; + +} // namespace UsdLayerEditor + +#endif // USDLAYEREDITOR_USDSYNTAXHIGHLIGHTER_H diff --git a/lib/usdLayerEditor/lib/utilFileSystem.cpp b/lib/usdLayerEditor/lib/utilFileSystem.cpp index 843290d801..436204e8e8 100644 --- a/lib/usdLayerEditor/lib/utilFileSystem.cpp +++ b/lib/usdLayerEditor/lib/utilFileSystem.cpp @@ -15,6 +15,8 @@ // #include "utilFileSystem.h" +#include "layerEditorDCCFunctions.h" + #include "pxr/usd/sdf/attributeSpec.h" #include "pxr/usd/sdf/primSpec.h" #include "pxr/usd/sdf/reference.h" @@ -23,15 +25,9 @@ #include -#include +#include #include -namespace { -std::function writeAccessCheckFunc; -std::function dccSceneSaveLocationFunc; -std::function dccWorkspaceSceneSaveLocationFunc; -} - namespace { PXR_NAMESPACE_USING_DIRECTIVE @@ -55,7 +51,7 @@ std::string generateUniqueName() struct PostponedRelativeInfo { - std::set paths; + std::set paths; std::set attrs; }; @@ -85,12 +81,12 @@ std::string resolvePath(const std::string& filePath) std::string getDir(const std::string& fullFilePath) { - return std::filesystem::path(fullFilePath).parent_path().string(); + return fs::filesystem::path(fullFilePath).parent_path().string(); } std::string getDCCSceneFileDir() { - return dccSceneSaveLocationFunc(); + return UsdLayerEditor::getDCCSceneDir(); } std::string getLayerFileDir(const PXR_NS::SdfLayerHandle& layer) @@ -108,7 +104,7 @@ std::string getLayerFileDir(const PXR_NS::SdfLayerHandle& layer) std::pair makePathRelativeTo(const std::string& fileName, const std::string& relativeToDir) { - std::filesystem::path absolutePath(fileName); + fs::filesystem::path absolutePath(fileName); // If the anchor relative-to-directory doesn't exist yet, use the unchanged path, // but don't return a failure. The anchor path being empty is not considered @@ -118,7 +114,7 @@ makePathRelativeTo(const std::string& fileName, const std::string& relativeToDir return std::make_pair(fileName, true); } - std::filesystem::path relativePath = absolutePath.lexically_relative(relativeToDir); + fs::filesystem::path relativePath = absolutePath.lexically_relative(relativeToDir); if (relativePath.empty()) { return std::make_pair(fileName, false); @@ -127,56 +123,6 @@ makePathRelativeTo(const std::string& fileName, const std::string& relativeToDir return std::make_pair(relativePath.generic_string(), true); } -std::string getPathRelativeToProject(const std::string& fileName) -{ - return {}; - // TODO LE-EXTRACT Get path relative to DCC project. - // if (fileName.empty()) - // return {}; - - // const std::string projectPath(UsdMayaUtil::GetCurrentMayaWorkspacePath().asChar()); - // if (projectPath.empty()) - // return {}; - - //// Note: we do *not* use filesystem function to attempt to make the - //// path relative sinceit would succeed as long as both paths - //// are on the same drive. We really only want to know if the - //// project path is the prefix of the file path. Maya will - //// preserve paths entered manually with relative folder ("..") - //// by keping an absolute path with ".." embedded in them, - //// so this works even in this situation. - // const auto pos = fileName.rfind(projectPath, 0); - // if (pos != 0) - // return {}; - - // auto relativePathAndSuccess = makePathRelativeTo(fileName, projectPath); - - // if (!relativePathAndSuccess.second) - // return {}; - - // return relativePathAndSuccess.first; -} - -std::string makeProjectRelatedPath(const std::string& fileName) -{ - return {}; - - // TODO LE-EXTRACT Relative patgh to DCC project. - /*const std::string projectPath(UsdMayaUtil::GetCurrentMayaWorkspacePath().asChar()); - if (projectPath.empty()) - return {}; - - // Attempt to create a relative path relative to the project folder. - // If that fails, we cannot create the project-relative path. - const auto pathAndSuccess = makePathRelativeTo(fileName, projectPath); - if (!pathAndSuccess.second) - return {}; - - // Make the path absolute but relative to the project folder. That is an absolute - // path that starts with the project path. - return appendPaths(projectPath, pathAndSuccess.first);*/ -} - std::string getPathRelativeToDirectory(const std::string& fileName, const std::string& relativeToDir) { @@ -240,7 +186,7 @@ void markPathAsPostponedRelative( const PXR_NS::SdfLayerHandle& layer, const std::string& contentPath) { - std::filesystem::path filePath(contentPath); + fs::filesystem::path filePath(contentPath); auto& postponedRelativePaths = getPostponedRelativePaths(); postponedRelativePaths[layer].paths.insert(filePath); } @@ -252,7 +198,7 @@ void unmarkPathAsPostponedRelative( auto& postponedRelativePaths = getPostponedRelativePaths(); auto layerEntry = postponedRelativePaths.find(layer); if (layerEntry != postponedRelativePaths.end()) { - std::filesystem::path filePath(contentPath); + fs::filesystem::path filePath(contentPath); layerEntry->second.paths.erase(filePath); } } @@ -265,8 +211,7 @@ void updatePathList( { for (auto proxy : list) { typename TypePolicy::value_type item = proxy; - std::filesystem::path filePath(item.GetAssetPath()); - filePath = filePath; + fs::filesystem::path filePath(item.GetAssetPath()); auto it = layerEntry->second.paths.find(filePath); if (it == layerEntry->second.paths.end()) { @@ -328,7 +273,7 @@ void updatePostponedRelativePathsForPrim( VtValue filePathValue = attr->GetDefaultValue(); auto filePathStr = filePathValue.Get().GetAssetPath(); - std::filesystem::path filePath(filePathStr); + fs::filesystem::path filePath(filePathStr); auto it = layerEntry->second.paths.find(filePath); if (it == layerEntry->second.paths.end()) { continue; @@ -356,7 +301,7 @@ void updatePostponedRelativePaths( return; } - auto anchorDir = std::filesystem::path(layerFileName).remove_filename(); + auto anchorDir = fs::filesystem::path(layerFileName).remove_filename(); auto anchorDirStr = anchorDir.generic_string(); // Update sublayer paths @@ -367,8 +312,7 @@ void updatePostponedRelativePaths( continue; } - std::filesystem::path filePath(subLayer->GetRealPath()); - filePath = filePath; + fs::filesystem::path filePath(subLayer->GetRealPath()); auto it = layerEntry->second.paths.find(filePath); if (it == layerEntry->second.paths.end()) { @@ -399,85 +343,45 @@ bool prepareLayerSaveUILayer(const PXR_NS::SdfLayerHandle& layer, bool useSceneF bool prepareLayerSaveUILayer(const std::string& relativeAnchor) { - // TODO LE-EXTRACT Prepare save UI Layer. - /* const char* script = "import mayaUsd_USDRootFileRelative as murel\n" - "murel.usdFileRelative.setRelativeFilePathRoot(r'''%s''')"; - - const std::string commandString = TfStringPrintf(script, relativeAnchor.c_str()); - return MGlobal::executePythonCommand(commandString.c_str());*/ - return true; + return UsdLayerEditor::prepareLayerSaveUILayer(relativeAnchor); } bool requireUsdPathsRelativeToDCCSceneFile() { - // TODO LE-EXTRACT Get option : requireUsdPathsRelativeToMayaSceneFile - // static const MString MAKE_PATH_RELATIVE_TO_SCENE_FILE = - // "mayaUsd_MakePathRelativeToSceneFile"; return - // MGlobal::optionVarExists(MAKE_PATH_RELATIVE_TO_SCENE_FILE) - // && MGlobal::optionVarIntValue(MAKE_PATH_RELATIVE_TO_SCENE_FILE); - return true; + return UsdLayerEditor::requireUsdPathsRelativeToSceneFile(); } bool requireUsdPathsRelativeToParentLayer() { - // TODO LE-EXTRACT Get option : requireUsdPathsRelativeToParentLayer - // static const MString MAKE_PATH_RELATIVE_TO_PARENT_LAYER_FILE - // = "mayaUsd_MakePathRelativeToParentLayer"; - // return MGlobal::optionVarExists(MAKE_PATH_RELATIVE_TO_PARENT_LAYER_FILE) - // && MGlobal::optionVarIntValue(MAKE_PATH_RELATIVE_TO_PARENT_LAYER_FILE); - return true; + return UsdLayerEditor::requireUsdPathsRelativeToParentLayer(); } bool requireUsdPathsRelativeToEditTargetLayer() { - // TODO LE-EXTRACT Get option : requireUsdPathsRelativeToEditTargetLayer - /*static const MString MAKE_PATH_RELATIVE_TO_EDIT_TARGET_LAYER_FILE - = "mayaUsd_MakePathRelativeToEditTargetLayer"; - return MGlobal::optionVarExists(MAKE_PATH_RELATIVE_TO_EDIT_TARGET_LAYER_FILE) - && MGlobal::optionVarIntValue(MAKE_PATH_RELATIVE_TO_EDIT_TARGET_LAYER_FILE);*/ - return true; + return UsdLayerEditor::requireUsdPathsRelativeToEditTargetLayer(); } -bool wantReferenceCompositionArc() -{ - // TODO LE-EXTRACT Get option : wantReferenceCompositionArc - // static const MString WANT_REFERENCE_COMPOSITION_ARC = "mayaUsd_WantReferenceCompositionArc"; - // return MGlobal::optionVarExists(WANT_REFERENCE_COMPOSITION_ARC) - // && MGlobal::optionVarIntValue(WANT_REFERENCE_COMPOSITION_ARC); - return false; -} +bool wantReferenceCompositionArc() { return UsdLayerEditor::wantReferenceCompositionArc(); } -bool wantPrependCompositionArc() -{ - // TODO LE-EXTRACT Get option : wantPrependCompositionArc - /*static const MString WANT_PREPEND_COMPOSITION_ARC = "mayaUsd_WantPrependCompositionArc"; - return MGlobal::optionVarExists(WANT_PREPEND_COMPOSITION_ARC) - && MGlobal::optionVarIntValue(WANT_PREPEND_COMPOSITION_ARC);*/ - return true; -} +bool wantPrependCompositionArc() { return UsdLayerEditor::wantPrependCompositionArc(); } + +bool wantPayloadLoaded() { return UsdLayerEditor::wantPayloadLoaded(); } + +std::string getReferencedPrimPath() { return UsdLayerEditor::getReferencedPrimPath(); } -bool wantPayloadLoaded() +void setRequireUsdPathsRelativeToDCCSceneFile(bool value) { - // TODO LE-EXTRACT Get option : wantPayloadLoaded - // static const MString WANT_PAYLOAD_LOADED = "mayaUsd_WantPayloadLoaded"; - // return MGlobal::optionVarExists(WANT_PAYLOAD_LOADED) - // && MGlobal::optionVarIntValue(WANT_PAYLOAD_LOADED); - return true; + UsdLayerEditor::setRequireUsdPathsRelativeToSceneFile(value); } -std::string getReferencedPrimPath() +void setRequireUsdPathsRelativeToParentLayer(bool value) { - // TODO LE-EXTRACT Get option : getReferencedPrimPath - // static const MString WANT_REFERENCE_COMPOSITION_ARC = "mayaUsd_ReferencedPrimPath"; - // if (!MGlobal::optionVarExists(WANT_REFERENCE_COMPOSITION_ARC)) - // return {}; - // return MGlobal::optionVarStringValue(WANT_REFERENCE_COMPOSITION_ARC).asChar(); - return {}; + UsdLayerEditor::setRequireUsdPathsRelativeToParentLayer(value); } std::string getDCCWorkspaceScenesDir() { - return dccWorkspaceSceneSaveLocationFunc(); + return UsdLayerEditor::getDCCWorkspaceScenesDir(); } std::string @@ -485,7 +389,7 @@ getUniqueFileName(const std::string& dir, const std::string& basename, const std { const std::string fileNameModel = basename + '-' + generateUniqueName() + '.' + ext; - std::filesystem::path pathModel(dir); + fs::filesystem::path pathModel(dir); pathModel.append(fileNameModel); return pathModel.generic_string(); @@ -495,7 +399,7 @@ std::string ensureUniqueFileName(const std::string& filename) { std::string uniqueName = filename; while (true) { - if (!std::filesystem::exists(std::filesystem::path(uniqueName))) + if (!fs::filesystem::exists(fs::filesystem::path(uniqueName))) return uniqueName; // Algorithm to generate a unique name: @@ -503,7 +407,7 @@ std::string ensureUniqueFileName(const std::string& filename) // 2. Replace the filename with the filename plus random text // 3. Put the extension back. - std::filesystem::path uniquePath(filename); + fs::filesystem::path uniquePath(filename); const std::string extOnly = uniquePath.extension().generic_string(); uniquePath = uniquePath.replace_extension(); @@ -548,11 +452,11 @@ std::string increaseNumberSuffix(const std::string& text) bool pathAppendPath(std::string& a, const std::string& b) { - if (!std::filesystem::is_directory(a)) { + if (!fs::filesystem::is_directory(a)) { return false; } - std::filesystem::path aPath(a); - std::filesystem::path bPath(b); + fs::filesystem::path aPath(a); + fs::filesystem::path bPath(b); aPath /= b; a.assign(aPath.string()); return true; @@ -560,8 +464,8 @@ bool pathAppendPath(std::string& a, const std::string& b) std::string appendPaths(const std::string& a, const std::string& b) { - std::filesystem::path aPath(a); - std::filesystem::path bPath(b); + fs::filesystem::path aPath(a); + fs::filesystem::path bPath(b); aPath /= b; return aPath.string(); @@ -587,56 +491,36 @@ size_t writeToFilePath(const char* filePath, const void* buffer, const size_t si void pathStripPath(std::string& filePath) { - std::filesystem::path p(filePath); - std::filesystem::path filename = p.filename(); + fs::filesystem::path p(filePath); + fs::filesystem::path filename = p.filename(); filePath.assign(filename.string()); return; } void pathRemoveExtension(std::string& filePath) { - std::filesystem::path p(filePath); - std::filesystem::path dir = p.parent_path(); - std::filesystem::path finalPath = dir / p.stem(); + fs::filesystem::path p(filePath); + fs::filesystem::path dir = p.parent_path(); + fs::filesystem::path finalPath = dir / p.stem(); filePath.assign(finalPath.string()); return; } std::string pathFindExtension(std::string& filePath) { - std::filesystem::path p(filePath); + fs::filesystem::path p(filePath); if (!p.has_extension()) { return std::string(); } - std::filesystem::path ext = p.extension(); + fs::filesystem::path ext = p.extension(); return ext.string(); } -void setFileWriteAccessFunction(WriteAccessCheckFn checkFileWriteAccess) -{ - writeAccessCheckFunc = checkFileWriteAccess; -} - bool checkWriteAccess(const std::string& filePath) { - if (writeAccessCheckFunc) { - return writeAccessCheckFunc(filePath); - } - TF_CODING_ERROR("No implementation provided for FileSystem::checkWriteAccess()"); - return false; -} - -void setDCCSceneLocationFunc(std::function fn) -{ - dccSceneSaveLocationFunc = fn; + return UsdLayerEditor::checkWriteAccess(filePath); } -void setDCCWorkspaceSceneLocationFunc(std::function fn) -{ - dccWorkspaceSceneSaveLocationFunc = fn; -} - - FileBackup::FileBackup(const std::string& filename) : _filename(filename) { @@ -664,7 +548,7 @@ std::string FileBackup::getBackupFilename() const void FileBackup::backup() { - if (!std::filesystem::exists(std::filesystem::path(_filename))) + if (!fs::filesystem::exists(fs::filesystem::path(_filename))) return; const std::string backupFileName = getBackupFilename(); diff --git a/lib/usdLayerEditor/lib/utilFileSystem.h b/lib/usdLayerEditor/lib/utilFileSystem.h index d301a21436..8642543b36 100644 --- a/lib/usdLayerEditor/lib/utilFileSystem.h +++ b/lib/usdLayerEditor/lib/utilFileSystem.h @@ -24,8 +24,6 @@ namespace UsdLayerEditor { namespace FileSystem { -typedef std::function WriteAccessCheckFn; - /*! \brief returns the resolved filesystem path for the file identified by the given path */ LayerEditorAPI std::string resolvePath(const std::string& filePath); @@ -60,16 +58,6 @@ LayerEditorAPI std::pair LayerEditorAPI std::string getPathRelativeToDirectory(const std::string& fileName, const std::string& relativeToDir); -/*! \brief returns the path of a file relative to the DCC scene project folder. - Returns an empty string if the path is not relative to the project. - */ -LayerEditorAPI std::string getPathRelativeToProject(const std::string& fileName); - -/*! \brief returns the absolute path of a file but relative to the DCC scene project folder. - Returns an empty string if the path cannot be made relative to the project. - */ -LayerEditorAPI std::string makeProjectRelatedPath(const std::string& fileName); - /*! \brief returns parent directory of opened DCC scene file */ LayerEditorAPI std::string getDCCSceneFileDir(); @@ -78,8 +66,6 @@ LayerEditorAPI std::string getDCCSceneFileDir(); */ LayerEditorAPI std::string getLayerFileDir(const PXR_NS::SdfLayerHandle& layer); -// TODO LE-EXTRACT Does the concept of workspace exist outside of maya / should the terminology be -// different? /*! \brief returns the DCC workspace file rule entry for scenes */ LayerEditorAPI std::string getDCCWorkspaceScenesDir(); @@ -155,6 +141,16 @@ LayerEditorAPI bool requireUsdPathsRelativeToParentLayer(); */ LayerEditorAPI bool requireUsdPathsRelativeToEditTargetLayer(); +/*! \brief sets the flag specifying whether USD file paths should be saved as relative to Maya + * scene file + */ +LayerEditorAPI void setRequireUsdPathsRelativeToDCCSceneFile(bool value); + +/*! \brief sets the flag specifying whether USD file paths should be saved + * as relative to the given parent layer. + */ +LayerEditorAPI void setRequireUsdPathsRelativeToParentLayer(bool value); + /*! \brief returns a unique file name */ LayerEditorAPI std::string @@ -194,10 +190,10 @@ LayerEditorAPI bool pathAppendPath(std::string& a, const std::string& b); /** * Appends `b` to the path `a` and returns a path (by appending two input paths). * - * @param a A string that respresents the first path - * @param b A string that respresents the second path + * @param a A string that represents the first path + * @param b A string that represents the second path * - * @return the two paths joined by a seperator + * @return the two paths joined by a separator */ LayerEditorAPI std::string appendPaths(const std::string& a, const std::string& b); @@ -225,25 +221,14 @@ LayerEditorAPI void pathRemoveExtension(std::string& filePath); LayerEditorAPI std::string pathFindExtension(std::string& filePath); /** - * Sets a function to check write access to a passed file. - * @param checkFileWriteAccess The write access check function. - */ -LayerEditorAPI void setFileWriteAccessFunction(WriteAccessCheckFn checkFileWriteAccess); - -/** - * Checks a file for write access by calling the function set via setFileWriteAccessFunction(). - * If no function is set, returns false and raises a coding error. + * Checks a file for write access via the DCC registry. * @param filePath The file to check for write access. * @return True if the file can be written to, false otherwise. */ LayerEditorAPI bool checkWriteAccess(const std::string& filePath); -LayerEditorAPI void setDCCSceneLocationFunc(std::function fn); - -LayerEditorAPI void setDCCWorkspaceSceneLocationFunc(std::function fn); - // Backup a file and restore it if not committed. -class FileBackup +class LayerEditorAPI FileBackup { public: FileBackup(const std::string& filename); diff --git a/lib/usdLayerEditor/lib/utilOptions.h b/lib/usdLayerEditor/lib/utilOptions.h deleted file mode 100644 index de6b679adf..0000000000 --- a/lib/usdLayerEditor/lib/utilOptions.h +++ /dev/null @@ -1,60 +0,0 @@ -// -// Copyright 2024 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 "tokens.h" - -namespace UsdLayerEditor { -namespace Options { - -inline bool optionVarExists(const std::string& name) -{ - // TODO LE-EXTRACT Options save - check existing. - - // Temporary exception for showing confirmation dialogs on saving layers. - if (name == pxr::UsdLayerEditorOptionVars->ConfirmExistingFileSave.GetText()) { - return true; - } - - return false; -} - -inline int optionVarIntValue(const std::string& name) -{ - // TODO LE-EXTRACT Options save - get int value. - - // Temporary exception for showing confirmation dialogs on saving layers. - if (name == pxr::UsdLayerEditorOptionVars->ConfirmExistingFileSave.GetText()) { - return 1; - } - return 0; -} - -inline int optionVarIntValue(const std::string& name, bool& exists) -{ - exists = optionVarExists(name); - if (exists) { - return optionVarIntValue(name); - } - return 0; -} - -inline void setOptionVarValue(const std::string& name, int value) -{ - // TODO LE-EXTRACT Options save - set int value. -} - -} // namespace Options -} // namespace UsdLayerEditor \ No newline at end of file diff --git a/lib/usdLayerEditor/lib/utilQT.cpp b/lib/usdLayerEditor/lib/utilQT.cpp index 56f7c38b64..5b540d8a39 100644 --- a/lib/usdLayerEditor/lib/utilQT.cpp +++ b/lib/usdLayerEditor/lib/utilQT.cpp @@ -15,6 +15,8 @@ // #include "utilQT.h" +#include + #include #include #include @@ -32,7 +34,6 @@ QIcon QtUtils::createIcon(const char* iconName) { return QIcon(iconName); } QPixmap QtUtils::createPNGResPixmap(QString const& in_pixmapName, int width, int height) { - QString pixmapName(in_pixmapName); if (pixmapName.indexOf(".png") == -1) { pixmapName += ".png"; @@ -43,14 +44,6 @@ QPixmap QtUtils::createPNGResPixmap(QString const& in_pixmapName, int width, int pixmapName = resourcePrefix + pixmapName; } - // TODO LE-EXTRACT Maya specific icon / pixmap behavior. - // Note: createPNGResPixmap calls QtUtils::createPNGResPixmap, but that is - // *not* the function below but rather MayaQtUtils::createPixmap, since - // these functions are virtual. - // - // The MayaQtUtils version calls MQtUtil::createPixmap which calls - // QmayaQtHelper::createPixmap which generates the scaled image name - // by adding the _150 or _200 suffix as necessary. return createPixmap(pixmapName, width, height); } @@ -184,4 +177,22 @@ QtUtils* getQtUtils() { return utils; } void setQtUtils(QtUtils* qtUtils) { utils = qtUtils; } +ValidTfIdentifierValidator::ValidTfIdentifierValidator(QObject* parent) + : QValidator(parent) +{ +} + +QValidator::State ValidTfIdentifierValidator::validate(QString& input, int& /*pos*/) const +{ + std::string orig = input.toStdString(); + std::string valid = PXR_NS::TfMakeValidIdentifier(orig); + if (input.isEmpty()) { + return Intermediate; // Allow user to type + } + if (orig == valid && !valid.empty()) { + return Acceptable; + } + return Invalid; +} + } // namespace UsdLayerEditor diff --git a/lib/usdLayerEditor/lib/utilQT.h b/lib/usdLayerEditor/lib/utilQT.h index 069a7832b1..ada7272694 100644 --- a/lib/usdLayerEditor/lib/utilQT.h +++ b/lib/usdLayerEditor/lib/utilQT.h @@ -28,9 +28,6 @@ class QString; namespace UsdLayerEditor { -/** - * Initializes the qt utilities. - */ LayerEditorAPI void initializeQtUtils(); /** @@ -94,6 +91,18 @@ class LayerEditorAPI QtDisableRepaintUpdates QWidget& _widget; }; +/** + * Used by the component-save widget to gate component-name entry to a USD-safe + * identifier. + */ +class LayerEditorAPI ValidTfIdentifierValidator : public QValidator +{ +public: + explicit ValidTfIdentifierValidator(QObject* parent = nullptr); + + State validate(QString& input, int& pos) const override; +}; + #ifdef Q_OS_DARWIN const bool IS_MAC_OS = true; #else diff --git a/lib/usdLayerEditor/lib/utilSerialization.cpp b/lib/usdLayerEditor/lib/utilSerialization.cpp index 2d7e3d2418..c6c661ec90 100644 --- a/lib/usdLayerEditor/lib/utilSerialization.cpp +++ b/lib/usdLayerEditor/lib/utilSerialization.cpp @@ -16,11 +16,11 @@ #include "utilSerialization.h" +#include "layerEditorDCCFunctions.h" #include "layerLocking.h" #include "layerMuting.h" #include "tokens.h" #include "utilFileSystem.h" -#include "utilOptions.h" #include @@ -38,19 +38,13 @@ #include #include #endif -#include - #include -#include +#include #include PXR_NAMESPACE_USING_DIRECTIVE -namespace { - std::function updateDCCObjectRootLayerFunction; -} - namespace UsdLayerEditor { namespace Serialization { @@ -78,11 +72,6 @@ namespace Serialization { }; -void setUpdateDCCObjectRootLayerFunction(std::function updateFunction) -{ - updateDCCObjectRootLayerFunction = updateFunction; -} - class RecursionDetector { public: @@ -179,6 +168,11 @@ void updateLockedLayers( } } +std::vector getStageCaches() +{ + return UsdLayerEditor::getStageCaches(); +} + void updateAllCachedStageWithLayer(SdfLayerRefPtr originalLayer, const std::string& newFilePath) { // Update all known stage caches managed by the Maya USD plugin that contained @@ -191,18 +185,18 @@ void updateAllCachedStageWithLayer(SdfLayerRefPtr originalLayer, const std::stri return; } - // TODO LE-EXTRACT : Maya has multiple stage caches, not just the global one. - // for (UsdStageCache& cache : UsdMayaStageCache::GetAllCaches()) { - auto& cache = pxr::UsdUtilsStageCache::Get(); - std::vector stages = cache.FindAllMatching(originalLayer); - std::vector updatedStages; - for (auto& stage : stages) { - auto sessionLayer = stage->GetSessionLayer(); - updatedStages.emplace_back(UsdStage::UsdStage::Open(newLayer, sessionLayer, UsdStage::InitialLoadSet::LoadNone)); - bool erased = cache.Erase(stage); - } - for (auto& updatedStage : updatedStages) { - cache.Insert(updatedStage); + for (PXR_NS::UsdStageCache* cache : getStageCaches()) { + std::vector stages = cache->FindAllMatching(originalLayer); + std::vector updatedStages; + for (auto& stage : stages) { + auto sessionLayer = stage->GetSessionLayer(); + updatedStages.emplace_back( + UsdStage::Open(newLayer, sessionLayer, UsdStage::InitialLoadSet::LoadNone)); + cache->Erase(stage); + } + for (auto& updatedStage : updatedStages) { + cache->Insert(updatedStage); + } } } @@ -247,14 +241,7 @@ std::string generateUniqueLayerFileName(const std::string& basename, const SdfLa std::string usdFormatArgOption() { - static const std::string kSaveLayerFormatBinaryOption( - UsdLayerEditorOptionVars->SaveLayerFormatArgBinaryOption.GetText()); - bool binary = true; - if (Options::optionVarExists(kSaveLayerFormatBinaryOption)) { - binary = Options::optionVarIntValue(kSaveLayerFormatBinaryOption) != 0; - } else { - Options::setOptionVarValue(kSaveLayerFormatBinaryOption, 1); - } + const bool binary = getSaveLayerFormatBinary(); #if PXR_VERSION >= 2511 return binary ? PXR_NS::SdfUsdcFileFormatTokens->Id.GetText() : PXR_NS::SdfUsdaFileFormatTokens->Id.GetText(); @@ -267,18 +254,13 @@ std::string usdFormatArgOption() /* static */ USDUnsavedEditsOption serializeUsdEditsLocationOption() { - static const std::string kSerializedUsdEditsLocation( - UsdLayerEditorOptionVars->SerializedUsdEditsLocation.GetText()); - - bool optVarExists = true; - int saveOption = Options::optionVarIntValue(kSerializedUsdEditsLocation, optVarExists); - // Default is to save back to .usd files, set it to that if the optionVar doesn't exist yet. // optionVar's are also just ints so make sure the value is a correct one. // If we end up initializing the value then write it back to the optionVar itself. - if (!optVarExists || (saveOption < kSaveToUSDFiles || saveOption > kIgnoreUSDEdits)) { + int saveOption = getSerializedUsdEditsLocation(); + if (saveOption < kSaveToUSDFiles || saveOption > kIgnoreUSDEdits) { saveOption = kSaveToUSDFiles; - Options::setOptionVarValue(kSerializedUsdEditsLocation, saveOption); + setSerializedUsdEditsLocation(saveOption); } if (saveOption == kSaveToSceneFile) { @@ -289,60 +271,6 @@ USDUnsavedEditsOption serializeUsdEditsLocationOption() return kSaveToUSDFiles; } } // namespace MAYAUSD_NS_DEF -// -// bool isProxyShapePathRelative(MayaUsdProxyShapeBase& proxyShape) -//{ -// MStatus status; -// MFnDependencyNode depNode(proxyShape.thisMObject(), &status); -// if (!status) -// return false; -// -// MPlug filePathRelativePlug = depNode.findPlug(MayaUsdProxyShapeBase::filePathRelativeAttr); -// return filePathRelativePlug.asBool(); -// } -// -// bool isProxyPathModeRelative(ProxyPathMode proxyPathMode, const MString& proxyNodeName) -//{ -// if (kProxyPathRelative == proxyPathMode) -// return true; -// -// if (kProxyPathAbsolute == proxyPathMode) -// return false; -// -// if (kProxyPathFollowProxyShape == proxyPathMode) { -// // Note: if we fail to find the proxy shape, we will fallback on -// // using the options var preference instead. -// MayaUsdProxyShapeBase* proxyShape -// = UsdMayaUtil::GetProxyShapeByProxyName(proxyNodeName.asChar()); -// if (proxyShape) { -// return isProxyShapePathRelative(*proxyShape); -// } -// } -// -// return UsdMayaUtilFileSystem::requireUsdPathsRelativeToMayaSceneFile(); -// } -// -// void setNewProxyPath( -// const MString& proxyNodeName, -// const MString& newRootLayerPath, -// ProxyPathMode proxyPathMode, -// const SdfLayerRefPtr& layer, -// bool isTargetLayer) -//{ -// const bool needRelativePath = isProxyPathModeRelative(proxyPathMode, proxyNodeName); -// const char* filePathCmd = "setAttr -type \"string\" ^1s.filePath \"^2s\"; " -// "setAttr ^1s.filePathRelative ^3s; "; -// -// MString script; -// script.format(filePathCmd, proxyNodeName, newRootLayerPath, needRelativePath ? "1" : "0"); -// MGlobal::executeCommand( -// script, -// /*display*/ true, -// /*undo*/ false); -// -// if (isTargetLayer) -// updateTargetLayer(proxyNodeName.asChar(), layer); -// } static bool isCompatibleWithSave( SdfLayerRefPtr layer, @@ -380,26 +308,9 @@ USDUnsavedEditsOption serializeUsdEditsLocationOption() void setLayerUpAxisAndUnits(const SdfLayerRefPtr& layer) { - if (!layer) - return; - - // Don't try to author the metadata on non-editable layers. - if (!layer->PermissionToEdit()) + if (!layer || !layer->PermissionToEdit()) return; - - //const PXR_NS::TfToken upAxis - // = MGlobal::isZAxisUp() ? PXR_NS::UsdGeomTokens->z : PXR_NS::UsdGeomTokens->y; - //const double metersPerUnit - // = UsdMayaUtil::ConvertMDistanceUnitToUsdGeomLinearUnit(MDistance::internalUnit()); - const PXR_NS::TfToken upAxis = PXR_NS::UsdGeomTokens->z; - const double metersPerUnit = 0.1; - - // Note: code similar to what UsdGeomSetStageUpAxis -> UsdStage::SetMetadata end-up doing, - // but without having to have a stage. We basically set metadata on the virtual root object - // of the layer. - layer->SetField( - PXR_NS::SdfPath::AbsoluteRootPath(), PXR_NS::UsdGeomTokens->metersPerUnit, metersPerUnit); - layer->SetField(PXR_NS::SdfPath::AbsoluteRootPath(), PXR_NS::UsdGeomTokens->upAxis, upAxis); + UsdLayerEditor::setLayerUpAxisAndUnits(layer); } bool saveLayerWithFormat( @@ -431,11 +342,7 @@ bool saveLayerWithFormat( } } - //TODO LE-EXTRACT: MIGHT NEED TO use this with cache updating approach. - // in the maya-usd codebase, they adjust the session layers in this function - // however, in the max codebase, we do this in the callback function that is - // provided to the `updateDCCObjectRootLayerFunction()`. - //updateAllCachedStageWithLayer(layer, filePath); + updateAllCachedStageWithLayer(layer, filePath); return true; } @@ -467,7 +374,7 @@ void updateRootLayer( const SdfLayerRefPtr& layer, bool isTargetLayer) { - updateDCCObjectRootLayerFunction(proxy, layerPath); + UsdLayerEditor::updateDCCObjectRootLayer(proxy, layerPath, layer, isTargetLayer); } SdfLayerRefPtr saveAnonymousLayer( @@ -495,35 +402,32 @@ void updateRootLayer( std::string filePath(pathInfo.absolutePath); if (!anonLayer) { - TF_ERROR(NoAnonLayerProvided, "No layer provided to save to '%s'", filePath); + TF_ERROR(NoAnonLayerProvided, "No layer provided to save to '%s'", filePath.c_str()); return nullptr; } if (!anonLayer->IsAnonymous()) { - TF_ERROR(CannotSaveNonAnonLayer, "Cannot save non-anonymous layer '%s' under a different file name", anonLayer->GetDisplayName()); + TF_ERROR(CannotSaveNonAnonLayer, "Cannot save non-anonymous layer '%s' under a different file name", anonLayer->GetDisplayName().c_str()); return nullptr; } if (isLayerSystemLocked(anonLayer)) { - TF_ERROR(CannotSaveAnonLayerWhenSysLocked, "Cannot save layer '%s' when system-locked", anonLayer->GetDisplayName()); + TF_ERROR(CannotSaveAnonLayerWhenSysLocked, "Cannot save layer '%s' when system-locked", anonLayer->GetDisplayName().c_str()); return nullptr; } - // TODO LE-EXTRACT: do a callback for DCCs to be able to provide these to the - // layer-editor -- normally this should be set before saving // Only set up-axis and units metadata on the root layer // and only if it is anonymous before being saved. - //if (stage->GetRootLayer() == anonLayer) { - // setLayerUpAxisAndUnits(anonLayer); - //} + if (stage->GetRootLayer() == anonLayer) { + setLayerUpAxisAndUnits(anonLayer); + } ensureUSDFileExtension(filePath); - // TODO LE-EXTRACT: double check how this was used in maya-usd when saying anon root layer - //const bool wasTargetLayer = (stage->GetEditTarget().GetLayer() == anonLayer); + const bool wasTargetLayer = (stage->GetEditTarget().GetLayer() == anonLayer); if (!saveLayerWithFormat(anonLayer, filePath, formatArg)) { - TF_ERROR(FailedAnonLayerSave, "Failed to save layer '%s' to '%s'", anonLayer->GetDisplayName(), filePath); + TF_ERROR(FailedAnonLayerSave, "Failed to save layer '%s' to '%s'", anonLayer->GetDisplayName().c_str(), filePath.c_str()); return nullptr; } @@ -537,7 +441,7 @@ void updateRootLayer( = FileSystem::makePathRelativeTo(filePath, relativePathAnchor).first; } else if (isSubLayer) { filePath = FileSystem::getPathRelativeToLayerFile(filePath, parentLayer); - if (std::filesystem::path(filePath).is_absolute()) { + if (fs::filesystem::path(filePath).is_absolute()) { FileSystem::markPathAsPostponedRelative(parentLayer, filePath); } } else { @@ -558,7 +462,7 @@ void updateRootLayer( SdfLayerRefPtr newLayer = SdfLayer::FindOrOpen(pathInfo.absolutePath); if (!newLayer) { - TF_ERROR(FailedAnonLayerReload, "Failed to reload layer '%s' from '%s'", anonLayer->GetDisplayName(), filePath); + TF_ERROR(FailedAnonLayerReload, "Failed to reload layer '%s' from '%s'", anonLayer->GetDisplayName().c_str(), filePath.c_str()); return nullptr; } @@ -568,7 +472,8 @@ void updateRootLayer( } else if (!parent._objectPath.empty()) { // if ever we support relative paths in the DCC, can return the relative path // i.e. "filePath" variable - updateDCCObjectRootLayerFunction(parent._objectPath, pathInfo.absolutePath); + UsdLayerEditor::updateDCCObjectRootLayer( + parent._objectPath, pathInfo.absolutePath, newLayer, wasTargetLayer); } updateTargetLayer(parent._objectPath, newLayer); @@ -621,9 +526,11 @@ void updateRootLayer( } } -void getLayersToSaveFromDCCObject(const std::string& objectPath, StageLayersToSave& layersInfo) +void getLayersToSaveFromStage( + const PXR_NS::UsdStageRefPtr& stage, + const std::string& objectPath, + StageLayersToSave& layersInfo) { - auto stage = UsdUfe::getStage(Ufe::PathString::path(objectPath)); if (!stage) { return; } @@ -657,5 +564,14 @@ void getLayersToSaveFromDCCObject(const std::string& objectPath, StageLayersToSa layersInfo._dirtyFileBackedLayers); } +void getLayersToSaveFromDCCObject(const std::string& objectPath, StageLayersToSave& layersInfo) +{ + auto stage = UsdUfe::getStage(Ufe::PathString::path(objectPath)); + if (!stage) { + return; + } + getLayersToSaveFromStage(stage, objectPath, layersInfo); +} + } // namespace Serialization } // namespace UsdLayerEditor \ No newline at end of file diff --git a/lib/usdLayerEditor/lib/utilSerialization.h b/lib/usdLayerEditor/lib/utilSerialization.h index aace105c3a..aa56b68dc3 100644 --- a/lib/usdLayerEditor/lib/utilSerialization.h +++ b/lib/usdLayerEditor/lib/utilSerialization.h @@ -23,20 +23,23 @@ #include #include #include +#include + +#include // General utility functions used when serializing Usd edits during a save operation namespace UsdLayerEditor { namespace Serialization { - std::string generateUniqueFileName(const std::string& basename); + LayerEditorAPI std::string generateUniqueFileName(const std::string& basename); - std::string + LayerEditorAPI std::string generateUniqueLayerFileName(const std::string& basename, const PXR_NS::SdfLayerRefPtr& layer); /*! \brief Queries the optionVar that decides what the internal format of a .usd file should be, either "usdc" or "usda". */ - std::string usdFormatArgOption(); + LayerEditorAPI std::string usdFormatArgOption(); enum USDUnsavedEditsOption { @@ -125,7 +128,7 @@ struct StageLayersToSave file (for the root layer) or its parent layer (for sub-layers). We assume the caller voluntarily made the path relative. */ -bool saveLayerWithFormat( +LayerEditorAPI bool saveLayerWithFormat( pxr::SdfLayerRefPtr layer, const std::string& requestedFilePath = "", const std::string& requestedFormatArg = ""); @@ -162,24 +165,27 @@ std::string getSceneFolder(); * different relative paths, so we cannot interrogate it about * what its path is. */ - void updateSubLayer( + LayerEditorAPI void updateSubLayer( const PXR_NS::SdfLayerRefPtr& parentLayer, const PXR_NS::SdfLayerRefPtr& oldSubLayer, const std::string& newSubLayerPath); /*! \brief Ensures that the filepath contains a valid USD extension. */ - void ensureUSDFileExtension(std::string& filePath); + LayerEditorAPI void ensureUSDFileExtension(std::string& filePath); /*! \brief Check the sublayer stack of the stage looking for any anonymous layers that will need to be saved. */ void getLayersToSaveFromDCCObject(const std::string& objectPath, StageLayersToSave& layersInfo); -/*! \brief Sets a function to be called to save the layer lock state in a DCC Stage object - * attribute. +/*! \brief Same as getLayersToSaveFromDCCObject but accepts the stage directly, + bypassing the UFE path-based stage lookup. */ -LayerEditorAPI void setUpdateDCCObjectRootLayerFunction(std::function saveFunction); +LayerEditorAPI void getLayersToSaveFromStage( + const PXR_NS::UsdStageRefPtr& stage, + const std::string& objectPath, + StageLayersToSave& layersInfo); } // namespace Serialization } // namespace UsdLayerEditor diff --git a/lib/usdLayerEditor/lib/utilUI.cpp b/lib/usdLayerEditor/lib/utilUI.cpp index 0a2aa38908..1384f531e3 100644 --- a/lib/usdLayerEditor/lib/utilUI.cpp +++ b/lib/usdLayerEditor/lib/utilUI.cpp @@ -16,27 +16,17 @@ #include "utilUI.h" -#include -#include - -namespace { - std::function errorDisplayCallbackFunction; -} +#include "layerEditorDCCFunctions.h" namespace UsdLayerEditor { namespace UIUtils { -void setErrorDisplayCallbackFunction(std::function errorFunction) -{ - errorDisplayCallbackFunction = errorFunction; -} - void displayError(const std::string& error) { - errorDisplayCallbackFunction(error); + UsdLayerEditor::displayError(error); } int dpiScale(int pixel) { return pixel; } } -} \ No newline at end of file +} diff --git a/lib/usdLayerEditor/lib/utilUI.h b/lib/usdLayerEditor/lib/utilUI.h index d02b92e684..1453d38c38 100644 --- a/lib/usdLayerEditor/lib/utilUI.h +++ b/lib/usdLayerEditor/lib/utilUI.h @@ -19,14 +19,11 @@ #include "layerEditorAPI.h" -#include #include namespace UsdLayerEditor { namespace UIUtils { -LayerEditorAPI void setErrorDisplayCallbackFunction(std::function errorFunction); - void displayError(const std::string& error); int dpiScale(int pixel); diff --git a/lib/usdLayerEditor/lib/warningDialogs.cpp b/lib/usdLayerEditor/lib/warningDialogs.cpp index 6bcca69d5a..e21e0fef22 100644 --- a/lib/usdLayerEditor/lib/warningDialogs.cpp +++ b/lib/usdLayerEditor/lib/warningDialogs.cpp @@ -15,6 +15,7 @@ // #include "warningDialogs.h" +#include "layerEditorDCCFunctions.h" #include "utilQT.h" namespace { @@ -34,19 +35,39 @@ QString getLayerBulletList(const QStringList* in_list) return text; } +// Test-only override; unset in production. See setModalDialogTestHandler. +UsdLayerEditor::ModalDialogTestHandler& modalDialogTestHandler() +{ + static UsdLayerEditor::ModalDialogTestHandler handler; + return handler; +} + } // namespace namespace UsdLayerEditor { +ModalDialogTestHandler setModalDialogTestHandler(ModalDialogTestHandler handler) +{ + auto previous = modalDialogTestHandler(); + modalDialogTestHandler() = std::move(handler); + return previous; +} + bool confirmDialog_internal( bool okCancel, const QString& title, const QString& message, const QStringList* bulletList, const QString* okButtonText, - QMessageBox::Icon icon) + QMessageBox::Icon icon, + QWidget* parent) { - QMessageBox msgBox; + if (auto& testHandler = modalDialogTestHandler()) + return testHandler(title, message); + + if (!parent) + parent = mainWindowParent(); + QMessageBox msgBox(parent); // there is no title bar text on mac, instead it's bold text if (IS_MAC_OS) msgBox.setText(title); @@ -94,9 +115,10 @@ bool confirmDialog( const QString& message, const QStringList* bulletList, const QString* okButtonText, - QMessageBox::Icon icon) + QMessageBox::Icon icon, + QWidget* parent) { - return confirmDialog_internal(true, title, message, bulletList, okButtonText, icon); + return confirmDialog_internal(true, title, message, bulletList, okButtonText, icon, parent); } // create a warning dialog, with an optional bullet list @@ -104,9 +126,10 @@ void warningDialog( const QString& title, const QString& message, const QStringList* bulletList, - QMessageBox::Icon icon) + QMessageBox::Icon icon, + QWidget* parent) { - confirmDialog_internal(false, title, message, bulletList, nullptr, icon); + confirmDialog_internal(false, title, message, bulletList, nullptr, icon, parent); } } // namespace UsdLayerEditor diff --git a/lib/usdLayerEditor/lib/warningDialogs.h b/lib/usdLayerEditor/lib/warningDialogs.h index 85c47730ba..61d3d73de7 100644 --- a/lib/usdLayerEditor/lib/warningDialogs.h +++ b/lib/usdLayerEditor/lib/warningDialogs.h @@ -16,30 +16,44 @@ #ifndef WARNINGDIALOGS_H #define WARNINGDIALOGS_H +#include "layerEditorAPI.h" + #include #include #include +#include + /** * @brief Helpers to easily pop a properly formatted and sized message box * */ namespace UsdLayerEditor { -// create a confirmation dialog, with an optional bullet list of stuff like layer names -bool confirmDialog( +// create a confirmation dialog, with an optional bullet list of stuff like layer names. +// When parent is null the dialog is parented to the DCC main window (mainWindowParent()). +LayerEditorAPI bool confirmDialog( const QString& title, const QString& message, const QStringList* bulletList = nullptr, const QString* okButtonText = nullptr, - QMessageBox::Icon icon = QMessageBox::Icon::NoIcon); + QMessageBox::Icon icon = QMessageBox::Icon::NoIcon, + QWidget* parent = nullptr); // create a dialog with a single OK button, with an optional bullet list -void warningDialog( +LayerEditorAPI void warningDialog( const QString& title, const QString& message, const QStringList* bulletList = nullptr, - QMessageBox::Icon icon = QMessageBox::Icon::NoIcon); + QMessageBox::Icon icon = QMessageBox::Icon::NoIcon, + QWidget* parent = nullptr); + +// Test seam: when a handler is installed, confirmDialog/warningDialog return its +// result instead of showing a blocking modal QMessageBox (headless tests would +// otherwise hang). Production never installs one, so behavior is unchanged. +// Returns the previously-installed handler. +using ModalDialogTestHandler = std::function; +LayerEditorAPI ModalDialogTestHandler setModalDialogTestHandler(ModalDialogTestHandler handler); } // namespace UsdLayerEditor diff --git a/lib/usdLayerEditor/python/CMakeLists.txt b/lib/usdLayerEditor/python/CMakeLists.txt index 92e4be44d1..5cda4fb9ff 100644 --- a/lib/usdLayerEditor/python/CMakeLists.txt +++ b/lib/usdLayerEditor/python/CMakeLists.txt @@ -1,59 +1,102 @@ -# Python extension library for UsdLayerEditor library bindings. Pixar's TF_WRAP_MODULE -# macro expects the library name to be prefixed with _, but adds an underscore -# to the package name when creating the Python extension module initialization -# function (see Pixar's pyModule.h). +# +# 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. +# -set(PYTHON_TARGET_NAME "_${PROJECT_NAME}") +# Python extension library for UsdLayerEditor library bindings. Pixar's +# TF_WRAP_MODULE macro expects the library name to be prefixed with _, but +# adds an underscore to the package name when creating the Python extension +# module initialization function (see Pixar's pyModule.h). +# +# This CMakeLists is invoked from lib/usdLayerEditor/CMakeLists.txt. +# Python, boost.python, UFE and USD are already found/defined by the parent +# maya-usd build — do not call find_package here. + +set(PACKAGE_NAME UsdLayerEditor) + +if(IS_WINDOWS AND MAYAUSD_DEFINE_DEBUG_PYTHON_FLAG) + # On Windows when compiling with debug python the library must be named with _d. + set(PYTHON_TARGET_NAME "_${PACKAGE_NAME}_d") +else() + set(PYTHON_TARGET_NAME "_${PACKAGE_NAME}") +endif() add_library(${PYTHON_TARGET_NAME} SHARED) # ----------------------------------------------------------------------------- # sources # ----------------------------------------------------------------------------- -target_sources(${PYTHON_TARGET_NAME} +target_sources(${PYTHON_TARGET_NAME} PRIVATE module.cpp wrapUsdLayerEditor.cpp ) -target_include_directories( ${PYTHON_TARGET_NAME} PUBLIC - ${CMAKE_SOURCE_DIR}/lib -) - # ----------------------------------------------------------------------------- # compiler configuration # ----------------------------------------------------------------------------- target_compile_definitions(${PYTHON_TARGET_NAME} PRIVATE - MFB_PACKAGE_NAME=${PROJECT_NAME} - MFB_ALT_PACKAGE_NAME=${PROJECT_NAME} - MFB_PACKAGE_MODULE=${PROJECT_NAME} + $<$:OSMac_> + MFB_PACKAGE_NAME=${PACKAGE_NAME} + MFB_ALT_PACKAGE_NAME=${PACKAGE_NAME} + MFB_PACKAGE_MODULE=${PACKAGE_NAME} ) -if(MSVC) - # Prevent the removal of unreferenced code. The MSVC compiler - # makes mistakes and remove code that is actually in use, with the - # PXR registration macros. - target_compile_options(${PYTHON_TARGET_NAME} PRIVATE /Zc:inline-) - # Parallel compile - add_definitions(/MP) -endif() +mayaUsd_compile_config(${PYTHON_TARGET_NAME}) # ----------------------------------------------------------------------------- # link libraries # ----------------------------------------------------------------------------- target_link_libraries(${PYTHON_TARGET_NAME} PRIVATE - ${PROJECT_NAME} + usdLayerEditor + # usdLayerEditor's public headers (layerEditorWidgetManager.h) expose + # Qt types (QPointer), so the binding TU also needs Qt::Core headers. + Qt::Core ) -set_target_properties(${PYTHON_TARGET_NAME} - PROPERTIES - PREFIX "" - SUFFIX ".pyd" -) +# ----------------------------------------------------------------------------- +# properties +# ----------------------------------------------------------------------------- +set_python_module_property(${PYTHON_TARGET_NAME}) + +# ----------------------------------------------------------------------------- +# run-time search paths +# ----------------------------------------------------------------------------- +if(IS_MACOSX OR IS_LINUX) + mayaUsd_init_rpath(rpath "lib/python/${PACKAGE_NAME}") + # back to /lib so we can find usdLayerEditor and usdUfe + mayaUsd_add_rpath(rpath "../../..") + if(IS_MACOSX AND DEFINED MAYAUSD_TO_USD_RELATIVE_PATH) + mayaUsd_add_rpath(rpath "../../../../../../Maya.app/Contents/MacOS") + endif() + if(DEFINED MAYAUSD_TO_USD_RELATIVE_PATH) + mayaUsd_add_rpath(rpath "../../../../${MAYAUSD_TO_USD_RELATIVE_PATH}/lib") + if (IS_LINUX) + mayaUsd_add_rpath(rpath "../../../../${MAYAUSD_TO_USD_RELATIVE_PATH}/lib64") + endif() + elseif(DEFINED PXR_USD_LOCATION) + mayaUsd_add_rpath(rpath "${PXR_USD_LOCATION}/lib") + endif() + mayaUsd_install_rpath(rpath ${PYTHON_TARGET_NAME}) +endif() -set(PYLIB_INSTALL_PREFIX ${CMAKE_INSTALL_PREFIX}/python/${PROJECT_NAME}) +# ----------------------------------------------------------------------------- +# install +# ----------------------------------------------------------------------------- +set(PYLIB_INSTALL_PREFIX ${CMAKE_INSTALL_PREFIX}/lib/python/${PACKAGE_NAME}) install(TARGETS ${PYTHON_TARGET_NAME} LIBRARY @@ -66,5 +109,7 @@ install(TARGETS ${PYTHON_TARGET_NAME} install(FILES __init__.py DESTINATION ${PYLIB_INSTALL_PREFIX}) -install(FILES $ - DESTINATION ${PYLIB_INSTALL_PREFIX} OPTIONAL) +if(IS_WINDOWS) + install(FILES $ + DESTINATION ${PYLIB_INSTALL_PREFIX} OPTIONAL) +endif() diff --git a/plugin/adsk/plugin/plugin.cpp b/plugin/adsk/plugin/plugin.cpp index ff0d7deb41..bc0d2466fd 100644 --- a/plugin/adsk/plugin/plugin.cpp +++ b/plugin/adsk/plugin/plugin.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -46,6 +47,7 @@ #include #include #include +#include #include #include @@ -77,7 +79,6 @@ #if defined(WANT_QT_BUILD) #include -#include #if defined(WANT_ADSK_USD_ASSET_RESOLVER_BUILD) #include #include @@ -103,6 +104,7 @@ #if defined(WANT_QT_BUILD) #include +#include #endif #if defined(MAYAUSD_VERSION) @@ -146,9 +148,6 @@ template void deregisterCommandCheck(MFnPlugin& plugin) MStatus registerStringResources() { MStatus status { MStatus::MStatusCode::kSuccess }; -#if defined(WANT_QT_BUILD) - status = MayaUsd::initStringResources(); -#endif return status; } @@ -458,12 +457,17 @@ MStatus initializePlugin(MObject obj) } MayaUsd::LayerManager::addSupportForNodeType(MayaUsd::ProxyShape::typeId); + + // Register the Qt-free layer-editor DCC functions unconditionally for headless/batch sessions; Qt builds layer the UI-dependent functions on top. + UsdLayerEditor::registerLayerEditorDCCFunctions(); #if defined(WANT_QT_BUILD) - UsdLayerEditor::initialize(); + // Add the Qt/UI-dependent layer-editor functions. + UsdLayerEditor::initializeUi(); MayaUsd::LayerManager::SetBatchSaveDelegate(UsdLayerEditor::batchSaveLayersUIDelegate); #endif UsdMayaSceneResetNotice::InstallListener(); + MayaUsd::registerLayerMutingSceneResetListener(); UsdMayaBeforeSceneResetNotice::InstallListener(); UsdMayaExitNotice::InstallListener(); UsdMayaDiagnosticDelegate::InstallDelegate(); @@ -503,6 +507,9 @@ MStatus uninitializePlugin(MObject obj) MFnPlugin plugin(obj); MStatus status; + MayaUsd::unregisterLayerMutingSceneResetListener(); + UsdLayerEditor::deregisterLayerEditorDCCFunctions(); + #if defined(WANT_QT_BUILD) #if defined(WANT_ADSK_USD_ASSET_RESOLVER_BUILD) MayaUsd::AssetResolverProjectChangeTracker::stopTracking(); diff --git a/plugin/adsk/scripts/mayaUSDRegisterStrings.py b/plugin/adsk/scripts/mayaUSDRegisterStrings.py index 011664405d..63aad21302 100644 --- a/plugin/adsk/scripts/mayaUSDRegisterStrings.py +++ b/plugin/adsk/scripts/mayaUSDRegisterStrings.py @@ -106,6 +106,7 @@ "kMenuUSDRenderSetupAnn": "Open the USD Render Setup", "kMenuRemove": "Remove", "kMenuSelectPrimsWithSpec": "Select Prims With Spec", + "kMenuStitchLayers": "Merge Layers", "kMenuStageCreateMenuError": "Could not create mayaUSD create menu", "kMenuStageWithNewLayer": "Stage with New Layer", "kMenuStageWithNewLayerAnn": "Create a new, empty USD Stage", diff --git a/test/lib/CMakeLists.txt b/test/lib/CMakeLists.txt index b92f0a3f75..cb7a77d502 100644 --- a/test/lib/CMakeLists.txt +++ b/test/lib/CMakeLists.txt @@ -161,6 +161,20 @@ foreach(script ${INTERACTIVE_TEST_SCRIPT_FILES}) set_property(TEST ${target} APPEND PROPERTY LABELS MayaUsd) endforeach() +# +# ----------------------------------------------------------------------------- +# Shared usdLayerEditor component tests (C++ GTests + UFE commands python tests). +# ----------------------------------------------------------------------------- +add_subdirectory(usdLayerEditor) + +# +# ----------------------------------------------------------------------------- +# Old layer editor parity tests — temporary, deleted when old editor is retired. +# Reuse the shared layer editor test sources against the old editor; coverage is +# lesser than the new editor suite. +# ----------------------------------------------------------------------------- +add_subdirectory(oldUsdLayerEditor) + # # ----------------------------------------------------------------------------- # Special interactive test to be able to start Maya with test configurations. diff --git a/test/lib/oldUsdLayerEditor/CMakeLists.txt b/test/lib/oldUsdLayerEditor/CMakeLists.txt new file mode 100644 index 0000000000..3e6d85eeb4 --- /dev/null +++ b/test/lib/oldUsdLayerEditor/CMakeLists.txt @@ -0,0 +1,32 @@ +# +# 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. +# + +# Old layer editor parity test suite. Temporary — deleted when the old editor +# is retired. + +add_subdirectory(cpp) + +mayaUsd_get_unittest_target(target testOldLayerEditorParity.py) +mayaUsd_add_test(${target} + INTERACTIVE + PYTHON_SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/testOldLayerEditorParity.py + ENV + "LD_LIBRARY_PATH=${ADDITIONAL_LD_LIBRARY_PATH}" + "MAYA_PLUG_IN_PATH=${CMAKE_INSTALL_PREFIX}/lib/maya" +) +install(FILES testOldLayerEditorParity.py + DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/tests +) diff --git a/test/lib/oldUsdLayerEditor/README.md b/test/lib/oldUsdLayerEditor/README.md new file mode 100644 index 0000000000..c876057924 --- /dev/null +++ b/test/lib/oldUsdLayerEditor/README.md @@ -0,0 +1,46 @@ +# Old Layer Editor Parity Tests + +Temporary test suite that runs the shared layer editor test logic against the +**old** (legacy) layer editor widgets. It exists to guard against regressions in +the old editor while the new editor is being adopted, and will be **deleted when +the old editor is retired**. + +## What it covers + +The C++ test logic lives in the new editor's test directory +(`test/lib/usdLayerEditor/cpp`) as `test*.cpp` files. Those same sources are +compiled here against the old editor's widgets and fixtures, so a single body of +tests exercises both editors. Coverage of the old editor is intentionally a +subset of the new editor suite — cases that only apply to the new editor are +skipped via the `MAYAUSD_OLD_LAYER_EDITOR` compile define. + +`cpp/CMakeLists.txt` builds these sources, the old editor's stubs/fixtures, and +the legacy widget sources (from `lib/usd/ui/layerEditor`) into a self-contained +Maya plugin. + +## How it works + +These are Qt-widget GTests: they need a running Maya with its Qt application and +USD runtime, so they cannot run as a standalone GTest executable. Instead: + +1. **Maya plugin** (`cpp/`, target `mayaUsdOldLayerEditorTests`) — bundles the + GTests and registers an MEL/Python command, `mayaUsd_runLayerEditorTests`, + that runs them via `RUN_ALL_TESTS()` and returns the results as JSON. The + command always returns success; per-test pass/fail is encoded in the JSON. + +2. **Python runner** (`testOldLayerEditorParity.py`) — the actual ctest entry + point. It launches Maya, loads the plugin, calls the command, parses the JSON, + and calls `self.fail()` with the details of any failing test so ctest reports + the failure. + +This plugin + Python split is what lets a C++ GTest suite run inside a live Maya +session under ctest. + +## Running + +``` +ctest -R testOldLayerEditorParity +``` + +This is an interactive test (it launches Maya), so it also carries the +auto-applied `interactive` ctest label. diff --git a/test/lib/oldUsdLayerEditor/cpp/CMakeLists.txt b/test/lib/oldUsdLayerEditor/cpp/CMakeLists.txt new file mode 100644 index 0000000000..67e96dba25 --- /dev/null +++ b/test/lib/oldUsdLayerEditor/cpp/CMakeLists.txt @@ -0,0 +1,153 @@ +# +# 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. +# + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +# *.cpp test sources and testUtils.h live in the new editor's test/cpp dir +# and are shared between old and new test suites. +set(NEW_LE_TEST_CPP + ${CMAKE_CURRENT_SOURCE_DIR}/../../usdLayerEditor/cpp) + +# Sources +set(OLD_LE_OWN_SOURCES + stubCommandHook.cpp + stubSessionState.cpp + testFixture.cpp + pluginMain.cpp +) + +# Shared test logic compiled directly from the new editor's test dir. Include +# path order (old test dir first) makes `#include "testFixture.h"` resolve to +# the old fixture, which selects the old-editor code paths. +set(OLD_LE_SHARED_LOGIC_SOURCES + ${NEW_LE_TEST_CPP}/testButtons.cpp + ${NEW_LE_TEST_CPP}/testComponentSaveWidget.cpp + ${NEW_LE_TEST_CPP}/testContextMenu.cpp + ${NEW_LE_TEST_CPP}/testEFMode.cpp + ${NEW_LE_TEST_CPP}/testLayerContentsWidget.cpp + ${NEW_LE_TEST_CPP}/testLayerEditorWidget.cpp + ${NEW_LE_TEST_CPP}/testLayerEditorWindow.cpp + ${NEW_LE_TEST_CPP}/testLayerLogicUtils.cpp + ${NEW_LE_TEST_CPP}/testLayerLocking.cpp + ${NEW_LE_TEST_CPP}/testLayerMuting.cpp + ${NEW_LE_TEST_CPP}/testLayerTreeItem.cpp + ${NEW_LE_TEST_CPP}/testLayerTreeItemDelegate.cpp + ${NEW_LE_TEST_CPP}/testLayerTreeModel.cpp + ${NEW_LE_TEST_CPP}/testLayerTreeView.cpp + ${NEW_LE_TEST_CPP}/testLayerTreeViewMouse.cpp + ${NEW_LE_TEST_CPP}/testLoadLayersDialog.cpp + ${NEW_LE_TEST_CPP}/testMenusAndStage.cpp + ${NEW_LE_TEST_CPP}/testPathChecker.cpp + ${NEW_LE_TEST_CPP}/testReorder.cpp + ${NEW_LE_TEST_CPP}/testSaveLayersDialog.cpp + ${NEW_LE_TEST_CPP}/testSessionState.cpp + ${NEW_LE_TEST_CPP}/testSharedStage.cpp + ${NEW_LE_TEST_CPP}/testStageSelectorWidget.cpp + ${NEW_LE_TEST_CPP}/testUsdSyntaxHighlighter.cpp +) + +# Legacy widget sources compiled directly into this test DLL. This gives the +# test a self-contained copy of the old editor, avoiding ODR conflicts with +# usdLayerEditor, which exports the new editor's versions of the same classes. +set(LEGACY_SOURCES + ${CMAKE_SOURCE_DIR}/lib/usd/ui/layerEditor/mayaQtUtils.cpp + ${CMAKE_SOURCE_DIR}/lib/usd/ui/layerEditor/componentSaveWidget.cpp + ${CMAKE_SOURCE_DIR}/lib/usd/ui/layerEditor/dirtyLayersCountBadge.cpp + ${CMAKE_SOURCE_DIR}/lib/usd/ui/layerEditor/generatedIconButton.cpp + ${CMAKE_SOURCE_DIR}/lib/usd/ui/layerEditor/layerContentsWidget.cpp + ${CMAKE_SOURCE_DIR}/lib/usd/ui/layerEditor/layerEditorWidget.cpp + ${CMAKE_SOURCE_DIR}/lib/usd/ui/layerEditor/layerTreeItem.cpp + ${CMAKE_SOURCE_DIR}/lib/usd/ui/layerEditor/layerTreeItemDelegate.cpp + ${CMAKE_SOURCE_DIR}/lib/usd/ui/layerEditor/layerTreeModel.cpp + ${CMAKE_SOURCE_DIR}/lib/usd/ui/layerEditor/layerTreeView.cpp + ${CMAKE_SOURCE_DIR}/lib/usd/ui/layerEditor/loadLayersDialog.cpp + ${CMAKE_SOURCE_DIR}/lib/usd/ui/layerEditor/pathChecker.cpp + ${CMAKE_SOURCE_DIR}/lib/usd/ui/layerEditor/qtUtils.cpp + ${CMAKE_SOURCE_DIR}/lib/usd/ui/layerEditor/resources.qrc + ${CMAKE_SOURCE_DIR}/lib/usd/ui/layerEditor/saveLayersDialog.cpp + ${CMAKE_SOURCE_DIR}/lib/usd/ui/layerEditor/sessionState.cpp + ${CMAKE_SOURCE_DIR}/lib/usd/ui/layerEditor/stageSelectorWidget.cpp + ${CMAKE_SOURCE_DIR}/lib/usd/ui/layerEditor/stringResources.cpp + ${CMAKE_SOURCE_DIR}/lib/usd/ui/layerEditor/usdSyntaxHighlighter.cpp + ${CMAKE_SOURCE_DIR}/lib/usd/ui/layerEditor/warningDialogs.cpp +) + +# The legacy layerEditorWidget.cpp above opens the edit-forward dialog, so its +# definition must be compiled in when edit-forward is available; exercised by +# the shared testEFMode.cpp cases. +if(AdskUsdEditForward_FOUND) + list(APPEND LEGACY_SOURCES + ${CMAKE_SOURCE_DIR}/lib/usd/ui/editForward/editForwardDialog.cpp) +endif() + +# Target +set(TARGET_NAME mayaUsdOldLayerEditorTests) + +add_library(${TARGET_NAME} SHARED + ${OLD_LE_OWN_SOURCES} + ${OLD_LE_SHARED_LOGIC_SOURCES} + ${LEGACY_SOURCES} +) + +if(IS_WINDOWS) + set_target_properties(${TARGET_NAME} PROPERTIES SUFFIX ".mll") +elseif(IS_MACOSX) + set_target_properties(${TARGET_NAME} PROPERTIES PREFIX "" SUFFIX ".bundle") +endif() + +mayaUsd_compile_config(${TARGET_NAME}) + +target_compile_definitions(${TARGET_NAME} + PRIVATE + MAYAUSD_UI_EXPORT + # Marks the shared logic-header tests as compiling against the old editor, + # so old-editor-only parity gaps can be skipped. Only this target defines it. + MAYAUSD_OLD_LAYER_EDITOR + $<$:WIN32> + $<$:LINUX> + $<$:OSMac_> +) + +# Include path order is critical: +# 1. OLD editor test dir FIRST — shim headers shadow new editor versions. +# 2. NEW editor test dir SECOND — shared test .cpp sources and testUtils.h. +# 3. OLD editor lib LAST — abstractCommandHook.h etc. must shadow the new lib versions. +target_include_directories(${TARGET_NAME} + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${NEW_LE_TEST_CPP} + ${CMAKE_SOURCE_DIR}/lib/usd/ui/layerEditor +) + +target_link_libraries(${TARGET_NAME} + PRIVATE + mayaUsd + GTest::GTest + Qt::Core Qt::Gui Qt::Widgets + sdf tf usd + ${MAYA_LIBRARIES} + $<$:${ADSK_USD_EDIT_FORWARD_UI_LIBRARY}> +) + +install(TARGETS ${TARGET_NAME} + LIBRARY DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/maya + RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/maya +) +if(IS_WINDOWS) + install(FILES $ + DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/maya OPTIONAL) +endif() diff --git a/lib/usd/ui/initStringResources.cpp b/test/lib/oldUsdLayerEditor/cpp/customLayerData.h similarity index 59% rename from lib/usd/ui/initStringResources.cpp rename to test/lib/oldUsdLayerEditor/cpp/customLayerData.h index 7631a882fd..336dd40e1c 100644 --- a/lib/usd/ui/initStringResources.cpp +++ b/test/lib/oldUsdLayerEditor/cpp/customLayerData.h @@ -1,5 +1,4 @@ -// -// Copyright 2020 Autodesk +// 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. @@ -13,12 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. // -#include "initStringResources.h" - -#include "layerEditor/stringResources.h" - -namespace MAYAUSD_NS_DEF { - -MStatus initStringResources() { return UsdLayerEditor::StringResources::registerAll(); } +// Compatibility shim: aliases MayaUsd::CustomLayerData into UsdLayerEditor::CustomLayerData +// so shared test files (e.g. testSharedStageLogic.h) compile unchanged against the old editor. +#pragma once +#include -} // namespace MAYAUSD_NS_DEF +namespace UsdLayerEditor { +namespace CustomLayerData = MayaUsd::CustomLayerData; +} // namespace UsdLayerEditor diff --git a/test/lib/oldUsdLayerEditor/cpp/layerLocking.h b/test/lib/oldUsdLayerEditor/cpp/layerLocking.h new file mode 100644 index 0000000000..d173dfadc0 --- /dev/null +++ b/test/lib/oldUsdLayerEditor/cpp/layerLocking.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. +// +// Compatibility shim: re-exports old-editor locking API into UsdLayerEditor namespace +// so shared test *Logic.h headers compile unchanged against the old editor. +#pragma once +#include + +namespace UsdLayerEditor { +using MayaUsd::LayerLockType; +using MayaUsd::LayerLock_Unlocked; +using MayaUsd::LayerLock_Locked; +using MayaUsd::LayerLock_SystemLocked; +using MayaUsd::lockLayer; +using MayaUsd::isLayerLocked; +using MayaUsd::isLayerSystemLocked; +using MayaUsd::addLockedLayer; +using MayaUsd::removeLockedLayer; +using MayaUsd::forgetLockedLayers; +using MayaUsd::addSystemLockedLayer; +using MayaUsd::removeSystemLockedLayer; +using MayaUsd::forgetSystemLockedLayers; +} // namespace UsdLayerEditor diff --git a/test/lib/oldUsdLayerEditor/cpp/layerMuting.h b/test/lib/oldUsdLayerEditor/cpp/layerMuting.h new file mode 100644 index 0000000000..2291b9c44c --- /dev/null +++ b/test/lib/oldUsdLayerEditor/cpp/layerMuting.h @@ -0,0 +1,26 @@ +// 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. +// +// Compatibility shim: re-exports old-editor muting API into UsdLayerEditor namespace +// so shared test files compile unchanged against the old editor. +#pragma once +#include + +namespace UsdLayerEditor { +using MayaUsd::addMutedLayer; +using MayaUsd::removeMutedLayer; +using MayaUsd::forgetMutedLayers; +using MayaUsd::getMutedLayers; +using MayaUsd::LayerRefSet; +} // namespace UsdLayerEditor diff --git a/test/lib/oldUsdLayerEditor/cpp/pluginMain.cpp b/test/lib/oldUsdLayerEditor/cpp/pluginMain.cpp new file mode 100644 index 0000000000..44dde370cf --- /dev/null +++ b/test/lib/oldUsdLayerEditor/cpp/pluginMain.cpp @@ -0,0 +1,183 @@ +// +// 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 "mayaQtUtils.h" +#include "qtUtils.h" + +#include + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +namespace { + +// ── JsonResultCollector ─────────────────────────────────────────────────────── + +struct TestResult { + std::string name; + bool passed; + std::string message; +}; + +class JsonResultCollector : public ::testing::TestEventListener +{ +public: + void OnTestStart(const ::testing::TestInfo& info) override + { + _current.name = std::string(info.test_suite_name()) + "." + info.name(); + _current.passed = true; + _current.message = ""; + } + + void OnTestPartResult(const ::testing::TestPartResult& result) override + { + // failed() is true only for actual failures; skipped parts (GTEST_SKIP) + // must not mark the test as not-passed. + if (result.failed()) { + _current.passed = false; + if (!_current.message.empty()) + _current.message += "\n"; + _current.message += result.message(); + } + } + + void OnTestEnd(const ::testing::TestInfo& /*info*/) override + { + _results.push_back(_current); + } + + std::string toJson() const + { + std::ostringstream os; + os << "["; + for (size_t i = 0; i < _results.size(); ++i) { + const auto& result = _results[i]; + if (i > 0) os << ","; + os << "{\"name\":\"" << escape(result.name) << "\"," + << "\"passed\":" << (result.passed ? "true" : "false") << "," + << "\"message\":\"" << escape(result.message) << "\"}"; + } + os << "]"; + return os.str(); + } + + // Unused events — required by pure-virtual base. + void OnTestProgramStart(const ::testing::UnitTest&) override { } + void OnTestIterationStart(const ::testing::UnitTest&, int) override { } + void OnEnvironmentsSetUpStart(const ::testing::UnitTest&) override { } + void OnEnvironmentsSetUpEnd(const ::testing::UnitTest&) override { } + void OnEnvironmentsTearDownStart(const ::testing::UnitTest&) override { } + void OnEnvironmentsTearDownEnd(const ::testing::UnitTest&) override { } + void OnTestIterationEnd(const ::testing::UnitTest&, int) override { } + void OnTestProgramEnd(const ::testing::UnitTest&) override { } + +private: + TestResult _current; + std::vector _results; + + static std::string escape(const std::string& str) + { + std::string out; + out.reserve(str.size()); + for (char ch : str) { + switch (ch) { + case '"': out += "\\\""; break; + case '\\': out += "\\\\"; break; + case '\n': out += "\\n"; break; + case '\r': out += "\\r"; break; + case '\t': out += "\\t"; break; + default: out += ch; + } + } + return out; + } +}; + +// ── MPxCommand ──────────────────────────────────────────────────────────────── + +// MPxCommand 'mayaUsd_runLayerEditorTests' — runs all layer editor tests and returns results as JSON. +class RunLayerEditorTestsCmd : public MPxCommand +{ +public: + static const MString kName; + static void* creator() { return new RunLayerEditorTestsCmd(); } + + MStatus doIt(const MArgList&) override + { + // InitGoogleTest may only be called once per process. Another plugin in the + // same process may have already initialized it, so detect that instead of + // relying solely on our own flag, and only remove the default printer once. + static bool sInitialized = false; + auto& listeners = ::testing::UnitTest::GetInstance()->listeners(); + if (!sInitialized && listeners.default_result_printer() != nullptr) { + int argc = 0; + char* argv[1] = { nullptr }; + ::testing::InitGoogleTest(&argc, argv); + // Remove default stdout printer so GTest output doesn't pollute Maya's output window. + delete listeners.Release(listeners.default_result_printer()); + sInitialized = true; + } + + auto* collector = new JsonResultCollector(); + ::testing::UnitTest::GetInstance()->listeners().Append(collector); + + int testResult = RUN_ALL_TESTS(); + + // GTest owns listeners after Append(); Release() transfers ownership back. + ::testing::UnitTest::GetInstance()->listeners().Release(collector); + setResult(MString(collector->toJson().c_str())); + delete collector; + // Always return kSuccess so the Python runner receives the JSON result + // via setResult() and can report per-test failures via self.fail(). + // CTest still fails because the Python unittest calls self.fail() when + // the JSON contains failing entries. + (void)testResult; + return MS::kSuccess; + } +}; + +const MString RunLayerEditorTestsCmd::kName("mayaUsd_runLayerEditorTests"); + +} // namespace + +// ── Plugin entry points ─────────────────────────────────────────────────────── + +MStatus initializePlugin(MObject obj) +{ + // utils is set by MayaLayerEditorWindow when the layer editor UI is first + // opened, but the tests construct LayerEditorWidget directly. Initialize it + // here so DPIScale() and Maya API guards work correctly. + if (!UsdLayerEditor::utils) + UsdLayerEditor::utils = new UsdLayerEditor::MayaQtUtils(); + + MFnPlugin plugin(obj, "Autodesk", "1.0", "Any"); + return plugin.registerCommand( + RunLayerEditorTestsCmd::kName, RunLayerEditorTestsCmd::creator); +} + +MStatus uninitializePlugin(MObject obj) +{ + MFnPlugin plugin(obj); + return plugin.deregisterCommand(RunLayerEditorTestsCmd::kName); +} diff --git a/test/lib/oldUsdLayerEditor/cpp/stubCommandHook.cpp b/test/lib/oldUsdLayerEditor/cpp/stubCommandHook.cpp new file mode 100644 index 0000000000..a885a080b2 --- /dev/null +++ b/test/lib/oldUsdLayerEditor/cpp/stubCommandHook.cpp @@ -0,0 +1,174 @@ +// 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 "stubCommandHook.h" + +#include + +namespace UsdLayerEditor { + +OldEditorStubCommandHook::OldEditorStubCommandHook(SessionState* sessionState) + : AbstractCommandHook(sessionState) +{ +} + +void OldEditorStubCommandHook::setEditTarget(UsdLayer layer) +{ + _calls.push_back({ "setEditTarget", { layer->GetIdentifier() } }); +} + +void OldEditorStubCommandHook::insertSubLayerPath(UsdLayer layer, Path path, int index) +{ + _calls.push_back( + { "insertSubLayerPath", { layer->GetIdentifier(), path, std::to_string(index) } }); + layer->InsertSubLayerPath(path, index); +} + +void OldEditorStubCommandHook::removeSubLayerPath(UsdLayer layer, Path path) +{ + _calls.push_back({ "removeSubLayerPath", { layer->GetIdentifier(), path } }); + auto paths = layer->GetSubLayerPaths(); + for (size_t i = 0; i < paths.size(); ++i) { + if (paths[i] == path) { + layer->RemoveSubLayerPath(i); + break; + } + } +} + +void OldEditorStubCommandHook::replaceSubLayerPath(UsdLayer layer, Path oldPath, Path newPath) +{ + _calls.push_back({ "replaceSubLayerPath", { layer->GetIdentifier(), oldPath, newPath } }); +} + +void OldEditorStubCommandHook::moveSubLayerPath( + Path path, UsdLayer oldParent, UsdLayer newParent, int index) +{ + _calls.push_back({ "moveSubLayerPath", { path, std::to_string(index) } }); + auto oldPaths = oldParent->GetSubLayerPaths(); + size_t fromIdx = static_cast(-1); + for (size_t i = 0; i < oldPaths.size(); ++i) { + if (oldPaths[i] == path) { + fromIdx = i; + break; + } + } + if (fromIdx != static_cast(-1)) { + oldParent->RemoveSubLayerPath(fromIdx); + } + newParent->InsertSubLayerPath(path, index); +} + +void OldEditorStubCommandHook::discardEdits(UsdLayer layer) +{ + _calls.push_back({ "discardEdits", { layer->GetIdentifier() } }); + layer->Clear(); +} + +void OldEditorStubCommandHook::clearLayer(UsdLayer layer) +{ + _calls.push_back({ "clearLayer", { layer->GetIdentifier() } }); + layer->Clear(); +} + +void OldEditorStubCommandHook::flattenLayer(UsdLayer layer) +{ + _calls.push_back({ "flattenLayer", { layer->GetIdentifier() } }); +} + +UsdLayer OldEditorStubCommandHook::addAnonymousSubLayer(UsdLayer layer, std::string newName) +{ + _calls.push_back({ "addAnonymousSubLayer", { layer->GetIdentifier(), newName } }); + auto newLayer = PXR_NS::SdfLayer::CreateAnonymous(newName); + layer->InsertSubLayerPath(newLayer->GetIdentifier(), 0); + return newLayer; +} + +void OldEditorStubCommandHook::muteSubLayer(UsdLayer layer, bool muteIt) +{ + _calls.push_back({ "muteSubLayer", { layer->GetIdentifier(), muteIt ? "true" : "false" } }); +} + +void OldEditorStubCommandHook::lockLayer( + UsdLayer layer, MayaUsd::LayerLockType lockState, bool /*includeSubLayers*/) +{ + _calls.push_back({ "lockLayer", { layer->GetIdentifier() } }); + layer->SetPermissionToEdit(lockState == MayaUsd::LayerLock_Unlocked); +} + +void OldEditorStubCommandHook::refreshLayerSystemLock(UsdLayer layer, bool /*refreshSubLayers*/) +{ + _calls.push_back({ "refreshLayerSystemLock", { layer->GetIdentifier() } }); +} + +void OldEditorStubCommandHook::stitchLayers(const std::vector& /*layers*/) +{ + _calls.push_back({ "stitchLayers", {} }); +} + +void OldEditorStubCommandHook::openUndoBracket(const QString& name) +{ + _calls.push_back({ "openUndoBracket", { name.toStdString() } }); +} + +void OldEditorStubCommandHook::closeUndoBracket() +{ + _calls.push_back({ "closeUndoBracket", {} }); +} + +void OldEditorStubCommandHook::showLayerEditorHelp() +{ + _calls.push_back({ "showLayerEditorHelp", {} }); +} + +void OldEditorStubCommandHook::selectPrimsWithSpec(UsdLayer layer) +{ + _calls.push_back({ "selectPrimsWithSpec", { layer->GetIdentifier() } }); +} + +void OldEditorStubCommandHook::clearCalls() +{ + _calls.clear(); +} + +bool OldEditorStubCommandHook::hasCall(const std::string& method) const +{ + return callCount(method) > 0; +} + +int OldEditorStubCommandHook::callCount(const std::string& method) const +{ + int count = 0; + for (const auto& call : _calls) { + if (call.name == method) + ++count; + } + return count; +} + +const CommandCall& OldEditorStubCommandHook::lastCall() const +{ + return _calls.back(); +} + +const CommandCall* OldEditorStubCommandHook::lastCallOf(const std::string& method) const +{ + for (auto it = _calls.rbegin(); it != _calls.rend(); ++it) + if (it->name == method) + return &*it; + return nullptr; +} + +} // namespace UsdLayerEditor diff --git a/test/lib/oldUsdLayerEditor/cpp/stubCommandHook.h b/test/lib/oldUsdLayerEditor/cpp/stubCommandHook.h new file mode 100644 index 0000000000..c7312ad709 --- /dev/null +++ b/test/lib/oldUsdLayerEditor/cpp/stubCommandHook.h @@ -0,0 +1,75 @@ +// 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. +// +#pragma once +#include "abstractCommandHook.h" + +#include +#include + +namespace UsdLayerEditor { + +struct CommandCall +{ + std::string name; + std::vector args; +}; + +class OldEditorStubCommandHook : public AbstractCommandHook +{ +public: + explicit OldEditorStubCommandHook(SessionState* sessionState); + + // Configurable flags — member names match new StubCommandHook exactly. + bool _isSharedStage = false; + bool _isStageIncoming = false; + + // Old editor uses isProxyShapeSharedStage / isProxyShapeStageIncoming + bool isProxyShapeSharedStage(const std::string&) override { return _isSharedStage; } + bool isProxyShapeStageIncoming(const std::string&) override { return _isStageIncoming; } + + void setEditTarget(UsdLayer layer) override; + void insertSubLayerPath(UsdLayer layer, Path path, int index) override; + void removeSubLayerPath(UsdLayer layer, Path path) override; + void replaceSubLayerPath(UsdLayer layer, Path oldPath, Path newPath) override; + void moveSubLayerPath(Path path, UsdLayer oldParent, UsdLayer newParent, int index) override; + void discardEdits(UsdLayer layer) override; + void clearLayer(UsdLayer layer) override; + void flattenLayer(UsdLayer layer) override; + UsdLayer addAnonymousSubLayer(UsdLayer layer, std::string newName) override; + void muteSubLayer(UsdLayer layer, bool muteIt) override; + // Old editor uses MayaUsd::LayerLockType (not UsdLayerEditor::LayerLockType) + void lockLayer(UsdLayer layer, MayaUsd::LayerLockType lockState, bool includeSubLayers) + override; + void refreshLayerSystemLock(UsdLayer layer, bool refreshSubLayers = false) override; + void stitchLayers(const std::vector& layers) override; + void openUndoBracket(const QString& name) override; + void closeUndoBracket() override; + void showLayerEditorHelp() override; + void selectPrimsWithSpec(UsdLayer layer) override; + + void clearCalls(); + bool hasCall(const std::string& method) const; + int callCount(const std::string& method) const; + const CommandCall& lastCall() const; + // Returns a pointer to the last call with the given name, or nullptr if none. + const CommandCall* lastCallOf(const std::string& method) const; + + std::vector _calls; + +protected: + void executeDelayedCommands() override { } +}; + +} // namespace UsdLayerEditor diff --git a/test/lib/oldUsdLayerEditor/cpp/stubLayerEditorWindow.h b/test/lib/oldUsdLayerEditor/cpp/stubLayerEditorWindow.h new file mode 100644 index 0000000000..844210dbf5 --- /dev/null +++ b/test/lib/oldUsdLayerEditor/cpp/stubLayerEditorWindow.h @@ -0,0 +1,211 @@ +// 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. +// +#pragma once + +#include "layerEditorWidget.h" +#include "layerTreeItem.h" +#include "layerTreeModel.h" +#include "layerTreeView.h" +#include "stubSessionState.h" + +#include + +#include + +#include +#include + +namespace UsdLayerEditor { + +class OldEditorStubLayerEditorWindow : public MayaUsd::AbstractLayerEditorWindow +{ +public: + OldEditorStubLayerEditorWindow( + OldEditorStubSessionState& sessionState, QMainWindow* parent) + : MayaUsd::AbstractLayerEditorWindow("stub_panel") + , _sessionState(sessionState) + { + _layerEditor = new LayerEditorWidget(sessionState, parent); + } + + LayerEditorWidget* widget() const { return _layerEditor; } + + // --- query methods --- + + int selectionLength() override + { + return static_cast(treeView()->getSelectedLayerItems().size()); + } + + bool hasCurrentLayerItem() override { return treeView()->currentLayerItem() != nullptr; } + +// Collapses the many identical query overrides below: each delegates one bool +// query to the current tree item, returning false when there is no selection. +#define ITEM_QUERY(method) \ + auto item = treeView()->currentLayerItem(); \ + return item ? item->method() : false + + bool isInvalidLayer() override { ITEM_QUERY(isInvalidLayer); } + bool isSessionLayer() override { ITEM_QUERY(isSessionLayer); } + bool isLayerDirty() override { ITEM_QUERY(isDirty); } + bool isSubLayer() override { ITEM_QUERY(isSublayer); } + bool isAnonymousLayer() override { ITEM_QUERY(isAnonymous); } + bool isIncomingLayer() override { ITEM_QUERY(isIncoming); } + bool layerNeedsSaving() override { ITEM_QUERY(needsSaving); } + bool layerAppearsMuted() override { ITEM_QUERY(appearsMuted); } + bool layerIsMuted() override { ITEM_QUERY(isMuted); } + bool layerIsReadOnly() override { ITEM_QUERY(isReadOnly); } + bool layerAppearsLocked() override { ITEM_QUERY(appearsLocked); } + bool layerIsLocked() override { ITEM_QUERY(isLocked); } + bool layerAppearsSystemLocked() override { ITEM_QUERY(appearsSystemLocked); } + bool layerIsSystemLocked() override { ITEM_QUERY(isSystemLocked); } + bool layerHasSubLayers() override { ITEM_QUERY(hasSubLayers); } + +#undef ITEM_QUERY + + std::string proxyShapeName(const bool /*fullPath*/ = false) const override + { + return "stubProxyShape"; + } + + // --- action methods --- + + void removeSubLayer() override + { + treeView()->callMethodOnSelectionNoDelay("Remove", &LayerTreeItem::removeSubLayer); + } + + void saveEdits() override + { + auto item = treeView()->currentLayerItem(); + if (item) { + QString name = item->isAnonymous() ? "Save As..." : "Save Edits"; + treeView()->callMethodOnSelection(name, &LayerTreeItem::saveEdits); + } + } + + void discardEdits() override + { + treeView()->callMethodOnSelection("Discard Edits", &LayerTreeItem::discardEdits); + } + + void addAnonymousSublayer() override + { + treeView()->callMethodOnSelection("Add Sublayer", &LayerTreeItem::addAnonymousSublayer); + } + + void addParentLayer() override { } + + void loadSubLayers() override + { + auto item = treeView()->currentLayerItem(); + if (item) + item->loadSubLayers(_layerEditor); + } + + void muteLayer() override + { + auto item = treeView()->currentLayerItem(); + if (item) { + QString name = item->isMuted() ? "Unmute" : "Mute"; + treeView()->onMuteLayer(name); + } + } + + void printLayer() override + { + treeView()->callMethodOnSelection("Print to Script Editor", &LayerTreeItem::printLayer); + } + + void clearLayer() override + { + // Suspend notices during the loop to avoid dangling-pointer crash on refresh mid-loop. + LayerTreeModel::suspendUsdNotices(true); + treeView()->callMethodOnSelection("Clear", &LayerTreeItem::clearLayer); + LayerTreeModel::suspendUsdNotices(false); + treeView()->layerTreeModel()->forceRefresh(); + } + + void mergeWithSublayers() override + { + treeView()->callMethodOnSelection( + "Merge with Sublayers", &LayerTreeItem::mergeWithSublayers); + } + + void selectPrimsWithSpec() override + { + auto item = treeView()->currentLayerItem(); + if (item) + _sessionState.commandHook()->selectPrimsWithSpec(item->layer()); + } + + void updateLayerModel() override { } + + void lockLayer() override + { + auto item = treeView()->currentLayerItem(); + if (item) { + QString name = item->isLocked() ? "Unlock" : "Lock"; + treeView()->onLockLayer(name); + } + } + + void lockLayerAndSubLayers() override + { + auto item = treeView()->currentLayerItem(); + if (item) { + QString name + = item->isLocked() ? "Unlock Layer and Sublayers" : "Lock Layer and Sublayers"; + treeView()->onLockLayerAndSublayers(name, /*includeSublayers=*/true); + } + } + + void stitchLayers() override + { + const auto selectedItems = treeView()->getSelectedLayerItems(); + if (selectedItems.size() < 2) + return; + + std::vector layers; + layers.reserve(selectedItems.size()); + for (const auto& item : selectedItems) { + if (!item) + continue; + const auto layer = item->layer(); + if (!layer) + continue; + layers.push_back(layer); + } + + if (layers.size() < 2) + return; + + _sessionState.commandHook()->stitchLayers(layers); + } + + void selectProxyShape(const char* /*shapePath*/) override { } + + std::vector getSelectedLayers() override { return {}; } + + void selectLayers(std::vector /*layerIds*/) override { } + +private: + LayerTreeView* treeView() { return _layerEditor->layerTree(); } + + OldEditorStubSessionState& _sessionState; + LayerEditorWidget* _layerEditor { nullptr }; +}; + +} // namespace UsdLayerEditor diff --git a/test/lib/oldUsdLayerEditor/cpp/stubSessionState.cpp b/test/lib/oldUsdLayerEditor/cpp/stubSessionState.cpp new file mode 100644 index 0000000000..8314a9e2c5 --- /dev/null +++ b/test/lib/oldUsdLayerEditor/cpp/stubSessionState.cpp @@ -0,0 +1,117 @@ +// 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 "stubSessionState.h" + +#include +#include + +#include + +namespace UsdLayerEditor { + +OldEditorStubSessionState::OldEditorStubSessionState() + : _commandHookImpl(this) +{ + // Create a couple of test stages to populate the layer editor. + for (int i = 0; i < 2; ++i) { + auto stage = PXR_NS::UsdStage::CreateInMemory(); + auto sublayer = PXR_NS::SdfLayer::CreateAnonymous("sublayer" + std::to_string(i)); + stage->GetRootLayer()->InsertSubLayerPath(sublayer->GetIdentifier(), 0); + _stages.push_back(makeEntry(stage, "stub_stage_" + std::to_string(i))); + } + setStageEntry(_stages[0]); +} + +AbstractCommandHook* OldEditorStubSessionState::commandHook() +{ + return &_commandHookImpl; +} + +std::vector OldEditorStubSessionState::allStages() const +{ + return _stages; +} + +std::string OldEditorStubSessionState::defaultLoadPath() const +{ + return "/tmp"; +} + +std::vector OldEditorStubSessionState::loadLayersUI( + const QString& /*title*/, const std::string& /*default_path*/) const +{ + ++_loadLayersCallCount; + if (!_stubbedLoadPath.empty()) { + return { _stubbedLoadPath }; + } + return {}; +} + +bool OldEditorStubSessionState::saveLayerUI( + QWidget* /*parent*/, + std::string* /*out_filePath*/, + const PXR_NS::SdfLayerRefPtr& /*parentLayer*/) const +{ + ++_saveLayerCallCount; + // Simulates user cancelling the save dialog. + return false; +} + +void OldEditorStubSessionState::printLayer(const PXR_NS::SdfLayerRefPtr& /*layer*/) const +{ + ++_printLayerCallCount; +} + +void OldEditorStubSessionState::setupCreateMenu(QMenu* menu) +{ + if (menu) { + menu->addAction("Stub Create Action"); + } +} + +void OldEditorStubSessionState::rootLayerPathChanged(std::string const& /*path*/) { } + +SessionState::StageEntry +OldEditorStubSessionState::makeEntry(PXR_NS::UsdStageRefPtr stage, const std::string& id) +{ + StageEntry entry; + entry._id = id; + entry._stage = stage; + entry._displayName = id; + entry._proxyShapePath = id; + return entry; +} + +void OldEditorStubSessionState::switchToCustomStage( + PXR_NS::UsdStageRefPtr stage, + const std::string& id) +{ + auto entry = makeEntry(stage, id); + _stages.push_back(entry); + setStageEntry(entry); +} + +void OldEditorStubSessionState::setProxyShapePath(int index, const std::string& path) +{ + if (index < 0 || index >= static_cast(_stages.size())) + return; + _stages[index]._proxyShapePath = path; + // Refresh the base-class active entry copy when this index is the current stage. + if (stageEntry()._id == _stages[index]._id) + SessionState::setStageEntry(_stages[index]); +} + +} // namespace UsdLayerEditor diff --git a/test/lib/oldUsdLayerEditor/cpp/stubSessionState.h b/test/lib/oldUsdLayerEditor/cpp/stubSessionState.h new file mode 100644 index 0000000000..273314eb09 --- /dev/null +++ b/test/lib/oldUsdLayerEditor/cpp/stubSessionState.h @@ -0,0 +1,85 @@ +// 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. +// +#pragma once + +#include "sessionState.h" +#include "stubCommandHook.h" + +#include + +#include +#include + +class QMenu; +class QWidget; + +namespace UsdLayerEditor { + +class OldEditorStubSessionState : public SessionState +{ + Q_OBJECT +public: + OldEditorStubSessionState(); + + AbstractCommandHook* commandHook() override; + std::vector allStages() const override; + std::string defaultLoadPath() const override; + std::vector loadLayersUI( + const QString& title, const std::string& default_path) const override; + bool saveLayerUI( + QWidget* parent, + std::string* out_filePath, + const PXR_NS::SdfLayerRefPtr& parentLayer) const override; + void printLayer(const PXR_NS::SdfLayerRefPtr& layer) const override; + void setupCreateMenu(QMenu* menu) override; + void rootLayerPathChanged(std::string const& path) override; + bool autoHideSessionLayer() const override { return false; } +#ifdef WANT_ADSK_USD_EDIT_FORWARD_BUILD + bool isEditForwardMode() const override { return _isEFModeActive; } +#endif + + // No-op setter: old editor SessionState has no editForwardingChanged signal, + // so we just update the flag. The EF-active styling test is guarded to new editor only. + void setIsEditForwardMode(bool v) { _isEFModeActive = v; } + + // Patches _stages[index]._proxyShapePath and refreshes the base-class + // active entry if it matches. Called by LayerEditorTestFixture::SetUp + // after real Maya proxy shape nodes are created. + void setProxyShapePath(int index, const std::string& path); + + // Pushes a new stage entry and makes it the active one. Mirrors the new + // editor's StubSessionState. + void switchToCustomStage(PXR_NS::UsdStageRefPtr stage, const std::string& id = "custom_stage"); + + // Used by LayerEditorWithEFFixture; has no effect on old editor widget since + // it uses a compile-time #ifdef guard rather than a runtime check. + bool _supportsEditForwarding { false }; + bool _isEFModeActive { false }; + + // Call counters — member names match new StubSessionState exactly. + mutable int _saveLayerCallCount { 0 }; + mutable int _printLayerCallCount { 0 }; + mutable int _loadLayersCallCount { 0 }; + + std::string _stubbedLoadPath; + + OldEditorStubCommandHook _commandHookImpl; + +private: + std::vector _stages; + StageEntry makeEntry(PXR_NS::UsdStageRefPtr stage, const std::string& id); +}; + +} // namespace UsdLayerEditor diff --git a/test/lib/oldUsdLayerEditor/cpp/testFixture.cpp b/test/lib/oldUsdLayerEditor/cpp/testFixture.cpp new file mode 100644 index 0000000000..6ff463608f --- /dev/null +++ b/test/lib/oldUsdLayerEditor/cpp/testFixture.cpp @@ -0,0 +1,137 @@ +// 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 "testFixture.h" + +#include "warningDialogs.h" + +#include +#include + +#include + +#include +#include +#include +#include + +namespace UsdLayerEditor { + +void LayerEditorTestFixture::SetUp() +{ + setModalDialogTestHandler([this](const QString&, const QString&) { + ++_modalDialogCount; + return _modalDialogAnswer; + }); + + // Back one real mayaUsdProxyShape per stub stage with the SAME in-memory stage (via the stage + // cache) so proxy-based discovery sees the identical layers. + { + const auto stages = _sessionState.allStages(); + for (int i = 0; i < 2; ++i) { + _stageCacheIds[i] = PXR_NS::UsdUtilsStageCache::Get().Insert(stages[i]._stage); + + const std::string xformName = "leTestXform" + std::to_string(i); + const std::string shapeName = "leTestProxy" + std::to_string(i); + MGlobal::executeCommand( + MString("createNode transform -n \"") + xformName.c_str() + "\""); + MGlobal::executeCommand( + MString("createNode mayaUsdProxyShape -n \"") + shapeName.c_str() + + "\" -p " + xformName.c_str()); + MGlobal::executeCommand( + MString("setAttr \"") + shapeName.c_str() + ".stageCacheId\" " + + std::to_string(_stageCacheIds[i].ToLongInt()).c_str()); + + MSelectionList sel; + sel.add(MString(shapeName.c_str())); + MDagPath dagPath; + sel.getDagPath(0, dagPath); + _proxyShapePaths[i] = dagPath.fullPathName().asChar(); + + _sessionState.setProxyShapePath(i, _proxyShapePaths[i]); + } + } + + _mainWindow = new QMainWindow(); + _window = std::make_unique(_sessionState, _mainWindow); + _widget = _window->widget(); + _mainWindow->show(); + _widget->show(); + QApplication::processEvents(); + _sessionState._commandHookImpl.clearCalls(); + _sessionState._saveLayerCallCount = 0; + _sessionState._printLayerCallCount = 0; + _sessionState._loadLayersCallCount = 0; +} + +void LayerEditorTestFixture::TearDown() +{ + setModalDialogTestHandler(nullptr); + _widget = nullptr; + _window.reset(); + delete _mainWindow; + _mainWindow = nullptr; + + // Delete real proxy shape nodes and erase the stage-cache entries created in SetUp. + { + for (int i = 0; i < 2; ++i) { + if (!_proxyShapePaths[i].empty()) { + const std::string xformPath = "|leTestXform" + std::to_string(i); + MGlobal::executeCommand( + MString("delete \"") + xformPath.c_str() + "\""); + _proxyShapePaths[i].clear(); + } + if (_stageCacheIds[i].IsValid()) { + PXR_NS::UsdUtilsStageCache::Get().Erase(_stageCacheIds[i]); + _stageCacheIds[i] = PXR_NS::UsdStageCache::Id(); + } + } + } +} + +LayerTreeView* LayerEditorTestFixture::layerTree() +{ + return _widget->layerTree(); +} + +LayerTreeModel* LayerEditorTestFixture::treeModel() +{ + return layerTree()->layerTreeModel(); +} + +QModelIndex LayerEditorTestFixture::sessionLayerIndex() +{ + // Stub always shows session layer (autoHideSessionLayer=false). + return treeModel()->index(0, 0); +} + +QModelIndex LayerEditorTestFixture::rootLayerIndex() +{ + return treeModel()->rootLayerIndex(); +} + +QModelIndex LayerEditorTestFixture::firstSublayerIndex() +{ + return treeModel()->index(0, 0, rootLayerIndex()); +} + +void LayerEditorTestFixture::selectRow(const QModelIndex& index) +{ + layerTree()->selectionModel()->select( + index, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); + layerTree()->setCurrentIndex(index); + QApplication::processEvents(); +} + +} // namespace UsdLayerEditor diff --git a/test/lib/oldUsdLayerEditor/cpp/testFixture.h b/test/lib/oldUsdLayerEditor/cpp/testFixture.h new file mode 100644 index 0000000000..74a8442bb2 --- /dev/null +++ b/test/lib/oldUsdLayerEditor/cpp/testFixture.h @@ -0,0 +1,76 @@ +// 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. +// +#pragma once + +#include "stubCommandHook.h" +#include "stubLayerEditorWindow.h" +#include "stubSessionState.h" + +#include "layerEditorWidget.h" +#include "layerTreeModel.h" +#include "layerTreeView.h" + +#include + +#include + +#include +#include +#include +#include + +namespace UsdLayerEditor { + +class LayerEditorTestFixture : public ::testing::Test +{ +protected: + void SetUp() override; + void TearDown() override; + + LayerTreeView* layerTree(); + LayerTreeModel* treeModel(); + QModelIndex sessionLayerIndex(); + QModelIndex rootLayerIndex(); + QModelIndex firstSublayerIndex(); + void selectRow(const QModelIndex& index); + + void setEditForwardingSupported(bool supported) { _sessionState._supportsEditForwarding = supported; } + void setSharedStage(bool shared) { _sessionState._commandHookImpl._isSharedStage = shared; } + void setStageIncoming(bool incoming) { _sessionState._commandHookImpl._isStageIncoming = incoming; } + + // Members mirror the new editor's testFixture.h, named identically so the + // shared test sources compile unchanged. + OldEditorStubSessionState _sessionState; + std::unique_ptr _window; + QMainWindow* _mainWindow { nullptr }; + LayerEditorWidget* _widget { nullptr }; + + bool _isComponent { false }; + bool _isUnsavedComponent { false }; + + int _modalDialogCount { 0 }; + bool _modalDialogAnswer { true }; + // Real Maya proxy shape DAG paths, e.g. "|leTestXform0|leTestProxy0". + // Set in SetUp, cleared in TearDown. + std::string _proxyShapePaths[2]; + // Stage-cache ids backing the proxy shapes with the stub's in-memory stages — + // erased in TearDown. + PXR_NS::UsdStageCache::Id _stageCacheIds[2]; + + void setIsComponent(bool v) { _isComponent = v; } + void setIsUnsavedComponent(bool v) { _isUnsavedComponent = v; } +}; + +} // namespace UsdLayerEditor diff --git a/test/lib/oldUsdLayerEditor/testOldLayerEditorParity.py b/test/lib/oldUsdLayerEditor/testOldLayerEditorParity.py new file mode 100644 index 0000000000..7177ab4465 --- /dev/null +++ b/test/lib/oldUsdLayerEditor/testOldLayerEditorParity.py @@ -0,0 +1,47 @@ +# +# 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. +# +import json +import unittest + +import fixturesUtils +import maya.cmds as cmds + + +class OldLayerEditorParityTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + fixturesUtils.setUpClass(__file__, initializeStandalone=False, loadPlugin=False) + cmds.loadPlugin('mayaUsdPlugin') + plugin = 'mayaUsdOldLayerEditorTests' + if not cmds.pluginInfo(plugin, q=True, loaded=True): + cmds.loadPlugin(plugin) + + def test_old_layer_editor_parity(self): + raw = cmds.mayaUsd_runLayerEditorTests() + results = json.loads(raw) + failures = [r for r in results if not r['passed']] + + if failures: + msg = '\n\n'.join( + '{name}:\n{message}'.format(**r) for r in failures + ) + self.fail('{n} test(s) failed:\n\n{msg}'.format( + n=len(failures), msg=msg)) + + +if __name__ == '__main__': + fixturesUtils.runTests(globals()) diff --git a/test/lib/usdLayerEditor/CMakeLists.txt b/test/lib/usdLayerEditor/CMakeLists.txt new file mode 100644 index 0000000000..6db0c6e474 --- /dev/null +++ b/test/lib/usdLayerEditor/CMakeLists.txt @@ -0,0 +1,31 @@ +# +# 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. +# + +# C++ GTest suite for the shared usdLayerEditor component. +add_subdirectory(cpp) + +# Maya-driven interactive Python test for the shared usdLayerEditor commands as UFE commands +mayaUsd_get_unittest_target(target testMayaUsdSharedLayerEditor.py) +mayaUsd_add_test(${target} + INTERACTIVE + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + PYTHON_SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/testMayaUsdSharedLayerEditor.py + ENV + "MAYA_PLUG_IN_PATH=${CMAKE_INSTALL_PREFIX}/lib/maya" + "LD_LIBRARY_PATH=${ADDITIONAL_LD_LIBRARY_PATH}" + "PYTHONPATH=${CMAKE_CURRENT_SOURCE_DIR}" +) +set_property(TEST ${target} APPEND PROPERTY LABELS MayaUsd) diff --git a/test/lib/usdLayerEditor/cpp/.gitignore b/test/lib/usdLayerEditor/cpp/.gitignore new file mode 100644 index 0000000000..0426f7421b --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/.gitignore @@ -0,0 +1,3 @@ +# Test-run artifacts: generated by save tests when getDCCSceneDir returns empty. +# Should never appear in-source now that testFixture redirects saves to /tmp. +stub_stage_*.usd diff --git a/test/lib/usdLayerEditor/cpp/CMakeLists.txt b/test/lib/usdLayerEditor/cpp/CMakeLists.txt new file mode 100644 index 0000000000..7316d9c625 --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/CMakeLists.txt @@ -0,0 +1,95 @@ +# +# 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. +# + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +set(LAYER_EDITOR_TEST_SOURCES + testMain.cpp + stubCommandHook.cpp + stubSessionState.cpp + testFixture.cpp + testButtons.cpp + testContextMenu.cpp + testReorder.cpp + testMenusAndStage.cpp + testLayerTreeItem.cpp + testLayerTreeModel.cpp + testLayerTreeView.cpp + testLayerContentsWidget.cpp + testSaveLayersDialog.cpp + testLoadLayersDialog.cpp + testLayerLocking.cpp + testLayerMuting.cpp + testSharedStage.cpp + testLayerEditorCommands.cpp + testEFMode.cpp + testDCCFunctions.cpp + testFileSystemUtils.cpp + testPathChecker.cpp + testSerializationUtils.cpp + testLayerTreeItemDelegate.cpp + testComponentSaveWidget.cpp + testLayerLogicUtils.cpp + testUsdSyntaxHighlighter.cpp + testLayerTreeViewMouse.cpp + testSessionState.cpp + testWidgetManager.cpp + testLayerEditorWidget.cpp + testLayerEditorWindow.cpp + testStageSelectorWidget.cpp +) + +# ------------------------------------------------------------------------------ +# usdLayerEditorTests +# Links usdLayerEditor (the DCC-agnostic shared component). +# ------------------------------------------------------------------------------ +set(NEW_LE_DIR ${CMAKE_SOURCE_DIR}/lib/usdLayerEditor/lib) + +add_executable(usdLayerEditorTests + ${LAYER_EDITOR_TEST_SOURCES} +) + +mayaUsd_compile_config(usdLayerEditorTests) + +target_compile_definitions(usdLayerEditorTests + PRIVATE + $<$:WIN32> + $<$:LINUX> + $<$:OSMac_> +) + +target_include_directories(usdLayerEditorTests + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${NEW_LE_DIR} + ${UFE_INCLUDE_DIR} +) + +target_link_libraries(usdLayerEditorTests + PRIVATE + usdLayerEditor + GTest::GTest + Qt::Core Qt::Gui Qt::Widgets + sdf tf usd + ${UFE_LIBRARY} +) + +mayaUsd_add_test(usdLayerEditorTests + COMMAND $ + ENV + "LD_LIBRARY_PATH=${ADDITIONAL_LD_LIBRARY_PATH}" +) diff --git a/test/lib/usdLayerEditor/cpp/scopedLayerEditorDCCFunctions.h b/test/lib/usdLayerEditor/cpp/scopedLayerEditorDCCFunctions.h new file mode 100644 index 0000000000..3cac8d68f1 --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/scopedLayerEditorDCCFunctions.h @@ -0,0 +1,41 @@ +// +// 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. +// +#pragma once + +#include "layerEditorDCCFunctions.h" + +namespace UsdLayerEditor { + +// Installs registry state on construction and restores the previous state on +// destruction, so tests that exercise component / EF / DCC-object behavior do +// not leak global registry state between cases. +class ScopedLayerEditorDCCFunctions +{ +public: + ScopedLayerEditorDCCFunctions() + : _saved(layerEditorDCCFunctions()) + { + } + ~ScopedLayerEditorDCCFunctions() { setLayerEditorDCCFunctions(_saved); } + + ScopedLayerEditorDCCFunctions(const ScopedLayerEditorDCCFunctions&) = delete; + ScopedLayerEditorDCCFunctions& operator=(const ScopedLayerEditorDCCFunctions&) = delete; + +private: + LayerEditorDCCFunctions _saved; +}; + +} // namespace UsdLayerEditor diff --git a/test/lib/usdLayerEditor/cpp/stubCommandHook.cpp b/test/lib/usdLayerEditor/cpp/stubCommandHook.cpp new file mode 100644 index 0000000000..394d40ff38 --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/stubCommandHook.cpp @@ -0,0 +1,174 @@ +// +// 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 "stubCommandHook.h" + +#include + +namespace UsdLayerEditor { + +StubCommandHook::StubCommandHook(SessionState* sessionState) + : AbstractCommandHook(sessionState) +{ +} + +void StubCommandHook::setEditTarget(UsdLayer layer) +{ + _calls.push_back({ "setEditTarget", { layer->GetIdentifier() } }); +} + +void StubCommandHook::insertSubLayerPath(UsdLayer layer, Path path, int index) +{ + _calls.push_back( + { "insertSubLayerPath", { layer->GetIdentifier(), path, std::to_string(index) } }); + layer->InsertSubLayerPath(path, index); +} + +void StubCommandHook::removeSubLayerPath(UsdLayer layer, Path path) +{ + _calls.push_back({ "removeSubLayerPath", { layer->GetIdentifier(), path } }); + auto paths = layer->GetSubLayerPaths(); + for (size_t i = 0; i < paths.size(); ++i) { + if (paths[i] == path) { + layer->RemoveSubLayerPath(i); + break; + } + } +} + +void StubCommandHook::replaceSubLayerPath(UsdLayer layer, Path oldPath, Path newPath) +{ + _calls.push_back({ "replaceSubLayerPath", { layer->GetIdentifier(), oldPath, newPath } }); +} + +void StubCommandHook::moveSubLayerPath( + Path path, UsdLayer oldParent, UsdLayer newParent, int index) +{ + _calls.push_back({ "moveSubLayerPath", { path, std::to_string(index) } }); + auto oldPaths = oldParent->GetSubLayerPaths(); + size_t fromIdx = static_cast(-1); + for (size_t i = 0; i < oldPaths.size(); ++i) { + if (oldPaths[i] == path) { + fromIdx = i; + break; + } + } + if (fromIdx != static_cast(-1)) { + oldParent->RemoveSubLayerPath(fromIdx); + } + newParent->InsertSubLayerPath(path, index); +} + +void StubCommandHook::discardEdits(UsdLayer layer) +{ + _calls.push_back({ "discardEdits", { layer->GetIdentifier() } }); + layer->Clear(); +} + +void StubCommandHook::clearLayer(UsdLayer layer) +{ + _calls.push_back({ "clearLayer", { layer->GetIdentifier() } }); + layer->Clear(); +} + +void StubCommandHook::flattenLayer(UsdLayer layer) +{ + _calls.push_back({ "flattenLayer", { layer->GetIdentifier() } }); +} + +UsdLayer StubCommandHook::addAnonymousSubLayer(UsdLayer layer, std::string newName) +{ + _calls.push_back({ "addAnonymousSubLayer", { layer->GetIdentifier(), newName } }); + auto newLayer = PXR_NS::SdfLayer::CreateAnonymous(newName); + layer->InsertSubLayerPath(newLayer->GetIdentifier(), 0); + return newLayer; +} + +void StubCommandHook::muteSubLayer(UsdLayer layer, bool muteIt) +{ + _calls.push_back({ "muteSubLayer", { layer->GetIdentifier(), muteIt ? "true" : "false" } }); +} + +void StubCommandHook::lockLayer(UsdLayer layer, LayerLockType lockState, bool /*includeSubLayers*/) +{ + _calls.push_back({ "lockLayer", { layer->GetIdentifier() } }); + layer->SetPermissionToEdit(lockState == LayerLock_Unlocked); +} + +void StubCommandHook::refreshLayerSystemLock(UsdLayer layer, bool /*refreshSubLayers*/) +{ + _calls.push_back({ "refreshLayerSystemLock", { layer->GetIdentifier() } }); +} + +void StubCommandHook::stitchLayers(const std::vector& /*layers*/) +{ + _calls.push_back({ "stitchLayers", {} }); +} + +void StubCommandHook::openUndoBracket(const QString& name) +{ + _calls.push_back({ "openUndoBracket", { name.toStdString() } }); +} + +void StubCommandHook::closeUndoBracket() +{ + _calls.push_back({ "closeUndoBracket", {} }); +} + +void StubCommandHook::showLayerEditorHelp() +{ + _calls.push_back({ "showLayerEditorHelp", {} }); +} + +void StubCommandHook::selectPrimsWithSpec(UsdLayer layer) +{ + _calls.push_back({ "selectPrimsWithSpec", { layer->GetIdentifier() } }); +} + +void StubCommandHook::clearCalls() +{ + _calls.clear(); +} + +bool StubCommandHook::hasCall(const std::string& method) const +{ + return callCount(method) > 0; +} + +int StubCommandHook::callCount(const std::string& method) const +{ + int count = 0; + for (const auto& call : _calls) { + if (call.name == method) + ++count; + } + return count; +} + +const CommandCall& StubCommandHook::lastCall() const +{ + return _calls.back(); +} + +const CommandCall* StubCommandHook::lastCallOf(const std::string& method) const +{ + for (auto it = _calls.rbegin(); it != _calls.rend(); ++it) + if (it->name == method) + return &*it; + return nullptr; +} + +} // namespace UsdLayerEditor diff --git a/test/lib/usdLayerEditor/cpp/stubCommandHook.h b/test/lib/usdLayerEditor/cpp/stubCommandHook.h new file mode 100644 index 0000000000..22abd4a843 --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/stubCommandHook.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. +// +#pragma once + +#include "abstractCommandHook.h" + +#include +#include + +namespace UsdLayerEditor { + +struct CommandCall { + std::string name; + std::vector args; +}; + +class StubCommandHook : public AbstractCommandHook +{ +public: + explicit StubCommandHook(SessionState* sessionState); + + void setEditTarget(UsdLayer layer) override; + void insertSubLayerPath(UsdLayer layer, Path path, int index) override; + void removeSubLayerPath(UsdLayer layer, Path path) override; + void replaceSubLayerPath(UsdLayer layer, Path oldPath, Path newPath) override; + void moveSubLayerPath(Path path, UsdLayer oldParent, UsdLayer newParent, int index) override; + void discardEdits(UsdLayer layer) override; + void clearLayer(UsdLayer layer) override; + void flattenLayer(UsdLayer layer) override; + UsdLayer addAnonymousSubLayer(UsdLayer layer, std::string newName) override; + void muteSubLayer(UsdLayer layer, bool muteIt) override; + void lockLayer(UsdLayer layer, LayerLockType lockState, bool includeSubLayers) override; + void refreshLayerSystemLock(UsdLayer layer, bool refreshSubLayers = false) override; + void stitchLayers(const std::vector& layers) override; + void openUndoBracket(const QString& name) override; + void closeUndoBracket() override; + void showLayerEditorHelp() override; + void selectPrimsWithSpec(UsdLayer layer) override; + + void clearCalls(); + bool hasCall(const std::string& method) const; + int callCount(const std::string& method) const; + const CommandCall& lastCall() const; + // Returns a pointer to the last call with the given name, or nullptr if none. + const CommandCall* lastCallOf(const std::string& method) const; + + std::vector _calls; + +protected: + void executeDelayedCommands() override { } +}; + +} // namespace UsdLayerEditor diff --git a/test/lib/usdLayerEditor/cpp/stubLayerEditorWindow.h b/test/lib/usdLayerEditor/cpp/stubLayerEditorWindow.h new file mode 100644 index 0000000000..990b59981b --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/stubLayerEditorWindow.h @@ -0,0 +1,50 @@ +// +// 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. +// +#pragma once + +#include "layerEditorWidget.h" +#include "layerEditorWindow.h" +#include "stubSessionState.h" + +#include + +namespace UsdLayerEditor { + +class StubLayerEditorWindow : public LayerEditorWindow +{ +public: + StubLayerEditorWindow(StubSessionState& sessionState, QMainWindow* parent) + : LayerEditorWindow("stub_panel") + , _sessionState(sessionState) + , _mainWindow(parent) + { + _layerEditor = new LayerEditorWidget(sessionState, parent); + } + + LayerEditorWidget* widget() const { return _layerEditor; } + + // AbstractLayerEditorWindow pure virtuals + std::string dccObjectName() const override { return "stub_panel"; } + void selectDccObject(const char*) override { } + SessionState* getSessionState() override { return &_sessionState; } + QMainWindow* getMainWindow() override { return _mainWindow; } + +private: + StubSessionState& _sessionState; + QMainWindow* _mainWindow; +}; + +} // namespace UsdLayerEditor diff --git a/test/lib/usdLayerEditor/cpp/stubSessionState.cpp b/test/lib/usdLayerEditor/cpp/stubSessionState.cpp new file mode 100644 index 0000000000..d5da9b76d5 --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/stubSessionState.cpp @@ -0,0 +1,141 @@ +// +// 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 "stubSessionState.h" + +#include +#include + +#include + +#include + +namespace UsdLayerEditor { + +StubSessionState::StubSessionState() + : _commandHookImpl(this) +{ + // Two in-memory stages, each with one anonymous sublayer. + for (int i = 0; i < 2; ++i) { + auto stage = PXR_NS::UsdStage::CreateInMemory(); + auto sublayer = PXR_NS::SdfLayer::CreateAnonymous("sublayer" + std::to_string(i)); + stage->GetRootLayer()->InsertSubLayerPath(sublayer->GetIdentifier(), 0); + _stages.push_back(makeEntry(stage, "stub_stage_" + std::to_string(i))); + } + setStageEntry(_stages[0]); +} + +AbstractCommandHook* StubSessionState::commandHook() +{ + return &_commandHookImpl; +} + +std::vector StubSessionState::allStages() const +{ + return _stages; +} + +std::vector StubSessionState::selectedStages() const +{ + return { _currentStageEntry }; +} + +std::string StubSessionState::defaultLoadPath() const +{ + return "/tmp"; +} + +std::vector StubSessionState::loadLayersUI( + const QString& /*title*/, const std::string& /*default_path*/) const +{ + ++_loadLayersCallCount; + if (!_stubbedLoadPath.empty()) { + return { _stubbedLoadPath }; + } + return {}; +} + +bool StubSessionState::saveLayerUI( + QWidget* /*parent*/, + std::string* out_filePath, + const PXR_NS::SdfLayerRefPtr& /*parentLayer*/) const +{ + ++_saveLayerCallCount; + // Simulates the user cancelling the save dialog; returning false keeps tests off the + // saveAnonymousLayer() path, which needs a real UFE path. + return false; +} + +void StubSessionState::printLayer(const PXR_NS::SdfLayerRefPtr& /*layer*/) const +{ + ++_printLayerCallCount; +} + +void StubSessionState::refreshCurrentStageEntry() { } +void StubSessionState::refreshStageEntry(std::string const& /*dccObjectPath*/) { } + +void StubSessionState::setupCreateMenu(QMenu* menu) +{ + if (menu) { + menu->addAction("Stub Create Action"); + } +} + +void StubSessionState::rootLayerPathChanged(std::string const& /*path*/) { } + +void StubSessionState::setIsEditForwardMode(bool v) +{ + _isEFModeActive = v; + Q_EMIT editForwardingChanged(); +} + +void StubSessionState::addStage(PXR_NS::UsdStageRefPtr stage) +{ + auto entry = makeEntry(stage, "added_stage_" + std::to_string(_stages.size())); + _stages.push_back(entry); + Q_EMIT stageListChangedSignal(entry); +} + +void StubSessionState::removeStage(const std::string& id) +{ + _stages.erase( + std::remove_if( + _stages.begin(), + _stages.end(), + [&id](const StageEntry& entry) { return entry._id == id; }), + _stages.end()); + Q_EMIT stageListChangedSignal(); +} + +void StubSessionState::switchToCustomStage(PXR_NS::UsdStageRefPtr stage, const std::string& id) +{ + auto entry = makeEntry(stage, id); + _stages.push_back(entry); + setStageEntry(entry); +} + +SessionState::StageEntry +StubSessionState::makeEntry(PXR_NS::UsdStageRefPtr stage, const std::string& id) +{ + StageEntry entry; + entry._id = id; + entry._stage = stage; + entry._displayName = id; + entry._dccObjectPath = id; + return entry; +} + +} // namespace UsdLayerEditor diff --git a/test/lib/usdLayerEditor/cpp/stubSessionState.h b/test/lib/usdLayerEditor/cpp/stubSessionState.h new file mode 100644 index 0000000000..2cc95b8ab8 --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/stubSessionState.h @@ -0,0 +1,81 @@ +// +// 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. +// +#pragma once + +#include "sessionState.h" +#include "stubCommandHook.h" + +#include + +#include +#include + +class QMenu; +class QWidget; + +namespace UsdLayerEditor { + +class StubSessionState : public SessionState +{ + Q_OBJECT +public: + StubSessionState(); + + AbstractCommandHook* commandHook() override; + std::vector allStages() const override; + std::vector selectedStages() const override; + std::string defaultLoadPath() const override; + std::vector loadLayersUI( + const QString& title, const std::string& default_path) const override; + bool saveLayerUI( + QWidget* parent, + std::string* out_filePath, + const PXR_NS::SdfLayerRefPtr& parentLayer) const override; + void printLayer(const PXR_NS::SdfLayerRefPtr& layer) const override; + void refreshCurrentStageEntry() override; + void refreshStageEntry(std::string const& dccObjectPath) override; + void setupCreateMenu(QMenu* menu) override; + void rootLayerPathChanged(std::string const& path) override; + bool autoObserveUfeSelection() const override { return false; } + bool autoHideSessionLayer() const override { return false; } + bool isEditForwardMode() const override { return _isEFModeActive; } + + void setIsEditForwardMode(bool v); + + // Test helpers + void addStage(PXR_NS::UsdStageRefPtr stage); + void removeStage(const std::string& id); + // Replace the current stage with a custom one (e.g. a file-backed stage for save tests). + void switchToCustomStage(PXR_NS::UsdStageRefPtr stage, const std::string& id = "custom_stage"); + + + // Call counters + mutable int _saveLayerCallCount { 0 }; + mutable int _printLayerCallCount { 0 }; + mutable int _loadLayersCallCount { 0 }; + + // Pre-baked path returned by loadLayersUI (set in tests) + std::string _stubbedLoadPath; + + StubCommandHook _commandHookImpl; + +private: + bool _isEFModeActive { false }; + std::vector _stages; + StageEntry makeEntry(PXR_NS::UsdStageRefPtr stage, const std::string& id); +}; + +} // namespace UsdLayerEditor diff --git a/test/lib/usdLayerEditor/cpp/testButtons.cpp b/test/lib/usdLayerEditor/cpp/testButtons.cpp new file mode 100644 index 0000000000..d561b1a197 --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/testButtons.cpp @@ -0,0 +1,265 @@ +// +// 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. +// +#pragma once + +#include +#include "testUtils.h" +#include "layerLocking.h" +#include "layerTreeItem.h" + +#include +#include +#include +#include +#include + +#include + +namespace UsdLayerEditor { + +// The Save Stage button is only created, shown, and enable-managed on a shared +// stage; updateButtons() leaves it hidden and unmanaged otherwise. +class ButtonsSharedStageFixture : public LayerEditorTestFixture +{ +protected: + void SetUp() override + { + setSharedStage(true); + LayerEditorTestFixture::SetUp(); + QApplication::processEvents(); + } +}; + +// Shared stage backed by a real file: root layer is non-anonymous and initially +// clean, so the Save button starts disabled and only enables when dirty. +class SaveStageCleanNonAnonFixture : public LayerEditorTestFixture +{ +protected: + QString _stagePath; + + void SetUp() override + { + setSharedStage(true); + LayerEditorTestFixture::SetUp(); + + _stagePath = QDir::tempPath() + "/le_save_clean_test.usda"; + QFile::remove(_stagePath); + auto stage = PXR_NS::UsdStage::CreateNew(_stagePath.toStdString()); + _sessionState.switchToCustomStage(stage); + QApplication::processEvents(); + QApplication::processEvents(); + } + + void TearDown() override + { + LayerEditorTestFixture::TearDown(); + QFile::remove(_stagePath); + } +}; + +// Clicking "Add a New Layer" with nothing selected inserts an anonymous sublayer at the root. +TEST_F(LayerEditorTestFixture, NewLayerButton_Click_CallsAddAnonymousSubLayer) +{ + QPushButton* btn = TestUtils::findButtonByTooltip(_widget, "Add a New Layer"); + ASSERT_NE(btn, nullptr) << "Could not find New Layer button"; + ASSERT_TRUE(btn->isEnabled()) << "New Layer button should be enabled"; + + _sessionState._commandHookImpl.clearCalls(); + btn->click(); + QApplication::processEvents(); + + const auto* call = _sessionState._commandHookImpl.lastCallOf("addAnonymousSubLayer"); + ASSERT_NE(call, nullptr) << "addAnonymousSubLayer should have been called"; + EXPECT_EQ(call->args[0], _sessionState.stage()->GetRootLayer()->GetIdentifier()) + << "addAnonymousSubLayer should target the root layer when nothing is selected"; +} + +// The "Load an Existing Layer" button should be present and enabled. +// We do not click it here because it shows a modal file-picker dialog. +TEST_F(LayerEditorTestFixture, LoadLayerButton_ExistsAndEnabled) +{ + QPushButton* btn = TestUtils::findButtonByTooltip(_widget, "Load an Existing Layer"); + ASSERT_NE(btn, nullptr) << "Could not find Load Layer button"; + EXPECT_TRUE(btn->isEnabled()) << "Load Layer button should be enabled"; +} + +TEST_F(ButtonsSharedStageFixture, SaveStageButton_EnabledWhenDirty) +{ + QPushButton* btn = TestUtils::findButtonByTooltip(_widget, "Save all edits in the Layer Stack"); + ASSERT_NE(btn, nullptr) << "Could not find Save Stage button"; + + auto stage = _sessionState.stage(); + ASSERT_TRUE(stage); + // Note: the stub stage's root is anonymous, so needsSaving() is already true before + // this SetComment call (anonymous layers are treated as always needing to be saved). + // This test confirms that setting a comment (making it additionally dirty) keeps the + // button enabled — for the disabled-then-enabled transition see + // SaveStageButton_DisabledInitially_EnabledWhenDirty. + stage->GetRootLayer()->SetComment("dirty"); + + // Pump twice: updateButtons() uses QTimer::singleShot(0,...), so the first + // processEvents() posts the deferred update and the second fires it. + QApplication::processEvents(); + QApplication::processEvents(); + + EXPECT_TRUE(btn->isVisible()) << "Save Stage button should be shown on a shared stage"; + EXPECT_TRUE(btn->isEnabled()) << "Save Stage button should be enabled when stage is dirty"; +} + +// ── updateNewLayerButton enable/disable matrix ──────────────────────────────── + +// When nothing is selected the button falls back to the root layer and stays enabled. +TEST_F(LayerEditorTestFixture, NewLayerButton_EnabledWhenNoSelectionDefaultsToRoot) +{ + layerTree()->selectionModel()->clearSelection(); + QApplication::processEvents(); + QPushButton* btn = TestUtils::findButtonByTooltip(_widget, "Add a New Layer"); + ASSERT_NE(btn, nullptr); + EXPECT_TRUE(btn->isEnabled()); +} + +TEST_F(LayerEditorTestFixture, NewLayerButton_EnabledForRootLayer) +{ + selectRow(rootLayerIndex()); + QPushButton* btn = TestUtils::findButtonByTooltip(_widget, "Add a New Layer"); + ASSERT_NE(btn, nullptr); + EXPECT_TRUE(btn->isEnabled()); +} + +TEST_F(LayerEditorTestFixture, NewLayerButton_EnabledForSessionLayer) +{ + selectRow(sessionLayerIndex()); + QPushButton* btn = TestUtils::findButtonByTooltip(_widget, "Add a New Layer"); + ASSERT_NE(btn, nullptr); + EXPECT_TRUE(btn->isEnabled()); +} + +TEST_F(LayerEditorTestFixture, NewLayerButton_DisabledWhenSelectionIsLocked) +{ + auto* rootItem = dynamic_cast( + treeModel()->itemFromIndex(rootLayerIndex())); + ASSERT_NE(rootItem, nullptr); + TestUtils::lockLayerDirect(rootItem->layer()); + + selectRow(rootLayerIndex()); + QPushButton* btn = TestUtils::findButtonByTooltip(_widget, "Add a New Layer"); + ASSERT_NE(btn, nullptr); + EXPECT_FALSE(btn->isEnabled()); + + TestUtils::unlockLayerDirect(rootItem->layer()); +} + +TEST_F(LayerEditorTestFixture, NewLayerButton_DisabledWhenSelectionIsSystemLocked) +{ + auto* rootItem = dynamic_cast( + treeModel()->itemFromIndex(rootLayerIndex())); + ASSERT_NE(rootItem, nullptr); + addSystemLockedLayer(rootItem->layer()); + rootItem->layer()->SetPermissionToEdit(false); + + selectRow(rootLayerIndex()); + QPushButton* btn = TestUtils::findButtonByTooltip(_widget, "Add a New Layer"); + ASSERT_NE(btn, nullptr); + EXPECT_FALSE(btn->isEnabled()); + + removeSystemLockedLayer(rootItem->layer()); + TestUtils::unlockLayerDirect(rootItem->layer()); +} + +TEST_F(LayerEditorTestFixture, SaveButton_HiddenWhenStageIsNotShared) +{ + // When the stage is not a shared stage, updateButtons() hides the save button. + QApplication::processEvents(); + QPushButton* btn = TestUtils::findButtonByTooltip(_widget, "Save all edits"); + ASSERT_NE(btn, nullptr); + EXPECT_FALSE(btn->isVisible()); +} + +// Selecting a sublayer enables the button (the click behaviour is covered by +// NewLayerButton_Click_WithSublayerSelectionAddsSibling). +TEST_F(LayerEditorTestFixture, NewLayerButton_EnabledForSublayerSelection) +{ + selectRow(firstSublayerIndex()); + QPushButton* btn = TestUtils::findButtonByTooltip(_widget, "Add a New Layer"); + ASSERT_NE(btn, nullptr); + EXPECT_TRUE(btn->isEnabled()); +} + +TEST_F(LayerEditorTestFixture, NewLayerButton_Click_WithSublayerSelectionAddsSibling) +{ + selectRow(firstSublayerIndex()); + QPushButton* btn = TestUtils::findButtonByTooltip(_widget, "Add a New Layer"); + ASSERT_NE(btn, nullptr); + ASSERT_TRUE(btn->isEnabled()); + + _sessionState._commandHookImpl.clearCalls(); + btn->click(); + QApplication::processEvents(); + + // Adding a sibling means inserting into the selected layer's parent (root). + const auto* call = _sessionState._commandHookImpl.lastCallOf("addAnonymousSubLayer"); + ASSERT_NE(call, nullptr) << "addAnonymousSubLayer should be called on the parent when adding a sibling"; + EXPECT_EQ(call->args[0], _sessionState.stage()->GetRootLayer()->GetIdentifier()) + << "parent should be the root layer when a direct sublayer of root is selected"; +} + +TEST_F(LayerEditorTestFixture, ToolbarButtons_HaveObjectNames) +{ + EXPECT_TRUE(_widget->findChild("LayerEditorAddLayerButton")); + EXPECT_TRUE(_widget->findChild("LayerEditorImportLayerButton")); + EXPECT_TRUE(_widget->findChild("LayerEditorSaveAllButton")); +} + +// ── Save Stage button: disabled → enabled transition ───────────────────────── + +// With a file-backed (non-anonymous), clean stage the Save button starts disabled. +// Making the stage dirty must enable it — the transition proves the button actually +// tracks needsSaving() rather than being permanently enabled by isAnonymous(). +TEST_F(SaveStageCleanNonAnonFixture, SaveStageButton_DisabledInitially_EnabledWhenDirty) +{ + QPushButton* btn = TestUtils::findButtonByTooltip(_widget, "Save all edits in the Layer Stack"); + ASSERT_NE(btn, nullptr) << "Could not find Save Stage button"; + + EXPECT_TRUE(btn->isVisible()) << "Save Stage button should be shown for a shared stage"; + EXPECT_FALSE(btn->isEnabled()) << "Save Stage button should be disabled for a clean non-anonymous stage"; + + _sessionState.stage()->GetRootLayer()->SetComment("dirty"); + QApplication::processEvents(); + QApplication::processEvents(); + + EXPECT_TRUE(btn->isEnabled()) << "Save Stage button should be enabled after stage becomes dirty"; +} + +// Selecting a locked layer disables the button; switching to an unlocked layer +// must re-enable it, demonstrating the disabled→enabled path. +TEST_F(LayerEditorTestFixture, NewLayerButton_ReenablesAfterSwitchFromLockedToUnlockedSelection) +{ + auto* rootItem = dynamic_cast( + treeModel()->itemFromIndex(rootLayerIndex())); + ASSERT_NE(rootItem, nullptr); + + TestUtils::lockLayerDirect(rootItem->layer()); + selectRow(rootLayerIndex()); + QPushButton* btn = TestUtils::findButtonByTooltip(_widget, "Add a New Layer"); + ASSERT_NE(btn, nullptr); + EXPECT_FALSE(btn->isEnabled()) << "New Layer button should be disabled for a locked layer"; + + TestUtils::unlockLayerDirect(rootItem->layer()); + selectRow(firstSublayerIndex()); // select an unlocked layer + EXPECT_TRUE(btn->isEnabled()) << "New Layer button should be re-enabled after switching to unlocked selection"; +} + +} // namespace UsdLayerEditor diff --git a/test/lib/usdLayerEditor/cpp/testComponentSaveWidget.cpp b/test/lib/usdLayerEditor/cpp/testComponentSaveWidget.cpp new file mode 100644 index 0000000000..c6f627a25e --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/testComponentSaveWidget.cpp @@ -0,0 +1,158 @@ +// 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. +// +#pragma once + +#include + +#include "componentSaveWidget.h" + +#include +#include +#include +#include + +#include + +#include +#include + +namespace UsdLayerEditor { + +class ComponentSaveWidgetTest : public LayerEditorTestFixture +{ +protected: + // No-arg: uses the default path for each editor build. + std::unique_ptr makeWidget() + { +#ifdef MAYAUSD_OLD_LAYER_EDITOR + return std::make_unique(_mainWindow, _proxyShapePaths[0]); +#else + return std::make_unique(_mainWindow, &_sessionState, "proxy|shape"); +#endif + } + // Explicit path: passes dccPath as-is (including empty string). + std::unique_ptr makeWidget(const std::string& dccPath) + { +#ifdef MAYAUSD_OLD_LAYER_EDITOR + return std::make_unique(_mainWindow, dccPath); +#else + return std::make_unique(_mainWindow, &_sessionState, dccPath); +#endif + } +}; + +// ── construction ────────────────────────────────────────────────────────────── + +// ── dccObjectPath ───────────────────────────────────────────────────────────── + +#ifndef MAYAUSD_OLD_LAYER_EDITOR +TEST_F(ComponentSaveWidgetTest, DccObjectPath_ReturnsConstructorValue) +{ + auto w = makeWidget("my|proxy|shape"); + EXPECT_EQ(w->dccObjectPath(), "my|proxy|shape"); +} +#endif + +// ── componentName ───────────────────────────────────────────────────────────── + +TEST_F(ComponentSaveWidgetTest, SetComponentName_RoundTrips) +{ + auto w = makeWidget(); + w->setComponentName("MyComponent"); + EXPECT_EQ(w->componentName(), "MyComponent"); +} + +TEST_F(ComponentSaveWidgetTest, SetComponentName_OverwritesPreviousValue) +{ + auto w = makeWidget(); + w->setComponentName("First"); + w->setComponentName("Second"); + EXPECT_EQ(w->componentName(), "Second"); +} + +// ── folderLocation ──────────────────────────────────────────────────────────── + +TEST_F(ComponentSaveWidgetTest, SetFolderLocation_RoundTrips) +{ + auto w = makeWidget(); + w->setFolderLocation("/tmp/my_folder"); + EXPECT_EQ(w->folderLocation(), "/tmp/my_folder"); +} + +// ── compact mode ────────────────────────────────────────────────────────────── + +TEST_F(ComponentSaveWidgetTest, IsCompactMode_FalseByDefault) +{ + auto w = makeWidget(); + EXPECT_FALSE(w->isCompactMode()); +} + +TEST_F(ComponentSaveWidgetTest, SetCompactMode_True) +{ + auto w = makeWidget(); + w->setCompactMode(true); + EXPECT_TRUE(w->isCompactMode()); +} + +TEST_F(ComponentSaveWidgetTest, SetCompactMode_RoundTrip) +{ + auto w = makeWidget(); + w->setCompactMode(true); + w->setCompactMode(false); + EXPECT_FALSE(w->isCompactMode()); +} + +// ── isExpanded / originalHeight (defaults) ─────────────────────────────────── + +TEST_F(ComponentSaveWidgetTest, IsExpanded_FalseByDefault) +{ + auto w = makeWidget(); + EXPECT_FALSE(w->isExpanded()); +} + +TEST_F(ComponentSaveWidgetTest, SetOriginalHeight_RoundTrips) +{ + auto w = makeWidget(); + w->setOriginalHeight(200); + EXPECT_EQ(w->originalHeight(), 200); +} + +// ── keyPressEvent ───────────────────────────────────────────────────────────── + +// ── onShowMore / toggleExpandedState ───────────────────────────────────────── + +TEST_F(ComponentSaveWidgetTest, OnShowMore_Expand_DoesNotCrash) +{ + auto w = makeWidget(); + w->show(); + QCoreApplication::processEvents(); + ASSERT_FALSE(w->isExpanded()); + // onShowMore is a private slot — invoke via the meta-object system. + EXPECT_NO_THROW(QMetaObject::invokeMethod(w.get(), "onShowMore", Qt::DirectConnection)); + EXPECT_TRUE(w->isExpanded()); +} + +TEST_F(ComponentSaveWidgetTest, OnShowMore_CollapseAfterExpand_DoesNotCrash) +{ + auto w = makeWidget(); + w->show(); + QCoreApplication::processEvents(); + QMetaObject::invokeMethod(w.get(), "onShowMore", Qt::DirectConnection); + ASSERT_TRUE(w->isExpanded()); + EXPECT_NO_THROW(QMetaObject::invokeMethod(w.get(), "onShowMore", Qt::DirectConnection)); + EXPECT_FALSE(w->isExpanded()); +} + +} // namespace UsdLayerEditor diff --git a/test/lib/usdLayerEditor/cpp/testContextMenu.cpp b/test/lib/usdLayerEditor/cpp/testContextMenu.cpp new file mode 100644 index 0000000000..31be15b43f --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/testContextMenu.cpp @@ -0,0 +1,416 @@ +// +// 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. +// +#pragma once + +#include +#include "testUtils.h" +#include "layerLocking.h" +#include "layerTreeItem.h" + +#include +#include +#include + +#include + +namespace UsdLayerEditor { + +// ------------------------------------------------------------------ +// Core operations — called directly on the window after selecting +// the appropriate tree row, so they don't depend on QMenu::exec(). +// ------------------------------------------------------------------ + +TEST_F(LayerEditorTestFixture, ContextMenu_AddAnonymousSublayer_CallsHook) +{ + auto* item = dynamic_cast( + treeModel()->itemFromIndex(firstSublayerIndex())); + ASSERT_NE(item, nullptr); + // Capture the identifier before the action: addAnonymousSublayer triggers a + // model rebuild on processEvents() that destroys this item, so it must not be + // dereferenced afterward. + const std::string layerId = item->layer()->GetIdentifier(); + selectRow(firstSublayerIndex()); + _sessionState._commandHookImpl.clearCalls(); + _window->addAnonymousSublayer(); + QApplication::processEvents(); + const auto* call = _sessionState._commandHookImpl.lastCallOf("addAnonymousSubLayer"); + ASSERT_NE(call, nullptr) << "addAnonymousSubLayer should have been called"; + EXPECT_EQ(call->args[0], layerId) + << "addAnonymousSubLayer should target the selected layer"; +} + +TEST_F(LayerEditorTestFixture, ContextMenu_MuteLayer_CallsHook) +{ + selectRow(firstSublayerIndex()); + auto* item = dynamic_cast( + treeModel()->itemFromIndex(firstSublayerIndex())); + ASSERT_NE(item, nullptr); + const std::string layerId = item->layer()->GetIdentifier(); + _sessionState._commandHookImpl.clearCalls(); + _window->muteLayer(); + QApplication::processEvents(); + const auto* call = _sessionState._commandHookImpl.lastCallOf("muteSubLayer"); + ASSERT_NE(call, nullptr) << "muteSubLayer should have been called"; + EXPECT_EQ(call->args[0], layerId) << "muteSubLayer should target the selected layer"; +} + +TEST_F(LayerEditorTestFixture, ContextMenu_LockLayer_CallsHook) +{ + selectRow(firstSublayerIndex()); + auto* item = dynamic_cast( + treeModel()->itemFromIndex(firstSublayerIndex())); + ASSERT_NE(item, nullptr); + const std::string layerId = item->layer()->GetIdentifier(); + _sessionState._commandHookImpl.clearCalls(); + _window->lockLayer(); + QApplication::processEvents(); + const auto* call = _sessionState._commandHookImpl.lastCallOf("lockLayer"); + ASSERT_NE(call, nullptr) << "lockLayer should have been called"; + EXPECT_EQ(call->args[0], layerId) << "lockLayer should target the selected layer"; +} + +TEST_F(LayerEditorTestFixture, ContextMenu_RemoveLayer_CallsHook) +{ + selectRow(firstSublayerIndex()); + auto* item = dynamic_cast( + treeModel()->itemFromIndex(firstSublayerIndex())); + ASSERT_NE(item, nullptr); + const std::string layerId = item->layer()->GetIdentifier(); + _sessionState._commandHookImpl.clearCalls(); + _window->removeSubLayer(); + QApplication::processEvents(); + const auto* call = _sessionState._commandHookImpl.lastCallOf("removeSubLayerPath"); + ASSERT_NE(call, nullptr) << "removeSubLayerPath should have been called"; + // args = { parentIdentifier, removedSubLayerPath }; the removed path is the sublayer. + EXPECT_EQ(call->args[1], layerId) << "removeSubLayerPath should target the selected sublayer"; +} + +TEST_F(LayerEditorTestFixture, ContextMenu_DiscardEdits_CallsHook) +{ + selectRow(firstSublayerIndex()); + auto* item = dynamic_cast( + treeModel()->itemFromIndex(firstSublayerIndex())); + ASSERT_NE(item, nullptr); + const std::string layerId = item->layer()->GetIdentifier(); + _sessionState._commandHookImpl.clearCalls(); + _window->discardEdits(); + QApplication::processEvents(); + const auto* call = _sessionState._commandHookImpl.lastCallOf("discardEdits"); + ASSERT_NE(call, nullptr) << "discardEdits should have been called"; + EXPECT_EQ(call->args[0], layerId) << "discardEdits should target the selected layer"; +} + +TEST_F(LayerEditorTestFixture, ContextMenu_PrintLayer_CallsSessionState) +{ + selectRow(firstSublayerIndex()); + _window->printLayer(); + QApplication::processEvents(); + EXPECT_GT(_sessionState._printLayerCallCount, 0); +} + +TEST_F(LayerEditorTestFixture, ContextMenu_SelectPrimsWithSpec_CallsHook) +{ + selectRow(firstSublayerIndex()); + auto* item = dynamic_cast( + treeModel()->itemFromIndex(firstSublayerIndex())); + ASSERT_NE(item, nullptr); + const std::string layerId = item->layer()->GetIdentifier(); + _sessionState._commandHookImpl.clearCalls(); + _window->selectPrimsWithSpec(); + QApplication::processEvents(); + const auto* call = _sessionState._commandHookImpl.lastCallOf("selectPrimsWithSpec"); + ASSERT_NE(call, nullptr) << "selectPrimsWithSpec should have been called"; + EXPECT_EQ(call->args[0], layerId) << "selectPrimsWithSpec should target the selected layer"; +} + +// ------------------------------------------------------------------ +// Layer-type queries via the window's state methods +// ------------------------------------------------------------------ + +TEST_F(LayerEditorTestFixture, LayerQuery_SessionLayer_IsSessionLayer) +{ + selectRow(sessionLayerIndex()); + EXPECT_TRUE(_window->isSessionLayer()); +} + +TEST_F(LayerEditorTestFixture, LayerQuery_Sublayer_IsNotSessionLayer) +{ + selectRow(firstSublayerIndex()); + EXPECT_FALSE(_window->isSessionLayer()); +} + +TEST_F(LayerEditorTestFixture, LayerQuery_Sublayer_IsSubLayer) +{ + selectRow(firstSublayerIndex()); + EXPECT_TRUE(_window->isSubLayer()); +} + +TEST_F(LayerEditorTestFixture, LayerQuery_SessionLayer_IsNotSubLayer) +{ + selectRow(sessionLayerIndex()); + EXPECT_FALSE(_window->isSubLayer()); +} + +// ------------------------------------------------------------------ +// Lock state affects isLocked() query +// ------------------------------------------------------------------ + +TEST_F(LayerEditorTestFixture, ContextMenu_LockedLayer_IsLocked) +{ + selectRow(firstSublayerIndex()); + // Lock the sublayer directly via the command hook. + _window->lockLayer(); + QApplication::processEvents(); + + EXPECT_TRUE(_window->layerIsLocked()) + << "Layer should report locked after lockLayer()"; +} + +TEST_F(LayerEditorTestFixture, ContextMenu_UnlockedLayer_IsNotLocked) +{ + selectRow(firstSublayerIndex()); + EXPECT_FALSE(_window->layerIsLocked()) + << "Fresh sublayer should not be locked"; +} + +// ── additional window actions ────────────────────────────────────────────────── + +TEST_F(LayerEditorTestFixture, ContextMenu_ClearLayer_CallsHook) +{ + selectRow(firstSublayerIndex()); + auto* item = dynamic_cast( + treeModel()->itemFromIndex(firstSublayerIndex())); + ASSERT_NE(item, nullptr); + const std::string layerId = item->layer()->GetIdentifier(); + _sessionState._commandHookImpl.clearCalls(); + _window->clearLayer(); + QApplication::processEvents(); + const auto* call = _sessionState._commandHookImpl.lastCallOf("clearLayer"); + ASSERT_NE(call, nullptr) << "clearLayer should have been called"; + EXPECT_EQ(call->args[0], layerId) << "clearLayer should target the selected layer"; +} + +TEST_F(LayerEditorTestFixture, ContextMenu_SaveEdits_DoesNotCrash) +{ + selectRow(firstSublayerIndex()); + _sessionState._saveLayerCallCount = 0; + // saveEdits on an anonymous layer routes through the save-layer UI hook + // (SessionState::saveLayerUI), not the command hook. The stub cancels the + // dialog, so the only observable effect is that the hook was invoked. + EXPECT_NO_THROW({ + _window->saveEdits(); + QApplication::processEvents(); + }); + EXPECT_GT(_sessionState._saveLayerCallCount, 0); +} + +TEST_F(LayerEditorTestFixture, ContextMenu_MergeWithSublayers_BlockedWhenNoSublayers) +{ + // A leaf sublayer has no children — mergeWithSublayers should be a no-op. + // mergeWithSublayers dispatches flattenLayer (not stitchLayers), so its + // absence is what proves the merge was blocked. + selectRow(firstSublayerIndex()); + _sessionState._commandHookImpl.clearCalls(); + _window->mergeWithSublayers(); + QApplication::processEvents(); + EXPECT_FALSE(_sessionState._commandHookImpl.hasCall("flattenLayer")); +} + +TEST_F(LayerEditorTestFixture, ContextMenu_MergeWithSublayers_BlockedWhenLayerIsLocked) +{ + auto* rootItem = dynamic_cast( + treeModel()->itemFromIndex(rootLayerIndex())); + ASSERT_NE(rootItem, nullptr); + TestUtils::lockLayerDirect(rootItem->layer()); + + selectRow(rootLayerIndex()); + _sessionState._commandHookImpl.clearCalls(); + _window->mergeWithSublayers(); + QApplication::processEvents(); + EXPECT_FALSE(_sessionState._commandHookImpl.hasCall("flattenLayer")); + + TestUtils::unlockLayerDirect(rootItem->layer()); +} + +// Positive control: an unlocked layer that has sublayers must dispatch flattenLayer. +// Without this, the two "blocked" tests above would pass even if merge never worked. +TEST_F(LayerEditorTestFixture, ContextMenu_MergeWithSublayers_CallsFlattenWhenLayerHasSublayers) +{ + auto* rootItem = dynamic_cast( + treeModel()->itemFromIndex(rootLayerIndex())); + ASSERT_NE(rootItem, nullptr); + ASSERT_TRUE(rootItem->hasSubLayers()) << "Root must have a sublayer for this test"; + + selectRow(rootLayerIndex()); + _sessionState._commandHookImpl.clearCalls(); + _window->mergeWithSublayers(); + QApplication::processEvents(); + EXPECT_TRUE(_sessionState._commandHookImpl.hasCall("flattenLayer")); +} + +// Discarding edits on an anonymous layer intentionally skips the confirm dialog +// (MAYA-104336): there is no on-disk content to lose. +TEST_F(LayerEditorTestFixture, ContextMenu_DiscardEdits_SkipsConfirmForAnonymousLayer) +{ + selectRow(firstSublayerIndex()); + auto* item = dynamic_cast( + treeModel()->itemFromIndex(firstSublayerIndex())); + ASSERT_NE(item, nullptr); + ASSERT_TRUE(item->isAnonymous()); + + _modalDialogCount = 0; + _sessionState._commandHookImpl.clearCalls(); + _window->discardEdits(); + QApplication::processEvents(); + EXPECT_TRUE(_sessionState._commandHookImpl.hasCall("discardEdits")); + EXPECT_EQ(_modalDialogCount, 0) + << "anonymous layer should discard without a confirm dialog"; +} + +// ── setEditTarget guards (via model) ────────────────────────────────────────── + +TEST_F(LayerEditorTestFixture, SetEditTarget_BlockedWhenLayerIsMuted) +{ + auto* item = dynamic_cast( + treeModel()->itemFromIndex(firstSublayerIndex())); + ASSERT_NE(item, nullptr); + _sessionState.stage()->MuteLayer(item->layer()->GetIdentifier()); + QApplication::processEvents(); + + _sessionState._commandHookImpl.clearCalls(); + treeModel()->setEditTarget(item); + EXPECT_FALSE(_sessionState._commandHookImpl.hasCall("setEditTarget")); + + _sessionState.stage()->UnmuteLayer(item->layer()->GetIdentifier()); +} + +TEST_F(LayerEditorTestFixture, SetEditTarget_BlockedWhenLayerIsLocked) +{ + auto* item = dynamic_cast( + treeModel()->itemFromIndex(firstSublayerIndex())); + ASSERT_NE(item, nullptr); + TestUtils::lockLayerDirect(item->layer()); + + _sessionState._commandHookImpl.clearCalls(); + treeModel()->setEditTarget(item); + EXPECT_FALSE(_sessionState._commandHookImpl.hasCall("setEditTarget")); + + TestUtils::unlockLayerDirect(item->layer()); +} + +TEST_F(LayerEditorTestFixture, SetEditTarget_BlockedWhenLayerIsSystemLocked) +{ + auto* item = dynamic_cast( + treeModel()->itemFromIndex(firstSublayerIndex())); + ASSERT_NE(item, nullptr); + // System-lock only — SetPermissionToEdit(false) would also block setEditTarget via + // isLocked(), but this test is specifically verifying the isSystemLocked() predicate. + addSystemLockedLayer(item->layer()); + + _sessionState._commandHookImpl.clearCalls(); + treeModel()->setEditTarget(item); + EXPECT_FALSE(_sessionState._commandHookImpl.hasCall("setEditTarget")); + + removeSystemLockedLayer(item->layer()); +} + +TEST_F(LayerEditorTestFixture, SetEditTarget_AllowedForNormalSublayer) +{ + auto* item = dynamic_cast( + treeModel()->itemFromIndex(firstSublayerIndex())); + ASSERT_NE(item, nullptr); + _sessionState._commandHookImpl.clearCalls(); + treeModel()->setEditTarget(item); + EXPECT_TRUE(_sessionState._commandHookImpl.hasCall("setEditTarget")); +} + +// ── discardEdits confirm path ───────────────────────────────────────────────── +// The confirm dialog fires only for layers that are both non-anonymous AND dirty. +// This fixture injects such a layer into the stage after widget construction. + +class DiscardConfirmFixture : public LayerEditorTestFixture +{ +protected: + QString _filePath; + PXR_NS::SdfLayerRefPtr _fileLayer; + + void SetUp() override + { + LayerEditorTestFixture::SetUp(); + + _filePath = QDir::tempPath() + "/le_discard_confirm_test.usda"; + QFile::remove(_filePath); + _fileLayer = PXR_NS::SdfLayer::CreateNew(_filePath.toStdString()); + _fileLayer->SetComment("dirty content"); // non-anonymous + dirty → triggers confirm + _sessionState.stage()->GetRootLayer()->InsertSubLayerPath( + _fileLayer->GetIdentifier(), 0); + QApplication::processEvents(); + } + + void TearDown() override + { + LayerEditorTestFixture::TearDown(); + QFile::remove(_filePath); + } + + // The injected file layer becomes the new index-0 child of root. + QModelIndex fileLayerIndex() + { + return treeModel()->index(0, 0, rootLayerIndex()); + } +}; + +// When the user confirms (answer = true), discardEdits runs and the hook is called. +TEST_F(DiscardConfirmFixture, ContextMenu_DiscardEdits_ConfirmAccepted_CallsHook) +{ + selectRow(fileLayerIndex()); + auto* item = dynamic_cast( + treeModel()->itemFromIndex(fileLayerIndex())); + ASSERT_NE(item, nullptr); + ASSERT_FALSE(item->isAnonymous()) << "file layer must be non-anonymous to trigger confirm"; + + _modalDialogAnswer = true; + _modalDialogCount = 0; + _sessionState._commandHookImpl.clearCalls(); + _window->discardEdits(); + QApplication::processEvents(); + + EXPECT_GT(_modalDialogCount, 0) << "confirm dialog should have been shown"; + EXPECT_TRUE(_sessionState._commandHookImpl.hasCall("discardEdits")) + << "discardEdits should be called when the user confirms"; +} + +// When the user cancels (answer = false), discardEdits is NOT called. +TEST_F(DiscardConfirmFixture, ContextMenu_DiscardEdits_ConfirmRejected_DoesNotCallHook) +{ + selectRow(fileLayerIndex()); + auto* item = dynamic_cast( + treeModel()->itemFromIndex(fileLayerIndex())); + ASSERT_NE(item, nullptr); + ASSERT_FALSE(item->isAnonymous()) << "file layer must be non-anonymous to trigger confirm"; + + _modalDialogAnswer = false; + _modalDialogCount = 0; + _sessionState._commandHookImpl.clearCalls(); + _window->discardEdits(); + QApplication::processEvents(); + + EXPECT_GT(_modalDialogCount, 0) << "confirm dialog should have been shown"; + EXPECT_FALSE(_sessionState._commandHookImpl.hasCall("discardEdits")) + << "discardEdits should NOT be called when the user cancels"; +} + +} // namespace UsdLayerEditor diff --git a/test/lib/usdLayerEditor/cpp/testDCCFunctions.cpp b/test/lib/usdLayerEditor/cpp/testDCCFunctions.cpp new file mode 100644 index 0000000000..84cb2b1290 --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/testDCCFunctions.cpp @@ -0,0 +1,247 @@ +// +// 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 "scopedLayerEditorDCCFunctions.h" + +#include + +#include +#include +#include + +#include + +#include + +#include + +using namespace UsdLayerEditor; + +// mainWindowParent() returns null when unset, the registered widget when set. +TEST(LayerEditorDCCFunctions, MainWindowParent_DefaultsToNull) +{ + ScopedLayerEditorDCCFunctions guard; + setEnvironmentFns(EnvironmentFns {}); + EXPECT_EQ(mainWindowParent(), nullptr); +} + +TEST(LayerEditorDCCFunctions, MainWindowParent_ReturnsRegisteredWidget) +{ + ScopedLayerEditorDCCFunctions guard; + QWidget w; + EnvironmentFns env; + env.mainWindowParent = [&w]() { return &w; }; + setEnvironmentFns(env); + EXPECT_EQ(mainWindowParent(), &w); +} + +// layer-contents size limits default to 8, return the registered values when set. +TEST(LayerEditorDCCFunctions, LayerContentsLimits_DefaultToEight) +{ + ScopedLayerEditorDCCFunctions guard; + setEnvironmentFns(EnvironmentFns {}); + EXPECT_EQ(layerContentsArraySizeLimit(), 8); + EXPECT_EQ(layerContentsTimeSamplesSizeLimit(), 8); +} + +TEST(LayerEditorDCCFunctions, LayerContentsLimits_ReturnRegisteredValues) +{ + ScopedLayerEditorDCCFunctions guard; + EnvironmentFns env; + env.layerContentsArraySizeLimit = []() -> int64_t { return 3; }; + env.layerContentsTimeSamplesSizeLimit = []() -> int64_t { return 5; }; + setEnvironmentFns(env); + EXPECT_EQ(layerContentsArraySizeLimit(), 3); + EXPECT_EQ(layerContentsTimeSamplesSizeLimit(), 5); +} + +// captureSessionLayer grabs a proxy's current session layer so its opinions can be +// transferred onto the recreated stage after a rename/repath; returns null when unset. +TEST(LayerEditorDCCFunctions, CaptureSessionLayer_NullByDefault) +{ + ScopedLayerEditorDCCFunctions guard; + setSerializationFns(SerializationFns {}); + EXPECT_FALSE(captureSessionLayer("|x")); +} + +TEST(LayerEditorDCCFunctions, TransferSessionLayer_DispatchesWhenRegistered) +{ + ScopedLayerEditorDCCFunctions guard; + PXR_NS::SdfLayerRefPtr seenSrc; + std::string seenDst; + SerializationFns fns; + fns.transferSessionLayer + = [&](const PXR_NS::SdfLayerRefPtr& src, const std::string& dst) { seenSrc = src; seenDst = dst; }; + setSerializationFns(fns); + auto layer = PXR_NS::SdfLayer::CreateAnonymous("xfer"); + transferSessionLayer(layer, "|newProxy"); + EXPECT_EQ(seenSrc, layer); + EXPECT_EQ(seenDst, "|newProxy"); +} + +// ── FileSystemFns ────────────────────────────────────────────────────────── + +TEST(LayerEditorDCCFunctions, GetDCCSceneDir_DefaultsToEmpty) +{ + ScopedLayerEditorDCCFunctions guard; + setFileSystemFns(FileSystemFns{}); + EXPECT_EQ(getDCCSceneDir(), std::string{}); +} + +TEST(LayerEditorDCCFunctions, GetDCCSceneDir_ReturnsRegisteredValue) +{ + ScopedLayerEditorDCCFunctions guard; + FileSystemFns fns; + fns.getDCCSceneDir = []() { return std::string("/scene/dir"); }; + setFileSystemFns(fns); + EXPECT_EQ(getDCCSceneDir(), "/scene/dir"); +} + +TEST(LayerEditorDCCFunctions, GetDCCWorkspaceScenesDir_DefaultsToEmpty) +{ + ScopedLayerEditorDCCFunctions guard; + setFileSystemFns(FileSystemFns{}); + EXPECT_EQ(getDCCWorkspaceScenesDir(), std::string{}); +} + +TEST(LayerEditorDCCFunctions, GetDCCWorkspaceScenesDir_ReturnsRegisteredValue) +{ + ScopedLayerEditorDCCFunctions guard; + FileSystemFns fns; + fns.getDCCWorkspaceScenesDir = []() { return std::string("/workspace/scenes"); }; + setFileSystemFns(fns); + EXPECT_EQ(getDCCWorkspaceScenesDir(), "/workspace/scenes"); +} + +TEST(LayerEditorDCCFunctions, PrepareLayerSaveUILayer_DefaultsToTrue) +{ + ScopedLayerEditorDCCFunctions guard; + setFileSystemFns(FileSystemFns{}); + EXPECT_TRUE(prepareLayerSaveUILayer("/some/dir")); +} + +TEST(LayerEditorDCCFunctions, PrepareLayerSaveUILayer_DispatchesWhenRegistered) +{ + ScopedLayerEditorDCCFunctions guard; + std::string seenAnchor; + FileSystemFns fns; + fns.prepareLayerSaveUILayer = [&](const std::string& anchor) -> bool { + seenAnchor = anchor; + return false; + }; + setFileSystemFns(fns); + EXPECT_FALSE(prepareLayerSaveUILayer("/my/anchor")); + EXPECT_EQ(seenAnchor, "/my/anchor"); +} + +TEST(LayerEditorDCCFunctions, CheckWriteAccess_DefaultsToFalse) +{ + ScopedLayerEditorDCCFunctions guard; + setFileSystemFns(FileSystemFns{}); + EXPECT_FALSE(checkWriteAccess("/tmp/test.usd")); +} + +TEST(LayerEditorDCCFunctions, CheckWriteAccess_DispatchesWhenRegistered) +{ + ScopedLayerEditorDCCFunctions guard; + std::string seenPath; + FileSystemFns fns; + fns.checkWriteAccess = [&](const std::string& path) -> bool { + seenPath = path; + return true; + }; + setFileSystemFns(fns); + EXPECT_TRUE(checkWriteAccess("/tmp/layer.usd")); + EXPECT_EQ(seenPath, "/tmp/layer.usd"); +} + +// ── SerializationFns ──────────────────────────────────────────────────────── + +TEST(LayerEditorDCCFunctions, GetStageCaches_DefaultsToUtilsStageCache) +{ + ScopedLayerEditorDCCFunctions guard; + setSerializationFns(SerializationFns{}); + auto caches = getStageCaches(); + ASSERT_EQ(caches.size(), 1u); + EXPECT_EQ(caches[0], &PXR_NS::UsdUtilsStageCache::Get()); +} + +TEST(LayerEditorDCCFunctions, GetStageCaches_ReturnsRegisteredCaches) +{ + ScopedLayerEditorDCCFunctions guard; + PXR_NS::UsdStageCache extra; + SerializationFns fns; + fns.getStageCaches = [&]() { + return std::vector{ &extra }; + }; + setSerializationFns(fns); + auto caches = getStageCaches(); + ASSERT_EQ(caches.size(), 1u); + EXPECT_EQ(caches[0], &extra); +} + +TEST(LayerEditorDCCFunctions, GetAllStages_ReturnsRegisteredStages) +{ + ScopedLayerEditorDCCFunctions guard; + auto stage = PXR_NS::UsdStage::CreateInMemory(); + SerializationFns fns; + fns.getAllStages = [&]() { return std::vector { stage }; }; + setSerializationFns(fns); + auto stages = getAllStages(); + ASSERT_EQ(stages.size(), 1u); + EXPECT_EQ(stages[0], stage); +} + +TEST(LayerEditorDCCFunctions, SetLayerUpAxisAndUnits_DispatchesWhenRegistered) +{ + ScopedLayerEditorDCCFunctions guard; + PXR_NS::SdfLayerRefPtr seenLayer; + SerializationFns fns; + fns.setLayerUpAxisAndUnits = [&](const PXR_NS::SdfLayerRefPtr& l) { seenLayer = l; }; + setSerializationFns(fns); + auto layer = PXR_NS::SdfLayer::CreateAnonymous("upaxis"); + setLayerUpAxisAndUnits(layer); + EXPECT_EQ(seenLayer, layer); +} + +TEST(LayerEditorDCCFunctions, UpdateDCCObjectRootLayer_DispatchesWhenRegistered) +{ + ScopedLayerEditorDCCFunctions guard; + std::string seenProxy, seenPath; + bool seenTarget = false; + DccObjectRootLayerPathMode seenMode = DccObjectRootLayerPathMode::ForceAbsolute; + SerializationFns fns; + fns.updateDCCObjectRootLayer + = [&](const std::string& proxy, + const std::string& path, + const PXR_NS::SdfLayerRefPtr&, + bool isTarget, + DccObjectRootLayerPathMode mode) { + seenProxy = proxy; + seenPath = path; + seenTarget = isTarget; + seenMode = mode; + }; + setSerializationFns(fns); + updateDCCObjectRootLayer("|proxy", "/tmp/new.usd", nullptr, true); + EXPECT_EQ(seenProxy, "|proxy"); + EXPECT_EQ(seenPath, "/tmp/new.usd"); + EXPECT_TRUE(seenTarget); + EXPECT_EQ(seenMode, DccObjectRootLayerPathMode::FollowPreference); // default when omitted + + updateDCCObjectRootLayer( + "|proxy", "/tmp/new.usd", nullptr, false, DccObjectRootLayerPathMode::ForceAbsolute); + EXPECT_EQ(seenMode, DccObjectRootLayerPathMode::ForceAbsolute); +} diff --git a/test/lib/usdLayerEditor/cpp/testEFMode.cpp b/test/lib/usdLayerEditor/cpp/testEFMode.cpp new file mode 100644 index 0000000000..e2d6a9fe85 --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/testEFMode.cpp @@ -0,0 +1,134 @@ +// +// 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. +// +#pragma once + +#include +#include "testUtils.h" + +#include "stringResources.h" + +#include +#include +#include + +namespace UsdLayerEditor { + +// ── Fixture that enables EF support on the stub session state ────────────── +// Sets _supportsEditForwarding before widget construction so the EF toggle +// button is created by setupLayout_toolbar(). +class LayerEditorWithEFFixture : public LayerEditorTestFixture +{ +protected: + void SetUp() override + { + setEditForwardingSupported(true); + LayerEditorTestFixture::SetUp(); + } +}; + +// ── Tests using the standard fixture (no EF support) ────────────────────── + +// new editor gates the button on supportsEditForwarding() at runtime, +// so with the stub (returns false) the button must be absent. The old editor +// uses a compile-time #ifdef guard instead, so this assertion doesn't apply there. +TEST_F(LayerEditorTestFixture, EFMode_ToggleButton_AbsentWhenNotSupported) +{ +#ifdef MAYAUSD_OLD_LAYER_EDITOR + // Old editor creates the button at compile time (WANT_ADSK_USD_EDIT_FORWARD_BUILD); + // supportsEditForwarding() is not consulted. Verify the button IS present instead. + QPushButton* btn = TestUtils::findButtonByObjectName(_widget, "LayerEditorToggleEFButton"); + if (!btn) { + GTEST_SKIP() << "Old editor test built without WANT_ADSK_USD_EDIT_FORWARD_BUILD"; + } + EXPECT_NE(btn, nullptr); +#else + QPushButton* btn = TestUtils::findButtonByObjectName(_widget, "LayerEditorToggleEFButton"); + EXPECT_EQ(btn, nullptr); +#endif +} + +// isEditForwardMode() default must be false. +// Guarded: old editor only exposes isEditForwardMode() under WANT_ADSK_USD_EDIT_FORWARD_BUILD. +#ifdef WANT_ADSK_USD_EDIT_FORWARD_BUILD +TEST_F(LayerEditorTestFixture, EFMode_IsEditForwardMode_FalseByDefault) +{ + EXPECT_FALSE(_sessionState.isEditForwardMode()); +} +#endif + +// effectiveTargetLayer() must equal targetLayer() when EF is off. +TEST_F(LayerEditorTestFixture, EFMode_EffectiveTargetLayer_EqualsTargetLayerByDefault) +{ + auto target = _sessionState.targetLayer(); + auto effective = _sessionState.effectiveTargetLayer(); + EXPECT_EQ(target, effective); +} + +// ── Tests using LayerEditorWithEFFixture (EF support enabled) ───────────── + +// the button tooltip must match the kToggleEditForwarding string resource. +// Guarded: kToggleEditForwarding only exists under WANT_ADSK_USD_EDIT_FORWARD_BUILD. +#ifdef WANT_ADSK_USD_EDIT_FORWARD_BUILD +TEST_F(LayerEditorWithEFFixture, EFMode_Button_Tooltip) +{ + QPushButton* btn = TestUtils::findButtonByObjectName(_widget, "LayerEditorToggleEFButton"); + ASSERT_NE(btn, nullptr); + EXPECT_EQ(btn->toolTip(), + StringResources::getAsQString(StringResources::kToggleEditForwarding)); +} +#endif + +// updateButtons() sets the button stylesheet to reflect EF active state. +// The icon switches between ef_default (off) and ef_on (on) via background-image. +// New editor only: the old editor lacks the editForwardingChanged→updateButtons connection. +#if defined(WANT_ADSK_USD_EDIT_FORWARD_BUILD) && !defined(MAYAUSD_OLD_LAYER_EDITOR) +TEST_F(LayerEditorWithEFFixture, EFMode_Button_IconReflectsActiveState) +{ + QPushButton* btn = TestUtils::findButtonByObjectName(_widget, "LayerEditorToggleEFButton"); + ASSERT_NE(btn, nullptr); + + // Initial state: EF off → stylesheet must reference ef_default. + QApplication::processEvents(); + EXPECT_TRUE(btn->styleSheet().contains("ef_default")) + << "Expected ef_default icon when EF is off"; + + // Activate EF → stylesheet must switch to ef_on. + _sessionState.setIsEditForwardMode(true); + QApplication::processEvents(); + EXPECT_TRUE(btn->styleSheet().contains("ef_on")) + << "Expected ef_on icon when EF is active"; + + // Deactivate → back to ef_default. + _sessionState.setIsEditForwardMode(false); + QApplication::processEvents(); + EXPECT_TRUE(btn->styleSheet().contains("ef_default")) + << "Expected ef_default icon after EF deactivated"; +} +#endif + +// With EF support enabled before construction, the new editor creates the EF toggle +// button and the EF option-menu items — both gated purely at runtime on +// supportsEditForwarding(), with no compile-time guard. This is unguarded (unlike the +// tooltip/icon tests above, which reference EF-build-only string resources). +#ifndef MAYAUSD_OLD_LAYER_EDITOR +TEST_F(LayerEditorWithEFFixture, EFMode_ToggleButton_CreatedWhenSupported) +{ + QPushButton* btn = TestUtils::findButtonByObjectName(_widget, "LayerEditorToggleEFButton"); + EXPECT_NE(btn, nullptr) << "EF toggle button should be created when EF is supported"; +} +#endif + +} // namespace UsdLayerEditor diff --git a/test/lib/usdLayerEditor/cpp/testFileSystemUtils.cpp b/test/lib/usdLayerEditor/cpp/testFileSystemUtils.cpp new file mode 100644 index 0000000000..c359815abe --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/testFileSystemUtils.cpp @@ -0,0 +1,378 @@ +// 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 "utilFileSystem.h" + +#include + +#include + +#include + +#include +#include +#include + +namespace UsdLayerEditor { +namespace FileSystem { + +// --- getDir ------------------------------------------------------------------- + +TEST(FileSystemUtils, GetDir_ReturnsParentDirectory) +{ + namespace fss = fs::filesystem; + // Construct platform-native paths for reliable parent_path() behaviour. + const fss::path dir = fss::temp_directory_path() / "le_test_dir"; + const fss::path file = dir / "baz.usd"; + EXPECT_EQ(getDir(file.string()), dir.string()); +} + +TEST(FileSystemUtils, GetDir_ReturnsEmptyForFilenameOnly) +{ + EXPECT_EQ(getDir("baz.usd"), ""); +} + +// --- appendPaths -------------------------------------------------------------- + +TEST(FileSystemUtils, AppendPaths_JoinsWithSeparator) +{ + namespace fss = fs::filesystem; + // appendPaths uses native separators, so build the expected value the same way. + const std::string expected = (fss::path("foo") / "bar.usd").string(); + EXPECT_EQ(appendPaths("foo", "bar.usd"), expected); +} + +TEST(FileSystemUtils, AppendPaths_AbsoluteBasePreserved) +{ + namespace fss = fs::filesystem; + const std::string expected = (fss::path("/root/dir") / "layer.usd").string(); + EXPECT_EQ(appendPaths("/root/dir", "layer.usd"), expected); +} + +// --- pathStripPath ------------------------------------------------------------ + +TEST(FileSystemUtils, PathStripPath_RemovesDirectory) +{ + namespace fss = fs::filesystem; + std::string path = (fss::temp_directory_path() / "subdir" / "baz.usd").string(); + pathStripPath(path); + EXPECT_EQ(path, "baz.usd"); +} + +TEST(FileSystemUtils, PathStripPath_FilenameAloneUnchanged) +{ + std::string path = "baz.usd"; + pathStripPath(path); + EXPECT_EQ(path, "baz.usd"); +} + +// --- pathRemoveExtension ------------------------------------------------------ + +TEST(FileSystemUtils, PathRemoveExtension_StripsExtension) +{ + namespace fss = fs::filesystem; + const fss::path dir = fss::temp_directory_path() / "le_dir"; + const std::string ext = (dir / "baz.usd").string(); + const std::string noext = (dir / "baz").string(); + std::string path = ext; + pathRemoveExtension(path); + EXPECT_EQ(path, noext); +} + +TEST(FileSystemUtils, PathRemoveExtension_NoOpWhenNoExtension) +{ + std::string path = "baz"; + std::string expected = path; + pathRemoveExtension(path); + EXPECT_EQ(path, expected); +} + +// --- pathFindExtension -------------------------------------------------------- + +TEST(FileSystemUtils, PathFindExtension_IncludesDot) +{ + std::string path = "baz.usd"; + EXPECT_EQ(pathFindExtension(path), ".usd"); +} + +TEST(FileSystemUtils, PathFindExtension_ReturnsEmptyForNoExtension) +{ + std::string path = "baz"; + EXPECT_EQ(pathFindExtension(path), ""); +} + +// --- getNumberSuffixPosition -------------------------------------------------- + +TEST(FileSystemUtils, GetNumberSuffixPosition_LocatesTrailingDigit) +{ + EXPECT_EQ(getNumberSuffixPosition("layer_1"), 6u); +} + +TEST(FileSystemUtils, GetNumberSuffixPosition_LocatesMultiDigitSuffix) +{ + EXPECT_EQ(getNumberSuffixPosition("layer_123"), 6u); +} + +TEST(FileSystemUtils, GetNumberSuffixPosition_ReturnsLengthWhenNoTrailingDigits) +{ + // "layer" has 5 chars; no trailing digit → suffix starts at end (position 5) + EXPECT_EQ(getNumberSuffixPosition("layer"), 5u); +} + +TEST(FileSystemUtils, GetNumberSuffixPosition_SingleCharSuffix) +{ + // "a9" → suffix at position 1 + EXPECT_EQ(getNumberSuffixPosition("a9"), 1u); +} + +// --- getNumberSuffix ---------------------------------------------------------- + +TEST(FileSystemUtils, GetNumberSuffix_ExtractsTrailingDigits) +{ + EXPECT_EQ(getNumberSuffix("layer_123"), "123"); +} + +TEST(FileSystemUtils, GetNumberSuffix_ReturnsEmptyWhenNoDigits) +{ + EXPECT_EQ(getNumberSuffix("layer"), ""); +} + +// --- increaseNumberSuffix ----------------------------------------------------- + +TEST(FileSystemUtils, IncreaseNumberSuffix_IncrementsTrailingDigit) +{ + EXPECT_EQ(increaseNumberSuffix("layer_1"), "layer_2"); +} + +TEST(FileSystemUtils, IncreaseNumberSuffix_HandlesRollover) +{ + EXPECT_EQ(increaseNumberSuffix("layer_9"), "layer_10"); +} + +TEST(FileSystemUtils, IncreaseNumberSuffix_AppendsOneWhenNoSuffix) +{ + EXPECT_EQ(increaseNumberSuffix("layer"), "layer1"); +} + +// --- makePathRelativeTo ------------------------------------------------------- + +TEST(FileSystemUtils, MakePathRelativeTo_ReturnsTrueAndRelativePath) +{ + // Build paths using the platform's temp dir so the paths are syntactically valid. + namespace fss = fs::filesystem; + const std::string dir = fss::temp_directory_path().generic_string(); + const std::string file = (fss::temp_directory_path() / "l.usd").generic_string(); + auto [path, ok] = makePathRelativeTo(file, dir); + EXPECT_TRUE(ok); + EXPECT_EQ(path, "l.usd"); +} + +TEST(FileSystemUtils, MakePathRelativeTo_EmptyAnchorReturnsOriginal) +{ + namespace fss = fs::filesystem; + const std::string file = (fss::temp_directory_path() / "layer.usd").generic_string(); + auto [path, ok] = makePathRelativeTo(file, ""); + EXPECT_TRUE(ok); + EXPECT_EQ(path, file); +} + +// --- getLayerFileDir ---------------------------------------------------------- + +TEST(FileSystemUtils, GetLayerFileDir_ReturnsEmptyForNullLayer) +{ + EXPECT_EQ(getLayerFileDir(PXR_NS::SdfLayerHandle()), ""); +} + +TEST(FileSystemUtils, GetLayerFileDir_ReturnsEmptyForAnonymousLayer) +{ + auto layer = PXR_NS::SdfLayer::CreateAnonymous("fileDir_test"); + // Anonymous layers have no real path, so dir is empty. + EXPECT_EQ(getLayerFileDir(layer), ""); +} + +// --- FileBackup --------------------------------------------------------------- + +namespace { +std::string tempPath(const char* filename) +{ + namespace fss = fs::filesystem; + return (fss::temp_directory_path() / filename).generic_string(); +} +} // namespace + +TEST(FileSystemUtils, FileBackup_GetBackupFilename_AppendsDotBackup) +{ + const std::string path = tempPath("le_backup_test.usd"); + const std::string backup = tempPath("le_backup_test.usd.backup"); + FileBackup fb(path); + EXPECT_EQ(fb.getBackupFilename(), backup); +} + +TEST(FileSystemUtils, FileBackup_BackedFlagFalseWhenFileAbsent) +{ + const std::string path = tempPath("le_backup_absent_12345.usd"); + std::remove(path.c_str()); + FileBackup fb(path); + EXPECT_FALSE(fb._backed); +} + +TEST(FileSystemUtils, FileBackup_DestructorRestoresFileWhenNotCommitted) +{ + const std::string path = tempPath("le_backup_restore_99.usd"); + const std::string backup = tempPath("le_backup_restore_99.usd.backup"); + std::remove(path.c_str()); + std::remove(backup.c_str()); + if (FILE* f = std::fopen(path.c_str(), "w")) { std::fclose(f); } + + { + FileBackup fb(path); + EXPECT_TRUE(fb._backed); + } + bool origExists = (std::fopen(path.c_str(), "r") != nullptr); + bool backupExists = (std::fopen(backup.c_str(), "r") != nullptr); + if (origExists) std::remove(path.c_str()); + if (backupExists) std::remove(backup.c_str()); + EXPECT_TRUE(origExists); + EXPECT_FALSE(backupExists); +} + +TEST(FileSystemUtils, FileBackup_CommitPreventsRestore) +{ + const std::string path = tempPath("le_backup_commit_99.usd"); + const std::string backup = tempPath("le_backup_commit_99.usd.backup"); + std::remove(path.c_str()); + std::remove(backup.c_str()); + if (FILE* f = std::fopen(path.c_str(), "w")) { std::fclose(f); } + + { + FileBackup fb(path); + EXPECT_TRUE(fb._backed); + fb.commit(); + } + bool origExists = (std::fopen(path.c_str(), "r") != nullptr); + bool backupExists = (std::fopen(backup.c_str(), "r") != nullptr); + if (origExists) std::remove(path.c_str()); + if (backupExists) std::remove(backup.c_str()); + EXPECT_FALSE(origExists); + EXPECT_TRUE(backupExists); +} + +// --- writeToFilePath ---------------------------------------------------------- + +TEST(FileSystemUtils, WriteToFilePath_WritesContentAndReturnsSize) +{ + const std::string path = tempPath("le_write_test.bin"); + std::remove(path.c_str()); + const char data[] = "hello usd"; + const size_t dataSize = sizeof(data) - 1; + size_t written = writeToFilePath(path.c_str(), data, dataSize); + EXPECT_EQ(written, dataSize); + + // Verify content was written to disk. + std::ifstream in(path, std::ios::binary); + ASSERT_TRUE(in.is_open()); + std::string content((std::istreambuf_iterator(in)), std::istreambuf_iterator()); + EXPECT_EQ(content, std::string(data, dataSize)); + in.close(); + std::remove(path.c_str()); +} + +TEST(FileSystemUtils, WriteToFilePath_ZeroForBadPath) +{ + size_t written = writeToFilePath("", "x", 1); + EXPECT_EQ(written, 0u); +} + +// --- pathAppendPath ----------------------------------------------------------- + +TEST(FileSystemUtils, PathAppendPath_ReturnsTrueAndAppends) +{ + namespace fss = fs::filesystem; + std::string dir = fss::temp_directory_path().string(); + bool ok = pathAppendPath(dir, "sub_file.usd"); + EXPECT_TRUE(ok); + EXPECT_NE(dir.find("sub_file.usd"), std::string::npos); +} + +TEST(FileSystemUtils, PathAppendPath_ReturnsFalseForNonExistentPath) +{ + std::string notADir = "/does/not/exist/at/all"; + bool ok = pathAppendPath(notADir, "file.usd"); + EXPECT_FALSE(ok); +} + +// --- getPathRelativeToDirectory ----------------------------------------------- + +TEST(FileSystemUtils, GetPathRelativeToDirectory_ReturnsFilename) +{ + namespace fss = fs::filesystem; + const std::string dir = fss::temp_directory_path().generic_string(); + const std::string file = (fss::temp_directory_path() / "rel_test.usd").generic_string(); + EXPECT_EQ(getPathRelativeToDirectory(file, dir), "rel_test.usd"); +} + +TEST(FileSystemUtils, GetPathRelativeToDirectory_EmptyDirReturnsFile) +{ + namespace fss = fs::filesystem; + const std::string file = (fss::temp_directory_path() / "abs.usd").generic_string(); + EXPECT_EQ(getPathRelativeToDirectory(file, ""), file); +} + +// --- getPathRelativeToLayerFile ----------------------------------------------- + +TEST(FileSystemUtils, GetPathRelativeToLayerFile_NullLayerReturnsFileName) +{ + const std::string file = "/some/file.usd"; + EXPECT_EQ(getPathRelativeToLayerFile(file, PXR_NS::SdfLayerHandle()), file); +} + +TEST(FileSystemUtils, GetPathRelativeToLayerFile_AnonymousLayerReturnsFileName) +{ + auto layer = PXR_NS::SdfLayer::CreateAnonymous("anon_rel"); + const std::string file = "/some/file.usd"; + EXPECT_EQ(getPathRelativeToLayerFile(file, layer), file); +} + +// --- getUniqueFileName -------------------------------------------------------- + +TEST(FileSystemUtils, GetUniqueFileName_ReturnsNonEmptyString) +{ + namespace fss = fs::filesystem; + const std::string dir = fss::temp_directory_path().string(); + std::string name = getUniqueFileName(dir, "layer", "usd"); + EXPECT_FALSE(name.empty()); + EXPECT_NE(name.find("layer"), std::string::npos); +} + +// --- ensureUniqueFileName ----------------------------------------------------- + +TEST(FileSystemUtils, EnsureUniqueFileName_ReturnsSamePathIfNotExists) +{ + const std::string path = tempPath("le_unique_nonexist_99999.usd"); + std::remove(path.c_str()); + EXPECT_EQ(ensureUniqueFileName(path), path); +} + +TEST(FileSystemUtils, EnsureUniqueFileName_ReturnsNewNameIfExists) +{ + const std::string path = tempPath("le_unique_exist.usd"); + if (FILE* f = std::fopen(path.c_str(), "w")) { std::fclose(f); } + std::string unique = ensureUniqueFileName(path); + std::remove(path.c_str()); + std::remove(unique.c_str()); + EXPECT_NE(unique, path); +} + +} // namespace FileSystem +} // namespace UsdLayerEditor diff --git a/test/lib/usdLayerEditor/cpp/testFixture.cpp b/test/lib/usdLayerEditor/cpp/testFixture.cpp new file mode 100644 index 0000000000..d340cdbde6 --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/testFixture.cpp @@ -0,0 +1,158 @@ +// +// 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 "testFixture.h" + +#include "layerEditorDCCFunctions.h" +#include "saveLayersDialog.h" +#include "warningDialogs.h" + +#include +#include +#include +#include + +namespace UsdLayerEditor { + +void LayerEditorTestFixture::SetUp() +{ + EditForwardingFns ef; + ef.supportsEditForwarding = [this]() { return _efSupported; }; + ef.echoEditForwarding = []() { return false; }; + ef.setEchoEditForwarding = [](bool) {}; + setEditForwardingFns(ef); + + DccObjectFns dcc; + dcc.isDccObjectStageIncoming = [this](const std::string&) { return _stageIncoming; }; + dcc.isDccObjectSharedStage = [this](const std::string&) { return _sharedStage; }; + dcc.renameObject + = [](const std::string&, const std::string& name) { return std::string("|") + name; }; + setDccObjectFns(dcc); + + SerializationFns serialization; + serialization.captureSessionLayer = [](const std::string&) { return PXR_NS::SdfLayerRefPtr {}; }; + serialization.transferSessionLayer + = [this](const PXR_NS::SdfLayerRefPtr&, const std::string&) { ++_transferSessionCalls; }; + serialization.updateDCCObjectRootLayer = [this]( + const std::string&, + const std::string&, + const PXR_NS::SdfLayerRefPtr&, + bool, + DccObjectRootLayerPathMode mode) { + if (mode == DccObjectRootLayerPathMode::ForceAbsolute) + ++_setProxyRootPathCalls; + }; + setSerializationFns(serialization); + + { + ComponentFns component; + component.isStageAComponent = [this](const std::string&) { return _isComponent; }; + component.isUnsavedComponent + = [this](const PXR_NS::UsdStageRefPtr&) { return _isUnsavedComponent; }; + component.shouldDisplayComponentInitialSaveDialog + = [](const PXR_NS::UsdStageRefPtr&, const std::string&) { return false; }; + component.saveComponent + = [this](const PXR_NS::UsdStageRefPtr&, const std::string&) { ++_saveComponentCalls; }; + component.reloadComponent = [this](const std::string&) { ++_reloadComponentCalls; }; + component.moveComponent = [this](const std::string&, const std::string&, const std::string&) { + return _moveComponentResult; + }; + setComponentFns(component); + } + + // Direct any generated save paths to the system temp dir so test-run artifacts + // are never created inside the source tree. + { + FileSystemFns fns; + fns.getDCCSceneDir = []() { return QDir::tempPath().toStdString(); }; + setFileSystemFns(fns); + } + + // Headless tests must not pop the overwrite-confirm / save-layers dialog. + // Defaulting this to false makes save paths take the non-interactive branch. + { + SaveOptionFns saveOption; + saveOption.confirmExistingFileSave = [this]() { return _confirmExistingFileSave; }; + setSaveOptionFns(saveOption); + } + + // Suppress blocking modal dialogs (confirmDialog/warningDialog) in headless + // tests; record how many would have shown and answer with _modalDialogAnswer. + setModalDialogTestHandler([this](const QString&, const QString&) { + ++_modalDialogCount; + return _modalDialogAnswer; + }); + + // The bulk save-layers dialog is modal too; never show it in headless tests. + SaveLayersDialog::setExecTestHandler([]() { return QDialog::Rejected; }); + + _mainWindow = new QMainWindow(); + _window = std::make_unique(_sessionState, _mainWindow); + _widget = _window->widget(); + _mainWindow->show(); + _widget->show(); + QApplication::processEvents(); + _sessionState._commandHookImpl.clearCalls(); + _sessionState._saveLayerCallCount = 0; + _sessionState._printLayerCallCount = 0; + _sessionState._loadLayersCallCount = 0; +} + +void LayerEditorTestFixture::TearDown() +{ + setModalDialogTestHandler(nullptr); + SaveLayersDialog::setExecTestHandler(nullptr); + _widget = nullptr; + _window.reset(); + delete _mainWindow; + _mainWindow = nullptr; +} + +LayerTreeView* LayerEditorTestFixture::layerTree() +{ + return _widget->layerTree(); +} + +LayerTreeModel* LayerEditorTestFixture::treeModel() +{ + return layerTree()->layerTreeModel(); +} + +QModelIndex LayerEditorTestFixture::sessionLayerIndex() +{ + // The stub always shows the session layer (autoHideSessionLayer=false). + // It is always the first top-level item in the tree. + return treeModel()->index(0, 0); +} + +QModelIndex LayerEditorTestFixture::rootLayerIndex() +{ + return treeModel()->rootLayerIndex(); +} + +QModelIndex LayerEditorTestFixture::firstSublayerIndex() +{ + return treeModel()->index(0, 0, rootLayerIndex()); +} + +void LayerEditorTestFixture::selectRow(const QModelIndex& index) +{ + layerTree()->selectionModel()->select(index, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); + layerTree()->setCurrentIndex(index); + QApplication::processEvents(); +} + +} // namespace UsdLayerEditor diff --git a/test/lib/usdLayerEditor/cpp/testFixture.h b/test/lib/usdLayerEditor/cpp/testFixture.h new file mode 100644 index 0000000000..f67cda66ce --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/testFixture.h @@ -0,0 +1,90 @@ +// +// 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. +// +#pragma once + +#include "scopedLayerEditorDCCFunctions.h" +#include "stubCommandHook.h" +#include "stubLayerEditorWindow.h" +#include "stubSessionState.h" + +#include "layerEditorWidget.h" +#include "layerTreeModel.h" +#include "layerTreeView.h" + +#include + +#include +#include +#include +#include +#include + +namespace UsdLayerEditor { + +class LayerEditorTestFixture : public ::testing::Test +{ +protected: + void SetUp() override; + void TearDown() override; + + LayerTreeView* layerTree(); + LayerTreeModel* treeModel(); + QModelIndex sessionLayerIndex(); + QModelIndex rootLayerIndex(); + QModelIndex firstSublayerIndex(); + + // Select a single tree row so LayerEditorWindow state queries are valid. + void selectRow(const QModelIndex& index); + + StubSessionState _sessionState; + std::unique_ptr _window; + QMainWindow* _mainWindow { nullptr }; + + // Convenience: the widget owned by _window. + LayerEditorWidget* _widget { nullptr }; + + // DCC-function registry driven by these flags (installed in SetUp, + // restored in TearDown). Lambdas read the flags at call time, so flips + // mid-test take effect on the next model rebuild. + bool _efSupported { false }; + bool _sharedStage { false }; + bool _stageIncoming { false }; + + bool _isComponent { false }; + bool _isUnsavedComponent { false }; + std::string _moveComponentResult; // non-empty => move "succeeds" + int _saveComponentCalls { 0 }; + int _reloadComponentCalls { 0 }; + int _transferSessionCalls { 0 }; + int _setProxyRootPathCalls { 0 }; + + // Modal dialogs (confirmDialog/warningDialog) are suppressed during tests via + // a handler installed in SetUp; this counts how many would have been shown, + // and _modalDialogAnswer is the value the suppressed dialog returns. + int _modalDialogCount { 0 }; + bool _modalDialogAnswer { true }; + bool _confirmExistingFileSave { false }; + + ScopedLayerEditorDCCFunctions _scopedDCCFunctions; + + void setEditForwardingSupported(bool supported) { _efSupported = supported; } + void setSharedStage(bool shared) { _sharedStage = shared; } + void setStageIncoming(bool incoming) { _stageIncoming = incoming; } + void setIsComponent(bool v) { _isComponent = v; } + void setIsUnsavedComponent(bool v) { _isUnsavedComponent = v; } +}; + +} // namespace UsdLayerEditor diff --git a/test/lib/usdLayerEditor/cpp/testLayerContentsWidget.cpp b/test/lib/usdLayerEditor/cpp/testLayerContentsWidget.cpp new file mode 100644 index 0000000000..2818da0769 --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/testLayerContentsWidget.cpp @@ -0,0 +1,143 @@ +// 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. +// +#pragma once + +#include +#include "layerContentsWidget.h" +#include "layerTreeItem.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +PXR_NAMESPACE_USING_DIRECTIVE + +namespace UsdLayerEditor { + +// Locate the LayerContentsWidget inside the LayerEditorWidget. +static LayerContentsWidget* findContentsWidget(QWidget* root) +{ + return root->findChild( + QString(), Qt::FindChildrenRecursively); +} + +class LayerContentsWidgetTest : public LayerEditorTestFixture {}; + +TEST_F(LayerContentsWidgetTest, ContentsWidget_ExistsInLayout) +{ + auto* cw = findContentsWidget(_widget); + EXPECT_NE(cw, nullptr); +} + +TEST_F(LayerContentsWidgetTest, IsEmpty_TrueByDefault) +{ + auto* cw = findContentsWidget(_widget); + ASSERT_NE(cw, nullptr); + EXPECT_TRUE(cw->isEmpty()); +} + +TEST_F(LayerContentsWidgetTest, SetLayer_SetsIsEmptyFalseForLayerWithContent) +{ + auto* cw = findContentsWidget(_widget); + ASSERT_NE(cw, nullptr); + + auto* item = dynamic_cast( + treeModel()->itemFromIndex(rootLayerIndex())); + ASSERT_NE(item, nullptr); + item->layer()->SetComment("test content"); + + cw->setLayer(item->layer()); + QApplication::processEvents(); + EXPECT_FALSE(cw->isEmpty()); +} + +TEST_F(LayerContentsWidgetTest, Clear_SetsIsEmptyTrue) +{ + auto* cw = findContentsWidget(_widget); + ASSERT_NE(cw, nullptr); + + auto* item = dynamic_cast( + treeModel()->itemFromIndex(rootLayerIndex())); + ASSERT_NE(item, nullptr); + cw->setLayer(item->layer()); + QApplication::processEvents(); + + cw->clear(); + EXPECT_TRUE(cw->isEmpty()); +} + +TEST_F(LayerContentsWidgetTest, SetLayer_WithNullLayer_IsEmpty) +{ + auto* cw = findContentsWidget(_widget); + ASSERT_NE(cw, nullptr); + cw->setLayer(nullptr); + QApplication::processEvents(); + EXPECT_TRUE(cw->isEmpty()); +} + +// The array size limit (from the DCC registry) is applied when rendering layer +// contents: a small limit truncates the displayed array. +#ifndef MAYAUSD_OLD_LAYER_EDITOR +TEST_F(LayerContentsWidgetTest, SetLayer_RespectsArraySizeLimit) +{ + auto* cw = findContentsWidget(_widget); + ASSERT_NE(cw, nullptr); + auto* textEdit = cw->findChild(QString(), Qt::FindChildrenRecursively); + ASSERT_NE(textEdit, nullptr); + + // Build a layer with a large array-valued attribute. + auto stage = PXR_NS::UsdStage::CreateInMemory(); + auto prim = stage->DefinePrim(PXR_NS::SdfPath("/Test")); + auto attr = prim.CreateAttribute( + PXR_NS::TfToken("arr"), PXR_NS::SdfValueTypeNames->IntArray); + PXR_NS::VtIntArray values(100); + for (int i = 0; i < 100; ++i) + values[i] = i; + attr.Set(values); + auto layer = stage->GetRootLayer(); + + ScopedLayerEditorDCCFunctions guard; + + // Override only the array-size getter, preserving any other environment functions. + EnvironmentFns smallEnv = layerEditorDCCFunctions().environment; + smallEnv.layerContentsArraySizeLimit = []() -> int64_t { return 2; }; + setEnvironmentFns(smallEnv); + cw->setLayer(layer); + QApplication::processEvents(); + const int smallLen = textEdit->toPlainText().length(); + + EnvironmentFns largeEnv = layerEditorDCCFunctions().environment; + largeEnv.layerContentsArraySizeLimit = []() -> int64_t { return 1000; }; + setEnvironmentFns(largeEnv); + cw->setLayer(layer); + QApplication::processEvents(); + const int largeLen = textEdit->toPlainText().length(); + + EXPECT_LT(smallLen, largeLen) + << "a smaller array size limit should truncate the displayed array, " + "yielding shorter output"; +} +#endif + +} // namespace UsdLayerEditor diff --git a/test/lib/usdLayerEditor/cpp/testLayerEditorCommands.cpp b/test/lib/usdLayerEditor/cpp/testLayerEditorCommands.cpp new file mode 100644 index 0000000000..cf07abae38 --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/testLayerEditorCommands.cpp @@ -0,0 +1,1045 @@ +// 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 "testUtils.h" +#include "LayerEditorCommands.h" +#include "layerLocking.h" +#include "layerMuting.h" +#include "layerEditorDCCFunctions.h" + +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include + +namespace UsdLayerEditor { + +namespace { + +// Stub stage-path accessor: returns an empty UFE path for any stage. +// This is sufficient for tests that don't inspect the path value. +Ufe::Path stubStagePathAccessor(PXR_NS::UsdStageWeakPtr /*stage*/) +{ + return Ufe::Path(); +} + +} // namespace + +class UpdateEditTargetTest : public ::testing::Test +{ +protected: + void SetUp() override + { + // Register a no-op stage-path accessor so that MuteLayerCmd::saveSelection() + // can call UsdUfe::stagePath() without crashing in this test context. + UsdUfe::setStagePathAccessorFn(stubStagePathAccessor); + + // Initialize the UFE global selection singleton if not already done. + // MuteLayerCmd::saveSelection() calls Ufe::GlobalSelection::get(). + if (!Ufe::GlobalSelection::get()) { + Ufe::GlobalSelection::initializeInstance( + std::make_shared()); + } + + forgetLockedLayers(); + _stage = PXR_NS::UsdStage::CreateInMemory(); + _subLayer = PXR_NS::SdfLayer::CreateAnonymous("sub"); + _stage->GetRootLayer()->InsertSubLayerPath(_subLayer->GetIdentifier(), 0); + } + + void TearDown() override + { + setEditForwardingFns(EditForwardingFns {}); + forgetLockedLayers(); + } + + PXR_NS::UsdStageRefPtr _stage; + PXR_NS::SdfLayerRefPtr _subLayer; +}; + +// When all layers are non-modifiable, updateEditTarget should switch to session layer. +TEST_F(UpdateEditTargetTest, WhenNoModifiableLayers_EditTargetChangesToSessionLayer) +{ + // Lock all non-session layers so nothing is modifiable. + lockLayer("", _stage->GetRootLayer(), LayerLock_Locked, /*updateDCCAttr=*/false); + lockLayer("", _subLayer, LayerLock_Locked, /*updateDCCAttr=*/false); + _stage->SetEditTarget(_stage->GetRootLayer()); + + // Mute the sublayer — this calls updateEditTarget() internally. + auto cmd = std::make_shared(_stage, _subLayer, /*muteIt=*/true); + cmd->execute(); + + EXPECT_EQ(_stage->GetEditTarget().GetLayer(), _stage->GetSessionLayer()); +} + +// Muting the current edit-target layer does not retarget: updateEditTarget() only retargets +// when the target is locked, so the muted layer stays the edit target. Documents current behavior. +TEST_F(UpdateEditTargetTest, UpdateEditTarget_MutingEditTargetLayer_LeavesTargetUnchanged) +{ + _stage->SetEditTarget(_subLayer); + ASSERT_EQ(_stage->GetEditTarget().GetLayer(), _subLayer); + + auto cmd = std::make_shared(_stage, _subLayer, /*muteIt=*/true); + cmd->execute(); + + EXPECT_TRUE(_stage->IsLayerMuted(_subLayer->GetIdentifier())); + EXPECT_EQ(_stage->GetEditTarget().GetLayer(), _subLayer); +} + +// When edit forwarding handles the edit-target update, updateEditTarget should +// skip the normal auto-targeting and leave the edit target unchanged. +TEST_F(UpdateEditTargetTest, WhenEditForwardingActive_EditTargetUnchanged) +{ + EditForwardingFns ef; + ef.handleEFEditTargetUpdate = [](const PXR_NS::UsdStageRefPtr&) { return true; }; + setEditForwardingFns(ef); + + lockLayer("", _stage->GetRootLayer(), LayerLock_Locked, false); + lockLayer("", _subLayer, LayerLock_Locked, false); + _stage->SetEditTarget(_stage->GetRootLayer()); + + auto cmd = std::make_shared(_stage, _subLayer, /*muteIt=*/true); + cmd->execute(); + + // Must NOT have changed to session layer. + EXPECT_NE(_stage->GetEditTarget().GetLayer(), _stage->GetSessionLayer()); +} + +class BackupEditTargetsTest : public ::testing::Test +{ +protected: + void SetUp() override + { + // Build: root -> A -> B. Edit target = B. + _stage = PXR_NS::UsdStage::CreateInMemory(); + _layerA = PXR_NS::SdfLayer::CreateAnonymous("A"); + _layerB = PXR_NS::SdfLayer::CreateAnonymous("B"); + _stage->GetRootLayer()->InsertSubLayerPath(_layerA->GetIdentifier(), 0); + _layerA->InsertSubLayerPath(_layerB->GetIdentifier(), 0); + _stage->SetEditTarget(_layerB); + } + + void TearDown() override + { + setSerializationFns(SerializationFns {}); + } + + PXR_NS::UsdStageRefPtr _stage; + PXR_NS::SdfLayerRefPtr _layerA; + PXR_NS::SdfLayerRefPtr _layerB; +}; + +TEST_F(BackupEditTargetsTest, WithoutProvider_EditTargetNotRestoredOnUndo) +{ + // _stage is NOT in UsdUtilsStageCache — backupEditTargets won't find it. + auto cmd = std::make_shared(_layerA); + cmd->execute(); + // B is gone from the graph; USD retains the stale edit target reference. + // Without the provider, no reset to root occurred during execute (unlike WithProvider). + EXPECT_EQ(_stage->GetEditTarget().GetLayer(), _layerB); + // Undo: restores A's content (B is back) but the edit target was never backed up. + cmd->undo(); + // Without the provider, no backup/restore cycle occurred; USD simply kept the stale + // edit target pointing at B throughout (never reset, never restored via backup). + EXPECT_EQ(_stage->GetEditTarget().GetLayer(), _layerB); +} + +// With the provider registered, backupEditTargets finds _stage → saves B → restores on undo. +TEST_F(BackupEditTargetsTest, WithProvider_EditTargetRestoredOnUndo) +{ + SerializationFns fns; + fns.getAllStages = [this]() -> std::vector { return { _stage }; }; + setSerializationFns(fns); + + auto cmd = std::make_shared(_layerA); + cmd->execute(); + // At this point edit target was backed up and reset to root. + EXPECT_NE(_stage->GetEditTarget().GetLayer(), _layerB); + + cmd->undo(); + // Undo restored A's content (B is back) and restored the edit target to B. + EXPECT_EQ(_stage->GetEditTarget().GetLayer(), _layerB); +} + +class ReplaceSubPathCmdTest : public ::testing::Test +{ +protected: + void SetUp() override + { + _parent = PXR_NS::SdfLayer::CreateAnonymous("parent"); + _layerA = PXR_NS::SdfLayer::CreateAnonymous("A"); + _layerB = PXR_NS::SdfLayer::CreateAnonymous("B"); + _parent->InsertSubLayerPath(_layerA->GetIdentifier(), 0); + } + + PXR_NS::SdfLayerRefPtr _parent; + PXR_NS::SdfLayerRefPtr _layerA; + PXR_NS::SdfLayerRefPtr _layerB; +}; + +TEST_F(ReplaceSubPathCmdTest, DoIt_ReplacesOldPathWithNewPath) +{ + auto cmd = std::make_shared( + _parent, _layerA->GetIdentifier(), _layerB->GetIdentifier()); + cmd->execute(); + + const auto& paths = _parent->GetSubLayerPaths(); + EXPECT_EQ(paths.Find(_layerA->GetIdentifier()), static_cast(-1)); + EXPECT_NE(paths.Find(_layerB->GetIdentifier()), static_cast(-1)); +} + +TEST_F(ReplaceSubPathCmdTest, UndoIt_RestoresOldPath) +{ + auto cmd = std::make_shared( + _parent, _layerA->GetIdentifier(), _layerB->GetIdentifier()); + cmd->execute(); + cmd->undo(); + + const auto& paths = _parent->GetSubLayerPaths(); + EXPECT_NE(paths.Find(_layerA->GetIdentifier()), static_cast(-1)); + EXPECT_EQ(paths.Find(_layerB->GetIdentifier()), static_cast(-1)); + + cmd->redo(); + // Re-fetch: the SdfListProxy reference above may be stale after redo. + const auto& pathsAfterRedo = _parent->GetSubLayerPaths(); + EXPECT_EQ(pathsAfterRedo.Find(_layerA->GetIdentifier()), static_cast(-1)); + EXPECT_NE(pathsAfterRedo.Find(_layerB->GetIdentifier()), static_cast(-1)); +} + +TEST_F(ReplaceSubPathCmdTest, DoIt_ReturnsFalse_WhenOldPathNotFound) +{ + auto cmd = std::make_shared( + _parent, "nonexistent.usda", _layerB->GetIdentifier()); + // doIt() returns false when the old path is not found, so redo() throws. + EXPECT_THROW(cmd->execute(), std::runtime_error); + + // Nothing should have changed. + const auto& paths = _parent->GetSubLayerPaths(); + EXPECT_NE(paths.Find(_layerA->GetIdentifier()), static_cast(-1)); +} + +// DiscardEditCmd and ClearLayerCmd + +class BackupLayerCmdTest : public ::testing::Test +{ +protected: + void SetUp() override + { + SerializationFns fns; + fns.getAllStages = [this]() -> std::vector { return { _stage }; }; + setSerializationFns(fns); + _stage = PXR_NS::UsdStage::CreateInMemory(); + _layer = PXR_NS::SdfLayer::CreateAnonymous("target"); + _stage->GetRootLayer()->InsertSubLayerPath(_layer->GetIdentifier(), 0); + // Write something to the layer so it is dirty. + _layer->SetComment("original content"); + } + + void TearDown() override + { + setSerializationFns(SerializationFns {}); + } + + PXR_NS::UsdStageRefPtr _stage; + PXR_NS::SdfLayerRefPtr _layer; +}; + +TEST_F(BackupLayerCmdTest, DiscardEditCmd_DoIt_ClearsLayerContent) +{ + auto cmd = std::make_shared(_layer); + cmd->execute(); + EXPECT_TRUE(_layer->GetComment().empty()); +} + +TEST_F(BackupLayerCmdTest, DiscardEditCmd_Undo_RestoresLayerContent) +{ + auto cmd = std::make_shared(_layer); + cmd->execute(); + cmd->undo(); + EXPECT_EQ(_layer->GetComment(), "original content"); + cmd->redo(); + EXPECT_TRUE(_layer->GetComment().empty()); +} + +TEST_F(BackupLayerCmdTest, ClearLayerCmd_DoIt_EmptiesLayer) +{ + auto cmd = std::make_shared(_layer); + cmd->execute(); + EXPECT_TRUE(_layer->GetComment().empty()); +} + +TEST_F(BackupLayerCmdTest, ClearLayerCmd_Undo_RestoresContent) +{ + auto cmd = std::make_shared(_layer); + cmd->execute(); + cmd->undo(); + EXPECT_EQ(_layer->GetComment(), "original content"); + cmd->redo(); + EXPECT_TRUE(_layer->GetComment().empty()); +} + +// MuteLayerCmd + +class MuteLayerCmdTest : public ::testing::Test +{ +protected: + void SetUp() override + { + UsdUfe::setStagePathAccessorFn(stubStagePathAccessor); + if (!Ufe::GlobalSelection::get()) { + Ufe::GlobalSelection::initializeInstance( + std::make_shared()); + } + forgetMutedLayers(); + _stage = PXR_NS::UsdStage::CreateInMemory(); + _layer = PXR_NS::SdfLayer::CreateAnonymous("mutable"); + _stage->GetRootLayer()->InsertSubLayerPath(_layer->GetIdentifier(), 0); + } + void TearDown() override { forgetMutedLayers(); } + + PXR_NS::UsdStageRefPtr _stage; + PXR_NS::SdfLayerRefPtr _layer; +}; + +TEST_F(MuteLayerCmdTest, DoIt_MutesLayer) +{ + auto cmd = std::make_shared(_stage, _layer, /*muteIt=*/true); + cmd->execute(); + EXPECT_TRUE(_stage->IsLayerMuted(_layer->GetIdentifier())); +} + +TEST_F(MuteLayerCmdTest, Undo_UnmutesLayer) +{ + auto cmd = std::make_shared(_stage, _layer, /*muteIt=*/true); + cmd->execute(); + cmd->undo(); + EXPECT_FALSE(_stage->IsLayerMuted(_layer->GetIdentifier())); + cmd->redo(); + EXPECT_TRUE(_stage->IsLayerMuted(_layer->GetIdentifier())); +} + +TEST_F(MuteLayerCmdTest, DoIt_Unmute_UnmutesAlreadyMutedLayer) +{ + _stage->MuteLayer(_layer->GetIdentifier()); + auto cmd = std::make_shared(_stage, _layer, /*muteIt=*/false); + cmd->execute(); + EXPECT_FALSE(_stage->IsLayerMuted(_layer->GetIdentifier())); +} + +TEST_F(MuteLayerCmdTest, Undo_Unmute_RestoresMutedState) +{ + _stage->MuteLayer(_layer->GetIdentifier()); + auto cmd = std::make_shared(_stage, _layer, /*muteIt=*/false); + cmd->execute(); + cmd->undo(); + EXPECT_TRUE(_stage->IsLayerMuted(_layer->GetIdentifier())); + cmd->redo(); + EXPECT_FALSE(_stage->IsLayerMuted(_layer->GetIdentifier())); +} + +// MuteLayerCmd must hold the layer before muting it (OpenUSD lets go of muted +// layers), so the authored content of a dirty anonymous sublayer survives a +// mute / unmute / undo / redo cycle. +TEST_F(MuteLayerCmdTest, MuteUnmuteUndoRedo_PreservesDirtyLayerContent) +{ + const PXR_NS::SdfPath fooPath("/Foo"); + PXR_NS::SdfPrimSpec::New(_layer, "Foo", PXR_NS::SdfSpecifierDef); + ASSERT_TRUE(_layer->GetPrimAtPath(fooPath)); + + auto cmd = std::make_shared(_stage, _layer, /*muteIt=*/true); + + cmd->execute(); // hold-before-mute, then mute + EXPECT_TRUE(_stage->IsLayerMuted(_layer->GetIdentifier())); + EXPECT_TRUE(_layer->GetPrimAtPath(fooPath)) << "muted dirty layer content must be retained"; + + cmd->undo(); // unmute, then release + EXPECT_FALSE(_stage->IsLayerMuted(_layer->GetIdentifier())); + EXPECT_TRUE(_layer->GetPrimAtPath(fooPath)); + + cmd->redo(); // re-hold, then re-mute + EXPECT_TRUE(_stage->IsLayerMuted(_layer->GetIdentifier())); + EXPECT_TRUE(_layer->GetPrimAtPath(fooPath)) << "content must survive a re-mute on redo"; +} + +// MuteLayerCmd must hold a strong ref to the layer before muting so OpenUSD (which drops +// muted layers) cannot destroy it. This test releases the only external ref before execute. +TEST_F(MuteLayerCmdTest, HoldsLayer_KeepsLayerAliveWhenNoExternalRefRemains) +{ + auto ephemeral = PXR_NS::SdfLayer::CreateAnonymous("ephemeral"); + const std::string ephemeralId = ephemeral->GetIdentifier(); + _stage->GetRootLayer()->InsertSubLayerPath(ephemeralId, 0); + PXR_NS::SdfPrimSpec::New(ephemeral, "Bar", PXR_NS::SdfSpecifierDef); + + auto cmd = std::make_shared(_stage, ephemeral, /*muteIt=*/true); + ephemeral = PXR_NS::SdfLayerRefPtr{}; // drop the only external ref + + cmd->execute(); + + // If the cmd holds the layer, SdfLayer::Find returns non-null. + auto recovered = PXR_NS::SdfLayer::Find(ephemeralId); + ASSERT_NE(recovered, nullptr) << "cmd must keep the layer alive after the external ref is dropped"; + EXPECT_TRUE(recovered->GetPrimAtPath(PXR_NS::SdfPath("/Bar"))) + << "layer content must survive when the only external ref is dropped before mute"; +} + +// LockLayerCmd + +class LockLayerCmdTest : public ::testing::Test +{ +protected: + void SetUp() override + { + UsdUfe::setStagePathAccessorFn(stubStagePathAccessor); + forgetLockedLayers(); + _stage = PXR_NS::UsdStage::CreateInMemory(); + _layer = PXR_NS::SdfLayer::CreateAnonymous("lockable"); + _stage->GetRootLayer()->InsertSubLayerPath(_layer->GetIdentifier(), 0); + } + void TearDown() override { forgetLockedLayers(); } + + PXR_NS::UsdStageRefPtr _stage; + PXR_NS::SdfLayerRefPtr _layer; +}; + +TEST_F(LockLayerCmdTest, DoIt_LocksLayer) +{ + auto cmd = std::make_shared(_stage, _layer, LayerLock_Locked); + cmd->execute(); + EXPECT_TRUE(isLayerLocked(_layer)); +} + +TEST_F(LockLayerCmdTest, Undo_UnlocksLayer) +{ + auto cmd = std::make_shared(_stage, _layer, LayerLock_Locked); + cmd->execute(); + cmd->undo(); + EXPECT_FALSE(isLayerLocked(_layer)); + cmd->redo(); + EXPECT_TRUE(isLayerLocked(_layer)); +} + +TEST_F(LockLayerCmdTest, SkipSystemLocked_DoesNotLockSystemLockedSublayers) +{ + auto sublayer = PXR_NS::SdfLayer::CreateAnonymous("sub"); + _layer->InsertSubLayerPath(sublayer->GetIdentifier(), 0); + // Mark sublayer as system-locked. + lockLayer("", sublayer, LayerLock_SystemLocked, /*updateDCCAttr=*/false); + + auto cmd = std::make_shared( + _stage, _layer, LayerLock_Locked, + /*includeSubLayers=*/true, /*skipSystemLocked=*/true); + cmd->execute(); + + // Parent should be locked, system-locked sublayer should remain system-locked (not relocked). + EXPECT_TRUE(isLayerLocked(_layer)); + EXPECT_TRUE(isLayerSystemLocked(sublayer)); + EXPECT_FALSE(isLayerLocked(sublayer)); // must not have been changed to plain locked +} + +TEST_F(LockLayerCmdTest, SkipSystemLocked_False_LocksSystemLockedSublayers) +{ + auto sublayer = PXR_NS::SdfLayer::CreateAnonymous("sub"); + _layer->InsertSubLayerPath(sublayer->GetIdentifier(), 0); + lockLayer("", sublayer, LayerLock_SystemLocked, /*updateDCCAttr=*/false); + + auto cmd = std::make_shared( + _stage, _layer, LayerLock_Locked, + /*includeSubLayers=*/true, /*skipSystemLocked=*/false); + cmd->execute(); + + EXPECT_TRUE(isLayerLocked(_layer)); + EXPECT_TRUE(isLayerLocked(sublayer)); + EXPECT_FALSE(isLayerSystemLocked(sublayer)); +} + +// InsertSubPathCmd, RemoveSubPathCmd, AddAnonSubLayerCmd + +class InsertSubPathCmdTest : public ::testing::Test +{ +protected: + void SetUp() override + { + _stage = PXR_NS::UsdStage::CreateInMemory(); + _parent = _stage->GetRootLayer(); + _sub = PXR_NS::SdfLayer::CreateAnonymous("sub"); + } + + PXR_NS::UsdStageRefPtr _stage; + PXR_NS::SdfLayerRefPtr _parent; + PXR_NS::SdfLayerRefPtr _sub; +}; + +TEST_F(InsertSubPathCmdTest, DoIt_InsertsSubLayerAtIndex) +{ + auto cmd = std::make_shared( + _stage, _parent, _sub->GetIdentifier(), 0); + cmd->execute(); + EXPECT_NE(_parent->GetSubLayerPaths().Find(_sub->GetIdentifier()), static_cast(-1)); +} + +TEST_F(InsertSubPathCmdTest, Undo_RemovesInsertedSubLayer) +{ + auto cmd = std::make_shared( + _stage, _parent, _sub->GetIdentifier(), 0); + cmd->execute(); + cmd->undo(); + EXPECT_EQ(_parent->GetSubLayerPaths().Find(_sub->GetIdentifier()), static_cast(-1)); + cmd->redo(); + EXPECT_NE(_parent->GetSubLayerPaths().Find(_sub->GetIdentifier()), static_cast(-1)); +} + +class RemoveSubPathCmdTest : public ::testing::Test +{ +protected: + void SetUp() override + { + UsdUfe::setStagePathAccessorFn(stubStagePathAccessor); + if (!Ufe::GlobalSelection::get()) { + Ufe::GlobalSelection::initializeInstance( + std::make_shared()); + } + _stage = PXR_NS::UsdStage::CreateInMemory(); + _parent = _stage->GetRootLayer(); + _sub = PXR_NS::SdfLayer::CreateAnonymous("sub"); + _parent->InsertSubLayerPath(_sub->GetIdentifier(), 0); + } + + PXR_NS::UsdStageRefPtr _stage; + PXR_NS::SdfLayerRefPtr _parent; + PXR_NS::SdfLayerRefPtr _sub; +}; + +TEST_F(RemoveSubPathCmdTest, DoIt_RemovesSubLayer) +{ + auto cmd = std::make_shared(_stage, _parent, 0); + cmd->execute(); + EXPECT_EQ(_parent->GetSubLayerPaths().Find(_sub->GetIdentifier()), static_cast(-1)); +} + +TEST_F(RemoveSubPathCmdTest, Undo_RestoresSubLayer) +{ + auto cmd = std::make_shared(_stage, _parent, 0); + cmd->execute(); + cmd->undo(); + EXPECT_NE(_parent->GetSubLayerPaths().Find(_sub->GetIdentifier()), static_cast(-1)); + cmd->redo(); + EXPECT_EQ(_parent->GetSubLayerPaths().Find(_sub->GetIdentifier()), static_cast(-1)); +} + +class AddAnonSubLayerCmdTest : public ::testing::Test +{ +protected: + void SetUp() override + { + _stage = PXR_NS::UsdStage::CreateInMemory(); + _parent = _stage->GetRootLayer(); + } + + PXR_NS::UsdStageRefPtr _stage; + PXR_NS::SdfLayerRefPtr _parent; +}; + +TEST_F(AddAnonSubLayerCmdTest, DoIt_InsertsAnonLayer) +{ + auto cmd = std::make_shared(_stage, _parent); + cmd->execute(); + EXPECT_EQ(_parent->GetNumSubLayerPaths(), static_cast(1)); +} + +TEST_F(AddAnonSubLayerCmdTest, Undo_RemovesAnonLayer) +{ + auto cmd = std::make_shared(_stage, _parent); + cmd->execute(); + cmd->undo(); + EXPECT_EQ(_parent->GetNumSubLayerPaths(), static_cast(0)); +} + +class MoveSubPathCmdTest : public ::testing::Test +{ +protected: + void SetUp() override + { + _stage = PXR_NS::UsdStage::CreateInMemory(); + _parent = _stage->GetRootLayer(); + _subA = PXR_NS::SdfLayer::CreateAnonymous("subA"); + _subB = PXR_NS::SdfLayer::CreateAnonymous("subB"); + _subC = PXR_NS::SdfLayer::CreateAnonymous("subC"); + _parent->InsertSubLayerPath(_subA->GetIdentifier(), 0); + _parent->InsertSubLayerPath(_subB->GetIdentifier(), 1); + _parent->InsertSubLayerPath(_subC->GetIdentifier(), 2); + // _parent sublayers: [A, B, C] at indices 0, 1, 2 + } + + PXR_NS::UsdStageRefPtr _stage; + PXR_NS::SdfLayerRefPtr _parent; + PXR_NS::SdfLayerRefPtr _subA, _subB, _subC; +}; + +TEST_F(MoveSubPathCmdTest, DoIt_SameParent_ReordersSubLayer) +{ + // Before: [A, B, C] + ASSERT_EQ(_parent->GetSubLayerPaths()[0], _subA->GetIdentifier()); + ASSERT_EQ(_parent->GetSubLayerPaths()[1], _subB->GetIdentifier()); + ASSERT_EQ(_parent->GetSubLayerPaths()[2], _subC->GetIdentifier()); + // Move A from index 0 to index 2 → expect [B, C, A] + auto cmd = std::make_shared(_parent, _parent, _subA->GetIdentifier(), 2); + cmd->execute(); + EXPECT_EQ(_parent->GetSubLayerPaths()[2], _subA->GetIdentifier()); + EXPECT_EQ(_parent->GetSubLayerPaths()[0], _subB->GetIdentifier()); +} + +TEST_F(MoveSubPathCmdTest, Undo_SameParent_RestoresOriginalOrder) +{ + // Before: [A, B, C] + ASSERT_EQ(_parent->GetSubLayerPaths()[0], _subA->GetIdentifier()); + ASSERT_EQ(_parent->GetSubLayerPaths()[1], _subB->GetIdentifier()); + ASSERT_EQ(_parent->GetSubLayerPaths()[2], _subC->GetIdentifier()); + auto cmd = std::make_shared(_parent, _parent, _subA->GetIdentifier(), 2); + cmd->execute(); + cmd->undo(); + EXPECT_EQ(_parent->GetSubLayerPaths()[0], _subA->GetIdentifier()); + EXPECT_EQ(_parent->GetSubLayerPaths()[1], _subB->GetIdentifier()); + EXPECT_EQ(_parent->GetSubLayerPaths()[2], _subC->GetIdentifier()); + cmd->redo(); + EXPECT_EQ(_parent->GetSubLayerPaths()[0], _subB->GetIdentifier()); + EXPECT_EQ(_parent->GetSubLayerPaths()[2], _subA->GetIdentifier()); +} + +TEST_F(MoveSubPathCmdTest, DoIt_CrossParent_MovesSubLayerToNewParent) +{ + auto newParent = PXR_NS::SdfLayer::CreateAnonymous("newParent"); + // Before: subA is in parent, not in newParent + ASSERT_NE(_parent->GetSubLayerPaths().Find(_subA->GetIdentifier()), static_cast(-1)); + ASSERT_EQ(newParent->GetSubLayerPaths().Find(_subA->GetIdentifier()), static_cast(-1)); + auto cmd = std::make_shared( + _parent, newParent, _subA->GetIdentifier(), 0); + cmd->execute(); + EXPECT_EQ( + _parent->GetSubLayerPaths().Find(_subA->GetIdentifier()), static_cast(-1)); + EXPECT_NE( + newParent->GetSubLayerPaths().Find(_subA->GetIdentifier()), static_cast(-1)); +} + +TEST_F(MoveSubPathCmdTest, Undo_CrossParent_RestoresSubLayerToOriginalParent) +{ + auto newParent = PXR_NS::SdfLayer::CreateAnonymous("newParent"); + // Before: subA is in parent, not in newParent + ASSERT_NE(_parent->GetSubLayerPaths().Find(_subA->GetIdentifier()), static_cast(-1)); + ASSERT_EQ(newParent->GetSubLayerPaths().Find(_subA->GetIdentifier()), static_cast(-1)); + auto cmd = std::make_shared( + _parent, newParent, _subA->GetIdentifier(), 0); + cmd->execute(); + cmd->undo(); + EXPECT_NE( + _parent->GetSubLayerPaths().Find(_subA->GetIdentifier()), static_cast(-1)); + EXPECT_EQ( + newParent->GetSubLayerPaths().Find(_subA->GetIdentifier()), static_cast(-1)); + + cmd->redo(); + EXPECT_EQ( + _parent->GetSubLayerPaths().Find(_subA->GetIdentifier()), static_cast(-1)); + EXPECT_NE( + newParent->GetSubLayerPaths().Find(_subA->GetIdentifier()), static_cast(-1)); +} + +TEST(RefreshSystemLockCallbackContextTest, AddCallbackContext_StoresEntry) +{ + auto stage = PXR_NS::UsdStage::CreateInMemory(); + auto rootLayer = stage->GetRootLayer(); + auto cmd = std::make_shared(stage, rootLayer, false); + cmd->addCallbackContext("proxyShapePath", PXR_NS::VtValue(std::string("/myShape"))); + const auto& context = cmd->extraCallbackContext(); + const auto it = context.find("proxyShapePath"); + ASSERT_NE(it, context.end()); + EXPECT_EQ(it->second.UncheckedGet(), std::string("/myShape")); +} + +// AddAnonSubLayerCmd: redo reuses the same identifier + +TEST_F(AddAnonSubLayerCmdTest, Redo_ReusesSameIdentifier) +{ + auto cmd = std::make_shared(_stage, _parent); + cmd->_anonName = "myLayer"; + cmd->execute(); + const std::string id1 = cmd->addedLayer(); + cmd->undo(); + cmd->redo(); + const std::string id2 = cmd->addedLayer(); + // The command intentionally caches the anonymous identifier so later commands + // referencing it stay valid across undo/redo. + EXPECT_EQ(id1, id2); + EXPECT_EQ(_parent->GetSubLayerPaths()[0], id2); +} + +// SetEditTargetCmd + +class SetEditTargetCmdTest : public ::testing::Test +{ +protected: + void SetUp() override + { + _stage = PXR_NS::UsdStage::CreateInMemory(); + _sub = PXR_NS::SdfLayer::CreateAnonymous("sub"); + _stage->GetRootLayer()->InsertSubLayerPath(_sub->GetIdentifier(), 0); + _stage->SetEditTarget(_stage->GetRootLayer()); + } + + PXR_NS::UsdStageRefPtr _stage; + PXR_NS::SdfLayerRefPtr _sub; +}; + +TEST_F(SetEditTargetCmdTest, DoIt_SetsTarget) +{ + auto cmd = std::make_shared(_stage, _sub); + cmd->execute(); + EXPECT_EQ(_stage->GetEditTarget().GetLayer(), _sub); +} + +TEST_F(SetEditTargetCmdTest, Undo_RestoresPreviousTarget) +{ + auto cmd = std::make_shared(_stage, _sub); + cmd->execute(); + cmd->undo(); + EXPECT_EQ(_stage->GetEditTarget().GetLayer(), _stage->GetRootLayer()); +} + +TEST_F(SetEditTargetCmdTest, Redo_ReappliesTarget) +{ + auto cmd = std::make_shared(_stage, _sub); + cmd->execute(); + cmd->undo(); + cmd->redo(); + EXPECT_EQ(_stage->GetEditTarget().GetLayer(), _sub); +} + +// FlattenLayerCmd + +class FlattenLayerCmdTest : public ::testing::Test +{ +protected: + void SetUp() override + { + _root = PXR_NS::SdfLayer::CreateAnonymous("flattenRoot"); + _sub = PXR_NS::SdfLayer::CreateAnonymous("flattenSub"); + _root->InsertSubLayerPath(_sub->GetIdentifier(), 0); + // Define a prim only in the sublayer; flattening must inline it into _root. + PXR_NS::SdfPrimSpec::New(_sub, "Foo", PXR_NS::SdfSpecifierDef); + } + + PXR_NS::SdfLayerRefPtr _root; + PXR_NS::SdfLayerRefPtr _sub; +}; + +TEST_F(FlattenLayerCmdTest, DoIt_FlattensSublayerContentIntoLayer) +{ + ASSERT_FALSE(_root->GetPrimAtPath(PXR_NS::SdfPath("/Foo"))); + auto cmd = std::make_shared(_root); + cmd->execute(); + EXPECT_TRUE(_root->GetPrimAtPath(PXR_NS::SdfPath("/Foo"))); +} + +TEST_F(FlattenLayerCmdTest, Undo_RestoresPreFlattenContent) +{ + auto cmd = std::make_shared(_root); + cmd->execute(); + cmd->undo(); + EXPECT_FALSE(_root->GetPrimAtPath(PXR_NS::SdfPath("/Foo"))); +} + +TEST_F(FlattenLayerCmdTest, Redo_ReflattensContent) +{ + auto cmd = std::make_shared(_root); + cmd->execute(); + cmd->undo(); + cmd->redo(); + EXPECT_TRUE(_root->GetPrimAtPath(PXR_NS::SdfPath("/Foo"))); +} + +// StitchLayersCmd + +class StitchLayersCmdTest : public ::testing::Test +{ +protected: + void SetUp() override + { + _stage = PXR_NS::UsdStage::CreateInMemory(); + _strong = PXR_NS::SdfLayer::CreateAnonymous("strong"); + _weak = PXR_NS::SdfLayer::CreateAnonymous("weak"); + // Index 0 is the strongest sublayer. + _stage->GetRootLayer()->InsertSubLayerPath(_strong->GetIdentifier(), 0); + _stage->GetRootLayer()->InsertSubLayerPath(_weak->GetIdentifier(), 1); + PXR_NS::SdfPrimSpec::New(_strong, "Strong", PXR_NS::SdfSpecifierDef); + PXR_NS::SdfPrimSpec::New(_weak, "Weak", PXR_NS::SdfSpecifierDef); + } + + std::vector identifiers() const + { + return { _strong->GetIdentifier(), _weak->GetIdentifier() }; + } + + PXR_NS::UsdStageRefPtr _stage; + PXR_NS::SdfLayerRefPtr _strong; + PXR_NS::SdfLayerRefPtr _weak; +}; + +TEST_F(StitchLayersCmdTest, DoIt_MergesWeakIntoStrongAndRemovesWeak) +{ + auto cmd = std::make_shared(_stage, identifiers()); + cmd->execute(); + + EXPECT_TRUE(_strong->GetPrimAtPath(PXR_NS::SdfPath("/Weak"))); + EXPECT_EQ( + _stage->GetRootLayer()->GetSubLayerPaths().Find(_weak->GetIdentifier()), + static_cast(-1)); + EXPECT_NE( + _stage->GetRootLayer()->GetSubLayerPaths().Find(_strong->GetIdentifier()), + static_cast(-1)); +} + +TEST_F(StitchLayersCmdTest, Undo_RestoresOriginalLayers) +{ + auto cmd = std::make_shared(_stage, identifiers()); + cmd->execute(); + cmd->undo(); + + EXPECT_NE( + _stage->GetRootLayer()->GetSubLayerPaths().Find(_weak->GetIdentifier()), + static_cast(-1)); + EXPECT_FALSE(_strong->GetPrimAtPath(PXR_NS::SdfPath("/Weak"))); +} + +TEST_F(StitchLayersCmdTest, Redo_RestitchesLayers) +{ + auto cmd = std::make_shared(_stage, identifiers()); + cmd->execute(); + cmd->undo(); + cmd->redo(); + + EXPECT_TRUE(_strong->GetPrimAtPath(PXR_NS::SdfPath("/Weak"))); + EXPECT_EQ( + _stage->GetRootLayer()->GetSubLayerPaths().Find(_weak->GetIdentifier()), + static_cast(-1)); +} + +TEST_F(StitchLayersCmdTest, DoIt_ReturnsFalse_WhenTargetLayerIsLocked) +{ + // Locking the merge target (strongest layer) aborts the entire operation. + _strong->SetPermissionToEdit(false); + auto cmd = std::make_shared(_stage, identifiers()); + EXPECT_THROW(cmd->execute(), std::runtime_error); + EXPECT_NE( + _stage->GetRootLayer()->GetSubLayerPaths().Find(_weak->GetIdentifier()), + static_cast(-1)); + _strong->SetPermissionToEdit(true); +} + +TEST(StitchLayersCmdPartialMergeTest, DoIt_SkipsWeakWithLockedParent_MergesRest) +{ + // Stack: root → layerA → layerB + // Lock layerA so layerB's parent is locked. + // Expected: layerA merges into root (layerA's parent root is unlocked); + // layerB is skipped (its parent layerA is locked). + auto stage = PXR_NS::UsdStage::CreateInMemory(); + auto root = stage->GetRootLayer(); + auto layerA = PXR_NS::SdfLayer::CreateAnonymous("A"); + auto layerB = PXR_NS::SdfLayer::CreateAnonymous("B"); + + root->InsertSubLayerPath(layerA->GetIdentifier(), 0); + layerA->InsertSubLayerPath(layerB->GetIdentifier(), 0); + + PXR_NS::SdfPrimSpec::New(layerA, "FromA", PXR_NS::SdfSpecifierDef); + PXR_NS::SdfPrimSpec::New(layerB, "FromB", PXR_NS::SdfSpecifierDef); + + layerA->SetPermissionToEdit(false); + + auto cmd = std::make_shared( + stage, + std::vector { + root->GetIdentifier(), + layerA->GetIdentifier(), + layerB->GetIdentifier() }); + + // Should succeed (partial merge, not a hard failure). + EXPECT_NO_THROW(cmd->execute()); + + // layerA's content was merged into root. + EXPECT_TRUE(root->GetPrimAtPath(PXR_NS::SdfPath("/FromA"))); + // layerB was skipped — its content did not reach root. + EXPECT_FALSE(root->GetPrimAtPath(PXR_NS::SdfPath("/FromB"))); + // layerA was removed from root's sublayers. + EXPECT_EQ( + root->GetSubLayerPaths().Find(layerA->GetIdentifier()), + static_cast(-1)); + // layerB is still a sublayer of layerA (unchanged). + EXPECT_NE( + layerA->GetSubLayerPaths().Find(layerB->GetIdentifier()), + static_cast(-1)); + // layerB was adopted by root as a sublayer (inherited from merged layerA). + EXPECT_NE( + root->GetSubLayerPaths().Find(layerB->GetIdentifier()), + static_cast(-1)); + + layerA->SetPermissionToEdit(true); +} + +// RefreshSystemLockLayerCmd (needs a real read-only file on disk) + +class RefreshSystemLockLayerCmdTest : public ::testing::Test +{ +protected: + void SetUp() override + { + UsdUfe::setStagePathAccessorFn(stubStagePathAccessor); + if (!Ufe::GlobalSelection::get()) { + Ufe::GlobalSelection::initializeInstance( + std::make_shared()); + } + forgetLockedLayers(); + forgetSystemLockedLayers(); + + namespace fss = fs::filesystem; + std::error_code ec; + _filePath = (fss::temp_directory_path() / "le_systemlock_test.usda").generic_string(); + // Clear any leftover from a previous run (restore write first so remove succeeds). + fss::permissions(_filePath, fss::perms::owner_write, fss::perm_options::add, ec); + fss::remove(_filePath, ec); + + _stage = PXR_NS::UsdStage::CreateInMemory(); + _fileLayer = PXR_NS::SdfLayer::CreateNew(_filePath); + _fileLayer->Save(); + _stage->GetRootLayer()->InsertSubLayerPath(_fileLayer->GetIdentifier(), 0); + + // Make the file read-only so checkWriteAccess() reports no write access. + fss::permissions( + _filePath, + fss::perms::owner_write | fss::perms::group_write | fss::perms::others_write, + fss::perm_options::remove); + } + + void TearDown() override + { + namespace fss = fs::filesystem; + std::error_code ec; + fss::permissions(_filePath, fss::perms::owner_write, fss::perm_options::add, ec); + fss::remove(_filePath, ec); + forgetSystemLockedLayers(); + forgetLockedLayers(); + } + + PXR_NS::UsdStageRefPtr _stage; + PXR_NS::SdfLayerRefPtr _fileLayer; + std::string _filePath; +}; + +TEST_F(RefreshSystemLockLayerCmdTest, DoIt_SystemLocksReadOnlyFileLayer) +{ + ASSERT_FALSE(isLayerSystemLocked(_fileLayer)); + auto cmd = std::make_shared(_stage, _fileLayer, false); + cmd->execute(); + EXPECT_TRUE(isLayerSystemLocked(_fileLayer)); +} + +TEST_F(RefreshSystemLockLayerCmdTest, Undo_RestoresLockState) +{ + auto cmd = std::make_shared(_stage, _fileLayer, false); + cmd->execute(); + cmd->undo(); + EXPECT_FALSE(isLayerSystemLocked(_fileLayer)); +} + +TEST_F(RefreshSystemLockLayerCmdTest, Redo_ReappliesSystemLock) +{ + auto cmd = std::make_shared(_stage, _fileLayer, false); + cmd->execute(); + cmd->undo(); + cmd->redo(); + EXPECT_TRUE(isLayerSystemLocked(_fileLayer)); +} + +// error paths + +TEST_F(InsertSubPathCmdTest, DoIt_ReturnsFalse_WhenIndexOutOfBounds) +{ + auto cmd = std::make_shared(_stage, _parent, _sub->GetIdentifier(), 99); + EXPECT_THROW(cmd->execute(), std::runtime_error); + EXPECT_EQ(_parent->GetSubLayerPaths().Find(_sub->GetIdentifier()), static_cast(-1)); +} + +TEST_F(RemoveSubPathCmdTest, DoIt_ReturnsFalse_WhenIndexOutOfBounds) +{ + auto cmd = std::make_shared(_stage, _parent, 99); + EXPECT_THROW(cmd->execute(), std::runtime_error); + EXPECT_NE(_parent->GetSubLayerPaths().Find(_sub->GetIdentifier()), static_cast(-1)); +} + +TEST_F(MoveSubPathCmdTest, DoIt_ReturnsFalse_WhenSubPathNotFound) +{ + auto cmd = std::make_shared(_parent, _parent, "nonexistent.usda", 0); + EXPECT_THROW(cmd->execute(), std::runtime_error); +} + +TEST_F(MoveSubPathCmdTest, DoIt_ReturnsFalse_WhenSameParentIndexOutOfBounds) +{ + auto cmd = std::make_shared(_parent, _parent, _subA->GetIdentifier(), 99); + EXPECT_THROW(cmd->execute(), std::runtime_error); +} + +TEST_F(MoveSubPathCmdTest, DoIt_ReturnsFalse_WhenCrossParentIndexOutOfBounds) +{ + auto newParent = PXR_NS::SdfLayer::CreateAnonymous("np_oob"); + auto cmd = std::make_shared(_parent, newParent, _subA->GetIdentifier(), 99); + EXPECT_THROW(cmd->execute(), std::runtime_error); +} + +TEST_F(MoveSubPathCmdTest, DoIt_ReturnsFalse_WhenSubPathExistsInNewParent) +{ + auto newParent = PXR_NS::SdfLayer::CreateAnonymous("np_dup"); + newParent->InsertSubLayerPath(_subA->GetIdentifier(), 0); + auto cmd = std::make_shared(_parent, newParent, _subA->GetIdentifier(), 0); + EXPECT_THROW(cmd->execute(), std::runtime_error); +} + +// RemoveSubPathCmd: edit-target redirect + +TEST_F(RemoveSubPathCmdTest, DoIt_RetargetsToRootWhenRemovingEditTargetLayer) +{ + _stage->SetEditTarget(_sub); + ASSERT_EQ(_stage->GetEditTarget().GetLayer(), _sub); + + auto cmd = std::make_shared(_stage, _parent, 0); + cmd->execute(); + EXPECT_EQ(_stage->GetEditTarget().GetLayer(), _stage->GetRootLayer()); +} + +TEST_F(RemoveSubPathCmdTest, Undo_RestoresEditTargetToReinsertedLayer) +{ + _stage->SetEditTarget(_sub); + + auto cmd = std::make_shared(_stage, _parent, 0); + cmd->execute(); + cmd->undo(); + EXPECT_EQ(_stage->GetEditTarget().GetLayer(), _sub); +} + +} // namespace UsdLayerEditor diff --git a/test/lib/usdLayerEditor/cpp/testLayerEditorWidget.cpp b/test/lib/usdLayerEditor/cpp/testLayerEditorWidget.cpp new file mode 100644 index 0000000000..a82d24d54e --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/testLayerEditorWidget.cpp @@ -0,0 +1,277 @@ +// 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. +// +#pragma once + +#include +#include "testUtils.h" + +#include "generatedIconButton.h" +#include "layerContentsWidget.h" +#include "layerEditorWidget.h" +#include "layerTreeItem.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace UsdLayerEditor { + +// ── getSelectedLayers / selectLayers ───────────────────────────────────────── + +// With nothing selected, getSelectedLayers returns empty. +TEST_F(LayerEditorTestFixture, Widget_GetSelectedLayers_NothingSelected_ReturnsEmpty) +{ +#ifdef MAYAUSD_OLD_LAYER_EDITOR + // Old editor's selectLayers({}) does not clear currentIndex, so getSelectedLayers + // falls back to the current item and is non-empty. Known parity gap, no production + // impact. See test/lib/oldUsdLayerEditor/cpp/PARITY_FAILURES_DETAIL.md (Failure 4). + GTEST_SKIP() << "Expected old-editor parity gap: selectLayers({}) does not clear currentIndex."; +#endif + // Use selectLayers({}) which also clears currentIndex, so the fallback in + // getSelectedLayerItems() does not return a stale current-index item. + _widget->selectLayers({}); + QApplication::processEvents(); + + auto layers = _widget->getSelectedLayers(); + EXPECT_TRUE(layers.empty()); +} + +// After selecting the root layer, getSelectedLayers returns its identifier. +TEST_F(LayerEditorTestFixture, Widget_GetSelectedLayers_RootSelected_ReturnsId) +{ + selectRow(rootLayerIndex()); + + auto layers = _widget->getSelectedLayers(); + ASSERT_EQ(layers.size(), 1u); + + auto* rootItem = dynamic_cast( + treeModel()->itemFromIndex(rootLayerIndex())); + ASSERT_NE(rootItem, nullptr); + EXPECT_EQ(layers[0], rootItem->layer()->GetIdentifier()); +} + +// selectLayers with an empty vector clears the selection and current index. +TEST_F(LayerEditorTestFixture, Widget_SelectLayers_Empty_ClearsSelection) +{ +#ifdef MAYAUSD_OLD_LAYER_EDITOR + // Old editor's selectLayers({}) clears selected rows but leaves currentIndex valid. + // Known parity gap, no production impact. See + // test/lib/oldUsdLayerEditor/cpp/PARITY_FAILURES_DETAIL.md (Failure 5). + GTEST_SKIP() << "Expected old-editor parity gap: selectLayers({}) does not clear currentIndex."; +#endif + selectRow(rootLayerIndex()); + _widget->selectLayers({}); + QApplication::processEvents(); + + // Both selectedRows() and currentIndex() should be cleared. + EXPECT_TRUE(layerTree()->selectionModel()->selectedRows().empty()); + EXPECT_FALSE(layerTree()->selectionModel()->currentIndex().isValid()); +} + +// selectLayers with a valid identifier selects the correct layer. +TEST_F(LayerEditorTestFixture, Widget_SelectLayers_ValidId_SelectsLayer) +{ + auto* rootItem = dynamic_cast( + treeModel()->itemFromIndex(rootLayerIndex())); + ASSERT_NE(rootItem, nullptr); + const std::string rootId = rootItem->layer()->GetIdentifier(); + + layerTree()->selectionModel()->clearSelection(); + QApplication::processEvents(); + + _widget->selectLayers({ rootId }); + QApplication::processEvents(); + + auto selected = _widget->getSelectedLayers(); + ASSERT_EQ(selected.size(), 1u); + EXPECT_EQ(selected[0], rootId); +} + +// selectLayers with an unknown identifier is a no-op (no crash, no selection). +TEST_F(LayerEditorTestFixture, Widget_SelectLayers_UnknownId_NoOp) +{ + layerTree()->selectionModel()->clearSelection(); + QApplication::processEvents(); + + _widget->selectLayers({ "anon:nonexistent_layer_identifier_xyz" }); + QApplication::processEvents(); + + EXPECT_TRUE(layerTree()->selectionModel()->selectedRows().empty()); +} + +// ── showDisplayLayerContents ────────────────────────────────────────────────── + +// showDisplayLayerContents(true) makes the LayerContentsWidget visible. +TEST_F(LayerEditorTestFixture, Widget_ShowDisplayLayerContents_True_MakesWidgetVisible) +{ + _widget->showDisplayLayerContents(true); + QApplication::processEvents(); + + auto* cw = TestUtils::findContentsWidget(_widget); + ASSERT_NE(cw, nullptr); + EXPECT_TRUE(cw->isVisible()); +} + +// showDisplayLayerContents(false) hides the LayerContentsWidget. +TEST_F(LayerEditorTestFixture, Widget_ShowDisplayLayerContents_False_HidesWidget) +{ + _widget->showDisplayLayerContents(false); + QApplication::processEvents(); + + auto* cw = TestUtils::findContentsWidget(_widget); + ASSERT_NE(cw, nullptr); + EXPECT_FALSE(cw->isVisible()); +} + +// Showing then hiding leaves the contents widget hidden. +TEST_F(LayerEditorTestFixture, Widget_ShowDisplayLayerContents_HideAfterShow_HidesWidget) +{ + _widget->showDisplayLayerContents(true); + QApplication::processEvents(); + _widget->showDisplayLayerContents(false); + QApplication::processEvents(); + + auto* cw = TestUtils::findContentsWidget(_widget); + ASSERT_NE(cw, nullptr); + EXPECT_FALSE(cw->isVisible()); +} + +// ── onSplitterMoved ─────────────────────────────────────────────────────────── + +// index==1 with the pane open drives the layer-contents update branch. +TEST_F(LayerEditorTestFixture, Widget_OnSplitterMoved_ContentsIndex_DoesNotCrash) +{ + _widget->showDisplayLayerContents(true); + QApplication::processEvents(); + + // index 1 is the layer-contents pane; pos=200 means the pane is open. + _widget->onSplitterMoved(200, 1); + QApplication::processEvents(); + SUCCEED(); +} + +// index != 1 is a no-op early-return branch: contents visibility and splitter +// sizes must be unchanged. +TEST_F(LayerEditorTestFixture, Widget_OnSplitterMoved_OtherIndex_DoesNotCrash) +{ + _widget->showDisplayLayerContents(true); + QApplication::processEvents(); + + auto* cw = TestUtils::findContentsWidget(_widget); + ASSERT_NE(cw, nullptr); + auto* splitter = _widget->findChild(); + ASSERT_NE(splitter, nullptr); + + const bool visibleBefore = cw->isVisible(); + const QList sizesBefore = splitter->sizes(); + + _widget->onSplitterMoved(100, 0); + QApplication::processEvents(); + + EXPECT_EQ(cw->isVisible(), visibleBefore); + EXPECT_EQ(splitter->sizes(), sizesBefore); +} + +// ── onLazyUpdateLayerContents / timerEvent ─────────────────────────────────── + +// Scheduling the lazy update and processing events exercises the timer path. +TEST_F(LayerEditorTestFixture, Widget_OnLazyUpdateLayerContents_DoesNotCrash) +{ + _widget->showDisplayLayerContents(true); + selectRow(rootLayerIndex()); + QApplication::processEvents(); + + _widget->onLazyUpdateLayerContents(); + QApplication::processEvents(); + + EXPECT_NE(TestUtils::findContentsWidget(_widget), nullptr); +} + +// ── updateButtonsOnIdle ─────────────────────────────────────────────────────── + +// With the (unlocked, unmuted, valid) root selected, the add-layer button is +// enabled. Running the idle update must not crash or disturb that state. +TEST_F(LayerEditorTestFixture, Widget_UpdateButtonsOnIdle_DoesNotCrash) +{ + selectRow(rootLayerIndex()); + + _widget->updateButtonsOnIdle(); + QApplication::processEvents(); + + auto* addButton + = TestUtils::findButtonByObjectName(_widget, "LayerEditorAddLayerButton"); + ASSERT_NE(addButton, nullptr); + EXPECT_TRUE(addButton->isEnabled()); +} + +// ── onSaveStageButtonClicked ────────────────────────────────────────────────── +// Drives saveStage(); the new editor's SaveLayersDialog has a setExecTestHandler +// seam (installed by the fixture) that suppresses the modal. The old editor's +// SaveLayersDialog has no such seam, so this would pop a blocking "Save " +// dialog — hence new-editor only. +#ifndef MAYAUSD_OLD_LAYER_EDITOR +TEST_F(LayerEditorTestFixture, Widget_OnSaveStageButtonClicked_DoesNotCrash) +{ + // The stub stage has an anonymous sublayer, so saveStage routes through the + // SaveLayersDialog (suppressed here, returns Rejected) rather than the generic + // per-layer save path. _saveLayerCallCount staying 0 confirms the anonymous + // layers were deferred to the dialog and not silently saved. + _sessionState._saveLayerCallCount = 0; + EXPECT_NO_THROW(_widget->onSaveStageButtonClicked()); + QApplication::processEvents(); + EXPECT_EQ(_sessionState._saveLayerCallCount, 0); +} +#endif + +// ── GeneratedIconButton paint states ────────────────────────────────────────── +// paint() picks base / hover / disabled pixmaps; rendering in each state must +// produce visibly different output. +TEST_F(LayerEditorTestFixture, GeneratedIconButton_PaintReflectsEnabledAndHoverState) +{ + QPixmap iconPixmap(16, 16); + iconPixmap.fill(QColor(100, 100, 100)); + GeneratedIconButton button(_mainWindow, QIcon(iconPixmap), 16); + button.resize(16, 16); + + auto renderToImage = [&button]() { + QPixmap target(button.size()); + target.fill(Qt::transparent); + button.render(&target); + return target.toImage(); + }; + + QImage base = renderToImage(); + + // Enter event sets the hover state; paint must use the (brighter) hover pixmap. + QEvent enterEvent(QEvent::Enter); + QCoreApplication::sendEvent(&button, &enterEvent); + QImage hover = renderToImage(); + + // Disabled: paint must use the (more transparent) disabled pixmap. + button.setEnabled(false); + QImage disabled = renderToImage(); + + EXPECT_NE(base, hover); + EXPECT_NE(base, disabled); +} + +} // namespace UsdLayerEditor diff --git a/test/lib/usdLayerEditor/cpp/testLayerEditorWindow.cpp b/test/lib/usdLayerEditor/cpp/testLayerEditorWindow.cpp new file mode 100644 index 0000000000..4ff5bbb957 --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/testLayerEditorWindow.cpp @@ -0,0 +1,109 @@ +// 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. +// +#pragma once + +#ifndef MAYAUSD_OLD_LAYER_EDITOR +#include +#include "abstractLayerEditorWindow.h" +#include "layerEditorWidget.h" +#include "layerEditorWindow.h" + +#include + +#include + +namespace UsdLayerEditor { + +// ── AbstractLayerEditorCreator singleton lifecycle ──────────────────────── + +// Minimal concrete subclass used to exercise singleton registration. +class TestCreator final : public AbstractLayerEditorCreator +{ +public: + AbstractLayerEditorWindow* createWindow(const char*) override { return nullptr; } + AbstractLayerEditorWindow* getWindow(const char*) const override { return nullptr; } + PanelNamesList getAllPanelNames() const override { return {}; } +}; + +TEST(AbstractLayerEditorCreatorTest, InstanceNonNullAfterConstruction) +{ + ASSERT_EQ(AbstractLayerEditorCreator::instance(), nullptr); + TestCreator creator; + EXPECT_EQ(AbstractLayerEditorCreator::instance(), &creator); +} + +TEST(AbstractLayerEditorCreatorTest, InstanceNullAfterDestruction) +{ + ASSERT_EQ(AbstractLayerEditorCreator::instance(), nullptr); + { + TestCreator creator; + ASSERT_NE(AbstractLayerEditorCreator::instance(), nullptr); + } + EXPECT_EQ(AbstractLayerEditorCreator::instance(), nullptr); +} + +// ── LayerEditorWindow delegation methods ───────────────────────────────── + +// Concrete subclass satisfying LayerEditorWindow's pure virtuals. +// Initializes _layerEditor so that treeView() is safe to call. +class TestableLayerEditorWindow : public LayerEditorWindow +{ +public: + explicit TestableLayerEditorWindow(SessionState& ss, QMainWindow* parent = nullptr) + : LayerEditorWindow("test_panel") + , _ss(&ss) + { + _layerEditor = new LayerEditorWidget(ss, parent); + } + + QMainWindow* getMainWindow() override { return nullptr; } + std::string dccObjectName() const override { return "test_obj"; } + void selectDccObject(const char*) override {} + SessionState* getSessionState() override { return _ss; } + +private: + SessionState* _ss; +}; + +class LayerEditorWindowTest : public LayerEditorTestFixture +{ +protected: + std::unique_ptr makeWindow() + { + return std::make_unique(_sessionState, _mainWindow); + } +}; + +TEST_F(LayerEditorWindowTest, SelectionLength_EmptySelection_ReturnsZero) +{ + auto w = makeWindow(); + EXPECT_EQ(w->selectionLength(), 0); +} + +// With no selection, every layer-state query must report the false/empty default. +TEST_F(LayerEditorWindowTest, AllQueries_NoSelection_ReturnFalse) +{ + auto w = makeWindow(); + EXPECT_FALSE(w->isInvalidLayer()); + EXPECT_FALSE(w->isSessionLayer()); + EXPECT_FALSE(w->isAnonymousLayer()); + EXPECT_FALSE(w->isSubLayer()); + EXPECT_FALSE(w->layerHasSubLayers()); + EXPECT_FALSE(w->layerIsMuted()); + EXPECT_FALSE(w->layerIsLocked()); +} + +} // namespace UsdLayerEditor +#endif diff --git a/test/lib/usdLayerEditor/cpp/testLayerLocking.cpp b/test/lib/usdLayerEditor/cpp/testLayerLocking.cpp new file mode 100644 index 0000000000..d901c80a46 --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/testLayerLocking.cpp @@ -0,0 +1,189 @@ +// 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. +// +#pragma once + +#include "testUtils.h" +#include "layerLocking.h" + +#include +#include + +#include + +#include + +PXR_NAMESPACE_USING_DIRECTIVE + +namespace UsdLayerEditor { + +class LayerLockingTest : public ::testing::Test +{ +protected: + void SetUp() override + { + forgetLockedLayers(); + forgetSystemLockedLayers(); + _layer = SdfLayer::CreateAnonymous("lock_test"); + } + void TearDown() override + { + if (_layer) { + _layer->SetPermissionToEdit(true); + _layer->SetPermissionToSave(true); + } + forgetLockedLayers(); + forgetSystemLockedLayers(); + } + SdfLayerRefPtr _layer; +}; + +TEST_F(LayerLockingTest, IsLayerLocked_FalseByDefault) +{ + EXPECT_FALSE(isLayerLocked(_layer)); +} + +TEST_F(LayerLockingTest, LockLayer_SetsLayerAsLocked) +{ + lockLayer("", _layer, LayerLock_Locked, /*updateDCCAttr=*/false); + EXPECT_TRUE(isLayerLocked(_layer)); +} + +TEST_F(LayerLockingTest, UnlockLayer_SetsLayerAsUnlocked) +{ + lockLayer("", _layer, LayerLock_Locked, false); + lockLayer("", _layer, LayerLock_Unlocked, false); + EXPECT_FALSE(isLayerLocked(_layer)); +} + +TEST_F(LayerLockingTest, LockLayer_RevokesPermissionToEdit) +{ + lockLayer("", _layer, LayerLock_Locked, false); + EXPECT_FALSE(_layer->PermissionToEdit()); +} + +TEST_F(LayerLockingTest, UnlockLayer_RestoresPermissionToEdit) +{ + lockLayer("", _layer, LayerLock_Locked, false); + lockLayer("", _layer, LayerLock_Unlocked, false); + EXPECT_TRUE(_layer->PermissionToEdit()); +} + +TEST_F(LayerLockingTest, SystemLockLayer_SetsSystemLocked) +{ + lockLayer("", _layer, LayerLock_SystemLocked, false); + EXPECT_TRUE(isLayerSystemLocked(_layer)); +} + +TEST_F(LayerLockingTest, SystemLockLayer_RevokesPermissionToEdit) +{ + lockLayer("", _layer, LayerLock_SystemLocked, false); + EXPECT_FALSE(_layer->PermissionToEdit()); + EXPECT_TRUE(isLayerSystemLocked(_layer)); +} + +TEST_F(LayerLockingTest, ForgetLockedLayers_ClearsAllState) +{ + lockLayer("", _layer, LayerLock_Locked, false); + ASSERT_TRUE(isLayerLocked(_layer)); + forgetLockedLayers(); + EXPECT_FALSE(isLayerLocked(_layer)); +} + +TEST_F(LayerLockingTest, AddLockedLayer_AppearsInLockedList) +{ + addLockedLayer(_layer); + EXPECT_TRUE(isLayerLocked(_layer)); +} + +TEST_F(LayerLockingTest, RemoveLockedLayer_DisappearsFromLockedList) +{ + addLockedLayer(_layer); + removeLockedLayer(_layer); + EXPECT_FALSE(isLayerLocked(_layer)); +} + +TEST_F(LayerLockingTest, AddSystemLockedLayer_AppearsInSystemLockedList) +{ + addSystemLockedLayer(_layer); + EXPECT_TRUE(isLayerSystemLocked(_layer)); +} + +TEST_F(LayerLockingTest, ForgetSystemLockedLayers_ClearsSystemLockedList) +{ + addSystemLockedLayer(_layer); + forgetSystemLockedLayers(); + EXPECT_FALSE(isLayerSystemLocked(_layer)); +} + +// ── getLockedLayersIdentifiers + loadLayerLockState (new editor only) ──────── +#ifndef MAYAUSD_OLD_LAYER_EDITOR + +TEST_F(LayerLockingTest, GetLockedLayersIdentifiers_EmptyWhenNoneAdded) +{ + std::vector ids = getLockedLayersIdentifiers(); + EXPECT_TRUE(ids.empty()); +} + +TEST_F(LayerLockingTest, GetLockedLayersIdentifiers_ContainsIdentifierAfterLock) +{ + lockLayer("", _layer, LayerLock_Locked, false); + auto ids = getLockedLayersIdentifiers(); + auto it = std::find(ids.begin(), ids.end(), _layer->GetIdentifier()); + EXPECT_NE(it, ids.end()); +} + +TEST_F(LayerLockingTest, GetLockedLayersIdentifiers_EmptyAfterForget) +{ + lockLayer("", _layer, LayerLock_Locked, false); + forgetLockedLayers(); + EXPECT_TRUE(getLockedLayersIdentifiers().empty()); +} + +TEST_F(LayerLockingTest, LoadLayerLockState_LocksListedLayer) +{ + auto stage = PXR_NS::UsdStage::CreateInMemory(); + stage->GetRootLayer()->InsertSubLayerPath(_layer->GetIdentifier(), 0); + std::vector locked = { _layer->GetIdentifier() }; + LayerNameMap nameMap; + loadLayerLockState(locked, nameMap, *stage); + EXPECT_TRUE(isLayerLocked(_layer)); +} + +TEST_F(LayerLockingTest, LoadLayerLockState_EmptyListLocksNothing) +{ + auto stage = PXR_NS::UsdStage::CreateInMemory(); + stage->GetRootLayer()->InsertSubLayerPath(_layer->GetIdentifier(), 0); + std::vector locked; + LayerNameMap nameMap; + loadLayerLockState(locked, nameMap, *stage); + EXPECT_FALSE(isLayerLocked(_layer)); +} + +TEST_F(LayerLockingTest, LoadLayerLockState_NameMapRemapsIdentifier) +{ + auto stage = PXR_NS::UsdStage::CreateInMemory(); + auto newLayer = SdfLayer::CreateAnonymous("remap_lock"); + stage->GetRootLayer()->InsertSubLayerPath(newLayer->GetIdentifier(), 0); + const std::string oldId = "anon:old-lock-id"; + const std::string newId = newLayer->GetIdentifier(); + LayerNameMap nameMap { { oldId, newId } }; + std::vector locked = { oldId }; + loadLayerLockState(locked, nameMap, *stage); + EXPECT_TRUE(isLayerLocked(newLayer)); +} + +#endif // MAYAUSD_OLD_LAYER_EDITOR + +} // namespace UsdLayerEditor diff --git a/test/lib/usdLayerEditor/cpp/testLayerLogicUtils.cpp b/test/lib/usdLayerEditor/cpp/testLayerLogicUtils.cpp new file mode 100644 index 0000000000..4ec2fe1486 --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/testLayerLogicUtils.cpp @@ -0,0 +1,179 @@ +#pragma once +// +// 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 "customLayerData.h" +#ifndef MAYAUSD_OLD_LAYER_EDITOR +// Layers:: utilities live in usdLayerEditor, which the old editor test binary +// does not link; only the guarded LayersTest cases below use them. +#include "layers.h" +#endif +#include "warningDialogs.h" + +#include +#include +#include +#include +#include + +#include +#include + +PXR_NAMESPACE_USING_DIRECTIVE + +namespace UsdLayerEditor { + +// ── CustomLayerData ─────────────────────────────────────────────────────────── + +TEST(CustomLayerDataTest, SetGetStringArray_RoundTrip) +{ + auto layer = SdfLayer::CreateAnonymous("cld_arr"); + VtArray data = {"alpha", "beta", "gamma"}; + CustomLayerData::setStringArray(data, layer, TfToken("myArr")); + EXPECT_EQ(CustomLayerData::getStringArray(layer, TfToken("myArr")), data); +} + +TEST(CustomLayerDataTest, GetStringArray_EmptyForAbsentKey) +{ + auto layer = SdfLayer::CreateAnonymous("cld_arr_missing"); + EXPECT_TRUE(CustomLayerData::getStringArray(layer, TfToken("noSuchKey")).empty()); +} + +TEST(CustomLayerDataTest, SetGetString_RoundTrip) +{ + auto layer = SdfLayer::CreateAnonymous("cld_str"); + CustomLayerData::setString("hello world", layer, TfToken("strKey")); + EXPECT_EQ(CustomLayerData::getString(layer, TfToken("strKey")), "hello world"); +} + +TEST(CustomLayerDataTest, GetString_EmptyForAbsentKey) +{ + auto layer = SdfLayer::CreateAnonymous("cld_str_missing"); + EXPECT_EQ(CustomLayerData::getString(layer, TfToken("absent")), ""); +} + +TEST(CustomLayerDataTest, SetStringArray_EmptyArrayClearsKey) +{ + auto layer = SdfLayer::CreateAnonymous("cld_clear"); + VtArray data = {"x"}; + CustomLayerData::setStringArray(data, layer, TfToken("k")); + CustomLayerData::setStringArray(VtArray{}, layer, TfToken("k")); + EXPECT_TRUE(CustomLayerData::getStringArray(layer, TfToken("k")).empty()); +} + +// ── Layers ──────────────────────────────────────────────────────────────────── +// Layers:: functions live in usdLayerEditor which the old editor test binary +// does not link (ODR conflict risk with LEGACY_SOURCES). Guard until a shim exists. +#ifndef MAYAUSD_OLD_LAYER_EDITOR + +TEST(LayersTest, GetLocalTargetLayerAsString_ReturnsSubLayerIdentifier) +{ + auto stage = PXR_NS::UsdStage::CreateInMemory(); + auto sublayer = SdfLayer::CreateAnonymous("tgt_str"); + stage->GetRootLayer()->InsertSubLayerPath(sublayer->GetIdentifier(), 0); + stage->SetEditTarget(UsdEditTarget(sublayer)); + EXPECT_EQ(Layers::getLocalTargetLayerAsString(stage), sublayer->GetIdentifier()); +} + +TEST(LayersTest, GetLocalTargetLayerAsString_RootLayerIsLocal) +{ + auto stage = PXR_NS::UsdStage::CreateInMemory(); + EXPECT_EQ(Layers::getLocalTargetLayerAsString(stage), stage->GetRootLayer()->GetIdentifier()); +} + +TEST(LayersTest, GetLocalTargetLayerFromString_FindsByIdentifier) +{ + auto stage = PXR_NS::UsdStage::CreateInMemory(); + auto sublayer = SdfLayer::CreateAnonymous("find_by_id"); + stage->GetRootLayer()->InsertSubLayerPath(sublayer->GetIdentifier(), 0); + Layers::LayerNameMap nameMap; + auto result = Layers::getLocalTargetLayerFromString(nameMap, *stage, sublayer->GetIdentifier()); + EXPECT_TRUE(result); + EXPECT_EQ(result->GetIdentifier(), sublayer->GetIdentifier()); +} + +TEST(LayersTest, GetLocalTargetLayerFromString_EmptyIdentifierReturnsNull) +{ + auto stage = PXR_NS::UsdStage::CreateInMemory(); + Layers::LayerNameMap nameMap; + EXPECT_FALSE(Layers::getLocalTargetLayerFromString(nameMap, *stage, "")); +} + +TEST(LayersTest, GetLocalTargetLayerFromString_NameMapRemapsIdentifier) +{ + auto stage = PXR_NS::UsdStage::CreateInMemory(); + auto layer = SdfLayer::CreateAnonymous("remap_target"); + stage->GetRootLayer()->InsertSubLayerPath(layer->GetIdentifier(), 0); + const std::string oldId = "anon:old-target-id"; + Layers::LayerNameMap nameMap = { { oldId, layer->GetIdentifier() } }; + auto result = Layers::getLocalTargetLayerFromString(nameMap, *stage, oldId); + EXPECT_TRUE(result); + EXPECT_EQ(result->GetIdentifier(), layer->GetIdentifier()); +} + +TEST(LayersTest, GetLocalTargetLayerFromString_UnknownIdentifierReturnsNull) +{ + auto stage = PXR_NS::UsdStage::CreateInMemory(); + Layers::LayerNameMap nameMap; + EXPECT_FALSE(Layers::getLocalTargetLayerFromString(nameMap, *stage, "anon:nonexistent")); +} + +#endif // !MAYAUSD_OLD_LAYER_EDITOR + +// ── warningDialogs ──────────────────────────────────────────────────────────── +// Old editor's confirmDialog/warningDialog take QWidget* as first arg; new +// editor's signatures are (const QString&, const QString&, ...) — guard these. +#ifndef MAYAUSD_OLD_LAYER_EDITOR + +TEST(WarningDialogsTest, ConfirmDialog_HandlerReturnsTrueReturnsTrue) +{ + auto prev = setModalDialogTestHandler([](const QString&, const QString&) { return true; }); + bool result = confirmDialog("Title", "Message"); + setModalDialogTestHandler(prev); + EXPECT_TRUE(result); +} + +TEST(WarningDialogsTest, ConfirmDialog_HandlerReturnsFalseReturnsFalse) +{ + auto prev = setModalDialogTestHandler([](const QString&, const QString&) { return false; }); + bool result = confirmDialog("Title", "Message"); + setModalDialogTestHandler(prev); + EXPECT_FALSE(result); +} + +TEST(WarningDialogsTest, WarningDialog_HandlerIsCalled) +{ + int count = 0; + auto prev = setModalDialogTestHandler([&](const QString&, const QString&) { + ++count; return true; + }); + warningDialog("W Title", "W Message"); + setModalDialogTestHandler(prev); + EXPECT_EQ(count, 1); +} + +TEST(WarningDialogsTest, SetModalDialogTestHandler_ReturnsPrevious) +{ + ModalDialogTestHandler h1 = [](const QString&, const QString&) { return true; }; + auto orig = setModalDialogTestHandler(h1); // capture original + ModalDialogTestHandler h2 = [](const QString&, const QString&) { return false; }; + auto prev2 = setModalDialogTestHandler(h2); + EXPECT_TRUE(static_cast(prev2)); // prev2 should be h1 + setModalDialogTestHandler(orig); // restore original +} + +#endif // !MAYAUSD_OLD_LAYER_EDITOR + +} // namespace UsdLayerEditor diff --git a/test/lib/usdLayerEditor/cpp/testLayerMuting.cpp b/test/lib/usdLayerEditor/cpp/testLayerMuting.cpp new file mode 100644 index 0000000000..048d5cd387 --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/testLayerMuting.cpp @@ -0,0 +1,111 @@ +// 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. +// +#pragma once + +#include "testUtils.h" +#include "layerMuting.h" + +#include +#include + +#include + +PXR_NAMESPACE_USING_DIRECTIVE + +namespace UsdLayerEditor { + +class LayerMutingTest : public ::testing::Test +{ +protected: + void SetUp() override + { + forgetMutedLayers(); + _stage = PXR_NS::UsdStage::CreateInMemory(); + _layer = SdfLayer::CreateAnonymous("mute_test"); + _stage->GetRootLayer()->InsertSubLayerPath(_layer->GetIdentifier(), 0); + } + void TearDown() override + { + if (_stage && _layer) + _stage->UnmuteLayer(_layer->GetIdentifier()); + forgetMutedLayers(); + } + UsdStageRefPtr _stage; + SdfLayerRefPtr _layer; +}; + +// ── loadLayerMuteState (new editor only) ───────────────────────────────────── +#ifndef MAYAUSD_OLD_LAYER_EDITOR + +TEST_F(LayerMutingTest, LoadLayerMuteState_MutesListedLayer) +{ + // loadLayerMuteState should mute a layer whose identifier appears in the list. + std::vector muted = { _layer->GetIdentifier() }; + LayerNameMap nameMap; + loadLayerMuteState(muted, nameMap, *_stage); + EXPECT_TRUE(_stage->IsLayerMuted(_layer->GetIdentifier())); +} + +TEST_F(LayerMutingTest, LoadLayerMuteState_EmptyListMutesNothing) +{ + std::vector muted; + LayerNameMap nameMap; + loadLayerMuteState(muted, nameMap, *_stage); + EXPECT_FALSE(_stage->IsLayerMuted(_layer->GetIdentifier())); +} + +TEST_F(LayerMutingTest, LoadLayerMuteState_NameMapRemapsIdentifier) +{ + // When an anonymous layer is saved and reloaded its identifier changes. + // The nameMap allows mapping the old identifier to the new one. + auto newLayer = SdfLayer::CreateAnonymous("remapped"); + _stage->GetRootLayer()->InsertSubLayerPath(newLayer->GetIdentifier(), 1); + const std::string oldId = "anon:old-identifier"; + const std::string newId = newLayer->GetIdentifier(); + LayerNameMap nameMap { { oldId, newId } }; + std::vector muted = { oldId }; + loadLayerMuteState(muted, nameMap, *_stage); + EXPECT_TRUE(_stage->IsLayerMuted(newId)); +} + +#endif // MAYAUSD_OLD_LAYER_EDITOR + +// ── getMutedLayers ──────────────────────────────────────────────────────────── + +TEST_F(LayerMutingTest, GetMutedLayers_ReturnsEmptyForUnknownIdentifier) +{ + // No layer has been added — querying any identifier must return an empty set. + const LayerRefSet& result = getMutedLayers("anon:does-not-exist"); + EXPECT_TRUE(result.empty()); +} + +TEST_F(LayerMutingTest, GetMutedLayers_ReturnsHeldLayerAfterAdd) +{ + // After addMutedLayer the layer must appear in the held set. + addMutedLayer(_layer); + const LayerRefSet& result = getMutedLayers(_layer->GetIdentifier()); + EXPECT_FALSE(result.empty()); + EXPECT_NE(result.find(_layer), result.end()); +} + +TEST_F(LayerMutingTest, GetMutedLayers_ReturnsEmptyAfterRemove) +{ + addMutedLayer(_layer); + removeMutedLayer(_layer); + const LayerRefSet& result = getMutedLayers(_layer->GetIdentifier()); + EXPECT_TRUE(result.empty()); +} + +} // namespace UsdLayerEditor diff --git a/test/lib/usdLayerEditor/cpp/testLayerTreeItem.cpp b/test/lib/usdLayerEditor/cpp/testLayerTreeItem.cpp new file mode 100644 index 0000000000..b872423578 --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/testLayerTreeItem.cpp @@ -0,0 +1,599 @@ +// 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. +// +#pragma once + +#include +#include "testUtils.h" +#include "layerLocking.h" +#include "layerTreeItem.h" + +#include +#include + +#include + +PXR_NAMESPACE_USING_DIRECTIVE + +namespace UsdLayerEditor { + +static LayerTreeItem* itemAt(LayerTreeModel* model, const QModelIndex& idx) +{ + return dynamic_cast(model->itemFromIndex(idx)); +} + +class LayerTreeItemTest : public LayerEditorTestFixture +{ +protected: + void TearDown() override + { + LayerEditorTestFixture::TearDown(); + forgetLockedLayers(); + forgetSystemLockedLayers(); + } +}; + +// ── isMuted / appearsMuted ──────────────────────────────────────────────────── + +TEST_F(LayerTreeItemTest, IsMuted_ReturnsFalseByDefault) +{ + auto* item = itemAt(treeModel(), firstSublayerIndex()); + ASSERT_NE(item, nullptr); + EXPECT_FALSE(item->isMuted()); +} + +TEST_F(LayerTreeItemTest, IsMuted_ReturnsTrueAfterStageMute) +{ + auto* item = itemAt(treeModel(), firstSublayerIndex()); + ASSERT_NE(item, nullptr); + auto stage = _sessionState.stage(); + stage->MuteLayer(item->layer()->GetIdentifier()); + QApplication::processEvents(); + EXPECT_TRUE(item->isMuted()); + stage->UnmuteLayer(item->layer()->GetIdentifier()); +} + +TEST_F(LayerTreeItemTest, AppearsMuted_FalseWhenNeitherSelfNorParentMuted) +{ + auto* item = itemAt(treeModel(), firstSublayerIndex()); + ASSERT_NE(item, nullptr); + EXPECT_FALSE(item->appearsMuted()); +} + +TEST_F(LayerTreeItemTest, AppearsMuted_TrueWhenSelfIsMuted) +{ + auto* item = itemAt(treeModel(), firstSublayerIndex()); + ASSERT_NE(item, nullptr); + auto stage = _sessionState.stage(); + stage->MuteLayer(item->layer()->GetIdentifier()); + QApplication::processEvents(); + EXPECT_TRUE(item->appearsMuted()); + stage->UnmuteLayer(item->layer()->GetIdentifier()); +} + +TEST_F(LayerTreeItemTest, AppearsMuted_TrueWhenParentIsMuted) +{ + // The stage root cannot be muted, so build root -> sublayer -> subSub and + // mute the (mutable) sublayer to verify appearsMuted() propagates to its child. + auto* sublayerItem = itemAt(treeModel(), firstSublayerIndex()); + ASSERT_NE(sublayerItem, nullptr); + auto sublayer = sublayerItem->layer(); // keep alive across model rebuilds + auto subSub = PXR_NS::SdfLayer::CreateAnonymous("subSub"); + sublayer->InsertSubLayerPath(subSub->GetIdentifier(), 0); + treeModel()->forceRefresh(); + QApplication::processEvents(); + + auto stage = _sessionState.stage(); + stage->MuteLayer(sublayer->GetIdentifier()); + QApplication::processEvents(); + + // Re-fetch after mutations; the muted sublayer keeps its authored child. + auto* child = itemAt(treeModel(), treeModel()->index(0, 0, firstSublayerIndex())); + ASSERT_NE(child, nullptr); + EXPECT_EQ(child->layer(), subSub); + EXPECT_TRUE(child->appearsMuted()); + + stage->UnmuteLayer(sublayer->GetIdentifier()); +} + +// ── isReadOnly ──────────────────────────────────────────────────────────────── + +TEST_F(LayerTreeItemTest, IsReadOnly_FalseForNormalSublayer) +{ + auto* item = itemAt(treeModel(), firstSublayerIndex()); + ASSERT_NE(item, nullptr); + EXPECT_FALSE(item->isReadOnly()); +} + +// ── isDirty / needsSaving ───────────────────────────────────────────────────── + +TEST_F(LayerTreeItemTest, IsDirty_FalseForCleanLayer) +{ + auto* item = itemAt(treeModel(), firstSublayerIndex()); + ASSERT_NE(item, nullptr); + EXPECT_FALSE(item->isDirty()); +} + +TEST_F(LayerTreeItemTest, IsDirty_TrueAfterLayerModified) +{ + auto* item = itemAt(treeModel(), firstSublayerIndex()); + ASSERT_NE(item, nullptr); + item->layer()->SetComment("mark dirty"); + EXPECT_TRUE(item->isDirty()); +} + +TEST_F(LayerTreeItemTest, NeedsSaving_FalseForSessionLayer) +{ + auto* item = itemAt(treeModel(), sessionLayerIndex()); + ASSERT_NE(item, nullptr); + item->layer()->SetComment("mark dirty"); + // Session layer: needsSaving always false regardless of dirty state. + EXPECT_FALSE(item->needsSaving()); +} + +// ── isLocked / appearsLocked ────────────────────────────────────────────────── + +TEST_F(LayerTreeItemTest, IsLocked_FalseByDefault) +{ + auto* item = itemAt(treeModel(), firstSublayerIndex()); + ASSERT_NE(item, nullptr); + EXPECT_FALSE(item->isLocked()); +} + +TEST_F(LayerTreeItemTest, IsLocked_TrueWhenPermissionToEditRevoked) +{ + auto* item = itemAt(treeModel(), firstSublayerIndex()); + ASSERT_NE(item, nullptr); + TestUtils::lockLayerDirect(item->layer()); + EXPECT_TRUE(item->isLocked()); + TestUtils::unlockLayerDirect(item->layer()); +} + +TEST_F(LayerTreeItemTest, AppearsLocked_FalseForRootItemWithUnlockedSelf) +{ + auto* item = itemAt(treeModel(), rootLayerIndex()); + ASSERT_NE(item, nullptr); + EXPECT_FALSE(item->appearsLocked()); +} + +TEST_F(LayerTreeItemTest, AppearsLocked_TrueWhenParentIsLocked) +{ + // The sublayer's parent in the tree is the root layer item. + auto* parentItem = itemAt(treeModel(), rootLayerIndex()); + ASSERT_NE(parentItem, nullptr); + TestUtils::lockLayerDirect(parentItem->layer()); + + auto* child = itemAt(treeModel(), firstSublayerIndex()); + ASSERT_NE(child, nullptr); + EXPECT_TRUE(child->appearsLocked()); + + TestUtils::unlockLayerDirect(parentItem->layer()); +} + +TEST_F(LayerTreeItemTest, AppearsLocked_DoesNotCheckSelf) +{ + // A locked item does NOT report appearsLocked for itself — only propagation from parent. + auto* item = itemAt(treeModel(), firstSublayerIndex()); + ASSERT_NE(item, nullptr); + TestUtils::lockLayerDirect(item->layer()); + EXPECT_FALSE(item->appearsLocked()); + TestUtils::unlockLayerDirect(item->layer()); +} + +// ── isSystemLocked / appearsSystemLocked ────────────────────────────────────── + +TEST_F(LayerTreeItemTest, IsSystemLocked_FalseByDefault) +{ + auto* item = itemAt(treeModel(), firstSublayerIndex()); + ASSERT_NE(item, nullptr); + EXPECT_FALSE(item->isSystemLocked()); +} + +TEST_F(LayerTreeItemTest, IsSystemLocked_TrueAfterSystemLockApplied) +{ + auto* item = itemAt(treeModel(), firstSublayerIndex()); + ASSERT_NE(item, nullptr); + addSystemLockedLayer(item->layer()); + item->layer()->SetPermissionToEdit(false); + EXPECT_TRUE(item->isSystemLocked()); + removeSystemLockedLayer(item->layer()); + TestUtils::unlockLayerDirect(item->layer()); +} + +TEST_F(LayerTreeItemTest, AppearsSystemLocked_FalseWhenParentNotSystemLocked) +{ + auto* item = itemAt(treeModel(), firstSublayerIndex()); + ASSERT_NE(item, nullptr); + EXPECT_FALSE(item->appearsSystemLocked()); +} + +// ── isMovable ───────────────────────────────────────────────────────────────── + +TEST_F(LayerTreeItemTest, IsMovable_FalseForSessionLayer) +{ + auto* item = itemAt(treeModel(), sessionLayerIndex()); + ASSERT_NE(item, nullptr); + EXPECT_FALSE(item->isMovable()); +} + +TEST_F(LayerTreeItemTest, IsMovable_FalseForRootLayer) +{ + auto* item = itemAt(treeModel(), rootLayerIndex()); + ASSERT_NE(item, nullptr); + EXPECT_FALSE(item->isMovable()); +} + +TEST_F(LayerTreeItemTest, IsMovable_TrueForNormalSublayer) +{ + auto* item = itemAt(treeModel(), firstSublayerIndex()); + ASSERT_NE(item, nullptr); + EXPECT_TRUE(item->isMovable()); +} + +TEST_F(LayerTreeItemTest, IsMovable_FalseWhenLocked) +{ + auto* item = itemAt(treeModel(), firstSublayerIndex()); + ASSERT_NE(item, nullptr); + TestUtils::lockLayerDirect(item->layer()); + EXPECT_FALSE(item->isMovable()); + TestUtils::unlockLayerDirect(item->layer()); +} + +TEST_F(LayerTreeItemTest, IsMovable_FalseWhenAppearsLocked) +{ + auto* parent = itemAt(treeModel(), rootLayerIndex()); + TestUtils::lockLayerDirect(parent->layer()); + auto* item = itemAt(treeModel(), firstSublayerIndex()); + ASSERT_NE(item, nullptr); + EXPECT_FALSE(item->isMovable()); + TestUtils::unlockLayerDirect(parent->layer()); +} + +TEST_F(LayerTreeItemTest, IsMovable_FalseWhenMuted) +{ + auto* item = itemAt(treeModel(), firstSublayerIndex()); + ASSERT_NE(item, nullptr); + _sessionState.stage()->MuteLayer(item->layer()->GetIdentifier()); + QApplication::processEvents(); + EXPECT_FALSE(item->isMovable()); + _sessionState.stage()->UnmuteLayer(item->layer()->GetIdentifier()); +} + +// ── misc ────────────────────────────────────────────────────────────────────── + +TEST_F(LayerTreeItemTest, IsTargetLayer_TrueForCurrentEditTarget) +{ + // Root layer is the default edit target. + auto* root = itemAt(treeModel(), rootLayerIndex()); + ASSERT_NE(root, nullptr); + EXPECT_TRUE(root->isTargetLayer()); +} + +TEST_F(LayerTreeItemTest, IsTargetLayer_FalseForNonTargetLayer) +{ + // Root layer is the default edit target, so the sublayer is not the target. + auto* sub = itemAt(treeModel(), firstSublayerIndex()); + ASSERT_NE(sub, nullptr); + EXPECT_FALSE(sub->isTargetLayer()); +} + +TEST_F(LayerTreeItemTest, HasSubLayers_TrueWhenSublayersExist) +{ + // StubSessionState creates a root layer with one sublayer. + auto* root = itemAt(treeModel(), rootLayerIndex()); + ASSERT_NE(root, nullptr); + EXPECT_TRUE(root->hasSubLayers()); +} + +TEST_F(LayerTreeItemTest, HasSubLayers_FalseForLeafSublayer) +{ + auto* sub = itemAt(treeModel(), firstSublayerIndex()); + ASSERT_NE(sub, nullptr); + EXPECT_FALSE(sub->hasSubLayers()); +} + +TEST_F(LayerTreeItemTest, IsAnonymous_TrueForAnonymousLayer) +{ + auto* sub = itemAt(treeModel(), firstSublayerIndex()); + ASSERT_NE(sub, nullptr); + // StubSessionState creates anonymous sublayers. + EXPECT_TRUE(sub->isAnonymous()); +} + +TEST_F(LayerTreeItemTest, GetActionButton_LockCheckedMatchesIsLocked) +{ + auto* item = itemAt(treeModel(), firstSublayerIndex()); + ASSERT_NE(item, nullptr); + TestUtils::lockLayerDirect(item->layer()); + + LayerActionInfo info; + item->getActionButton(LayerActionType::Lock, info); + EXPECT_TRUE(info._checked); + + TestUtils::unlockLayerDirect(item->layer()); +} + +TEST_F(LayerTreeItemTest, GetActionButton_MuteCheckedMatchesIsMuted) +{ + auto* item = itemAt(treeModel(), firstSublayerIndex()); + ASSERT_NE(item, nullptr); + _sessionState.stage()->MuteLayer(item->layer()->GetIdentifier()); + QApplication::processEvents(); + + LayerActionInfo info; + item->getActionButton(LayerActionType::Mute, info); + EXPECT_TRUE(info._checked); + + _sessionState.stage()->UnmuteLayer(item->layer()->GetIdentifier()); +} + +TEST_F(LayerTreeItemTest, ActionButtons_MuteAppliesToSublayerOnly) +{ + const auto& buttons = LayerTreeItem::actionButtonsDefinition(); + auto it = buttons.find(LayerActionType::Mute); + ASSERT_NE(it, buttons.end()); + EXPECT_TRUE(IsLayerActionAllowed(it->second, LayerMasks_SubLayer)); + EXPECT_FALSE(IsLayerActionAllowed(it->second, LayerMasks_Root)); +} + +TEST_F(LayerTreeItemTest, ActionButtons_LockAppliesToRootAndSublayer) +{ + const auto& buttons = LayerTreeItem::actionButtonsDefinition(); + auto it = buttons.find(LayerActionType::Lock); + ASSERT_NE(it, buttons.end()); + EXPECT_TRUE(IsLayerActionAllowed(it->second, LayerMasks_Root)); + EXPECT_TRUE(IsLayerActionAllowed(it->second, LayerMasks_SubLayer)); +} + +// ── depth ───────────────────────────────────────────────────────────────────── + +TEST_F(LayerTreeItemTest, Depth_IsZeroForSessionLayerItem) +{ + auto* item = itemAt(treeModel(), sessionLayerIndex()); + ASSERT_NE(item, nullptr); + EXPECT_EQ(item->depth(), 0); +} + +TEST_F(LayerTreeItemTest, Depth_IsZeroForRootLayerItem) +{ + auto* item = itemAt(treeModel(), rootLayerIndex()); + ASSERT_NE(item, nullptr); + EXPECT_EQ(item->depth(), 0); +} + +TEST_F(LayerTreeItemTest, Depth_IsOneForFirstSublayer) +{ + auto* item = itemAt(treeModel(), firstSublayerIndex()); + ASSERT_NE(item, nullptr); + EXPECT_EQ(item->depth(), 1); +} + +// ── childrenVector ──────────────────────────────────────────────────────────── + +TEST_F(LayerTreeItemTest, ChildrenVector_RootItemHasOneSublayer) +{ + // The stub stage was created with one anonymous sublayer on the root. + auto* item = itemAt(treeModel(), rootLayerIndex()); + ASSERT_NE(item, nullptr); + EXPECT_EQ(item->childrenVector().size(), 1u); +} + +TEST_F(LayerTreeItemTest, ChildrenVector_EmptyForLeafSublayer) +{ + // The single sublayer has no children of its own. + auto* item = itemAt(treeModel(), firstSublayerIndex()); + ASSERT_NE(item, nullptr); + EXPECT_TRUE(item->childrenVector().empty()); +} + +// ── isIdenticalItem ──────────────────────────────────────────────────────────── + +#ifndef MAYAUSD_OLD_LAYER_EDITOR +TEST_F(LayerTreeItemTest, IsIdenticalItem_NullOtherReturnsFalse) +{ + auto* item = itemAt(treeModel(), firstSublayerIndex()); + ASSERT_NE(item, nullptr); + EXPECT_FALSE(item->isIdenticalItem(nullptr)); +} + +TEST_F(LayerTreeItemTest, IsIdenticalItem_SamePointerReturnsTrue) +{ + auto* item = itemAt(treeModel(), firstSublayerIndex()); + ASSERT_NE(item, nullptr); + EXPECT_TRUE(item->isIdenticalItem(item)); +} + +TEST_F(LayerTreeItemTest, IsIdenticalItem_DifferentLayerReturnsFalse) +{ + // Root layer item vs first sublayer item — different layers. + auto* root = itemAt(treeModel(), treeModel()->rootLayerIndex()); + ASSERT_NE(root, nullptr); + auto* sub = itemAt(treeModel(), firstSublayerIndex()); + ASSERT_NE(sub, nullptr); + EXPECT_FALSE(root->isIdenticalItem(sub)); +} +#endif + +// ── saveAnonymousLayer early-out ─────────────────────────────────────── + +// A non-component stage's anonymous layer goes through the generic save path +// (SessionState::saveLayerUI), which the stub records via _saveLayerCallCount. +TEST_F(LayerTreeItemTest, SaveAnonymousLayer_NonComponentStage_UsesGenericPath) +{ +#ifndef MAYAUSD_OLD_LAYER_EDITOR + // Configures the new editor's component early-out to treat the stage as a + // non-component. The old editor has no such early-out and ignores the DCC + // registry, so this setup is omitted there (and the registry isn't linked). + ScopedLayerEditorDCCFunctions guard; + ComponentFns comp; + comp.isStageAComponent = [](const std::string&) { return false; }; + setComponentFns(comp); +#endif + + auto* item = itemAt(treeModel(), firstSublayerIndex()); + ASSERT_NE(item, nullptr); + ASSERT_TRUE(item->isAnonymous()); + + _sessionState._saveLayerCallCount = 0; + item->saveEditsNoPrompt(nullptr); + QApplication::processEvents(); + + EXPECT_EQ(_sessionState._saveLayerCallCount, 1) + << "non-component anonymous layer should use the generic saveLayerUI path"; +} + +// A component stage's anonymous layer must NOT take the generic save path; the +// early-out delegates to LayerTreeModel::saveStage (which shows a modal +// SaveLayersDialog, dismissed here). _saveLayerCallCount staying 0 proves the +// generic path was skipped -- it would be 1 if the early-out were missing. +#ifndef MAYAUSD_OLD_LAYER_EDITOR +TEST_F(LayerTreeItemTest, SaveAnonymousLayer_ComponentStage_SkipsGenericPath) +{ + ScopedLayerEditorDCCFunctions guard; + ComponentFns comp; + comp.isStageAComponent = [](const std::string&) { return true; }; + // isAnonymous() now reflects unsaved-component state for component stages; + // an unsaved component reports anonymous, which the save early-out requires. + comp.isUnsavedComponent = [](const PXR_NS::UsdStageRefPtr&) { return true; }; + setComponentFns(comp); + + auto* item = itemAt(treeModel(), firstSublayerIndex()); + ASSERT_NE(item, nullptr); + ASSERT_TRUE(item->isAnonymous()); + + // saveStage shows a modal SaveLayersDialog; schedule its dismissal so exec() returns. + TestUtils::dismissNextModal(100); + + _sessionState._saveLayerCallCount = 0; + item->saveEditsNoPrompt(nullptr); + QApplication::processEvents(); + + EXPECT_EQ(_sessionState._saveLayerCallCount, 0) + << "component stage should delegate to saveStage, skipping the generic " + "anonymous-save path"; +} +#endif + +// ── isAnonymous component override ───────────────────────── + +#ifndef MAYAUSD_OLD_LAYER_EDITOR +TEST_F(LayerTreeItemTest, IsAnonymous_FalseForSavedComponent) +{ + setIsComponent(true); + setIsUnsavedComponent(false); + + LayerTreeItem* root = itemAt(treeModel(), rootLayerIndex()); + ASSERT_NE(root, nullptr); + // Root layer is anonymous (in-memory stage); the component override must + // make a SAVED component report not-anonymous, flipping the layer flag. + EXPECT_FALSE(root->isAnonymous()); +} +#endif + +TEST_F(LayerTreeItemTest, IsAnonymous_TrueForUnsavedComponent) +{ + setIsComponent(true); + setIsUnsavedComponent(true); + + LayerTreeItem* root = itemAt(treeModel(), rootLayerIndex()); + ASSERT_NE(root, nullptr); + EXPECT_TRUE(root->isAnonymous()); +} + +#ifndef MAYAUSD_OLD_LAYER_EDITOR +TEST_F(LayerTreeItemTest, SaveEdits_ComponentRoutesToSaveStageSkippingOverwriteConfirm) +{ + // A component reports non-anonymous when saved, which would normally make + // saveEdits show its overwrite-confirm dialog. The component early-exit must + // route straight to saveStage (the component creator owns the save flow) + // before that confirm is ever shown -- matching the old editor. + setIsComponent(true); + setIsUnsavedComponent(false); + _confirmExistingFileSave = true; + _modalDialogCount = 0; + + LayerTreeItem* root = itemAt(treeModel(), rootLayerIndex()); + ASSERT_NE(root, nullptr); + + root->saveEdits(nullptr); + + EXPECT_EQ(_modalDialogCount, 0) + << "component saveEdits must route to saveStage before the overwrite-confirm dialog"; +} + +TEST_F(LayerTreeItemTest, DiscardEdits_ComponentStageConfirmsThenReloadsComponent) +{ + // A saved component (isUnsavedComponent=false) reports non-anonymous, so a dirty + // one must prompt for confirmation FIRST and then reload as a unit -- matching the + // old editor, which checks the component only after the revert confirmation. + setIsComponent(true); + setIsUnsavedComponent(false); + _reloadComponentCalls = 0; + _modalDialogCount = 0; + + LayerTreeItem* root = itemAt(treeModel(), rootLayerIndex()); + ASSERT_NE(root, nullptr); + root->layer()->SetComment("make dirty"); // force the confirmation path + + root->discardEdits(nullptr); + + EXPECT_EQ(_modalDialogCount, 1) + << "a dirty saved component must be confirmed before reloading"; + EXPECT_EQ(_reloadComponentCalls, 1) + << "after confirmation, discardEdits on a component must route through reloadComponent"; +} +#endif + +// ── type() ─────────────────────────────────────────────────────────────────── + +TEST_F(LayerTreeItemTest, Type_ReturnsUserType) +{ + auto* item = itemAt(treeModel(), firstSublayerIndex()); + ASSERT_NE(item, nullptr); + EXPECT_EQ(item->type(), QStandardItem::UserType); +} + +// ── invalid layer (unresolvable sublayer path) ──────────────────────────────── + +TEST_F(LayerTreeItemTest, FetchData_InvalidLayer_DisplayNameIsSubLayerPath) +{ + // Insert a path that cannot be resolved to exercise the invalid-layer + // branch of fetchData() where _displayName is set from _subLayerPath. + const std::string fakePath = "/nonexistent/fake_display_test.usda"; + _sessionState.stage()->GetRootLayer()->InsertSubLayerPath(fakePath, 0); + treeModel()->forceRefresh(); + QApplication::processEvents(); + + auto* invalid = itemAt(treeModel(), treeModel()->index(0, 0, rootLayerIndex())); + ASSERT_NE(invalid, nullptr); + ASSERT_TRUE(invalid->isInvalidLayer()); + EXPECT_EQ(invalid->displayName(), fakePath); +} + +TEST_F(LayerTreeItemTest, HasSubLayers_ReturnsFalse_ForInvalidLayer) +{ + // An invalid layer item has _layer == nullptr so hasSubLayers() must return false + // via the early-out guard, not the GetNumSubLayerPaths() path. + const std::string fakePath = "/nonexistent/fake_has_sublayers.usda"; + _sessionState.stage()->GetRootLayer()->InsertSubLayerPath(fakePath, 0); + treeModel()->forceRefresh(); + QApplication::processEvents(); + + auto* invalid = itemAt(treeModel(), treeModel()->index(0, 0, rootLayerIndex())); + ASSERT_NE(invalid, nullptr); + ASSERT_TRUE(invalid->isInvalidLayer()); + EXPECT_FALSE(invalid->hasSubLayers()); +} + +} // namespace UsdLayerEditor diff --git a/test/lib/usdLayerEditor/cpp/testLayerTreeItemDelegate.cpp b/test/lib/usdLayerEditor/cpp/testLayerTreeItemDelegate.cpp new file mode 100644 index 0000000000..3c6788a627 --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/testLayerTreeItemDelegate.cpp @@ -0,0 +1,148 @@ +// 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. +// +#pragma once + +#include + +#include "layerTreeItemDelegate.h" +#include "layerLocking.h" +#include "layerMuting.h" + +#include +#include +#include +#include +#include +#include + +#include + +namespace UsdLayerEditor { + +class LayerTreeItemDelegateTest : public LayerEditorTestFixture +{ +protected: + LayerTreeItemDelegate* delegate() + { + return qobject_cast(layerTree()->itemDelegate()); + } + + QStyleOptionViewItem styleOptionFor(const QModelIndex& idx) + { + QStyleOptionViewItem opt; + opt.rect = layerTree()->visualRect(idx); + opt.index = idx; + return opt; + } +}; + +// ── state accessors ─────────────────────────────────────────────────────────── + +TEST_F(LayerTreeItemDelegateTest, OnModelReset_ClearsState) +{ + ASSERT_NE(delegate(), nullptr); + delegate()->onModelReset(); + EXPECT_TRUE(delegate()->lastHitAction().isEmpty()); + EXPECT_FALSE(delegate()->isTargetPressed()); +} + +// ── editorEvent — non-mouse event ──────────────────────────────────────────── + +TEST_F(LayerTreeItemDelegateTest, EditorEvent_InvalidIndex_ReturnsFalse) +{ + // Exercises the invalid-index guard. + ASSERT_NE(delegate(), nullptr); + QStyleOptionViewItem opt; + QEvent ev(QEvent::KeyPress); + bool handled = delegate()->editorEvent(&ev, treeModel(), opt, QModelIndex()); + EXPECT_FALSE(handled); +} + +TEST_F(LayerTreeItemDelegateTest, EditorEvent_UnhandledEventType_ReturnsFalse) +{ + // Exercises the switch default: QEvent::KeyPress hits no case and returns false. + ASSERT_NE(delegate(), nullptr); + QModelIndex idx = firstSublayerIndex(); + ASSERT_TRUE(idx.isValid()); + QStyleOptionViewItem opt = styleOptionFor(idx); + QEvent ev(QEvent::KeyPress); + EXPECT_FALSE(delegate()->editorEvent(&ev, treeModel(), opt, idx)); +} + +// ── editorEvent — mouse move ────────────────────────────────────────────────── +// MouseMove only calls _treeView->update(); no dynamic_cast to QMouseEvent. + +TEST_F(LayerTreeItemDelegateTest, EditorEvent_MouseMove_ReturnsFalse) +{ + // Exercises the MouseMove case: only calls update() and returns false. + ASSERT_NE(delegate(), nullptr); + QModelIndex idx = firstSublayerIndex(); + ASSERT_TRUE(idx.isValid()); + QStyleOptionViewItem opt = styleOptionFor(idx); + QEvent ev(QEvent::MouseMove); + EXPECT_FALSE(delegate()->editorEvent(&ev, treeModel(), opt, idx)); +} + +// ── paint() — force full paint pipeline ────────────────────────────────────── + +// One smoke test exercising every item-state branch of paint(); a paint +// regression in any state (session/root/sublayer/selected/locked/muted) trips it. +TEST_F(LayerTreeItemDelegateTest, Paint_AllItemStates_DoesNotCrash) +{ + ASSERT_NE(delegate(), nullptr); + + // Render an index in a given state into its own QImage so distinct states + // can be pixel-compared. + auto renderIndex = [&](const QModelIndex& idx, QStyle::State state) { + EXPECT_TRUE(idx.isValid()); + QStyleOptionViewItem opt; + opt.rect = QRect(0, 0, 400, 22); + opt.state = state; + QPixmap pm(400, 22); + pm.fill(Qt::transparent); + QPainter p(&pm); + EXPECT_NO_THROW(delegate()->paint(&p, opt, idx)); + p.end(); + return pm.toImage(); + }; + + // Session / root rendering is exercised for crash coverage only; their layouts + // are not reliably distinct from a plain sublayer at this fixed rect. + EXPECT_NO_THROW(renderIndex(sessionLayerIndex(), QStyle::State_Enabled)); + EXPECT_NO_THROW(renderIndex(rootLayerIndex(), QStyle::State_Enabled)); + + // Headless rendering does not reliably differ between item states at a fixed + // rect, so assert each state paints a valid image rather than pixel-diffing. + QImage sublayer = renderIndex(firstSublayerIndex(), QStyle::State_Enabled); + QImage selected + = renderIndex(firstSublayerIndex(), QStyle::State_Enabled | QStyle::State_Selected); + EXPECT_FALSE(sublayer.isNull()); + EXPECT_EQ(sublayer.size(), QSize(400, 22)); + EXPECT_FALSE(selected.isNull()); + + auto* item = treeModel()->layerItemFromIndex(firstSublayerIndex()); + ASSERT_NE(item, nullptr); + + addMutedLayer(item->layer()); + QImage muted = renderIndex(firstSublayerIndex(), QStyle::State_Enabled); + removeMutedLayer(item->layer()); + EXPECT_FALSE(muted.isNull()); + + lockLayer("", item->layer(), LayerLock_Locked, false); + EXPECT_NO_THROW(renderIndex(firstSublayerIndex(), QStyle::State_Enabled)); + lockLayer("", item->layer(), LayerLock_Unlocked, false); +} + +} // namespace UsdLayerEditor diff --git a/test/lib/usdLayerEditor/cpp/testLayerTreeModel.cpp b/test/lib/usdLayerEditor/cpp/testLayerTreeModel.cpp new file mode 100644 index 0000000000..437f643e5b --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/testLayerTreeModel.cpp @@ -0,0 +1,356 @@ +// 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. +// +#pragma once + +#include +#include "testUtils.h" +#include "layerTreeItem.h" +#include "layerTreeModel.h" + +#include +#include +#include +#include + +#include +#include + +PXR_NAMESPACE_USING_DIRECTIVE + +namespace UsdLayerEditor { + +class LayerTreeModelTest : public LayerEditorTestFixture {}; + +// ── flags / MIME ─────────────────────────────────────────────────────────────── + +TEST_F(LayerTreeModelTest, Flags_DragEnabledOnlyForMovableItems) +{ + auto subFlags = treeModel()->flags(firstSublayerIndex()); + auto rootFlags = treeModel()->flags(rootLayerIndex()); + EXPECT_TRUE(subFlags & Qt::ItemIsDragEnabled); + EXPECT_FALSE(rootFlags & Qt::ItemIsDragEnabled); +} + +TEST_F(LayerTreeModelTest, Flags_DropAlwaysEnabled) +{ + EXPECT_TRUE(treeModel()->flags(rootLayerIndex()) & Qt::ItemIsDropEnabled); + EXPECT_TRUE(treeModel()->flags(firstSublayerIndex()) & Qt::ItemIsDropEnabled); +} + +TEST_F(LayerTreeModelTest, SupportedDropActions_OnlyMoveAction) +{ + EXPECT_EQ(treeModel()->supportedDropActions(), Qt::MoveAction); +} + +TEST_F(LayerTreeModelTest, MimeTypes_ReturnsTextPlain) +{ + auto types = treeModel()->mimeTypes(); + ASSERT_EQ(types.size(), 1); + EXPECT_EQ(types.at(0), QString("text/plain")); +} + +TEST_F(LayerTreeModelTest, MimeData_SerializesIdentifiersWithSemicolon) +{ + // Two indices so the ';' separator between serialized identifiers is exercised. + QModelIndexList indexes = { firstSublayerIndex(), rootLayerIndex() }; + std::unique_ptr mime(treeModel()->mimeData(indexes)); + ASSERT_NE(mime, nullptr); + EXPECT_TRUE(mime->hasFormat("text/plain")); + QString data = QString::fromUtf8(mime->data("text/plain")); + + auto* subItem = treeModel()->layerItemFromIndex(firstSublayerIndex()); + auto* rootItem = treeModel()->layerItemFromIndex(rootLayerIndex()); + ASSERT_NE(subItem, nullptr); + ASSERT_NE(rootItem, nullptr); + + // Identifiers are joined in index order, separated by ';'. + const QString expected = QString::fromStdString(subItem->layer()->GetIdentifier()) + ';' + + QString::fromStdString(rootItem->layer()->GetIdentifier()); + EXPECT_EQ(data, expected); +} + +TEST_F(LayerTreeModelTest, CanDrop_ReturnsFalseForNullMimeData) +{ + EXPECT_FALSE(treeModel()->canDropMimeData(nullptr, Qt::MoveAction, 0, 0, rootLayerIndex())); +} + +// ── rebuildModel / session layer visibility ──────────────────────────────────── + +TEST_F(LayerTreeModelTest, Rebuild_AlwaysShowsSessionLayerWhenAutoHideFalse) +{ + // StubSessionState::autoHideSessionLayer() returns false. + // Session layer must always be the first top-level item. + treeModel()->forceRefresh(); + QApplication::processEvents(); + auto* first = treeModel()->layerItemFromIndex(treeModel()->index(0, 0)); + ASSERT_NE(first, nullptr); + EXPECT_TRUE(first->isSessionLayer()); +} + +TEST_F(LayerTreeModelTest, RebuildOnIdle_DeduplicatesScheduling) +{ + // Calling forceRefresh twice before processing events should + // result in only one rebuild (not two). + int resetCount = 0; + QObject::connect(treeModel(), &QAbstractItemModel::modelReset, + [&resetCount]() { ++resetCount; }); + treeModel()->forceRefresh(); + treeModel()->forceRefresh(); + QApplication::processEvents(); + // The guard deduplicates the two explicit calls to one rebuild. However, rebuilding + // the model resets _rebuildOnIdlePending before endResetModel(), so a USD notice fired + // during item construction can schedule a second rebuild. + EXPECT_LE(resetCount, 2); +} + +TEST_F(LayerTreeModelTest, Rebuild_SkipsResetWhenLayersAreIdentical) +{ + // EMSUSD-3680: rebuilding the model when layer structure has not changed + // should not emit modelReset, to avoid redundant tree redraws. + QApplication::processEvents(); // let initial build settle + int resetCount = 0; + QObject::connect(treeModel(), &QAbstractItemModel::modelReset, + [&resetCount]() { ++resetCount; }); + // Force a second rebuild with the same layer state — should be a no-op. + treeModel()->forceRefresh(); + QApplication::processEvents(); + EXPECT_EQ(resetCount, 0) << "modelReset should not fire when layers are identical"; +} + +// ── filtering helpers ────────────────────────────────────────────────────────── + +TEST_F(LayerTreeModelTest, GetAllNeedsSavingLayers_EmptyWhenNoLayersAreDirtyAndShared) +{ + // StubSessionState is not a shared stage, so needsSaving = false for all. + auto layers = treeModel()->getAllNeedsSavingLayers(); + EXPECT_TRUE(layers.empty()); +} + +TEST_F(LayerTreeModelTest, GetAllAnonymousLayers_ExcludesSessionLayer) +{ + auto anonLayers = treeModel()->getAllAnonymousLayers(); + for (auto* item : anonLayers) { + EXPECT_FALSE(item->isSessionLayer()) + << "getAllAnonymousLayers should not include the session layer"; + } +} + +TEST_F(LayerTreeModelTest, GetAllAnonymousLayers_IncludesAnonymousSublayers) +{ + // The stub has one anonymous sublayer per stage. + auto anonLayers = treeModel()->getAllAnonymousLayers(); + EXPECT_GE(anonLayers.size(), 1u); +} + +TEST_F(LayerTreeModelTest, FindNameForNewAnonymousLayer_ReturnsNonEmptyString) +{ + std::string name = treeModel()->findNameForNewAnonymousLayer(); + EXPECT_FALSE(name.empty()); +} + +TEST_F(LayerTreeModelTest, FindNameForNewAnonymousLayer_DoesNotCollideWithExisting) +{ + // Call once, add a layer with that name, then ask again. + std::string name1 = treeModel()->findNameForNewAnonymousLayer(); + auto newLayer = SdfLayer::CreateAnonymous(name1); + _sessionState.stage()->GetRootLayer()->InsertSubLayerPath( + newLayer->GetIdentifier(), 0); + QApplication::processEvents(); + std::string name2 = treeModel()->findNameForNewAnonymousLayer(); + EXPECT_NE(name1, name2); +} + +// ── setEditTarget guards ─────────────────────────────────────────────────────── + +TEST_F(LayerTreeModelTest, SetEditTarget_CallsHookForAccessibleLayer) +{ + auto* item = treeModel()->layerItemFromIndex(firstSublayerIndex()); + ASSERT_NE(item, nullptr); + _sessionState._commandHookImpl.clearCalls(); + treeModel()->setEditTarget(item); + EXPECT_TRUE(_sessionState._commandHookImpl.hasCall("setEditTarget")); +} + +TEST_F(LayerTreeModelTest, SetEditTarget_BlockedWhenLayerIsLocked) +{ + auto* item = treeModel()->layerItemFromIndex(firstSublayerIndex()); + ASSERT_NE(item, nullptr); + TestUtils::lockLayerDirect(item->layer()); + _sessionState._commandHookImpl.clearCalls(); + treeModel()->setEditTarget(item); + EXPECT_FALSE(_sessionState._commandHookImpl.hasCall("setEditTarget")); + TestUtils::unlockLayerDirect(item->layer()); +} + +TEST_F(LayerTreeModelTest, SetEditTarget_BlockedWhenLayerIsMuted) +{ + auto* item = treeModel()->layerItemFromIndex(firstSublayerIndex()); + ASSERT_NE(item, nullptr); + const std::string layerId = item->layer()->GetIdentifier(); + _sessionState.stage()->MuteLayer(layerId); + QApplication::processEvents(); + + // Muting fired a LayersDidChange notice that rebuilt the model and deleted the + // original item, so re-fetch it before use to avoid a dangling pointer. + item = treeModel()->layerItemFromIndex(firstSublayerIndex()); + ASSERT_NE(item, nullptr); + + _sessionState._commandHookImpl.clearCalls(); + treeModel()->setEditTarget(item); + EXPECT_FALSE(_sessionState._commandHookImpl.hasCall("setEditTarget")); + _sessionState.stage()->UnmuteLayer(layerId); +} + +// ── rootLayerIndex ───────────────────────────────────────────────────────────── + +TEST_F(LayerTreeModelTest, RootLayerIndex_IsValid) +{ + EXPECT_TRUE(rootLayerIndex().isValid()); +} + +TEST_F(LayerTreeModelTest, RootLayerIndex_ItemIsRootLayer) +{ + auto* item = treeModel()->layerItemFromIndex(rootLayerIndex()); + ASSERT_NE(item, nullptr); + EXPECT_TRUE(item->isRootLayer()); +} + +// ── invalid layer item ───────────────────────────────────────────────────────── + +TEST_F(LayerTreeModelTest, Flags_InvalidLayerItem_ReturnsOnlySelectableAndEnabled) +{ + const std::string fakePath = "/nonexistent/flags_invalid.usda"; + _sessionState.stage()->GetRootLayer()->InsertSubLayerPath(fakePath, 0); + treeModel()->forceRefresh(); + QApplication::processEvents(); + + QModelIndex invalidIdx = treeModel()->index(0, 0, rootLayerIndex()); + auto* invalid = treeModel()->layerItemFromIndex(invalidIdx); + ASSERT_NE(invalid, nullptr); + ASSERT_TRUE(invalid->isInvalidLayer()); + + Qt::ItemFlags flags = treeModel()->flags(invalidIdx); + EXPECT_TRUE(flags & Qt::ItemIsSelectable); + EXPECT_TRUE(flags & Qt::ItemIsEnabled); + EXPECT_FALSE(flags & Qt::ItemIsDragEnabled); + EXPECT_FALSE(flags & Qt::ItemIsDropEnabled); +} + +TEST_F(LayerTreeModelTest, MimeData_InvalidLayerItem_UsesSubLayerPath) +{ + const std::string fakePath = "/nonexistent/mime_invalid.usda"; + _sessionState.stage()->GetRootLayer()->InsertSubLayerPath(fakePath, 0); + treeModel()->forceRefresh(); + QApplication::processEvents(); + + QModelIndex invalidIdx = treeModel()->index(0, 0, rootLayerIndex()); + auto* invalid = treeModel()->layerItemFromIndex(invalidIdx); + ASSERT_NE(invalid, nullptr); + ASSERT_TRUE(invalid->isInvalidLayer()); + + QModelIndexList indexes = { invalidIdx }; + std::unique_ptr mime(treeModel()->mimeData(indexes)); + ASSERT_NE(mime, nullptr); + EXPECT_TRUE(mime->hasFormat("text/plain")); + QString data = QString::fromUtf8(mime->data("text/plain")); + EXPECT_EQ(data, QString::fromStdString(fakePath)); +} + +// ── dropMimeData ─────────────────────────────────────────────────────────────── + +TEST_F(LayerTreeModelTest, DropMimeData_WrongMimeFormat_ReturnsFalse) +{ + auto mimeData = std::make_unique(); + mimeData->setHtml("wrong format"); + EXPECT_FALSE(treeModel()->dropMimeData( + mimeData.get(), Qt::MoveAction, 0, 0, rootLayerIndex())); +} + +// ── selectUsdLayerOnIdle ─────────────────────────────────────────────────────── + +TEST_F(LayerTreeModelTest, SelectUsdLayerOnIdle_EmitsSelectSignalForExistingLayer) +{ + auto* item = treeModel()->layerItemFromIndex(firstSublayerIndex()); + ASSERT_NE(item, nullptr); + auto layer = item->layer(); + + QModelIndex receivedIndex; + QObject::connect( + treeModel(), &LayerTreeModel::selectLayerSignal, + [&receivedIndex](const QModelIndex& idx) { receivedIndex = idx; }); + + treeModel()->selectUsdLayerOnIdle(layer); + QApplication::processEvents(); + + EXPECT_TRUE(receivedIndex.isValid()); +} + +// ── USD notice: usd_editTargetChanged ───────────────────────────────────────── + +TEST_F(LayerTreeModelTest, UsdEditTargetChanged_UpdatesTargetLayerOnIdle) +{ + auto* subItem = treeModel()->layerItemFromIndex(firstSublayerIndex()); + ASSERT_NE(subItem, nullptr); + auto sublayerRef = subItem->layer(); + ASSERT_FALSE(subItem->isTargetLayer()); + + // Directly change the USD stage's edit target to fire + // UsdNotice::StageEditTargetChanged, bypassing the stub command hook. + _sessionState.stage()->SetEditTarget(UsdEditTarget(sublayerRef)); + QApplication::processEvents(); + + // Re-fetch in case model rebuilt during event processing. + subItem = treeModel()->layerItemFromIndex(firstSublayerIndex()); + ASSERT_NE(subItem, nullptr); + EXPECT_TRUE(subItem->isTargetLayer()); +} + +// ── selectedLayerDataChangedSignal (EMSUSD-3823) ─────────────────────────────── + +TEST_F(LayerTreeModelTest, SelectedLayerDataChanged_EmittedOnLayerDataChange) +{ + // setSessionState schedules a data-changed rebuild; flush it before counting. + QApplication::processEvents(); + + int dataChangedCount = 0; + QObject::connect(treeModel(), &LayerTreeModel::selectedLayerDataChangedSignal, + [&dataChangedCount]() { ++dataChangedCount; }); + + // Author data into a layer without altering the layer tree structure: this is + // the case the model-rebuild optimization stopped refreshing the contents for. + _sessionState.stage()->DefinePrim(SdfPath("/testDataChange")); + QApplication::processEvents(); + + EXPECT_GE(dataChangedCount, 1) + << "selectedLayerDataChangedSignal should fire when layer data changes"; +} + +TEST_F(LayerTreeModelTest, SelectedLayerDataChanged_NotEmittedOnPlainRefresh) +{ + QApplication::processEvents(); // settle the initial build + + int dataChangedCount = 0; + QObject::connect(treeModel(), &LayerTreeModel::selectedLayerDataChangedSignal, + [&dataChangedCount]() { ++dataChangedCount; }); + + // forceRefresh() rebuilds without flagging a layer-data change. + treeModel()->forceRefresh(); + QApplication::processEvents(); + + EXPECT_EQ(dataChangedCount, 0) + << "selectedLayerDataChangedSignal should not fire for a non-data-change rebuild"; +} + +} // namespace UsdLayerEditor diff --git a/test/lib/usdLayerEditor/cpp/testLayerTreeView.cpp b/test/lib/usdLayerEditor/cpp/testLayerTreeView.cpp new file mode 100644 index 0000000000..3a8f5ba6aa --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/testLayerTreeView.cpp @@ -0,0 +1,470 @@ +// 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. +// +#pragma once + +#include +#include "testUtils.h" +#include "layerLocking.h" +#include "layerTreeItem.h" +#include "layerTreeItemDelegate.h" +#include "layerTreeModel.h" +#include "layerTreeView.h" + +#include + +#include +#include +#include +#include +#include + +#include + +PXR_NAMESPACE_USING_DIRECTIVE + +namespace UsdLayerEditor { + +// Exposes protected delegate methods for testing. +class TestableDelegateWrapper : public LayerTreeItemDelegate +{ +public: + explicit TestableDelegateWrapper(LayerTreeView* view) + : LayerTreeItemDelegate(view) {} + + using LayerTreeItemDelegate::getTargetIconRect; + using LayerTreeItemDelegate::getAdjustedItemRect; +}; + +class LayerTreeViewTest : public LayerEditorTestFixture +{ +protected: + void TearDown() override + { + LayerEditorTestFixture::TearDown(); + forgetLockedLayers(); + forgetSystemLockedLayers(); + } +}; + +// Shared-stage variant: on a shared stage anonymous layers report needsSaving()==true, +// which is required to reach the save-related branches of the double-click handler. +class LayerTreeViewSharedTest : public LayerTreeViewTest +{ +protected: + void SetUp() override + { + setSharedStage(true); + LayerEditorTestFixture::SetUp(); + QApplication::processEvents(); + } +}; + +// ── LayerViewMemento ─────────────────────────────────────────────────────────── + +// The constructor calls preserve() immediately — the memento is non-empty on construction. +TEST_F(LayerTreeViewTest, Memento_PopulatedOnConstruction) +{ + LayerViewMemento memento(*layerTree(), *treeModel()); + EXPECT_FALSE(memento.empty()); +} + +TEST_F(LayerTreeViewTest, Memento_PreservesExpandedStateByIdentifier) +{ + // Expand the root layer item. + layerTree()->expand(rootLayerIndex()); + QApplication::processEvents(); + + LayerViewMemento memento(*layerTree(), *treeModel()); + memento.preserve(*layerTree(), *treeModel()); + + auto state = memento.getItemsState(); + auto* root = treeModel()->layerItemFromIndex(rootLayerIndex()); + ASSERT_NE(root, nullptr); + + auto it = state.find(root->layer()->GetIdentifier()); + ASSERT_NE(it, state.end()); + EXPECT_TRUE(it->second._expanded); +} + +TEST_F(LayerTreeViewTest, Memento_RestoredAfterModelReset) +{ + // Collapse the root so the restored state differs from the default expandAll + // that runs when no memento exists — this is what makes the assertion + // discriminating rather than tautological. + layerTree()->collapse(rootLayerIndex()); + QApplication::processEvents(); + ASSERT_FALSE(layerTree()->isExpanded(rootLayerIndex())); + + // Add a sublayer so the rebuild is a genuine reset (not skipped by the + // identical-item optimization), exercising onModelAboutToBeReset/onModelReset. + _sessionState.stage()->GetRootLayer()->InsertSubLayerPath( + SdfLayer::CreateAnonymous("memento_extra")->GetIdentifier(), 0); + treeModel()->forceRefresh(); + QApplication::processEvents(); + + // The memento must restore the collapsed state across the reset. + EXPECT_FALSE(layerTree()->isExpanded(rootLayerIndex())); +} + +TEST_F(LayerTreeViewTest, Memento_RestoreHandlesMissingItemsGracefully) +{ + // Collapse the root so a restore that re-expands it is observable. + layerTree()->collapse(rootLayerIndex()); + QApplication::processEvents(); + ASSERT_FALSE(layerTree()->isExpanded(rootLayerIndex())); + + LayerViewMemento memento(*layerTree(), *treeModel()); + memento.preserve(*layerTree(), *treeModel()); + + auto* root = treeModel()->layerItemFromIndex(rootLayerIndex()); + ASSERT_NE(root, nullptr); + const std::string rootId = root->layer()->GetIdentifier(); + + auto state = memento.getItemsState(); + // Set a real layer's expanded state so a successful restore is verifiable. + state[rootId]._expanded = true; + // Plus a fake entry that doesn't exist in the tree. + state["anon:nonexistent_layer_xyz"] = { true }; + memento.setItemsState(state); + + const int rowsBefore = treeModel()->rowCount(rootLayerIndex()); + + // Restore must not crash even when an identifier is not found. + EXPECT_NO_THROW(memento.restore(*layerTree(), *treeModel())); + + // The real layer's expanded state was applied. + EXPECT_TRUE(layerTree()->isExpanded(rootLayerIndex())); + // The bogus entry created no row. + EXPECT_EQ(treeModel()->rowCount(rootLayerIndex()), rowsBefore); +} + +// ── selection helpers ───────────────────────────────────────────────────────── + +TEST_F(LayerTreeViewTest, GetSelectedLayerItems_ReturnsAllSelected) +{ + selectRow(firstSublayerIndex()); + auto items = layerTree()->getSelectedLayerItems(); + EXPECT_EQ(items.size(), 1u); +} + +TEST_F(LayerTreeViewTest, GetSelectedLayerItems_ReturnsAllSelectedForMultiSelection) +{ + selectRow(firstSublayerIndex()); + layerTree()->selectionModel()->select( + rootLayerIndex(), QItemSelectionModel::Select | QItemSelectionModel::Rows); + QApplication::processEvents(); + + auto* sub = treeModel()->layerItemFromIndex(firstSublayerIndex()); + auto* root = treeModel()->layerItemFromIndex(rootLayerIndex()); + ASSERT_NE(sub, nullptr); + ASSERT_NE(root, nullptr); + + auto items = layerTree()->getSelectedLayerItems(); + ASSERT_EQ(items.size(), 2u); + EXPECT_NE(std::find(items.begin(), items.end(), sub), items.end()); + EXPECT_NE(std::find(items.begin(), items.end(), root), items.end()); +} + +TEST_F(LayerTreeViewTest, CurrentLayerItem_ReturnsNullForInvalidIndex) +{ + layerTree()->setCurrentIndex(QModelIndex()); + EXPECT_EQ(layerTree()->currentLayerItem(), nullptr); +} + +TEST_F(LayerTreeViewTest, CurrentLayerItem_ReturnsItemForValidIndex) +{ + selectRow(firstSublayerIndex()); + EXPECT_NE(layerTree()->currentLayerItem(), nullptr); +} + +// ── mute / lock button dispatch ─────────────────────────────────────────────── +// These tests verify that the mute/lock actions result in the expected command +// hook calls. We invoke the actions via the window (public interface) rather +// than the protected LayerTreeView slots, both of which ultimately reach the +// same command hook. + +TEST_F(LayerTreeViewTest, MuteAction_CallsMuteSubLayerOnSelectedItem) +{ + auto* selected = treeModel()->layerItemFromIndex(firstSublayerIndex()); + ASSERT_NE(selected, nullptr); + const std::string selectedId = selected->layer()->GetIdentifier(); + + selectRow(firstSublayerIndex()); + _sessionState._commandHookImpl.clearCalls(); + _window->muteLayer(); + QApplication::processEvents(); + + const CommandCall* call = _sessionState._commandHookImpl.lastCallOf("muteSubLayer"); + ASSERT_NE(call, nullptr); + ASSERT_FALSE(call->args.empty()); + EXPECT_EQ(call->args[0], selectedId) + << "muteSubLayer must act on the selected layer"; +} + +TEST_F(LayerTreeViewTest, LockAction_CallsLockLayerOnSelectedItem) +{ + auto* selected = treeModel()->layerItemFromIndex(firstSublayerIndex()); + ASSERT_NE(selected, nullptr); + const std::string selectedId = selected->layer()->GetIdentifier(); + + selectRow(firstSublayerIndex()); + _sessionState._commandHookImpl.clearCalls(); + _window->lockLayer(); + QApplication::processEvents(); + + const CommandCall* call = _sessionState._commandHookImpl.lastCallOf("lockLayer"); + ASSERT_NE(call, nullptr); + ASSERT_FALSE(call->args.empty()); + EXPECT_EQ(call->args[0], selectedId) + << "lockLayer must act on the selected layer"; +} + +// ── delegate geometry (via TestableDelegateWrapper) ─────────────────────────── + +TEST_F(LayerTreeViewTest, Delegate_TargetIconRect_XOffsetIsArrowAreaWidth) +{ + TestableDelegateWrapper delegate(layerTree()); + QRect itemRect(0, 0, 200, 24); + QRect targetRect = delegate.getTargetIconRect(itemRect); + // x should be shifted right by ARROW_AREA_WIDTH (DPIScale(16) = 16 at 1x). + EXPECT_GT(targetRect.left(), itemRect.left()); +} + +TEST_F(LayerTreeViewTest, Delegate_TargetIconRect_HasPositiveWidth) +{ + TestableDelegateWrapper delegate(layerTree()); + QRect itemRect(0, 0, 200, 24); + QRect targetRect = delegate.getTargetIconRect(itemRect); + EXPECT_GT(targetRect.width(), 0); +} + +// ── LayerActionInfo state queries ───────────────────────────────────────────── + +TEST_F(LayerTreeViewTest, DoubleClick_SkipsWhenLayerDoesNotNeedSaving) +{ + // Default stub stage is not shared, so needsSaving() == false and the handler + // must return before attempting any save. + auto* item = treeModel()->layerItemFromIndex(firstSublayerIndex()); + ASSERT_NE(item, nullptr); + ASSERT_FALSE(item->needsSaving()); + + _sessionState._saveLayerCallCount = 0; + // onItemDoubleClicked is connected to the view's doubleClicked signal; emit it + // to drive the handler (the slot itself is not publicly callable). + bool invoked = QMetaObject::invokeMethod( + layerTree(), "doubleClicked", Qt::DirectConnection, + Q_ARG(QModelIndex, firstSublayerIndex())); + ASSERT_TRUE(invoked) << "failed to emit doubleClicked"; + QApplication::processEvents(); + EXPECT_EQ(_sessionState._saveLayerCallCount, 0) + << "No save should be attempted for a layer that does not need saving"; +} + +TEST_F(LayerTreeViewSharedTest, DoubleClick_SkipsWhenSystemLocked) +{ + // On a shared stage the anonymous sublayer needs saving, so the handler reaches + // the system-lock guard — which must still skip the save. + auto* item = treeModel()->layerItemFromIndex(firstSublayerIndex()); + ASSERT_NE(item, nullptr); + ASSERT_TRUE(item->needsSaving()) << "Anonymous layer on a shared stage should need saving"; + + addSystemLockedLayer(item->layer()); + item->layer()->SetPermissionToEdit(false); + ASSERT_TRUE(item->isSystemLocked()); + + _sessionState._saveLayerCallCount = 0; + bool invoked = QMetaObject::invokeMethod( + layerTree(), "doubleClicked", Qt::DirectConnection, + Q_ARG(QModelIndex, firstSublayerIndex())); + ASSERT_TRUE(invoked) << "failed to emit doubleClicked"; + QApplication::processEvents(); + EXPECT_EQ(_sessionState._saveLayerCallCount, 0) + << "System-locked layers must not be saved on double-click"; + + removeSystemLockedLayer(item->layer()); + TestUtils::unlockLayerDirect(item->layer()); +} + +// ── layerItemFromIndex / layerTreeModel ─────────────────────────────────────── + +TEST_F(LayerTreeViewTest, LayerItemFromIndex_ValidIndex_ReturnsNonNull) +{ + EXPECT_NE(layerTree()->layerItemFromIndex(rootLayerIndex()), nullptr); +} + +TEST_F(LayerTreeViewTest, LayerItemFromIndex_InvalidIndex_ReturnsNull) +{ + EXPECT_EQ(layerTree()->layerItemFromIndex(QModelIndex()), nullptr); +} + +TEST_F(LayerTreeViewTest, LayerTreeModel_MatchesTreeModel) +{ + EXPECT_EQ(layerTree()->layerTreeModel(), treeModel()); +} + +// ── getSelectedLayerItems empty case ───────────────────────────────────────── + +TEST_F(LayerTreeViewTest, GetSelectedLayerItems_EmptyByDefault) +{ + layerTree()->clearSelection(); + layerTree()->setCurrentIndex(QModelIndex()); + auto items = layerTree()->getSelectedLayerItems(); + EXPECT_TRUE(items.empty()); +} + +// ── expand / collapse children ──────────────────────────────────────────────── +// expandChildren/collapseChildren/shouldExpandOrCollapseAll are protected, so +// we subclass LayerTreeView to expose them for tests. + +class TestableLayerTreeView : public LayerTreeView +{ +public: + explicit TestableLayerTreeView(SessionState* s, QWidget* parent = nullptr) + : LayerTreeView(s, parent) {} + + using LayerTreeView::expandChildren; + using LayerTreeView::collapseChildren; + using LayerTreeView::shouldExpandOrCollapseAll; + using LayerTreeView::onMuteLayerButtonPushed; + using LayerTreeView::onLockLayerButtonPushed; +}; + +TEST_F(LayerTreeViewTest, CollapseChildren_AlsoCollapsesRoot) +{ + TestableLayerTreeView tree(&_sessionState, _mainWindow); + tree.show(); + QApplication::processEvents(); + + QModelIndex root = tree.layerTreeModel()->rootLayerIndex(); + tree.expand(root); + QApplication::processEvents(); + ASSERT_TRUE(tree.isExpanded(root)); + + // collapseChildren collapses the index itself as well as all its descendants. + tree.collapseChildren(root); + QApplication::processEvents(); + EXPECT_FALSE(tree.isExpanded(root)); +} + +TEST_F(LayerTreeViewTest, ExpandChildren_ExpandsCollapsedIndex) +{ + TestableLayerTreeView tree(&_sessionState, _mainWindow); + tree.show(); + QApplication::processEvents(); + + QModelIndex root = tree.layerTreeModel()->rootLayerIndex(); + tree.collapse(root); + QApplication::processEvents(); + ASSERT_FALSE(tree.isExpanded(root)); + + tree.expandChildren(root); + QApplication::processEvents(); + EXPECT_TRUE(tree.isExpanded(root)); +} + +// ── mute / lock button-push slots ────────────────────────────────────────────── +// onMuteLayerButtonPushed/onLockLayerButtonPushed act on the current item (the +// action-button path), distinct from the selection-based onMuteLayer/onLockLayer. + +TEST_F(LayerTreeViewTest, MuteLayerButtonPushed_CallsMuteSubLayerOnCurrentItem) +{ + TestableLayerTreeView tree(&_sessionState, _mainWindow); + tree.show(); + QApplication::processEvents(); + + QModelIndex root = tree.layerTreeModel()->rootLayerIndex(); + tree.setCurrentIndex(tree.layerTreeModel()->index(0, 0, root)); + ASSERT_NE(tree.currentLayerItem(), nullptr); + const std::string currentId = tree.currentLayerItem()->layer()->GetIdentifier(); + + _sessionState._commandHookImpl.clearCalls(); + tree.onMuteLayerButtonPushed(); + QApplication::processEvents(); + + const CommandCall* call = _sessionState._commandHookImpl.lastCallOf("muteSubLayer"); + ASSERT_NE(call, nullptr); + ASSERT_FALSE(call->args.empty()); + EXPECT_EQ(call->args[0], currentId) + << "muteSubLayer must act on the current item"; +} + +TEST_F(LayerTreeViewTest, LockLayerButtonPushed_CallsLockLayerOnCurrentItem) +{ + TestableLayerTreeView tree(&_sessionState, _mainWindow); + tree.show(); + QApplication::processEvents(); + + QModelIndex root = tree.layerTreeModel()->rootLayerIndex(); + tree.setCurrentIndex(tree.layerTreeModel()->index(0, 0, root)); + ASSERT_NE(tree.currentLayerItem(), nullptr); + const std::string currentId = tree.currentLayerItem()->layer()->GetIdentifier(); + + _sessionState._commandHookImpl.clearCalls(); + tree.onLockLayerButtonPushed(); + QApplication::processEvents(); + + const CommandCall* call = _sessionState._commandHookImpl.lastCallOf("lockLayer"); + ASSERT_NE(call, nullptr); + ASSERT_FALSE(call->args.empty()); + EXPECT_EQ(call->args[0], currentId) + << "lockLayer must act on the current item"; +} + +// ── keyboard handling ────────────────────────────────────────────────────────── + +TEST_F(LayerTreeViewTest, KeyPress_Delete_RemovesSelectedSublayer) +{ + selectRow(firstSublayerIndex()); + _sessionState._commandHookImpl.clearCalls(); + + QKeyEvent keyEvent(QEvent::KeyPress, Qt::Key_Delete, Qt::NoModifier); + layerTree()->keyPressEvent(&keyEvent); + QApplication::processEvents(); + + EXPECT_TRUE(_sessionState._commandHookImpl.hasCall("removeSubLayerPath")); +} + +TEST_F(LayerTreeViewTest, KeyPress_R_RefreshesModel) +{ + // Insert a sublayer directly into the stack (bypassing the model) so a refresh + // is observable as an additional row under the root. + const int before = treeModel()->rowCount(rootLayerIndex()); + _sessionState.stage()->GetRootLayer()->InsertSubLayerPath( + SdfLayer::CreateAnonymous("refresh_extra")->GetIdentifier(), 0); + + // Without event processing the model has not yet rebuilt, so the new row is absent. + EXPECT_EQ(treeModel()->rowCount(rootLayerIndex()), before); + + QKeyEvent keyEvent(QEvent::KeyPress, Qt::Key_R, Qt::NoModifier); + layerTree()->keyPressEvent(&keyEvent); + QApplication::processEvents(); + + EXPECT_EQ(treeModel()->rowCount(rootLayerIndex()), before + 1); +} + +// ── add parent layer ─────────────────────────────────────────────────────────── + +TEST_F(LayerTreeViewTest, AddParentLayer_ReplacesSelectedWithAnonymousParent) +{ + selectRow(firstSublayerIndex()); + _sessionState._commandHookImpl.clearCalls(); + + layerTree()->onAddParentLayer("Add Parent Layer"); + QApplication::processEvents(); + + EXPECT_TRUE(_sessionState._commandHookImpl.hasCall("replaceSubLayerPath")); +} + +} // namespace UsdLayerEditor diff --git a/test/lib/usdLayerEditor/cpp/testLayerTreeViewMouse.cpp b/test/lib/usdLayerEditor/cpp/testLayerTreeViewMouse.cpp new file mode 100644 index 0000000000..ee62bd054e --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/testLayerTreeViewMouse.cpp @@ -0,0 +1,107 @@ +// 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. +// +#pragma once + +#include + +#include "layerTreeView.h" + +#include +#include +#include +#include + +#include + +namespace UsdLayerEditor { + +namespace { + +QPoint itemCenter(LayerTreeView* tree, const QModelIndex& idx) +{ + QRect rect = tree->visualRect(idx); + return rect.isEmpty() ? QPoint(10, 10) : rect.center(); +} + +void sendMousePress(QWidget* widget, const QPoint& pos) +{ + QMouseEvent event( + QEvent::MouseButtonPress, QPointF(pos), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); + QCoreApplication::sendEvent(widget, &event); +} + +void sendMouseRelease(QWidget* widget, const QPoint& pos) +{ + QMouseEvent event( + QEvent::MouseButtonRelease, QPointF(pos), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); + QCoreApplication::sendEvent(widget, &event); +} + +void sendMouseMove(QWidget* widget, const QPoint& pos) +{ + QMouseEvent event( + QEvent::MouseMove, QPointF(pos), Qt::NoButton, Qt::NoButton, Qt::NoModifier); + QCoreApplication::sendEvent(widget, &event); +} + +} // namespace + +// These drive the LayerTreeView mouse/paint event handlers (and the action-button +// GeneratedIconButton paint path via repaint). The action-button hit state is only +// reachable through real delegate hit-testing, so these guard the handlers against +// segfaults rather than asserting a selection outcome. +class LayerTreeViewMouseTest : public LayerEditorTestFixture { }; + +TEST_F(LayerTreeViewMouseTest, MousePress_OnValidItem_DoesNotCrash) +{ + ASSERT_NE(layerTree(), nullptr); + QModelIndex idx = firstSublayerIndex(); + ASSERT_TRUE(idx.isValid()); + QPoint pos = itemCenter(layerTree(), idx); + + EXPECT_NO_THROW(sendMousePress(layerTree()->viewport(), pos)); +} + +TEST_F(LayerTreeViewMouseTest, MouseRelease_AfterPress_DoesNotCrash) +{ + ASSERT_NE(layerTree(), nullptr); + QModelIndex idx = firstSublayerIndex(); + ASSERT_TRUE(idx.isValid()); + QPoint pos = itemCenter(layerTree(), idx); + + sendMousePress(layerTree()->viewport(), pos); + EXPECT_NO_THROW(sendMouseRelease(layerTree()->viewport(), pos)); +} + +TEST_F(LayerTreeViewMouseTest, MouseMove_OverItem_DoesNotCrash) +{ + ASSERT_NE(layerTree(), nullptr); + QModelIndex idx = firstSublayerIndex(); + ASSERT_TRUE(idx.isValid()); + QPoint pos = itemCenter(layerTree(), idx); + + EXPECT_NO_THROW(sendMouseMove(layerTree()->viewport(), pos)); +} + +TEST_F(LayerTreeViewMouseTest, MouseClick_RootLayerItem_DoesNotCrash) +{ + ASSERT_NE(layerTree(), nullptr); + QPoint pos = itemCenter(layerTree(), rootLayerIndex()); + + sendMousePress(layerTree()->viewport(), pos); + EXPECT_NO_THROW(sendMouseRelease(layerTree()->viewport(), pos)); +} + +} // namespace UsdLayerEditor diff --git a/test/lib/usdLayerEditor/cpp/testLoadLayersDialog.cpp b/test/lib/usdLayerEditor/cpp/testLoadLayersDialog.cpp new file mode 100644 index 0000000000..3238c0a193 --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/testLoadLayersDialog.cpp @@ -0,0 +1,135 @@ +// 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. +// +#pragma once + +#include +#include "testUtils.h" +#include "layerTreeItem.h" +#include "loadLayersDialog.h" + +#include +#include +#include +#include + +PXR_NAMESPACE_USING_DIRECTIVE + +namespace UsdLayerEditor { + +class LoadLayersDialogTest : public LayerEditorTestFixture {}; + +TEST_F(LoadLayersDialogTest, LoadLayersDialog_HasOkAndCancelButtons) +{ + auto* rootItem = dynamic_cast( + treeModel()->itemFromIndex(rootLayerIndex())); + ASSERT_NE(rootItem, nullptr); + LoadLayersDialog dlg(rootItem, _mainWindow); + EXPECT_NE(TestUtils::findButtonByText(&dlg, { "Load", "OK" }), nullptr) + << "LoadLayersDialog should have an OK/Load button"; + EXPECT_NE(TestUtils::findButtonByText(&dlg, "Cancel"), nullptr) + << "LoadLayersDialog should have a Cancel button"; +} + +TEST_F(LoadLayersDialogTest, LoadLayersDialog_StartsWithEmptyPath) +{ + auto* rootItem = dynamic_cast( + treeModel()->itemFromIndex(rootLayerIndex())); + ASSERT_NE(rootItem, nullptr); + LoadLayersDialog dlg(rootItem, _mainWindow); + auto lineEdits = dlg.findChildren(); + ASSERT_GE(lineEdits.size(), 1); + // The first editable row starts empty. + EXPECT_TRUE(lineEdits.first()->text().isEmpty()); +} + +TEST_F(LoadLayersDialogTest, LoadLayersDialog_FindDirectoryToUse_WithNonEmptyPath) +{ + auto* rootItem = dynamic_cast( + treeModel()->itemFromIndex(rootLayerIndex())); + ASSERT_NE(rootItem, nullptr); + LoadLayersDialog dlg(rootItem, _mainWindow); + // Passing a file path: should strip the filename and return the directory. + std::string result = dlg.findDirectoryToUse("/tmp/some/file.usd"); + EXPECT_EQ(result, "/tmp/some"); +} + +TEST_F(LoadLayersDialogTest, LoadLayersDialog_OnAddRow_IncreasesRowCount) +{ + auto* rootItem = dynamic_cast( + treeModel()->itemFromIndex(rootLayerIndex())); + ASSERT_NE(rootItem, nullptr); + LoadLayersDialog dlg(rootItem, _mainWindow); + int beforeCount = dlg.findChildren().size(); + // onAddRow() is public (connected by LayerPathRow). Call it directly. + dlg.onAddRow(); + QApplication::processEvents(); + int afterCount = dlg.findChildren().size(); + EXPECT_GT(afterCount, beforeCount); +} + +// Trigger onOk() with all-empty row text: all rows are skipped, accept() is +// called, and pathsToLoad() stays empty. +TEST_F(LoadLayersDialogTest, LoadLayersDialog_OnOk_WithEmptyPaths_AcceptsAndPathsEmpty) +{ + auto* rootItem = dynamic_cast( + treeModel()->itemFromIndex(rootLayerIndex())); + ASSERT_NE(rootItem, nullptr); + LoadLayersDialog dlg(rootItem, _mainWindow); + // Find and click the OK/Load button. + QPushButton* okBtn = TestUtils::findButtonByText(&dlg, { "Load", "OK" }); + ASSERT_NE(okBtn, nullptr); + TestUtils::dismissNextModal(50); // guard against exec() being called + okBtn->click(); + QApplication::processEvents(); + EXPECT_TRUE(dlg.pathsToLoad().empty()); +} + +// Trigger onOk() with a non-existent path: checkIfPathIsSafeToAdd returns true +// for paths that cannot be opened (no such layer on disk). The path is +// added to pathsToLoad() and accept() is called. +TEST_F(LoadLayersDialogTest, LoadLayersDialog_OnOk_WithNonExistentPath_AddsToPathList) +{ + auto* rootItem = dynamic_cast( + treeModel()->itemFromIndex(rootLayerIndex())); + ASSERT_NE(rootItem, nullptr); + LoadLayersDialog dlg(rootItem, _mainWindow); + // Set text in the first (non-inserter) line edit. + auto lineEdits = dlg.findChildren(); + ASSERT_GE(lineEdits.size(), 1); + lineEdits.first()->setText("/nonexistent/layer_test.usd"); + QApplication::processEvents(); + // Click the OK button without dismissal since it calls accept() directly. + QPushButton* okBtn = TestUtils::findButtonByText(&dlg, { "Load", "OK" }); + ASSERT_NE(okBtn, nullptr); + okBtn->click(); + QApplication::processEvents(); + EXPECT_GE(dlg.pathsToLoad().size(), 1u); +} + +// Trigger onOk() with a cancel: clicking cancel leaves pathsToLoad() empty. +TEST_F(LoadLayersDialogTest, LoadLayersDialog_OnCancel_LeavesPathsEmpty) +{ + auto* rootItem = dynamic_cast( + treeModel()->itemFromIndex(rootLayerIndex())); + ASSERT_NE(rootItem, nullptr); + LoadLayersDialog dlg(rootItem, _mainWindow); + QPushButton* cancelBtn = TestUtils::findButtonByText(&dlg, "Cancel"); + ASSERT_NE(cancelBtn, nullptr); + cancelBtn->click(); + QApplication::processEvents(); + EXPECT_TRUE(dlg.pathsToLoad().empty()); +} + +} // namespace UsdLayerEditor diff --git a/test/lib/usdLayerEditor/cpp/testMain.cpp b/test/lib/usdLayerEditor/cpp/testMain.cpp new file mode 100644 index 0000000000..9336c94685 --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/testMain.cpp @@ -0,0 +1,31 @@ +// +// 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 "utilQT.h" + +#include + +#include + +int main(int argc, char** argv) +{ + // QApplication must be created before any QWidget and must outlive all tests. + QApplication app(argc, argv); + // Initialize the DPI/Qt utilities singleton used by all layer editor widgets. + UsdLayerEditor::initializeQtUtils(); + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/test/lib/usdLayerEditor/cpp/testMenusAndStage.cpp b/test/lib/usdLayerEditor/cpp/testMenusAndStage.cpp new file mode 100644 index 0000000000..89b25ba07e --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/testMenusAndStage.cpp @@ -0,0 +1,150 @@ +// +// 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. +// +#pragma once + +#include + +#include "stringResources.h" +#include "testUtils.h" + +#include + +#include +#include +#include +#include +#include + +PXR_NAMESPACE_USING_DIRECTIVE + +namespace UsdLayerEditor { + +static QMenu* findMenuByTitle(QMainWindow* win, const QString& title) +{ + if (!win || !win->menuBar()) + return nullptr; + for (QAction* top : win->menuBar()->actions()) { + if (QMenu* menu = top->menu()) { + if (menu->title() == title) + return menu; + } + } + return nullptr; +} + +static QAction* findActionInMenuBar(QMainWindow* win, const QString& text) +{ + if (!win || !win->menuBar()) + return nullptr; + for (QAction* top : win->menuBar()->actions()) { + if (QMenu* menu = top->menu()) { + QAction* found = TestUtils::findAction(menu, text); + if (found) + return found; + } + } + return nullptr; +} + +TEST_F(LayerEditorTestFixture, OptionMenu_DisplayLayerContentsAction_Exists) +{ + auto* win = qobject_cast(_widget->parent()); + auto* action = findActionInMenuBar(win, "Display Layer Content"); + EXPECT_NE(action, nullptr) + << "Display Layer Content action should exist in the Option menu"; +} + +TEST_F(LayerEditorTestFixture, OptionMenu_DisplayLayerContents_Toggles) +{ + auto* win = qobject_cast(_widget->parent()); + auto* action = findActionInMenuBar(win, "Display Layer Content"); + ASSERT_NE(action, nullptr); + ASSERT_TRUE(action->isCheckable()); + + bool before = action->isChecked(); + action->trigger(); + QApplication::processEvents(); + EXPECT_NE(action->isChecked(), before) << "Action should toggle"; +} + +TEST_F(LayerEditorTestFixture, StageSelector_ChangeStage_UpdatesSessionState) +{ + auto* combo = _widget->findChild( + QString(), Qt::FindChildrenRecursively); + ASSERT_NE(combo, nullptr) << "No stage selector QComboBox found"; + ASSERT_GE(combo->count(), 2) << "Expected at least 2 stages in selector"; + + auto stageBefore = _sessionState.stage(); + combo->setCurrentIndex(1); + QApplication::processEvents(); + + // The session state's current stage should have changed. + auto stageAfter = _sessionState.stage(); + EXPECT_NE(stageAfter, stageBefore) + << "Active stage should change when stage selector changes"; +} + +#ifndef MAYAUSD_OLD_LAYER_EDITOR +#endif + +// ── stage selector pin / content toggle ─────────────────────────────────────── + +TEST_F(LayerEditorTestFixture, StageSelector_InitialCountMatchesSessionStageCount) +{ + auto* combo = _widget->findChild( + QString(), Qt::FindChildrenRecursively); + ASSERT_NE(combo, nullptr); + int sessionCount = static_cast(_sessionState.allStages().size()); + EXPECT_EQ(combo->count(), sessionCount); +} + +#ifndef MAYAUSD_OLD_LAYER_EDITOR +#endif + +#ifndef MAYAUSD_OLD_LAYER_EDITOR +#endif + +// The Auto-Hide Session Layer action must be the first Option-menu entry, +// checkable, and followed by a separator then the Display-Layer-Contents action. +TEST_F(LayerEditorTestFixture, OptionMenu_AutoHideAction_IsFirstAndCheckable) +{ + auto* win = qobject_cast(_widget->parent()); + ASSERT_NE(win, nullptr); + + QMenu* optionMenu + = findMenuByTitle(win, StringResources::getAsQString(StringResources::kOption)); + ASSERT_NE(optionMenu, nullptr) << "Option menu should exist"; + + const QList actions = optionMenu->actions(); + ASSERT_GE(actions.size(), 3); + + const QString autoHideText + = StringResources::getAsQString(StringResources::kAutoHideSessionLayer); + EXPECT_EQ(actions[0]->text(), autoHideText) + << "Auto-Hide should be the first action in the Option menu"; + EXPECT_TRUE(actions[0]->isCheckable()); + EXPECT_EQ(actions[0]->isChecked(), _sessionState.autoHideSessionLayer()); + + EXPECT_TRUE(actions[1]->isSeparator()) << "a separator should follow the Auto-Hide action"; + + QAction* displayContents = TestUtils::findAction( + optionMenu, StringResources::getAsQString(StringResources::kDisplayLayerContents)); + ASSERT_NE(displayContents, nullptr); + EXPECT_GE(actions.indexOf(displayContents), 2) + << "Display Layer Content should come after Auto-Hide and the separator"; +} + +} // namespace UsdLayerEditor diff --git a/test/lib/usdLayerEditor/cpp/testPathChecker.cpp b/test/lib/usdLayerEditor/cpp/testPathChecker.cpp new file mode 100644 index 0000000000..243384cd77 --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/testPathChecker.cpp @@ -0,0 +1,188 @@ +// 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. +// +#pragma once + +#include + +#include "pathChecker.h" +#include "layerTreeItem.h" + +#include +#include + +#include +#include + +#include + +PXR_NAMESPACE_USING_DIRECTIVE + +namespace UsdLayerEditor { + +class PathCheckerTest : public LayerEditorTestFixture { }; + +// ── checkIfPathIsSafeToAdd ──────────────────────────────────────────────────── + +TEST_F(PathCheckerTest, NullParentItemAlwaysTrue) +{ + // No parent item → always safe (early return in implementation). + EXPECT_TRUE(checkIfPathIsSafeToAdd(nullptr, QString(), nullptr, "any/path.usda")); +} + +TEST_F(PathCheckerTest, NonExistentPathIsSafe) +{ + // A path that can't be opened by FindOrOpen is assumed safe + // (could be a custom URI or future path). + auto* parentItem = treeModel()->layerItemFromIndex(rootLayerIndex()); + ASSERT_NE(parentItem, nullptr); + EXPECT_TRUE(checkIfPathIsSafeToAdd( + nullptr, QString("test"), parentItem, "/does/not/exist/layer.usda")); +} + +TEST_F(PathCheckerTest, DuplicatePathInStackIsFalse) +{ + // The fixture root layer already has the first sublayer in its stack. + // Trying to add the same identifier again must be rejected. + auto* parentItem = treeModel()->layerItemFromIndex(rootLayerIndex()); + auto* sublayerItem = treeModel()->layerItemFromIndex(firstSublayerIndex()); + ASSERT_NE(parentItem, nullptr); + ASSERT_NE(sublayerItem, nullptr); + + // Modal warning is suppressed by the fixture's dialog handler. + const std::string existingId = sublayerItem->layer()->GetIdentifier(); + EXPECT_FALSE(checkIfPathIsSafeToAdd(nullptr, QString("test"), parentItem, existingId)); +} + +// ── Cycle / alias detection (file-based layers) ─────────────────────────────── + +#ifndef MAYAUSD_OLD_LAYER_EDITOR +#include + +// Base fixture that wires up two real .usda files (A and B) and registers a +// stage backed by A as the active session entry. Subclasses control exactly +// which sublayer relationships exist on disk. +class PathCheckerFileTestBase : public LayerEditorTestFixture +{ +protected: + void SetUp() override + { + LayerEditorTestFixture::SetUp(); + + namespace fss = fs::filesystem; + std::error_code ec; + + _pathA = (fss::temp_directory_path() / "pc_test_a.usda").generic_string(); + _pathB = (fss::temp_directory_path() / "pc_test_b.usda").generic_string(); + fss::remove(_pathA, ec); + fss::remove(_pathB, ec); + + createFiles(); + + _stage = PXR_NS::UsdStage::Open(_pathA); + ASSERT_TRUE(_stage); + _sessionState.addStage(_stage); + _sessionState.setStageEntry(_sessionState.allStages().back()); + QApplication::processEvents(); + } + + // Subclasses create _layerA and _layerB with desired relationships. + virtual void createFiles() = 0; + + void TearDown() override + { + _stage.Reset(); + _layerA.Reset(); + _layerB.Reset(); + + namespace fss = fs::filesystem; + std::error_code ec; + fss::remove(_pathA, ec); + fss::remove(_pathB, ec); + + LayerEditorTestFixture::TearDown(); + } + + std::string _pathA; + std::string _pathB; + SdfLayerRefPtr _layerA; + SdfLayerRefPtr _layerB; + PXR_NS::UsdStageRefPtr _stage; +}; + +// A sublayers B via relative path; passing B's absolute path must be rejected +// via the handle-comparison loop (foundLayerInStack = true). +class PathCheckerAliasTest : public PathCheckerFileTestBase +{ +protected: + void createFiles() override + { + _layerB = SdfLayer::CreateNew(_pathB); + ASSERT_TRUE(_layerB); + _layerB->Save(); + + // Use a relative path so the stack string differs from _pathB. + _layerA = SdfLayer::CreateNew(_pathA); + ASSERT_TRUE(_layerA); + _layerA->InsertSubLayerPath("./pc_test_b.usda", 0); + _layerA->Save(); + } +}; + +TEST_F(PathCheckerAliasTest, AliasPathRejected) +{ + // A's stack stores B as "./pc_test_b.usda"; proxy.Find(_pathB) == -1. + // FindOrOpen(_pathB) returns the same handle, so the handle-comparison + // loop sets foundLayerInStack=true and the function returns false. + auto* parentItem = treeModel()->layerItemFromIndex(rootLayerIndex()); + ASSERT_NE(parentItem, nullptr); + + EXPECT_FALSE(checkIfPathIsSafeToAdd(nullptr, QString("test"), parentItem, _pathB)); + EXPECT_GE(_modalDialogCount, 1); +} + +// A has no sublayers; B has A as a sublayer, creating a potential cycle. +// Adding B to A must be rejected by checkPathRecursive. +class PathCheckerCycleTest : public PathCheckerFileTestBase +{ +protected: + void createFiles() override + { + // B sublayers A — adding B to A creates the cycle A → B → A. + _layerA = SdfLayer::CreateNew(_pathA); + ASSERT_TRUE(_layerA); + _layerA->Save(); + + _layerB = SdfLayer::CreateNew(_pathB); + ASSERT_TRUE(_layerB); + _layerB->InsertSubLayerPath(_pathA, 0); + _layerB->Save(); + } +}; + +TEST_F(PathCheckerCycleTest, CycleDetected) +{ + // A has no sublayers, so proxy.Find(_pathB) == -1. + // FindOrOpen(_pathB) succeeds; the handle-comparison loop finds nothing + // (foundLayerInStack=false). checkPathRecursive walks B's children, + // encounters A which is in parentHandles, and returns false. + auto* parentItem = treeModel()->layerItemFromIndex(rootLayerIndex()); + ASSERT_NE(parentItem, nullptr); + + EXPECT_FALSE(checkIfPathIsSafeToAdd(nullptr, QString("test"), parentItem, _pathB)); + EXPECT_GE(_modalDialogCount, 1); +} +#endif + +} // namespace UsdLayerEditor diff --git a/test/lib/usdLayerEditor/cpp/testReorder.cpp b/test/lib/usdLayerEditor/cpp/testReorder.cpp new file mode 100644 index 0000000000..53f916ed65 --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/testReorder.cpp @@ -0,0 +1,200 @@ +// +// 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. +// +#pragma once + +#include +#include "testUtils.h" +#include "layerTreeItem.h" +#include "layerTreeModel.h" + +#include + +#include +#include + +PXR_NAMESPACE_USING_DIRECTIVE + +namespace UsdLayerEditor { + +static void addSecondSublayer(PXR_NS::UsdStageRefPtr stage) +{ + auto rootLayer = stage->GetRootLayer(); + auto extra = SdfLayer::CreateAnonymous("extra_sublayer"); + rootLayer->InsertSubLayerPath(extra->GetIdentifier(), 1); +} + +TEST_F(LayerEditorTestFixture, DragDrop_MoveRowDown_CallsMoveSubLayerPath) +{ + addSecondSublayer(_sessionState.stage()); + QApplication::processEvents(); + + auto rootLayer = _sessionState.stage()->GetRootLayer(); + auto paths = rootLayer->GetSubLayerPaths(); + ASSERT_GE(paths.size(), 2u) << "Need at least 2 sublayers"; + + const std::string draggedPath = paths[0]; + + QModelIndex parentIndex = rootLayerIndex(); + QMimeData* mimeData = treeModel()->mimeData({ treeModel()->index(0, 0, parentIndex) }); + ASSERT_NE(mimeData, nullptr) << "Model must supply MIME data for drag"; + + bool accepted = treeModel()->dropMimeData(mimeData, Qt::MoveAction, 2, 0, parentIndex); + delete mimeData; + + ASSERT_TRUE(accepted) << "dropMimeData should accept a valid downward move"; + QApplication::processEvents(); + EXPECT_TRUE(_sessionState._commandHookImpl.hasCall("moveSubLayerPath")) + << "moveSubLayerPath should be called on reorder"; + + // Moving the first sublayer down to the end must leave it last in the order. + auto newPaths = rootLayer->GetSubLayerPaths(); + ASSERT_FALSE(newPaths.empty()); + EXPECT_EQ(newPaths[newPaths.size() - 1], draggedPath) + << "Dragged layer should now be last"; +} + +TEST_F(LayerEditorTestFixture, DragDrop_MoveRowUp_CallsMoveSubLayerPath) +{ + addSecondSublayer(_sessionState.stage()); + QApplication::processEvents(); + + auto rootLayer = _sessionState.stage()->GetRootLayer(); + auto paths = rootLayer->GetSubLayerPaths(); + ASSERT_GE(paths.size(), 2u); + + const std::string draggedPath = paths[1]; + + QModelIndex parentIndex = rootLayerIndex(); + QMimeData* mimeData = treeModel()->mimeData({ treeModel()->index(1, 0, parentIndex) }); + ASSERT_NE(mimeData, nullptr); + + bool accepted = treeModel()->dropMimeData(mimeData, Qt::MoveAction, 0, 0, parentIndex); + delete mimeData; + + ASSERT_TRUE(accepted) << "dropMimeData should accept a valid upward move"; + QApplication::processEvents(); + EXPECT_TRUE(_sessionState._commandHookImpl.hasCall("moveSubLayerPath")) + << "moveSubLayerPath should be called on reorder"; + + // Moving the second sublayer up to the top must leave it first in the order. + auto newPaths = rootLayer->GetSubLayerPaths(); + ASSERT_FALSE(newPaths.empty()); + EXPECT_EQ(newPaths[0], draggedPath) << "Dragged layer should now be first"; +} + +// ── canDropMimeData validation ───────────────────────────────────────────────── + +TEST_F(LayerEditorTestFixture, DragDrop_CanDrop_ReturnsFalseForNonMoveAction) +{ + QModelIndexList indexes = { firstSublayerIndex() }; + std::unique_ptr mime(treeModel()->mimeData(indexes)); + ASSERT_NE(mime, nullptr); + EXPECT_FALSE(treeModel()->canDropMimeData( + mime.get(), Qt::CopyAction, 0, 0, rootLayerIndex())); +} + +TEST_F(LayerEditorTestFixture, DragDrop_CanDrop_ReturnsFalseForWrongMimeType) +{ + auto mime = std::make_unique(); + mime->setData("application/x-wrong", QByteArray("data")); + EXPECT_FALSE(treeModel()->canDropMimeData( + mime.get(), Qt::MoveAction, 0, 0, rootLayerIndex())); +} + +TEST_F(LayerEditorTestFixture, DragDrop_CanDrop_ReturnsFalseForLockedParent) +{ + QModelIndexList indexes = { firstSublayerIndex() }; + std::unique_ptr mime(treeModel()->mimeData(indexes)); + ASSERT_NE(mime, nullptr); + + auto* rootItem = dynamic_cast( + treeModel()->itemFromIndex(rootLayerIndex())); + ASSERT_NE(rootItem, nullptr); + TestUtils::lockLayerDirect(rootItem->layer()); + + EXPECT_FALSE(treeModel()->canDropMimeData( + mime.get(), Qt::MoveAction, 0, 0, rootLayerIndex())); + + TestUtils::unlockLayerDirect(rootItem->layer()); +} + +TEST_F(LayerEditorTestFixture, DragDrop_CanDrop_ReturnsFalseForReadOnlyParent) +{ + QModelIndexList indexes = { firstSublayerIndex() }; + std::unique_ptr mime(treeModel()->mimeData(indexes)); + ASSERT_NE(mime, nullptr); + + auto* rootItem = dynamic_cast( + treeModel()->itemFromIndex(rootLayerIndex())); + ASSERT_NE(rootItem, nullptr); + rootItem->layer()->SetPermissionToEdit(false); + rootItem->layer()->SetPermissionToSave(false); + + EXPECT_FALSE(treeModel()->canDropMimeData( + mime.get(), Qt::MoveAction, 0, 0, rootLayerIndex())); + + rootItem->layer()->SetPermissionToEdit(true); + rootItem->layer()->SetPermissionToSave(true); +} + +TEST_F(LayerEditorTestFixture, DragDrop_CanDrop_ReturnsTrueForValidMove) +{ + QModelIndexList indexes = { firstSublayerIndex() }; + std::unique_ptr mime(treeModel()->mimeData(indexes)); + ASSERT_NE(mime, nullptr); + EXPECT_TRUE(treeModel()->canDropMimeData( + mime.get(), Qt::MoveAction, 0, 0, rootLayerIndex())); +} + +// ── dropMimeData ordering ───────────────────────────────────────────────────── + +static void addTwoSublayers(PXR_NS::UsdStageRefPtr stage) +{ + auto root = stage->GetRootLayer(); + if (root->GetNumSubLayerPaths() < 2) { + auto extra = SdfLayer::CreateAnonymous("extra_drop_test"); + root->InsertSubLayerPath(extra->GetIdentifier(), 1); + } +} + +// ── add-sibling-layer undo bracketing ────────────────────────────────────────── + +TEST_F(LayerEditorTestFixture, AddSiblingLayer_IsSingleUndoBracket) +{ + // Give the root a second sublayer so a selection at row 1 forces a reorder. + auto* rootItem = treeModel()->layerItemFromIndex(rootLayerIndex()); + ASSERT_NE(rootItem, nullptr); + rootItem->layer()->InsertSubLayerPath( + SdfLayer::CreateAnonymous("extraSub")->GetIdentifier(), 1); + treeModel()->forceRefresh(); + QApplication::processEvents(); + + // Select the sublayer now at row 1 (a non-top sibling). + selectRow(treeModel()->index(1, 0, rootLayerIndex())); + + auto& hook = _sessionState._commandHookImpl; + hook.clearCalls(); + + _widget->onNewLayerButtonClicked(); + QApplication::processEvents(); + + EXPECT_EQ(hook.callCount("openUndoBracket"), 1) + << "add + reorder must be a single undo bracket"; + // sanity: the reorder actually happened (remove + insert within that bracket) + EXPECT_GE(hook.callCount("insertSubLayerPath"), 1); +} + +} // namespace UsdLayerEditor diff --git a/test/lib/usdLayerEditor/cpp/testSaveLayersDialog.cpp b/test/lib/usdLayerEditor/cpp/testSaveLayersDialog.cpp new file mode 100644 index 0000000000..7f03d59071 --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/testSaveLayersDialog.cpp @@ -0,0 +1,214 @@ +// 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. +// +#pragma once + +#include +#include "testUtils.h" +#include "saveLayersDialog.h" + +#include +#include +#include +#include + +namespace UsdLayerEditor { + +// Exposes protected onCancel / onSaveAll for direct invocation in headless tests. +class TestableSaveLayersDialog : public SaveLayersDialog +{ +public: + using SaveLayersDialog::SaveLayersDialog; + void callOnCancel() { onCancel(); } + void callOnSaveAll() { onSaveAll(); } + void callOnAllAsRelativeChanged() { onAllAsRelativeChanged(); } +}; + +class SaveLayersDialogTest : public LayerEditorTestFixture {}; + +TEST_F(SaveLayersDialogTest, SaveLayersDialog_HasSaveAllButton) +{ + SaveLayersDialog dlg(&_sessionState, _mainWindow, /*isExporting=*/false); + auto* btn = dlg.findChild(QString(), Qt::FindChildrenRecursively); + // There must be at least one push button (Save All / Cancel). + EXPECT_NE(btn, nullptr); +} + +TEST_F(SaveLayersDialogTest, SaveLayersDialog_HasCancelButton) +{ + SaveLayersDialog dlg(&_sessionState, _mainWindow, /*isExporting=*/false); + bool found = false; + for (auto* btn : dlg.findChildren()) { + if (btn->text().contains("Cancel", Qt::CaseInsensitive)) { + found = true; + break; + } + } + EXPECT_TRUE(found) << "SaveLayersDialog should have a Cancel button"; +} + +TEST_F(SaveLayersDialogTest, SaveLayersDialog_AllAsRelativeCheckboxExists) +{ + SaveLayersDialog dlg(&_sessionState, _mainWindow, /*isExporting=*/false); + auto* cb = dlg.findChild(QString(), Qt::FindChildrenRecursively); + // Both editors discover the stub's anonymous sublayers (the old editor via a proxy backed by + // the same in-memory stage), so the all-as-relative checkbox is always created. + EXPECT_NE(cb, nullptr) + << "all-as-relative checkbox should exist when anonymous layers are present"; +} + +// Both editors discover the stub stage's anonymous layers, so forEachEntry sees them as entries. +TEST_F(SaveLayersDialogTest, ForEachEntry_CountsAnonLayerRows) +{ + SaveLayersDialog dlg(&_sessionState, _mainWindow, /*isExporting=*/false); + int count = 0; + dlg.forEachEntry([&count](QWidget*) { ++count; }); + EXPECT_GE(count, 1); +} + +// buildTooltipForLayer with a null layer must return an empty string without crashing. +TEST_F(SaveLayersDialogTest, BuildTooltipForLayer_NullLayer_ReturnsEmpty) +{ + SaveLayersDialog dlg(&_sessionState, _mainWindow, /*isExporting=*/false); + PXR_NS::SdfLayerRefPtr nullLayer; + EXPECT_EQ(dlg.buildTooltipForLayer(nullLayer), QString()); +} + +// buildTooltipForLayer with a layer that is in the stageLayerMap returns a non-empty tooltip. +TEST_F(SaveLayersDialogTest, BuildTooltipForLayer_KnownLayer_ReturnsNonEmptyTooltip) +{ + SaveLayersDialog dlg(&_sessionState, _mainWindow, /*isExporting=*/false); + // The stub's current stage has one anonymous sublayer in the stageLayerMap. + const auto& stageMap = dlg.stageLayers(); + if (stageMap.empty()) { + GTEST_SKIP() << "no layers in stage map (old editor or empty stage)"; + } + auto layer = stageMap.begin()->first; + QString tooltip = dlg.buildTooltipForLayer(layer); + EXPECT_FALSE(tooltip.isEmpty()); +} + +// findEntry with a layer that is in the rows returns non-null. +TEST_F(SaveLayersDialogTest, FindEntry_KnownLayer_ReturnsWidget) +{ + SaveLayersDialog dlg(&_sessionState, _mainWindow, /*isExporting=*/false); + // The stub's anonymous sublayers produce row entries, so at least one stage layer + // resolves to a non-null widget. + const auto& stageMap = dlg.stageLayers(); + QWidget* found = nullptr; + for (auto& kv : stageMap) { + found = dlg.findEntry(kv.first); + if (found) + break; + } + EXPECT_NE(found, nullptr); +} + +// findEntry with a layer not in the dialog returns nullptr. +TEST_F(SaveLayersDialogTest, FindEntry_UnknownLayer_ReturnsNull) +{ + SaveLayersDialog dlg(&_sessionState, _mainWindow, /*isExporting=*/false); + auto unknownLayer = PXR_NS::SdfLayer::CreateAnonymous("unknown"); + EXPECT_EQ(dlg.findEntry(unknownLayer), nullptr); +} + +// sessionState() accessor returns a non-null pointer matching what was passed in. +TEST_F(SaveLayersDialogTest, SessionState_Accessor_ReturnsSessionState) +{ + SaveLayersDialog dlg(&_sessionState, _mainWindow, /*isExporting=*/false); + EXPECT_NE(dlg.sessionState(), nullptr); +} + +#ifndef MAYAUSD_OLD_LAYER_EDITOR +// ── New-editor-only tests ──────────────────────────────────────────────────── +// These depend on UsdLayerEditor::StageSavingInfo and setExecTestHandler, which +// are only present in the new editor's SaveLayersDialog. + +// exec() test handler returns a pre-set value without showing the dialog. +TEST_F(SaveLayersDialogTest, ExecTestHandler_ReturnsInjectedResult) +{ + // Install a handler that returns Accepted; capture the previous one to restore. + auto prev = SaveLayersDialog::setExecTestHandler([]() { return QDialog::Accepted; }); + { + SaveLayersDialog dlg(&_sessionState, _mainWindow, /*isExporting=*/false); + EXPECT_EQ(dlg.exec(), QDialog::Accepted); + } + SaveLayersDialog::setExecTestHandler(std::move(prev)); +} + +// Bulk constructor: sessionState() is null (no session state provided). +TEST_F(SaveLayersDialogTest, BulkConstructor_SessionState_IsNull) +{ + auto stage = TestUtils::makeStageWithSublayer("ns_sub"); + StageSavingInfo info; + info.stage = stage; + info.stageName = "ns_stage"; + info.dccObjectPath = "ns_stage"; + SaveLayersDialog dlg(_mainWindow, { info }, /*isExporting=*/false); + EXPECT_EQ(dlg.sessionState(), nullptr); +} + +#endif // !MAYAUSD_OLD_LAYER_EDITOR + +// ── onCancel() coverage ─────────────────────────────────────────────────── + +TEST_F(SaveLayersDialogTest, OnCancel_SetsResultToRejected) +{ + TestableSaveLayersDialog dlg(&_sessionState, _mainWindow, /*isExporting=*/false); + dlg.callOnCancel(); + EXPECT_EQ(dlg.result(), QDialog::Rejected); +} + +// ── onSaveAll() + okToSave() coverage ──────────────────────────────────── +// Rows always have auto-generated paths, so onSaveAll() takes the non-empty +// path branch and calls saveAnonymousLayer (which throws on a stub DCC path). +// Test that calling onSaveAll() directly doesn't crash when there are no rows. + +TEST_F(SaveLayersDialogTest, OnSaveAll_NoRows_DoesNotCrash) +{ + // Construct with an empty StageSavingInfo list so there are no rows. + TestableSaveLayersDialog dlg(_mainWindow, {}, /*isExporting=*/false); + EXPECT_NO_THROW(dlg.callOnSaveAll()); + EXPECT_TRUE(dlg.layersNotSaved().isEmpty()); +} + +// ── all-as-relative checkbox ────────────────────────────────────────────── + +// quietlyUncheckAllAsRelative clears the checkbox without re-triggering the +// per-entry callback. +TEST_F(SaveLayersDialogTest, QuietlyUncheckAllAsRelative_UnchecksCheckbox) +{ + SaveLayersDialog dlg(&_sessionState, _mainWindow, /*isExporting=*/false); + auto* cb = dlg.findChild(QString(), Qt::FindChildrenRecursively); + ASSERT_NE(cb, nullptr); + cb->setCheckState(Qt::Checked); + dlg.quietlyUncheckAllAsRelative(); + EXPECT_EQ(cb->checkState(), Qt::Unchecked); +} + +// onAllAsRelativeChanged propagates the checkbox state to every layer row. +TEST_F(SaveLayersDialogTest, OnAllAsRelativeChanged_AppliesToEntries) +{ + TestableSaveLayersDialog dlg(&_sessionState, _mainWindow, /*isExporting=*/false); + auto* cb = dlg.findChild(QString(), Qt::FindChildrenRecursively); + ASSERT_NE(cb, nullptr); + // SaveLayerPathRow's save-as-relative state has no public getter (the row type is + // defined only in the .cpp), so exercise both transitions and assert neither throws. + cb->setCheckState(Qt::Checked); + EXPECT_NO_THROW(dlg.callOnAllAsRelativeChanged()); + cb->setCheckState(Qt::Unchecked); + EXPECT_NO_THROW(dlg.callOnAllAsRelativeChanged()); +} + +} // namespace UsdLayerEditor diff --git a/test/lib/usdLayerEditor/cpp/testSerializationUtils.cpp b/test/lib/usdLayerEditor/cpp/testSerializationUtils.cpp new file mode 100644 index 0000000000..ba62c9aaee --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/testSerializationUtils.cpp @@ -0,0 +1,231 @@ +// 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 "utilSerialization.h" + +#include "layerEditorDCCFunctions.h" +#include "scopedLayerEditorDCCFunctions.h" + +#include +#include + +#include + +#include + +#include +#include + +namespace UsdLayerEditor { +namespace Serialization { + +namespace { +bool sublayerPathsContain(const PXR_NS::SdfLayerRefPtr& layer, const std::string& path) +{ + for (const auto& p : layer->GetSubLayerPaths()) { + if (p == path) + return true; + } + return false; +} +} // namespace + +// --- ensureUSDFileExtension --------------------------------------------------- + +TEST(SerializationUtils, EnsureUSDFileExtension_AppendsUsdWhenNoExtension) +{ + std::string path = "myfile"; + ensureUSDFileExtension(path); + EXPECT_EQ(path, "myfile.usd"); +} + +TEST(SerializationUtils, EnsureUSDFileExtension_NoOpForUsd) +{ + std::string path = "myfile.usd"; + ensureUSDFileExtension(path); + EXPECT_EQ(path, "myfile.usd"); +} + +TEST(SerializationUtils, EnsureUSDFileExtension_NoOpForUsdc) +{ + std::string path = "myfile.usdc"; + ensureUSDFileExtension(path); + EXPECT_EQ(path, "myfile.usdc"); +} + +TEST(SerializationUtils, EnsureUSDFileExtension_NoOpForUsda) +{ + std::string path = "myfile.usda"; + ensureUSDFileExtension(path); + EXPECT_EQ(path, "myfile.usda"); +} + +TEST(SerializationUtils, EnsureUSDFileExtension_NoOpForUsdz) +{ + std::string path = "myfile.usdz"; + ensureUSDFileExtension(path); + EXPECT_EQ(path, "myfile.usdz"); +} + +TEST(SerializationUtils, EnsureUSDFileExtension_AppendsUsdForForeignExtension) +{ + std::string path = "myfile.txt"; + ensureUSDFileExtension(path); + EXPECT_EQ(path, "myfile.txt.usd"); +} + +// --- updateSubLayer ----------------------------------------------------------- + +TEST(SerializationUtils, UpdateSubLayer_NoOpForNullParent) +{ + auto sublayer = PXR_NS::SdfLayer::CreateAnonymous("sub_noparent"); + EXPECT_NO_THROW(updateSubLayer(nullptr, sublayer, "/new/path.usd")); + EXPECT_TRUE(sublayer->GetSubLayerPaths().empty()); +} + +TEST(SerializationUtils, UpdateSubLayer_NoOpForNullOldLayer) +{ + auto parent = PXR_NS::SdfLayer::CreateAnonymous("parent_nosub"); + EXPECT_NO_THROW(updateSubLayer(parent, nullptr, "/new/path.usd")); + EXPECT_TRUE(parent->GetSubLayerPaths().empty()); +} + +TEST(SerializationUtils, UpdateSubLayer_ReplacesIdentifierInParent) +{ + auto parent = PXR_NS::SdfLayer::CreateAnonymous("parent_upd"); + auto sublayer = PXR_NS::SdfLayer::CreateAnonymous("sub_upd"); + + parent->InsertSubLayerPath(sublayer->GetIdentifier(), 0); + ASSERT_TRUE(sublayerPathsContain(parent, sublayer->GetIdentifier())) + << "precondition: sublayer identifier must be in parent's paths"; + + const std::string newPath = "/new/layer.usd"; + updateSubLayer(parent, sublayer, newPath); + + // Old identifier removed; new path present. + EXPECT_FALSE(sublayerPathsContain(parent, sublayer->GetIdentifier())); + EXPECT_TRUE(sublayerPathsContain(parent, newPath)); +} + +TEST(SerializationUtils, UpdateSubLayer_NewParentHasNoSubLayerBecomesNoOp) +{ + auto parent = PXR_NS::SdfLayer::CreateAnonymous("parent_empty"); + auto sublayer = PXR_NS::SdfLayer::CreateAnonymous("sub_notadded"); + // sublayer was never added to parent → Replace finds nothing → no crash, no change + EXPECT_NO_THROW(updateSubLayer(parent, sublayer, "/new/layer.usd")); + EXPECT_TRUE(parent->GetSubLayerPaths().empty()); +} + +// --- generateUniqueFileName --------------------------------------------------- + +TEST(SerializationUtils, GenerateUniqueFileName_ReturnsNonEmptyString) +{ + std::string first = generateUniqueFileName("test"); + std::string second = generateUniqueFileName("test"); + EXPECT_FALSE(first.empty()); + EXPECT_NE(first.find("test"), std::string::npos); + EXPECT_EQ(first.substr(first.size() - 4), ".usd"); + EXPECT_FALSE(second.empty()); + EXPECT_NE(second.find("test"), std::string::npos); + EXPECT_EQ(second.substr(second.size() - 4), ".usd"); + // A random suffix is appended, so successive calls never collide. + EXPECT_NE(first, second); +} + +// --- generateUniqueLayerFileName ---------------------------------------------- + +TEST(SerializationUtils, GenerateUniqueLayerFileName_WithLayer_AvoidsExistingFile) +{ + namespace fss = fs::filesystem; + + // Point the scene folder at a dedicated temp dir we control, restored on scope exit. + const fss::path sceneDir = fss::temp_directory_path() / "le_unique_layer_name_test"; + fss::create_directories(sceneDir); + + ScopedLayerEditorDCCFunctions guard; + FileSystemFns fns; + fns.getDCCSceneDir = [dir = sceneDir.generic_string()]() { return dir; }; + setFileSystemFns(fns); + + auto layer = PXR_NS::SdfLayer::CreateAnonymous("sublayer0"); + + // First call yields the deterministic candidate; pre-create it to force a conflict. + const std::string firstCandidate = generateUniqueLayerFileName("scene", layer); + ASSERT_FALSE(firstCandidate.empty()); + if (FILE* f = std::fopen(firstCandidate.c_str(), "w")) { + std::fclose(f); + } + + const std::string result = generateUniqueLayerFileName("scene", layer); + EXPECT_NE(result, firstCandidate); + EXPECT_FALSE(fss::exists(fss::path(result))); + + fss::remove(fss::path(firstCandidate)); + fss::remove_all(sceneDir); +} + +// --- usdFormatArgOption ------------------------------------------------------- + +TEST(SerializationUtils, UsdFormatArgOption_DefaultsToUsdc) +{ + // No DCC handler installed: getSaveLayerFormatBinary defaults to true → binary → usdc. + ScopedLayerEditorDCCFunctions guard; + setSaveOptionFns(SaveOptionFns{}); + EXPECT_EQ(usdFormatArgOption(), "usdc"); +} + +TEST(SerializationUtils, UsdFormatArgOption_NonBinaryReturnsUsda) +{ + ScopedLayerEditorDCCFunctions guard; + SaveOptionFns fns; + fns.getSaveLayerFormatBinary = []() { return false; }; + setSaveOptionFns(fns); + EXPECT_EQ(usdFormatArgOption(), "usda"); +} + +// --- getLayersToSaveFromStage ------------------------------------------------- + +TEST(SerializationUtils, GetLayersToSaveFromStage_NullStage_DoesNotCrash) +{ + StageLayersToSave info; + EXPECT_NO_THROW(getLayersToSaveFromStage(nullptr, "obj", info)); + EXPECT_TRUE(info._anonLayers.empty()); + EXPECT_TRUE(info._dirtyFileBackedLayers.empty()); +} + +TEST(SerializationUtils, GetLayersToSaveFromStage_ValidStage_PopulatesAnonLayers) +{ + auto stage = PXR_NS::UsdStage::CreateInMemory(); + auto sublayer = PXR_NS::SdfLayer::CreateAnonymous("sublayer_to_save"); + stage->GetRootLayer()->InsertSubLayerPath(sublayer->GetIdentifier(), 0); + + StageLayersToSave info; + getLayersToSaveFromStage(stage, "test_obj", info); + + // Root layer is anonymous and at least one anonymous sublayer was added. + EXPECT_GE(info._anonLayers.size(), 1u); +} + +// --- saveLayerWithFormat ------------------------------------------------------ + +TEST(SerializationUtils, SaveLayerWithFormat_EmptyPath_ReturnsFalse) +{ + auto layer = PXR_NS::SdfLayer::CreateAnonymous("save_test"); + // Empty path → falls back to GetRealPath() which is also empty for an anonymous + // layer → Save() with no backing file returns false on all platforms. + EXPECT_FALSE(saveLayerWithFormat(layer, "", "usda")); +} + +} // namespace Serialization +} // namespace UsdLayerEditor diff --git a/test/lib/usdLayerEditor/cpp/testSessionState.cpp b/test/lib/usdLayerEditor/cpp/testSessionState.cpp new file mode 100644 index 0000000000..6caaa95aba --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/testSessionState.cpp @@ -0,0 +1,172 @@ +// 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. +// +#pragma once + +#include + +#include "sessionState.h" +// stubSessionState.h is already included by testFixture.h in both old/new builds; +// including it here via relative path would find the wrong version in the old-editor build. +#ifndef MAYAUSD_OLD_LAYER_EDITOR +#include "stubSessionState.h" +#endif + +#include +#include + +namespace UsdLayerEditor { + +// setAutoHideSessionLayer emits the autoHideSessionLayerSignal. +TEST_F(LayerEditorTestFixture, SessionState_SetAutoHideSessionLayer_EmitsSignal) +{ + int signalCount = 0; + bool lastValue = false; + QObject::connect( + &_sessionState, + &SessionState::autoHideSessionLayerSignal, + &_sessionState, + [&signalCount, &lastValue](bool v) { + ++signalCount; + lastValue = v; + }); + + // Call the base implementation directly, bypassing the stub override. + _sessionState.SessionState::setAutoHideSessionLayer(true); + EXPECT_EQ(signalCount, 1); + EXPECT_TRUE(lastValue); + + _sessionState.SessionState::setAutoHideSessionLayer(false); + EXPECT_EQ(signalCount, 2); + EXPECT_FALSE(lastValue); +} + +// setDisplayLayerContents writes the flag (accessible via the public getter) and emits. +TEST_F(LayerEditorTestFixture, SessionState_SetDisplayLayerContents_UpdatesAndEmits) +{ + int signalCount = 0; + QObject::connect( + &_sessionState, + &SessionState::showDisplayLayerContents, + &_sessionState, + [&signalCount](bool) { ++signalCount; }); + + // Read via base getter (stub does not override this one). + bool initial = _sessionState.displayLayerContents(); + _sessionState.setDisplayLayerContents(!initial); + EXPECT_EQ(signalCount, 1); + EXPECT_EQ(_sessionState.displayLayerContents(), !initial); +} + +#ifndef MAYAUSD_OLD_LAYER_EDITOR +// setDisplayLayerExpandAllValues writes the flag and emits showDisplayLayerContents. +TEST_F(LayerEditorTestFixture, SessionState_SetDisplayLayerExpandAllValues_UpdatesAndEmits) +{ + int signalCount = 0; + QObject::connect( + &_sessionState, + &SessionState::showDisplayLayerContents, + &_sessionState, + [&signalCount](bool) { ++signalCount; }); + + bool initial = _sessionState.displayLayerExpandAllValues(); + _sessionState.setDisplayLayerExpandAllValues(!initial); + EXPECT_EQ(signalCount, 1); + EXPECT_EQ(_sessionState.displayLayerExpandAllValues(), !initial); +} +#endif + +#ifndef MAYAUSD_OLD_LAYER_EDITOR +// setDisplayLayerHideIndices writes the flag and emits showDisplayLayerContents. +TEST_F(LayerEditorTestFixture, SessionState_SetDisplayLayerHideIndices_UpdatesAndEmits) +{ + int signalCount = 0; + QObject::connect( + &_sessionState, + &SessionState::showDisplayLayerContents, + &_sessionState, + [&signalCount](bool) { ++signalCount; }); + + bool initial = _sessionState.displayLayerHideIndices(); + _sessionState.setDisplayLayerHideIndices(!initial); + EXPECT_EQ(signalCount, 1); + EXPECT_EQ(_sessionState.displayLayerHideIndices(), !initial); +} +#endif + +#ifndef MAYAUSD_OLD_LAYER_EDITOR +// setStageEntry with a different entry emits currentStageChangedSignal. +TEST_F(LayerEditorTestFixture, SessionState_SetStageEntry_NewEntry_EmitsSignal) +{ + int signalCount = 0; + QObject::connect( + &_sessionState, + &SessionState::currentStageChangedSignal, + &_sessionState, + [&signalCount]() { ++signalCount; }); + + auto newStage = PXR_NS::UsdStage::CreateInMemory(); + SessionState::StageEntry newEntry; + newEntry._id = "test_stage"; + newEntry._stage = newStage; + newEntry._displayName = "test_stage"; + newEntry._dccObjectPath = "test_stage"; + + _sessionState.setStageEntry(newEntry); + EXPECT_EQ(signalCount, 1); +} +#endif + +// setStageEntry with the same entry must NOT emit. +TEST_F(LayerEditorTestFixture, SessionState_SetStageEntry_SameEntry_NoSignal) +{ + int signalCount = 0; + QObject::connect( + &_sessionState, + &SessionState::currentStageChangedSignal, + &_sessionState, + [&signalCount]() { ++signalCount; }); + + _sessionState.setStageEntry(_sessionState.stageEntry()); + EXPECT_EQ(signalCount, 0); +} + +// targetLayer returns null when no stage is active. +#ifndef MAYAUSD_OLD_LAYER_EDITOR +TEST_F(LayerEditorTestFixture, SessionState_TargetLayer_NullWhenNoStage) +{ + StubSessionState emptyState; + // Replace the stub's stage with an empty entry so the null path is hit. + SessionState::StageEntry empty; + emptyState.SessionState::setStageEntry(empty); + EXPECT_EQ(emptyState.targetLayer(), nullptr); +} + +// isValid returns false when no stage is set. +TEST_F(LayerEditorTestFixture, SessionState_IsValid_FalseWhenNoStage) +{ + StubSessionState emptyState; + SessionState::StageEntry empty; + emptyState.SessionState::setStageEntry(empty); + EXPECT_FALSE(emptyState.isValid()); +} +#endif // !MAYAUSD_OLD_LAYER_EDITOR + +// isValid returns true with a valid stage. +TEST_F(LayerEditorTestFixture, SessionState_IsValid_TrueWithValidStage) +{ + EXPECT_TRUE(_sessionState.isValid()); +} + +} // namespace UsdLayerEditor diff --git a/test/lib/usdLayerEditor/cpp/testSharedStage.cpp b/test/lib/usdLayerEditor/cpp/testSharedStage.cpp new file mode 100644 index 0000000000..c0050757d9 --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/testSharedStage.cpp @@ -0,0 +1,242 @@ +// +// 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. +// +#pragma once + +#include +#include "testUtils.h" +#include "customLayerData.h" +#include "layerTreeItem.h" + +#include +#include +#include + +#include + +#include +#include + +namespace UsdLayerEditor { + +// ── SharedStageFixture ──────────────────────────────────────────────────────── +// isDccObjectSharedStage() = true — exercises the "owned stage" path where the +// layer editor is responsible for saving layers. + +class SharedStageFixture : public LayerEditorTestFixture +{ +protected: + void SetUp() override + { + setSharedStage(true); + LayerEditorTestFixture::SetUp(); + QApplication::processEvents(); + } +}; + +TEST_F(SharedStageFixture, NeedsSaving_TrueForAnonymousLayer) +{ + // Anonymous layers on a shared stage must be saved by the layer editor. + auto* item = treeModel()->layerItemFromIndex(firstSublayerIndex()); + ASSERT_NE(item, nullptr); + ASSERT_TRUE(item->isAnonymous()); + EXPECT_TRUE(item->needsSaving()); +} + +TEST_F(SharedStageFixture, NeedsSaving_TrueForDirtyRootLayer) +{ + _sessionState.stage()->GetRootLayer()->SetComment("make dirty"); + auto* root = treeModel()->layerItemFromIndex(rootLayerIndex()); + ASSERT_NE(root, nullptr); + EXPECT_TRUE(root->needsSaving()); +} + +TEST_F(SharedStageFixture, NeedsSaving_FalseForSessionLayer) +{ + // Session layers are Maya-managed — never counted as needing saving here. + auto* session = treeModel()->layerItemFromIndex(sessionLayerIndex()); + ASSERT_NE(session, nullptr); + session->layer()->SetComment("mark dirty"); + EXPECT_FALSE(session->needsSaving()); +} + +TEST_F(SharedStageFixture, SaveButton_VisibleOnSharedStage) +{ + QPushButton* btn = TestUtils::findButtonByTooltip(_widget, "Save all edits"); + ASSERT_NE(btn, nullptr); + EXPECT_TRUE(btn->isVisible()); +} + +TEST_F(SharedStageFixture, SaveButton_EnabledWhenLayersNeedSaving) +{ + // The stub stage has an anonymous sublayer, so count >= 1 → button enabled. + QPushButton* btn = TestUtils::findButtonByTooltip(_widget, "Save all edits"); + ASSERT_NE(btn, nullptr); + EXPECT_TRUE(btn->isVisible()); + EXPECT_TRUE(btn->isEnabled()); +} + +TEST_F(SharedStageFixture, NeedsSaving_FalseAfterSwitchingToNonSharedStage) +{ + // Dynamically flip to non-shared and rebuild the model. + setSharedStage(false); + treeModel()->forceRefresh(); + QApplication::processEvents(); + + auto* item = treeModel()->layerItemFromIndex(firstSublayerIndex()); + ASSERT_NE(item, nullptr); + EXPECT_FALSE(item->needsSaving()); +} + +TEST_F(SharedStageFixture, GetAllNeedsSavingLayers_EmptyAfterSwitchingToNonSharedStage) +{ + setSharedStage(false); + treeModel()->forceRefresh(); + QApplication::processEvents(); + + EXPECT_TRUE(treeModel()->getAllNeedsSavingLayers().empty()); +} + +// ── ReferencedLayersFixture ─────────────────────────────────────────────────── +// Uses the DCC-agnostic "adskSharedLayers" token. +// Skipped for the old Maya editor (which only writes/reads "mayaSharedLayers"). +// See MayaReferencedLayersFixture below for the token the old editor uses. + +#ifndef MAYAUSD_OLD_LAYER_EDITOR +class ReferencedLayersFixture : public LayerEditorTestFixture +{ +protected: + void SetUp() override + { + auto rootLayer = _sessionState.stage()->GetRootLayer(); + std::string sublayerPath = rootLayer->GetSubLayerPaths()[0]; + PXR_NS::VtArray refs = { sublayerPath }; + CustomLayerData::setStringArray(refs, rootLayer, PXR_NS::TfToken("adskSharedLayers")); + + LayerEditorTestFixture::SetUp(); + QApplication::processEvents(); + } +}; + +TEST_F(ReferencedLayersFixture, IsReadOnly_TrueForReferencedLayer) +{ + auto* item = treeModel()->layerItemFromIndex(firstSublayerIndex()); + ASSERT_NE(item, nullptr); + EXPECT_TRUE(item->isReadOnly()); +} + +TEST_F(ReferencedLayersFixture, NeedsSaving_FalseForReferencedLayerOnNonSharedStage) +{ + auto* item = treeModel()->layerItemFromIndex(firstSublayerIndex()); + ASSERT_NE(item, nullptr); + EXPECT_FALSE(item->needsSaving()); +} + +TEST_F(ReferencedLayersFixture, IsReadOnly_FalseForNonReferencedRootLayer) +{ + auto* root = treeModel()->layerItemFromIndex(rootLayerIndex()); + ASSERT_NE(root, nullptr); + EXPECT_FALSE(root->isReadOnly()); +} +#endif // MAYAUSD_OLD_LAYER_EDITOR + +// ── MayaReferencedLayersFixture ─────────────────────────────────────────────── +// Uses the legacy Maya-specific "mayaSharedLayers" token written by proxyShapeBase. +// Both the old and new editors must honour this token. + +class MayaReferencedLayersFixture : public LayerEditorTestFixture +{ +protected: + void SetUp() override + { + auto rootLayer = _sessionState.stage()->GetRootLayer(); + std::string sublayerPath = rootLayer->GetSubLayerPaths()[0]; + PXR_NS::VtArray refs = { sublayerPath }; + CustomLayerData::setStringArray(refs, rootLayer, PXR_NS::TfToken("mayaSharedLayers")); + + LayerEditorTestFixture::SetUp(); + QApplication::processEvents(); + } +}; + +TEST_F(MayaReferencedLayersFixture, IsReadOnly_TrueForMayaReferencedLayer) +{ + // A sublayer stamped with the legacy "mayaSharedLayers" token must be read-only. + auto* item = treeModel()->layerItemFromIndex(firstSublayerIndex()); + ASSERT_NE(item, nullptr); + EXPECT_TRUE(item->isReadOnly()); +} + +TEST_F(MayaReferencedLayersFixture, NeedsSaving_FalseForMayaReferencedLayerOnNonSharedStage) +{ + auto* item = treeModel()->layerItemFromIndex(firstSublayerIndex()); + ASSERT_NE(item, nullptr); + EXPECT_FALSE(item->needsSaving()); +} + +TEST_F(MayaReferencedLayersFixture, IsReadOnly_FalseForNonMayaReferencedRootLayer) +{ + auto* root = treeModel()->layerItemFromIndex(rootLayerIndex()); + ASSERT_NE(root, nullptr); + EXPECT_FALSE(root->isReadOnly()); +} + +// ── IncomingStageFixture ────────────────────────────────────────────────────── +// isDccObjectStageIncoming() = true — the stage is driven by an external source, +// so all layers are tagged as incoming. + +class IncomingStageFixture : public LayerEditorTestFixture +{ +protected: + void SetUp() override + { + setSharedStage(true); + setStageIncoming(true); + LayerEditorTestFixture::SetUp(); + QApplication::processEvents(); + } +}; + +TEST_F(IncomingStageFixture, IsIncoming_TrueForRootLayer) +{ + auto* root = treeModel()->layerItemFromIndex(rootLayerIndex()); + ASSERT_NE(root, nullptr); + EXPECT_TRUE(root->isIncoming()); +} + +TEST_F(IncomingStageFixture, IsIncoming_TrueForSublayer) +{ + auto* item = treeModel()->layerItemFromIndex(firstSublayerIndex()); + ASSERT_NE(item, nullptr); + EXPECT_TRUE(item->isIncoming()); +} + +TEST_F(IncomingStageFixture, IsIncoming_FalseForSessionLayer) +{ + // Session layers are never listed in the incoming set. + auto* session = treeModel()->layerItemFromIndex(sessionLayerIndex()); + ASSERT_NE(session, nullptr); + EXPECT_FALSE(session->isIncoming()); +} + +TEST_F(IncomingStageFixture, NeedsSaving_TrueForAnonymousLayerOnIncomingStage) +{ + // Incoming flag does not suppress saving; shared-stage flag drives that. + auto* item = treeModel()->layerItemFromIndex(firstSublayerIndex()); + ASSERT_NE(item, nullptr); + EXPECT_TRUE(item->needsSaving()); +} + +} // namespace UsdLayerEditor diff --git a/test/lib/usdLayerEditor/cpp/testStageSelectorWidget.cpp b/test/lib/usdLayerEditor/cpp/testStageSelectorWidget.cpp new file mode 100644 index 0000000000..390660d9f9 --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/testStageSelectorWidget.cpp @@ -0,0 +1,188 @@ +// 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. +// +#pragma once + +#include +#include "stageSelectorWidget.h" + +#include +#include +#include + +#include + +#include + +namespace UsdLayerEditor { + +// Expose protected slots as public methods for headless testing. +class TestableStageSelectorWidget : public StageSelectorWidget +{ +public: + using StageSelectorWidget::StageSelectorWidget; + + void testUpdateFromSessionState() { updateFromSessionState(); } + void testUpdateFromSessionStateWithEntry(SessionState::StageEntry const& entry) + { + updateFromSessionState(entry); + } + void testSessionStageChanged() { sessionStageChanged(); } + void testSelectedIndexChanged(int index) { selectedIndexChanged(index); } + void testUpdateContentButton() { updateContentButton(); } + void testStageRenamed(SessionState::StageEntry const& entry) { stageRenamed(entry); } + void testStageReset(SessionState::StageEntry const& entry) { stageReset(entry); } + void testStagePinClicked() { stagePinClicked(); } + void testCollapseContentClicked() { collapseContentClicked(); } + + QComboBox* dropDown() { return findChild(); } +}; + +class StageSelectorWidgetTest : public LayerEditorTestFixture +{ +protected: + std::unique_ptr makeWidget() + { + return std::make_unique(&_sessionState, nullptr); + } +}; + +// ── updateFromSessionState ──────────────────────────────────────────────── + +// The combo mirrors the session's stage list. +TEST_F(StageSelectorWidgetTest, UpdateFromSessionState_PopulatesComboFromStageList) +{ + auto w = makeWidget(); + w->testUpdateFromSessionState(); + ASSERT_NE(w->dropDown(), nullptr); + EXPECT_EQ(w->dropDown()->count(), static_cast(_sessionState.allStages().size())); +} + +TEST_F(StageSelectorWidgetTest, UpdateFromSessionState_Twice_KeepsComboCount) +{ + auto w = makeWidget(); + w->testUpdateFromSessionState(); + w->testUpdateFromSessionState(); + ASSERT_NE(w->dropDown(), nullptr); + EXPECT_EQ(w->dropDown()->count(), static_cast(_sessionState.allStages().size())); +} + +// ── sessionStageChanged ─────────────────────────────────────────────────── + +// An externally-driven session stage change moves the combo to the matching entry. +TEST_F(StageSelectorWidgetTest, SessionStageChanged_SelectsMatchingComboEntry) +{ + auto w = makeWidget(); + w->testUpdateFromSessionState(); + ASSERT_NE(w->dropDown(), nullptr); + ASSERT_GT(w->dropDown()->count(), 1); + + const auto& stages = _sessionState.allStages(); + _sessionState.setStageEntry(stages[1]); + w->testSessionStageChanged(); + + EXPECT_EQ(w->dropDown()->currentIndex(), 1); +} + +// ── selectedIndexChanged ────────────────────────────────────────────────── + +TEST_F(StageSelectorWidgetTest, SelectedIndexChanged_IndexMinusOne_DoesNotCrash) +{ + auto w = makeWidget(); + EXPECT_NO_THROW(w->testSelectedIndexChanged(-1)); +} + +// Selecting a combo entry pushes that entry to the session state. +TEST_F(StageSelectorWidgetTest, SelectedIndexChanged_SetsSessionStageToSelectedEntry) +{ + auto w = makeWidget(); + w->testUpdateFromSessionState(); + ASSERT_NE(w->dropDown(), nullptr); + ASSERT_GT(w->dropDown()->count(), 0); + + w->dropDown()->setCurrentIndex(0); + w->testSelectedIndexChanged(0); + + EXPECT_EQ(_sessionState.stageEntry(), _sessionState.allStages()[0]); +} + +// ── updateContentButton ─────────────────────────────────────────────────── + +// The collapse-content button exists and refreshing its icon does not crash. +// (updateContentButton early-returns when the button is null, so a present +// button is what makes this path observable.) +TEST_F(StageSelectorWidgetTest, UpdateContentButton_ButtonPresent) +{ + auto w = makeWidget(); + EXPECT_NE(w->findChild("collapseContentButton"), nullptr); + EXPECT_NO_THROW(w->testUpdateContentButton()); +} + +// ── collapseContentClicked ──────────────────────────────────────────────── + +// Clicking the collapse button toggles the session's display-layer-contents flag. +TEST_F(StageSelectorWidgetTest, CollapseContentClicked_TogglesDisplayLayerContents) +{ + auto w = makeWidget(); + const bool initial = _sessionState.displayLayerContents(); + w->testCollapseContentClicked(); + EXPECT_EQ(_sessionState.displayLayerContents(), !initial); +} + +// ── stagePinClicked ─────────────────────────────────────────────────────── + +// Clicking the pin button toggles whether the combo follows the UFE selection. +// The actual selection-following depends on DCC global selection state that isn't +// available in this headless test (and the old editor reads Maya UFE selection), +// so we only assert the toggle does not crash. +TEST_F(StageSelectorWidgetTest, StagePinClicked_TogglesWithoutCrash) +{ + auto w = makeWidget(); + EXPECT_NO_THROW(w->testStagePinClicked()); + EXPECT_NO_THROW(w->testStagePinClicked()); +} + +// ── stageRenamed / stageReset ───────────────────────────────────────────── + +// Renaming a stage updates the matching combo entry's text. +TEST_F(StageSelectorWidgetTest, StageRenamed_UpdatesComboItemText) +{ + auto w = makeWidget(); + w->testUpdateFromSessionState(); + ASSERT_NE(w->dropDown(), nullptr); + ASSERT_GT(w->dropDown()->count(), 0); + + SessionState::StageEntry renamed = _sessionState.allStages()[0]; + renamed._displayName = "RenamedStage"; + w->testStageRenamed(renamed); + + EXPECT_EQ(w->dropDown()->itemText(0), QString("RenamedStage")); +} + +TEST_F(StageSelectorWidgetTest, StageReset_DefaultEntry_DoesNotCrash) +{ + auto w = makeWidget(); + SessionState::StageEntry entry; + EXPECT_NO_THROW(w->testStageReset(entry)); +} + +// ── selectionChanged ────────────────────────────────────────────────────── + +TEST_F(StageSelectorWidgetTest, SelectionChanged_DoesNotCrash) +{ + auto w = makeWidget(); + EXPECT_NO_THROW(w->selectionChanged()); +} + +} // namespace UsdLayerEditor diff --git a/test/lib/usdLayerEditor/cpp/testUsdSyntaxHighlighter.cpp b/test/lib/usdLayerEditor/cpp/testUsdSyntaxHighlighter.cpp new file mode 100644 index 0000000000..7c84e93fb6 --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/testUsdSyntaxHighlighter.cpp @@ -0,0 +1,106 @@ +// 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. +// +#pragma once + +#include "usdSyntaxHighlighter.h" + +#include + +#include +#include +#include +#include + +#include + +#include +#include +#include + +namespace UsdLayerEditor { + +TEST(UsdSyntaxHighlighterTest, HighlightBlock_DoesNotCrashOnUsdContent) +{ + QTextDocument doc; + UsdSyntaxHighlighter hl(&doc); + EXPECT_NO_THROW(doc.setPlainText( + "#usda 1.0\n" + "def Sphere \"MySphere\" {\n" + " double radius = 1.0\n" + " # comment\n" + " string myAttr = \"hello\"\n" + "}\n")); +} + +TEST(UsdSyntaxHighlighterTest, HighlightBlock_EmptyTextDoesNotCrash) +{ + QTextDocument doc; + UsdSyntaxHighlighter hl(&doc); + EXPECT_NO_THROW(doc.setPlainText("")); +} + +// Tests the MAYAUSD_USD_SYNTAX_HIGHLIGHTING_CONFIG env var path. +// loadConfigFromJson() checks this env var first; setting it before construction +// makes the highlighter load rules from the provided file. +TEST(UsdSyntaxHighlighterTest, LoadConfigFromJson_CustomEnvVarPath) +{ + namespace fss = fs::filesystem; + const fss::path configFile = fss::temp_directory_path() / "le_test_syntax_config.json"; + + struct EnvGuard { + QByteArray name; + QByteArray original; + explicit EnvGuard(const char* n) : name(n), original(qgetenv(n)) {} + ~EnvGuard() { qputenv(name.constData(), original); } + } envGuard("MAYAUSD_USD_SYNTAX_HIGHLIGHTING_CONFIG"); + + // Minimal valid config: one specifier category with a word pattern. + const char* jsonContent = R"({ + "syntaxHighlighting": { + "specifiers": { + "color": "#E98FE6", + "fontWeight": "normal", + "wordPatterns": ["def", "over"] + }, + "numbers": { + "color": "#C3DCB5", + "fontWeight": "normal", + "patterns": ["\\b\\d+(\\.\\d+)?\\b"] + } + } +})"; + + { + QFile f(QString::fromStdString(configFile.string())); + ASSERT_TRUE(f.open(QIODevice::WriteOnly)); + f.write(QByteArray(jsonContent)); + } + + qputenv("MAYAUSD_USD_SYNTAX_HIGHLIGHTING_CONFIG", configFile.string().c_str()); + + { + QTextDocument doc; + UsdSyntaxHighlighter hl(&doc); + // Exercise highlightBlock with content that matches the loaded rules. + EXPECT_NO_THROW(doc.setPlainText( + "def Sphere \"S\" {\n" + " double r = 1.0\n" + "}\n")); + } + + fss::remove(configFile); +} + +} // namespace UsdLayerEditor diff --git a/test/lib/usdLayerEditor/cpp/testUtils.h b/test/lib/usdLayerEditor/cpp/testUtils.h new file mode 100644 index 0000000000..0a6dbd0aa1 --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/testUtils.h @@ -0,0 +1,129 @@ +// +// 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. +// +#pragma once + +#include "layerContentsWidget.h" + +#include +#include + +#include +#include +#include +#include +#include + +#include + +namespace UsdLayerEditor { +namespace TestUtils { + +// Find a QPushButton by case-insensitive substring of its tooltip. +inline QPushButton* findButtonByTooltip(QWidget* root, const QString& tooltip) +{ + for (auto* btn : root->findChildren()) { + if (btn->toolTip().contains(tooltip, Qt::CaseInsensitive)) + return btn; + } + return nullptr; +} + +// Find a QPushButton by its Qt object name. +inline QPushButton* findButtonByObjectName(QWidget* root, const QString& name) +{ + return root->findChild(name); +} + +// Find a QPushButton whose visible text contains any of the given substrings +// (case-insensitive). Useful for dialog buttons identified by label. +inline QPushButton* findButtonByText(QWidget* root, std::initializer_list texts) +{ + for (auto* btn : root->findChildren()) { + for (const auto& text : texts) { + if (btn->text().contains(text, Qt::CaseInsensitive)) + return btn; + } + } + return nullptr; +} + +inline QPushButton* findButtonByText(QWidget* root, const QString& text) +{ + return findButtonByText(root, { text }); +} + +// Locate the LayerContentsWidget inside a LayerEditorWidget hierarchy. +inline LayerContentsWidget* findContentsWidget(QWidget* root) +{ + return root->findChild(QString(), Qt::FindChildrenRecursively); +} + +// Stage with one anonymous sublayer already inserted at index 0. +inline PXR_NS::UsdStageRefPtr makeStageWithSublayer(const std::string& sublayerName = "sub") +{ + auto stage = PXR_NS::UsdStage::CreateInMemory(); + auto sub = PXR_NS::SdfLayer::CreateAnonymous(sublayerName); + stage->GetRootLayer()->InsertSubLayerPath(sub->GetIdentifier(), 0); + return stage; +} + +// Mark the root layer dirty by setting a comment. +inline void makeDirty(const PXR_NS::UsdStageRefPtr& stage) +{ + stage->GetRootLayer()->SetComment("dirty"); +} + +// Lock a layer by revoking edit permission directly (no DCC attr update). +inline void lockLayerDirect(const PXR_NS::SdfLayerRefPtr& layer) +{ + layer->SetPermissionToEdit(false); +} + +// Unlock a layer by restoring edit permission. +inline void unlockLayerDirect(const PXR_NS::SdfLayerRefPtr& layer) +{ + layer->SetPermissionToEdit(true); +} + +// Schedule closing any active modal dialog after `ms` milliseconds. +inline void dismissNextModal(int ms = 200) +{ + QTimer::singleShot(ms, []() { + QWidget* modal = QApplication::activeModalWidget(); + if (modal) + modal->close(); + }); +} + +// Find a named action in a menu (searches recursively into submenus). +inline QAction* findAction(QMenu* menu, const QString& text) +{ + if (!menu) + return nullptr; + for (QAction* action : menu->actions()) { + if (action->text() == text) + return action; + if (action->menu()) { + QAction* found = findAction(action->menu(), text); + if (found) + return found; + } + } + return nullptr; +} + +} // namespace TestUtils +} // namespace UsdLayerEditor diff --git a/test/lib/usdLayerEditor/cpp/testWidgetManager.cpp b/test/lib/usdLayerEditor/cpp/testWidgetManager.cpp new file mode 100644 index 0000000000..bc5709c877 --- /dev/null +++ b/test/lib/usdLayerEditor/cpp/testWidgetManager.cpp @@ -0,0 +1,91 @@ +// 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. +// +#pragma once + +#include + +#include "layerEditorWidgetManager.h" +#include "layerEditorWidget.h" + +#include + +namespace UsdLayerEditor { + +// getInstance() always returns the same non-null pointer. +TEST_F(LayerEditorTestFixture, WidgetManager_GetInstance_ReturnsSamePointer) +{ + auto* a = LayerEditorWidgetManager::getInstance(); + auto* b = LayerEditorWidgetManager::getInstance(); + EXPECT_NE(a, nullptr); + EXPECT_EQ(a, b); +} + +// The fixture's SetUp creates a LayerEditorWidget which calls setWidget(this). +// So the manager already has a live widget at test time. +TEST_F(LayerEditorTestFixture, WidgetManager_GetSelectedLayers_NoSelectionReturnsEmpty) +{ + // Clear selection AND current index so getSelectedLayerItems() returns nothing. + _widget->selectLayers({}); + QApplication::processEvents(); + + auto* mgr = LayerEditorWidgetManager::getInstance(); + auto layers = mgr->getSelectedLayers(); + EXPECT_TRUE(layers.empty()); +} + +TEST_F(LayerEditorTestFixture, WidgetManager_GetSelectedLayers_WithSelection_ReturnsIds) +{ + selectRow(rootLayerIndex()); + + auto* mgr = LayerEditorWidgetManager::getInstance(); + auto layers = mgr->getSelectedLayers(); + EXPECT_FALSE(layers.empty()); + EXPECT_EQ(layers.size(), 1u); +} + +// selectLayers with an empty list clears the selection and current index. +TEST_F(LayerEditorTestFixture, WidgetManager_SelectLayers_EmptyList_ClearsSelection) +{ + selectRow(rootLayerIndex()); + + auto* mgr = LayerEditorWidgetManager::getInstance(); + mgr->selectLayers({}); + QApplication::processEvents(); + + EXPECT_TRUE(layerTree()->selectionModel()->selectedRows().empty()); + EXPECT_FALSE(layerTree()->selectionModel()->currentIndex().isValid()); +} + +// selectLayers with a valid layer identifier selects it. +TEST_F(LayerEditorTestFixture, WidgetManager_SelectLayers_ValidId_SelectsLayer) +{ + auto* rootItem = dynamic_cast( + treeModel()->itemFromIndex(rootLayerIndex())); + ASSERT_NE(rootItem, nullptr); + const std::string rootId = rootItem->layer()->GetIdentifier(); + + layerTree()->selectionModel()->clearSelection(); + QApplication::processEvents(); + + auto* mgr = LayerEditorWidgetManager::getInstance(); + mgr->selectLayers({ rootId }); + QApplication::processEvents(); + + auto selected = mgr->getSelectedLayers(); + ASSERT_EQ(selected.size(), 1u); + EXPECT_EQ(selected[0], rootId); +} + +} // namespace UsdLayerEditor diff --git a/lib/usdLayerEditor/test/data/child_layer.usda b/test/lib/usdLayerEditor/data/child_layer.usda similarity index 100% rename from lib/usdLayerEditor/test/data/child_layer.usda rename to test/lib/usdLayerEditor/data/child_layer.usda diff --git a/lib/usdLayerEditor/test/data/empty.usda b/test/lib/usdLayerEditor/data/empty.usda similarity index 100% rename from lib/usdLayerEditor/test/data/empty.usda rename to test/lib/usdLayerEditor/data/empty.usda diff --git a/lib/usdLayerEditor/test/data/grandchild_layer.usda b/test/lib/usdLayerEditor/data/grandchild_layer.usda similarity index 100% rename from lib/usdLayerEditor/test/data/grandchild_layer.usda rename to test/lib/usdLayerEditor/data/grandchild_layer.usda diff --git a/lib/usdLayerEditor/test/data/layerLocking.usda b/test/lib/usdLayerEditor/data/layerLocking.usda similarity index 100% rename from lib/usdLayerEditor/test/data/layerLocking.usda rename to test/lib/usdLayerEditor/data/layerLocking.usda diff --git a/lib/usdLayerEditor/test/data/layerLockingSubLayer.usda b/test/lib/usdLayerEditor/data/layerLockingSubLayer.usda similarity index 100% rename from lib/usdLayerEditor/test/data/layerLockingSubLayer.usda rename to test/lib/usdLayerEditor/data/layerLockingSubLayer.usda diff --git a/lib/usdLayerEditor/test/data/layerLockingSubSubLayer.usda b/test/lib/usdLayerEditor/data/layerLockingSubSubLayer.usda similarity index 100% rename from lib/usdLayerEditor/test/data/layerLockingSubSubLayer.usda rename to test/lib/usdLayerEditor/data/layerLockingSubSubLayer.usda diff --git a/lib/usdLayerEditor/test/data/root.usda b/test/lib/usdLayerEditor/data/root.usda similarity index 100% rename from lib/usdLayerEditor/test/data/root.usda rename to test/lib/usdLayerEditor/data/root.usda diff --git a/lib/usdLayerEditor/test/data/root_3_layers.usda b/test/lib/usdLayerEditor/data/root_3_layers.usda similarity index 100% rename from lib/usdLayerEditor/test/data/root_3_layers.usda rename to test/lib/usdLayerEditor/data/root_3_layers.usda diff --git a/lib/usdLayerEditor/test/data/sublayer.usda b/test/lib/usdLayerEditor/data/sublayer.usda similarity index 100% rename from lib/usdLayerEditor/test/data/sublayer.usda rename to test/lib/usdLayerEditor/data/sublayer.usda diff --git a/lib/usdLayerEditor/test/data/sublayer_1.usda b/test/lib/usdLayerEditor/data/sublayer_1.usda similarity index 100% rename from lib/usdLayerEditor/test/data/sublayer_1.usda rename to test/lib/usdLayerEditor/data/sublayer_1.usda diff --git a/lib/usdLayerEditor/test/data/sublayer_2.usda b/test/lib/usdLayerEditor/data/sublayer_2.usda similarity index 100% rename from lib/usdLayerEditor/test/data/sublayer_2.usda rename to test/lib/usdLayerEditor/data/sublayer_2.usda diff --git a/lib/usdLayerEditor/test/layer_editor_test.py b/test/lib/usdLayerEditor/layer_editor_test.py similarity index 92% rename from lib/usdLayerEditor/test/layer_editor_test.py rename to test/lib/usdLayerEditor/layer_editor_test.py index 0aeb63aee1..54e2bd1960 100644 --- a/lib/usdLayerEditor/test/layer_editor_test.py +++ b/test/lib/usdLayerEditor/layer_editor_test.py @@ -59,6 +59,13 @@ def _redo(): def _openStageLayerEditor(): raise Exception("_openStageLayerEditor() function not set - must be configured by the DCC") return None + + @staticmethod + def _executeCmd(cmd): + # DCC-specific. 3dsmax/pybind11 hosts can delegate to ufe.UndoableCommandMgr. + # Maya wraps cmd.execute() in an MEL undo chunk because its UFE Python is + # pybind11 while UsdLayerEditor commands are boost.python-bound. + raise Exception("_executeCmd() function not set - must be configured by the DCC") def test_edit_target_cmd(self): @@ -78,13 +85,13 @@ def test_edit_target_cmd(self): sublayer = Sdf.Layer.FindOrOpen(self.script_folder + "/data/sublayer.usda") cmd = UsdLayerEditor.SetEditTargetCommand(stage, sublayer) - mgr.executeCmd(cmd); + UsdLayerEditorTest._executeCmd(cmd); self.assertEqual(stage.GetEditTarget().GetLayer().identifier, sublayer.identifier) # Can we set the target back to the root? cmd2 = UsdLayerEditor.SetEditTargetCommand(stage, rootLayer) - mgr.executeCmd(cmd2); + UsdLayerEditorTest._executeCmd(cmd2); self.assertEqual(stage.GetEditTarget().GetLayer().identifier, rootLayer.identifier) @@ -98,18 +105,18 @@ def test_edit_target_cmd(self): # Test with anonymous sublayer created by AddAnonSubLayerCommand addAnonCmd = UsdLayerEditor.AddAnonSubLayerCommand(stage, rootLayer) - mgr.executeCmd(addAnonCmd) + UsdLayerEditorTest._executeCmd(addAnonCmd) anonLayerId = addAnonCmd.addedLayer() anonLayer = Sdf.Layer.Find(anonLayerId) # Set edit target to the anonymous layer cmd3 = UsdLayerEditor.SetEditTargetCommand(stage, anonLayer) - mgr.executeCmd(cmd3) + UsdLayerEditorTest._executeCmd(cmd3) self.assertEqual(stage.GetEditTarget().GetLayer().identifier, anonLayerId) # Verify we can set target back to root from anonymous layer cmd4 = UsdLayerEditor.SetEditTargetCommand(stage, rootLayer) - mgr.executeCmd(cmd4) + UsdLayerEditorTest._executeCmd(cmd4) self.assertEqual(stage.GetEditTarget().GetLayer().identifier, rootLayer.identifier) # Test undo/redo with anonymous layer @@ -132,7 +139,7 @@ def test_clear_cmd(self): # Clear the layer cmd = UsdLayerEditor.ClearLayerCommand(rootLayer) mgr = ufe.UndoableCommandMgr.instance() - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) prim_count = sum(1 for _ in stage.Traverse()) self.assertEqual(prim_count, 0) @@ -157,11 +164,11 @@ def test_clear_cmd(self): # Add anonymous sublayers using AddAnonSubLayerCommand addAnonCmd1 = UsdLayerEditor.AddAnonSubLayerCommand(stage, rootLayer) - mgr.executeCmd(addAnonCmd1) + UsdLayerEditorTest._executeCmd(addAnonCmd1) anonLayerId1 = addAnonCmd1.addedLayer() addAnonCmd2 = UsdLayerEditor.AddAnonSubLayerCommand(stage, rootLayer) - mgr.executeCmd(addAnonCmd2) + UsdLayerEditorTest._executeCmd(addAnonCmd2) anonLayerId2 = addAnonCmd2.addedLayer() # Should now have 3 sublayers (1 original + 2 anonymous) @@ -171,7 +178,7 @@ def test_clear_cmd(self): # Clear the layer - should remove all sublayers including anonymous ones clearCmd = UsdLayerEditor.ClearLayerCommand(rootLayer) - mgr.executeCmd(clearCmd) + UsdLayerEditorTest._executeCmd(clearCmd) self.assertEqual(len(rootLayer.subLayerPaths), 0) @@ -213,7 +220,7 @@ def checkUnMuted(layer, stage): # Mute the layer cmd = UsdLayerEditor.MuteLayerCommand(stage, layer, True) mgr = ufe.UndoableCommandMgr.instance() - mgr.executeCmd(cmd); + UsdLayerEditorTest._executeCmd(cmd); # undo mute UsdLayerEditorTest._undo() @@ -261,7 +268,7 @@ def test_discard_edits_cmd(self): cmd = UsdLayerEditor.DiscardEditsCommand(rootLayer) mgr = ufe.UndoableCommandMgr.instance() - mgr.executeCmd(cmd); + UsdLayerEditorTest._executeCmd(cmd); # Test everything is gone self.assertEqual(len(rootLayer.subLayerPaths), 0) @@ -277,11 +284,11 @@ def test_discard_edits_cmd(self): # Test discarding edits on a layer with anonymous sublayers created by AddAnonSubLayerCommand # First, add some anonymous sublayers addAnonCmd1 = UsdLayerEditor.AddAnonSubLayerCommand(stage, rootLayer) - mgr.executeCmd(addAnonCmd1) + UsdLayerEditorTest._executeCmd(addAnonCmd1) anonLayerId1 = addAnonCmd1.addedLayer() addAnonCmd2 = UsdLayerEditor.AddAnonSubLayerCommand(stage, rootLayer) - mgr.executeCmd(addAnonCmd2) + UsdLayerEditorTest._executeCmd(addAnonCmd2) anonLayerId2 = addAnonCmd2.addedLayer() # Verify anonymous layers were added @@ -291,7 +298,7 @@ def test_discard_edits_cmd(self): # Discard edits - this should remove the anonymous sublayers discardCmd = UsdLayerEditor.DiscardEditsCommand(rootLayer) - mgr.executeCmd(discardCmd) + UsdLayerEditorTest._executeCmd(discardCmd) self.assertEqual(len(rootLayer.subLayerPaths), 0) @@ -315,12 +322,12 @@ def test_lock_layer_cmd(self): # Make sure we start with an unlocked layer. cmd = UsdLayerEditor.LockLayerCommand(stage, subLayer, UsdLayerEditor.LayerLock_Unlocked) - mgr.executeCmd(cmd); + UsdLayerEditorTest._executeCmd(cmd); self.assertTrue(subLayer.permissionToEdit) # Locking a layer cmd = UsdLayerEditor.LockLayerCommand(stage, subLayer, UsdLayerEditor.LayerLock_Locked) - mgr.executeCmd(cmd); + UsdLayerEditorTest._executeCmd(cmd); self.assertFalse(subLayer.permissionToEdit) UsdLayerEditorTest._undo() @@ -330,7 +337,7 @@ def test_lock_layer_cmd(self): # Unlocking a layer cmd = UsdLayerEditor.LockLayerCommand(stage, subLayer, UsdLayerEditor.LayerLock_Unlocked) - mgr.executeCmd(cmd); + UsdLayerEditorTest._executeCmd(cmd); self.assertTrue(subLayer.permissionToEdit) UsdLayerEditorTest._undo() @@ -340,7 +347,7 @@ def test_lock_layer_cmd(self): # System locking a layer cmd = UsdLayerEditor.LockLayerCommand(stage, subLayer, UsdLayerEditor.LayerLock_SystemLocked) - mgr.executeCmd(cmd); + UsdLayerEditorTest._executeCmd(cmd); self.assertFalse(subLayer.permissionToEdit) self.assertFalse(subLayer.permissionToSave) @@ -353,13 +360,13 @@ def test_lock_layer_cmd(self): # Unlock the system lock cmd = UsdLayerEditor.LockLayerCommand(stage, subLayer, UsdLayerEditor.LayerLock_Unlocked) - mgr.executeCmd(cmd); + UsdLayerEditorTest._executeCmd(cmd); self.assertTrue(subLayer.permissionToEdit) self.assertTrue(subLayer.permissionToSave) # Test locking anonymous layers created by AddAnonSubLayerCommand addAnonCmd = UsdLayerEditor.AddAnonSubLayerCommand(stage, stage.GetRootLayer()) - mgr.executeCmd(addAnonCmd) + UsdLayerEditorTest._executeCmd(addAnonCmd) anonLayerId = addAnonCmd.addedLayer() anonLayer = Sdf.Layer.Find(anonLayerId) @@ -370,7 +377,7 @@ def test_lock_layer_cmd(self): # Note: permissionToSave is related to filepath, and since anonymous layers are not file-backed, # it is always false. lockAnonCmd = UsdLayerEditor.LockLayerCommand(stage, anonLayer, UsdLayerEditor.LayerLock_Locked) - mgr.executeCmd(lockAnonCmd) + UsdLayerEditorTest._executeCmd(lockAnonCmd) self.assertFalse(anonLayer.permissionToEdit) @@ -384,13 +391,13 @@ def test_lock_layer_cmd(self): # System lock the anonymous layer sysLockAnonCmd = UsdLayerEditor.LockLayerCommand(stage, anonLayer, UsdLayerEditor.LayerLock_SystemLocked) - mgr.executeCmd(sysLockAnonCmd) + UsdLayerEditorTest._executeCmd(sysLockAnonCmd) self.assertFalse(anonLayer.permissionToEdit) # Unlock the anonymous layer unlockAnonCmd = UsdLayerEditor.LockLayerCommand(stage, anonLayer, UsdLayerEditor.LayerLock_Unlocked) - mgr.executeCmd(unlockAnonCmd) + UsdLayerEditorTest._executeCmd(unlockAnonCmd) self.assertTrue(anonLayer.permissionToEdit) @@ -405,7 +412,7 @@ def test_recursive_lock_single_layer(self): # Start all unlocked. cmd = UsdLayerEditor.LockLayerCommand(stage, topLayer, UsdLayerEditor.LayerLock_Unlocked, True) - mgr.executeCmd(cmd); + UsdLayerEditorTest._executeCmd(cmd); self.assertTrue(topLayer.permissionToEdit) self.assertTrue(topLayer.permissionToSave) @@ -416,7 +423,7 @@ def test_recursive_lock_single_layer(self): # Locking a layer recursively cmd = UsdLayerEditor.LockLayerCommand(stage, subLayer1_1, UsdLayerEditor.LayerLock_Locked, True) - mgr.executeCmd(cmd); + UsdLayerEditorTest._executeCmd(cmd); self.assertFalse(subLayer1_1.permissionToEdit) self.assertTrue(subLayer1_1.permissionToSave) @@ -429,7 +436,7 @@ def test_recursive_lock_single_layer(self): # Unlocking a layer recursively cmd = UsdLayerEditor.LockLayerCommand(stage, subLayer1_1, UsdLayerEditor.LayerLock_Unlocked, True) - mgr.executeCmd(cmd); + UsdLayerEditorTest._executeCmd(cmd); self.assertTrue(subLayer1_1.permissionToEdit) self.assertTrue(subLayer1_1.permissionToSave) UsdLayerEditorTest._undo() @@ -441,7 +448,7 @@ def test_recursive_lock_single_layer(self): # System locking a layer recursively cmd = UsdLayerEditor.LockLayerCommand(stage, subLayer1_1, UsdLayerEditor.LayerLock_SystemLocked, True) - mgr.executeCmd(cmd); + UsdLayerEditorTest._executeCmd(cmd); self.assertFalse(subLayer1_1.permissionToEdit) self.assertFalse(subLayer1_1.permissionToSave) UsdLayerEditorTest._undo() @@ -453,7 +460,7 @@ def test_recursive_lock_single_layer(self): # Unlocking a system-locked layer recursively cmd = UsdLayerEditor.LockLayerCommand(stage, subLayer1_1, UsdLayerEditor.LayerLock_Unlocked, True) - mgr.executeCmd(cmd); + UsdLayerEditorTest._executeCmd(cmd); self.assertTrue(subLayer1_1.permissionToEdit) self.assertTrue(subLayer1_1.permissionToSave) UsdLayerEditorTest._undo() @@ -474,7 +481,7 @@ def test_recursive_lock_multiLayers(self): # Start all unlocked. cmd = UsdLayerEditor.LockLayerCommand(stage, topLayer, UsdLayerEditor.LayerLock_Unlocked, True) - mgr.executeCmd(cmd); + UsdLayerEditorTest._executeCmd(cmd); self.assertTrue(subLayer1.permissionToEdit) self.assertTrue(subLayer1.permissionToSave) @@ -483,7 +490,7 @@ def test_recursive_lock_multiLayers(self): # Locking a layer recursively cmd = UsdLayerEditor.LockLayerCommand(stage, subLayer1, UsdLayerEditor.LayerLock_Locked, True) - mgr.executeCmd(cmd); + UsdLayerEditorTest._executeCmd(cmd); self.assertFalse(subLayer1.permissionToEdit) self.assertTrue(subLayer1.permissionToSave) @@ -502,7 +509,7 @@ def test_recursive_lock_multiLayers(self): # Unlocking a layer recursively cmd = UsdLayerEditor.LockLayerCommand(stage, subLayer1, UsdLayerEditor.LayerLock_Unlocked, True) - mgr.executeCmd(cmd); + UsdLayerEditorTest._executeCmd(cmd); self.assertTrue(subLayer1.permissionToEdit) self.assertTrue(subLayer1.permissionToSave) @@ -521,7 +528,7 @@ def test_recursive_lock_multiLayers(self): # System locking a layer cmd = UsdLayerEditor.LockLayerCommand(stage, subLayer1, UsdLayerEditor.LayerLock_SystemLocked, True) - mgr.executeCmd(cmd); + UsdLayerEditorTest._executeCmd(cmd); self.assertFalse(subLayer1.permissionToEdit) self.assertFalse(subLayer1.permissionToSave) @@ -547,7 +554,7 @@ def test_recursive_lock_multiLayers(self): # Otherwise, unlocking recursively inthe UI would unlock system # layers, which is not something we want the user to do. cmd = UsdLayerEditor.LockLayerCommand(stage, subLayer1, UsdLayerEditor.LayerLock_Unlocked, True, True) - mgr.executeCmd(cmd); + UsdLayerEditorTest._executeCmd(cmd); self.assertTrue(subLayer1.permissionToEdit) self.assertTrue(subLayer1.permissionToSave) @@ -567,7 +574,7 @@ def test_recursive_lock_multiLayers(self): # Unlocking a system-locked layer recursively cmd = UsdLayerEditor.LockLayerCommand(stage, subLayer1, UsdLayerEditor.LayerLock_Unlocked, True) - mgr.executeCmd(cmd); + UsdLayerEditorTest._executeCmd(cmd); self.assertTrue(subLayer1.permissionToEdit) self.assertTrue(subLayer1.permissionToSave) @@ -602,12 +609,12 @@ def test_remove_sub_path_using_index(self): # Remove second sublayer cmd = UsdLayerEditor.RemoveSubPathCommand(stage, rootLayer, 1) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) self.assertEqual(rootLayer.subLayerPaths, [layer1Id, layer3Id]) # Remove second sublayer again to leave only one cmd = UsdLayerEditor.RemoveSubPathCommand(stage, rootLayer, 1) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) self.assertEqual(rootLayer.subLayerPaths, [layer1Id]) # Remove second sublayer, out of bounds -> Exception @@ -630,7 +637,7 @@ def test_remove_sub_path_using_index(self): # layer3 has a sub layer which it self also has a sublayer. # delete the top layer. See if it comes back after redo. cmd = UsdLayerEditor.RemoveSubPathCommand(stage, rootLayer, 2) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) # check layer3 was deleted self.assertEqual(rootLayer.subLayerPaths, [layer1Id, layer2Id]) # bring it back @@ -654,15 +661,15 @@ def test_remove_sub_path_using_index(self): # Add anonymous sublayers using AddAnonSubLayerCommand addAnonCmd1 = UsdLayerEditor.AddAnonSubLayerCommand(stage2, rootLayer2) - mgr.executeCmd(addAnonCmd1) + UsdLayerEditorTest._executeCmd(addAnonCmd1) anonLayerId1 = addAnonCmd1.addedLayer() addAnonCmd2 = UsdLayerEditor.AddAnonSubLayerCommand(stage2, rootLayer2) - mgr.executeCmd(addAnonCmd2) + UsdLayerEditorTest._executeCmd(addAnonCmd2) anonLayerId2 = addAnonCmd2.addedLayer() addAnonCmd3 = UsdLayerEditor.AddAnonSubLayerCommand(stage2, rootLayer2) - mgr.executeCmd(addAnonCmd3) + UsdLayerEditorTest._executeCmd(addAnonCmd3) anonLayerId3 = addAnonCmd3.addedLayer() # Should have 3 anonymous sublayers @@ -671,13 +678,13 @@ def test_remove_sub_path_using_index(self): # Remove middle anonymous sublayer (index 1) removeCmd1 = UsdLayerEditor.RemoveSubPathCommand(stage2, rootLayer2, 1) - mgr.executeCmd(removeCmd1) + UsdLayerEditorTest._executeCmd(removeCmd1) self.assertEqual(len(rootLayer2.subLayerPaths), 2) self.assertEqual(rootLayer2.subLayerPaths, [anonLayerId3, anonLayerId1]) # Remove first anonymous sublayer (index 0) removeCmd2 = UsdLayerEditor.RemoveSubPathCommand(stage2, rootLayer2, 0) - mgr.executeCmd(removeCmd2) + UsdLayerEditorTest._executeCmd(removeCmd2) self.assertEqual(len(rootLayer2.subLayerPaths), 1) self.assertEqual(rootLayer2.subLayerPaths, [anonLayerId1]) @@ -708,12 +715,12 @@ def test_remove_sub_path_using_path(self): # Remove second sublayer cmd = UsdLayerEditor.RemoveSubPathCommand(stage, rootLayer, layer2Id) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) self.assertEqual(rootLayer.subLayerPaths, [layer1Id, layer3Id]) # Remove second sublayer again to leave only one cmd = UsdLayerEditor.RemoveSubPathCommand(stage, rootLayer, layer3Id) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) self.assertEqual(rootLayer.subLayerPaths, [layer1Id]) # undo twice to get back to three layers @@ -730,7 +737,7 @@ def test_remove_sub_path_using_path(self): # layer3 has a sub layer which it self also has a sublayer. # delete the top layer. See if it comes back after redo. cmd = UsdLayerEditor.RemoveSubPathCommand(stage, rootLayer, layer3Id) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) # check layer3 was deleted self.assertEqual(rootLayer.subLayerPaths, [layer1Id, layer2Id]) # bring it back @@ -754,11 +761,11 @@ def test_remove_sub_path_using_path(self): # Add anonymous sublayers using AddAnonSubLayerCommand addAnonCmd1 = UsdLayerEditor.AddAnonSubLayerCommand(stage2, rootLayer2) - mgr.executeCmd(addAnonCmd1) + UsdLayerEditorTest._executeCmd(addAnonCmd1) anonLayerId1 = addAnonCmd1.addedLayer() addAnonCmd2 = UsdLayerEditor.AddAnonSubLayerCommand(stage2, rootLayer2) - mgr.executeCmd(addAnonCmd2) + UsdLayerEditorTest._executeCmd(addAnonCmd2) anonLayerId2 = addAnonCmd2.addedLayer() # Should have 2 anonymous sublayers @@ -768,14 +775,14 @@ def test_remove_sub_path_using_path(self): # Remove first anonymous sublayer by path removeCmd1 = UsdLayerEditor.RemoveSubPathCommand(stage2, rootLayer2, anonLayerId1) - mgr.executeCmd(removeCmd1) + UsdLayerEditorTest._executeCmd(removeCmd1) self.assertEqual(len(rootLayer2.subLayerPaths), 1) self.assertNotIn(anonLayerId1, rootLayer2.subLayerPaths) self.assertIn(anonLayerId2, rootLayer2.subLayerPaths) # Remove second anonymous sublayer by path removeCmd2 = UsdLayerEditor.RemoveSubPathCommand(stage2, rootLayer2, anonLayerId2) - mgr.executeCmd(removeCmd2) + UsdLayerEditorTest._executeCmd(removeCmd2) self.assertEqual(len(rootLayer2.subLayerPaths), 0) # Test undo/redo @@ -802,22 +809,22 @@ def test_insert_sub_path(self): end = "child_layer.usda" cmd = UsdLayerEditor.InsertSubPathCommand(stage, rootLayer, second, 0) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) self.assertEqual(rootLayer.subLayerPaths, [second]) cmd = UsdLayerEditor.InsertSubPathCommand(stage, rootLayer, first, 0) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) self.assertEqual(rootLayer.subLayerPaths, [first, second]) cmd = UsdLayerEditor.InsertSubPathCommand(stage, rootLayer, middle, 1) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) self.assertEqual(rootLayer.subLayerPaths, [first, middle, second]) cmd = UsdLayerEditor.InsertSubPathCommand(stage, rootLayer, end, 3) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) self.assertEqual(rootLayer.subLayerPaths, [first, middle, second, end]) @@ -848,11 +855,11 @@ def test_insert_sub_path(self): # First add some anonymous layers using AddAnonSubLayerCommand addAnonCmd1 = UsdLayerEditor.AddAnonSubLayerCommand(stage2, rootLayer2) - mgr.executeCmd(addAnonCmd1) + UsdLayerEditorTest._executeCmd(addAnonCmd1) anonLayerId1 = addAnonCmd1.addedLayer() addAnonCmd2 = UsdLayerEditor.AddAnonSubLayerCommand(stage2, rootLayer2) - mgr.executeCmd(addAnonCmd2) + UsdLayerEditorTest._executeCmd(addAnonCmd2) anonLayerId2 = addAnonCmd2.addedLayer() # Should have 2 anonymous layers, most recent first @@ -861,17 +868,17 @@ def test_insert_sub_path(self): # Insert a regular layer at the beginning (index 0) insertCmd1 = UsdLayerEditor.InsertSubPathCommand(stage2, rootLayer2, first, 0) - mgr.executeCmd(insertCmd1) + UsdLayerEditorTest._executeCmd(insertCmd1) self.assertEqual(rootLayer2.subLayerPaths, [first, anonLayerId2, anonLayerId1]) # Insert a regular layer in the middle (index 2) insertCmd2 = UsdLayerEditor.InsertSubPathCommand(stage2, rootLayer2, middle, 2) - mgr.executeCmd(insertCmd2) + UsdLayerEditorTest._executeCmd(insertCmd2) self.assertEqual(rootLayer2.subLayerPaths, [first, anonLayerId2, middle, anonLayerId1]) # Insert a regular layer at the end insertCmd3 = UsdLayerEditor.InsertSubPathCommand(stage2, rootLayer2, end, 4) - mgr.executeCmd(insertCmd3) + UsdLayerEditorTest._executeCmd(insertCmd3) self.assertEqual(rootLayer2.subLayerPaths, [first, anonLayerId2, middle, anonLayerId1, end]) # Test undo/redo @@ -897,14 +904,14 @@ def test_refresh_system_lock(self): # 2- Setting a system lock on a layer loaded from a file # System locking a layer cmd = UsdLayerEditor.LockLayerCommand(stage, topLayer, UsdLayerEditor.LayerLock_SystemLocked) - mgr.executeCmd(cmd); + UsdLayerEditorTest._executeCmd(cmd); self.assertFalse(topLayer.permissionToEdit) self.assertFalse(topLayer.permissionToSave) # 3- Refreshing the system lock should remove the lock if the file is writable cmd = UsdLayerEditor.RefreshSystemLockLayerCommand(stage, topLayer, False) - mgr.executeCmd(cmd); + UsdLayerEditorTest._executeCmd(cmd); self.assertTrue(topLayer.permissionToEdit) self.assertTrue(topLayer.permissionToSave) @@ -920,7 +927,7 @@ def test_refresh_system_lock_callback(self): # 2- Setting a system lock on a layer loaded from a file and its sub-layer cmd = UsdLayerEditor.LockLayerCommand(stage, topLayer, UsdLayerEditor.LayerLock_SystemLocked) - mgr.executeCmd(cmd); + UsdLayerEditorTest._executeCmd(cmd); self.assertFalse(topLayer.permissionToEdit) self.assertFalse(topLayer.permissionToSave) @@ -942,7 +949,7 @@ def refreshSystemLockCallback(context, callbackData): usdUfe.registerUICallback('onRefreshSystemLock', refreshSystemLockCallback) # 4- Refreshing the system lock should remove the lock. cmd = UsdLayerEditor.RefreshSystemLockLayerCommand(stage, topLayer, False) - mgr.executeCmd(cmd); + UsdLayerEditorTest._executeCmd(cmd); self.assertEqual(self.callCount, 1) # 5- Unregistering the callback and refreshing the system lock should not call @@ -951,11 +958,11 @@ def refreshSystemLockCallback(context, callbackData): # Note: we must relock the layer for the callback to be called, otherwise it does not get # called as the status of the layer would not have changed during the refresh. cmd = UsdLayerEditor.LockLayerCommand(stage, topLayer, UsdLayerEditor.LayerLock_SystemLocked) - mgr.executeCmd(cmd); + UsdLayerEditorTest._executeCmd(cmd); usdUfe.unregisterUICallback('onRefreshSystemLock', refreshSystemLockCallback) cmd = UsdLayerEditor.RefreshSystemLockLayerCommand(stage, topLayer, False) - mgr.executeCmd(cmd); + UsdLayerEditorTest._executeCmd(cmd); self.assertEqual(self.callCount, 1) # 6- Unregistering again should do nothing and not crash. @@ -993,10 +1000,10 @@ def _verifyStageAfterRefreshSystemLock( # refreshSystemLock. lockStatus = UsdLayerEditor.LayerLock_Unlocked if UsdLayerEditor.isLayerSystemLocked(rootLayer) else UsdLayerEditor.LayerLock_SystemLocked cmd = UsdLayerEditor.LockLayerCommand(stage, rootLayer, lockStatus) - mgr.executeCmd(cmd); + UsdLayerEditorTest._executeCmd(cmd); cmd = UsdLayerEditor.RefreshSystemLockLayerCommand(stage, rootLayer, True) - mgr.executeCmd(cmd); + UsdLayerEditorTest._executeCmd(cmd); if callback is not None: usdUfe.unregisterUICallback('onRefreshSystemLock', callback) @@ -1135,11 +1142,11 @@ def selectionChangedCallback(context, callbackData): mgr = ufe.UndoableCommandMgr.instance() addAnonCmd1 = UsdLayerEditor.AddAnonSubLayerCommand(stage, stage.GetRootLayer()) - mgr.executeCmd(addAnonCmd1) + UsdLayerEditorTest._executeCmd(addAnonCmd1) anonLayerId1 = addAnonCmd1.addedLayer() addAnonCmd2 = UsdLayerEditor.AddAnonSubLayerCommand(stage, stage.GetRootLayer()) - mgr.executeCmd(addAnonCmd2) + UsdLayerEditorTest._executeCmd(addAnonCmd2) anonLayerId2 = addAnonCmd2.addedLayer() # Verify anonymous layers were added to the stage @@ -1188,7 +1195,7 @@ def test_add_anon_sublayer_cmd(self): # Create and execute AddAnonSubLayerCommand cmd = UsdLayerEditor.AddAnonSubLayerCommand(stage, rootLayer) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) # Verify that a sublayer was added self.assertEqual(len(rootLayer.subLayerPaths), 1) @@ -1229,7 +1236,7 @@ def test_add_anon_sublayer_cmd_multiple_layers(self): # Add first anonymous layer cmd1 = UsdLayerEditor.AddAnonSubLayerCommand(stage, rootLayer) - mgr.executeCmd(cmd1) + UsdLayerEditorTest._executeCmd(cmd1) addedLayerId1 = cmd1.addedLayer() self.assertEqual(len(rootLayer.subLayerPaths), 1) @@ -1237,7 +1244,7 @@ def test_add_anon_sublayer_cmd_multiple_layers(self): # Add second anonymous layer (should be inserted at index 0, becoming the first) cmd2 = UsdLayerEditor.AddAnonSubLayerCommand(stage, rootLayer) - mgr.executeCmd(cmd2) + UsdLayerEditorTest._executeCmd(cmd2) addedLayerId2 = cmd2.addedLayer() self.assertEqual(len(rootLayer.subLayerPaths), 2) @@ -1288,7 +1295,7 @@ def test_add_anon_sublayer_cmd_to_existing_sublayers(self): # Add anonymous sublayer cmd = UsdLayerEditor.AddAnonSubLayerCommand(stage, rootLayer) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) addedLayerId = cmd.addedLayer() # Should now have one more sublayer, with the anonymous layer at index 0 @@ -1316,7 +1323,7 @@ def _createAnonymousLayer(self, stage, parentLayer, name=""): """Helper to create an anonymous sublayer and return its identifier.""" mgr = ufe.UndoableCommandMgr.instance() cmd = UsdLayerEditor.AddAnonSubLayerCommand(stage, parentLayer) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) return cmd.addedLayer() def test_stitch_layers(self): @@ -1340,11 +1347,11 @@ def test_stitch_layers(self): # Clear and reinsert in desired strength order rootLayer.subLayerPaths.clear() cmd = UsdLayerEditor.InsertSubPathCommand(stage, rootLayer, layer1Id, 0) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) cmd = UsdLayerEditor.InsertSubPathCommand(stage, rootLayer, layer2Id, 1) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) cmd = UsdLayerEditor.InsertSubPathCommand(stage, rootLayer, layer3Id, 2) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) layer1 = Sdf.Layer.Find(layer1Id) layer2 = Sdf.Layer.Find(layer2Id) @@ -1375,7 +1382,7 @@ def test_stitch_layers(self): # Stitch - order passed does not matter, strongest receives all other layers. cmd = UsdLayerEditor.StitchLayersCommand(stage, [layer2Id, layer1Id, layer3Id]) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) self.assertEqual(len(rootLayer.subLayerPaths), 1) self.assertEqual(rootLayer.subLayerPaths[0], layer1Id) @@ -1424,16 +1431,16 @@ def test_stitch_layers_with_edit_target(self): weakLayerId = self._createAnonymousLayer(stage, rootLayer) rootLayer.subLayerPaths.clear() cmd = UsdLayerEditor.InsertSubPathCommand(stage, rootLayer, strongLayerId, 0) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) cmd = UsdLayerEditor.InsertSubPathCommand(stage, rootLayer, weakLayerId, 1) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) weakLayer = Sdf.Layer.Find(weakLayerId) stage.SetEditTarget(weakLayer) self.assertEqual(stage.GetEditTarget().GetLayer().identifier, weakLayerId) cmd = UsdLayerEditor.StitchLayersCommand(stage, [strongLayerId, weakLayerId]) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) currentTarget = stage.GetEditTarget().GetLayer().identifier self.assertNotEqual(currentTarget, weakLayerId) @@ -1441,6 +1448,47 @@ def test_stitch_layers_with_edit_target(self): UsdLayerEditorTest._undo() self.assertEqual(stage.GetEditTarget().GetLayer().identifier, weakLayerId) + def test_stitch_layers_with_locked_parent_layer(self): + """Test stitching fails when one selected layer is under a locked parent layer (EMSUSD-3706)""" + + stage = UsdLayerEditorTest._createStage(self.script_folder + "/data/empty.usda") + rootLayer = stage.GetRootLayer() + + layer1Id = self._createAnonymousLayer(stage, rootLayer) + layer2Id = self._createAnonymousLayer(stage, rootLayer) + layer3Id = self._createAnonymousLayer(stage, Sdf.Layer.Find(layer2Id)) + + rootLayer.subLayerPaths.clear() + cmd = UsdLayerEditor.InsertSubPathCommand(stage, rootLayer, layer1Id, 0) + UsdLayerEditorTest._executeCmd(cmd) + cmd = UsdLayerEditor.InsertSubPathCommand(stage, rootLayer, layer2Id, 1) + UsdLayerEditorTest._executeCmd(cmd) + + layer1 = Sdf.Layer.Find(layer1Id) + layer2 = Sdf.Layer.Find(layer2Id) + layer3 = Sdf.Layer.Find(layer3Id) + + with Sdf.ChangeBlock(): + prim = Sdf.CreatePrimInLayer(layer3, '/Cube') + prim.SetInfo('typeName', 'Cube') + + self.assertIsNotNone(layer3.GetPrimAtPath('/Cube')) + self.assertIsNone(layer1.GetPrimAtPath('/Cube')) + + # Lock layer2 (the parent of layer3) + lockCmd = UsdLayerEditor.LockLayerCommand(stage, layer2, UsdLayerEditor.LayerLock_Locked) + UsdLayerEditorTest._executeCmd(lockCmd) + self.assertFalse(layer2.permissionToEdit) + + # Stitching layer1 and layer3 should fail because layer3's parent (layer2) is locked + with self.assertRaises(RuntimeError): + cmd = UsdLayerEditor.StitchLayersCommand(stage, [layer1Id, layer3Id]) + cmd.execute() + + # Verify no content was moved + self.assertIsNotNone(layer3.GetPrimAtPath('/Cube')) + self.assertIsNone(layer1.GetPrimAtPath('/Cube')) + def test_stitch_layers_partial_selection(self): """Test stitching only some layers while leaving others untouched""" @@ -1454,19 +1502,19 @@ def test_stitch_layers_partial_selection(self): layer4Id = self._createAnonymousLayer(stage, rootLayer) rootLayer.subLayerPaths.clear() cmd = UsdLayerEditor.InsertSubPathCommand(stage, rootLayer, layer1Id, 0) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) cmd = UsdLayerEditor.InsertSubPathCommand(stage, rootLayer, layer2Id, 1) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) cmd = UsdLayerEditor.InsertSubPathCommand(stage, rootLayer, layer3Id, 2) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) cmd = UsdLayerEditor.InsertSubPathCommand(stage, rootLayer, layer4Id, 3) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) self.assertEqual(len(rootLayer.subLayerPaths), 4) # Stitch only layer1 and layer3 (layer1 is stronger, so it receives layer3's content) cmd = UsdLayerEditor.StitchLayersCommand(stage, [layer1Id, layer3Id]) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) self.assertEqual(len(rootLayer.subLayerPaths), 3) self.assertIn(layer1Id, rootLayer.subLayerPaths) @@ -1495,9 +1543,9 @@ def test_stitch_layers_hierarchical(self): childWeakId = self._createAnonymousLayer(stage, parentLayer) parentLayer.subLayerPaths.clear() cmd = UsdLayerEditor.InsertSubPathCommand(stage, parentLayer, childStrongId, 0) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) cmd = UsdLayerEditor.InsertSubPathCommand(stage, parentLayer, childWeakId, 1) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) childStrong = Sdf.Layer.Find(childStrongId) childWeak = Sdf.Layer.Find(childWeakId) @@ -1517,7 +1565,7 @@ def test_stitch_layers_hierarchical(self): self.assertEqual(len(parentLayer.subLayerPaths), 2) cmd = UsdLayerEditor.StitchLayersCommand(stage, [childStrongId, childWeakId]) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) self.assertEqual(len(parentLayer.subLayerPaths), 1) self.assertEqual(parentLayer.subLayerPaths[0], childStrongId) @@ -1564,7 +1612,7 @@ def test_stitch_layers_with_dirty_anonymous_layers(self): self.assertTrue(weak.dirty) cmd = UsdLayerEditor.StitchLayersCommand(stage, [strongId, weakId]) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) UsdLayerEditorTest._undo() @@ -1622,7 +1670,7 @@ def test_stitch_layers_with_single_sublayer(self): rootLayer.subLayerPaths.clear() cmd = UsdLayerEditor.InsertSubPathCommand(stage, rootLayer, parent1Id, 0) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) self.assertEqual(len(rootLayer.subLayerPaths), 1) self.assertEqual(rootLayer.subLayerPaths[0], parent1Id) @@ -1630,7 +1678,7 @@ def test_stitch_layers_with_single_sublayer(self): self.assertEqual(len(layer2Layer.subLayerPaths), 1) cmd = UsdLayerEditor.StitchLayersCommand(stage, [parent1Id, layer2Id]) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) self.assertEqual(len(rootLayer.subLayerPaths), 1) self.assertEqual(rootLayer.subLayerPaths[0], parent1Id) @@ -1757,9 +1805,9 @@ def test_batch_stitch(self): rootLayer.subLayerPaths.clear() cmd = UsdLayerEditor.InsertSubPathCommand(stage, rootLayer, parent1Id, 0) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) cmd = UsdLayerEditor.InsertSubPathCommand(stage, rootLayer, parent2Id, 1) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) self.assertEqual(len(rootLayer.subLayerPaths), 2) self.assertEqual(len(parent1Layer.subLayerPaths), 1) @@ -1769,7 +1817,7 @@ def test_batch_stitch(self): self.assertEqual(len(sub3Layer.subLayerPaths), 1) cmd = UsdLayerEditor.StitchLayersCommand(stage, [sub1Id, layer2Id, sub3Id]) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) self.assertEqual(len(parent2Layer.subLayerPaths), 0, "Layer2 should be removed from Parent2") @@ -1889,9 +1937,9 @@ def test_shared_sublayers(self): rootLayer.subLayerPaths.clear() cmd = UsdLayerEditor.InsertSubPathCommand(stage, rootLayer, layer1Id, 0) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) cmd = UsdLayerEditor.InsertSubPathCommand(stage, rootLayer, layer2Id, 1) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) self.assertEqual(len(layer1Layer.subLayerPaths), 1) self.assertEqual(len(layer2Layer.subLayerPaths), 1) @@ -1902,7 +1950,7 @@ def test_shared_sublayers(self): self.assertEqual(layer2Sub.identifier, sharedSubId) cmd = UsdLayerEditor.StitchLayersCommand(stage, [layer1Id, layer2Id]) - mgr.executeCmd(cmd) + UsdLayerEditorTest._executeCmd(cmd) self.assertEqual(len(layer1Layer.subLayerPaths), 1, "SharedSublayer should appear only once in Layer1 (no duplicate)") @@ -1975,22 +2023,22 @@ def testMergeWithSublayers(self): # Layer2 # Layer2Sub addLayer1Cmd = UsdLayerEditor.AddAnonSubLayerCommand(stage, rootLayer) - mgr.executeCmd(addLayer1Cmd) + UsdLayerEditorTest._executeCmd(addLayer1Cmd) layer1Id = addLayer1Cmd.addedLayer() addLayer2Cmd = UsdLayerEditor.AddAnonSubLayerCommand(stage, rootLayer) - mgr.executeCmd(addLayer2Cmd) + UsdLayerEditorTest._executeCmd(addLayer2Cmd) layer2Id = addLayer2Cmd.addedLayer() layer1 = Sdf.Layer.Find(layer1Id) layer2 = Sdf.Layer.Find(layer2Id) addLayer1SubCmd = UsdLayerEditor.AddAnonSubLayerCommand(stage, layer1) - mgr.executeCmd(addLayer1SubCmd) + UsdLayerEditorTest._executeCmd(addLayer1SubCmd) layer1SubId = addLayer1SubCmd.addedLayer() addLayer2SubCmd = UsdLayerEditor.AddAnonSubLayerCommand(stage, layer2) - mgr.executeCmd(addLayer2SubCmd) + UsdLayerEditorTest._executeCmd(addLayer2SubCmd) layer2SubId = addLayer2SubCmd.addedLayer() layer1Sub = Sdf.Layer.Find(layer1SubId) @@ -2013,18 +2061,18 @@ def testMergeWithSublayers(self): self.assertEqual(len(rootLayer.subLayerPaths), 2) flattenLayer1Cmd = UsdLayerEditor.FlattenLayerCommand(layer1) - mgr.executeCmd(flattenLayer1Cmd) + UsdLayerEditorTest._executeCmd(flattenLayer1Cmd) self.assertEqual(len(layer1.subLayerPaths), 0) self.assertIsNotNone(layer1.GetPrimAtPath("/Ball1")) flattenLayer2Cmd = UsdLayerEditor.FlattenLayerCommand(layer2) - mgr.executeCmd(flattenLayer2Cmd) + UsdLayerEditorTest._executeCmd(flattenLayer2Cmd) self.assertEqual(len(layer2.subLayerPaths), 0) self.assertIsNotNone(layer2.GetPrimAtPath("/Ball2")) self.assertEqual(len(rootLayer.subLayerPaths), 2) flattenRootCmd = UsdLayerEditor.FlattenLayerCommand(rootLayer) - mgr.executeCmd(flattenRootCmd) + UsdLayerEditorTest._executeCmd(flattenRootCmd) self.assertEqual(len(rootLayer.subLayerPaths), 0) self.assertIsNotNone(rootLayer.GetPrimAtPath("/Ball1")) @@ -2067,7 +2115,7 @@ def testMergeWithSublayersWithDirtySavedLayers(self): mgr = ufe.UndoableCommandMgr.instance() addAnonCmd = UsdLayerEditor.AddAnonSubLayerCommand(stage, rootLayer) - mgr.executeCmd(addAnonCmd) + UsdLayerEditorTest._executeCmd(addAnonCmd) anonLayerId = addAnonCmd.addedLayer() anonLayer = Sdf.Layer.Find(anonLayerId) @@ -2075,14 +2123,14 @@ def testMergeWithSublayersWithDirtySavedLayers(self): cleanLayer = Sdf.Layer.CreateNew(cleanLayerPath) cleanLayer.Save() insertCleanCmd = UsdLayerEditor.InsertSubPathCommand(stage, rootLayer, cleanLayerPath, 0) - mgr.executeCmd(insertCleanCmd) + UsdLayerEditorTest._executeCmd(insertCleanCmd) cleanLayer = Sdf.Layer.FindOrOpen(cleanLayerPath) dirtyLayerPath = os.path.join(testDir, "dirtyLayer.usda") dirtyLayer = Sdf.Layer.CreateNew(dirtyLayerPath) dirtyLayer.Save() insertDirtyCmd = UsdLayerEditor.InsertSubPathCommand(stage, rootLayer, dirtyLayerPath, 0) - mgr.executeCmd(insertDirtyCmd) + UsdLayerEditorTest._executeCmd(insertDirtyCmd) dirtyLayer = Sdf.Layer.FindOrOpen(dirtyLayerPath) ball1Spec = Sdf.PrimSpec(anonLayer, "Ball1", Sdf.SpecifierDef, "Sphere") @@ -2114,7 +2162,7 @@ def testMergeWithSublayersWithDirtySavedLayers(self): self.assertEqual(dirtyLayer.GetPrimAtPath("/Ball3").attributes["radius"].default, 20.0) flattenCmd = UsdLayerEditor.FlattenLayerCommand(rootLayer) - mgr.executeCmd(flattenCmd) + UsdLayerEditorTest._executeCmd(flattenCmd) self.assertEqual(len(rootLayer.subLayerPaths), 0) self.assertIsNotNone(rootLayer.GetPrimAtPath("/Ball1")) @@ -2197,12 +2245,12 @@ def testFlattenLayerUndoRestoresDeeplyNestedDirtyLayer(self): mgr = ufe.UndoableCommandMgr.instance() insertLayer2Cmd = UsdLayerEditor.InsertSubPathCommand(stage, layer1, layer2Path, 0) - mgr.executeCmd(insertLayer2Cmd) + UsdLayerEditorTest._executeCmd(insertLayer2Cmd) layer2 = Sdf.Layer.FindOrOpen(layer2Path) self.assertIsNotNone(layer2) insertLayer3Cmd = UsdLayerEditor.InsertSubPathCommand(stage, layer2, layer3Path, 0) - mgr.executeCmd(insertLayer3Cmd) + UsdLayerEditorTest._executeCmd(insertLayer3Cmd) layer3 = Sdf.Layer.FindOrOpen(layer3Path) self.assertIsNotNone(layer3) @@ -2228,7 +2276,7 @@ def testFlattenLayerUndoRestoresDeeplyNestedDirtyLayer(self): layer3 = None flattenCmd = UsdLayerEditor.FlattenLayerCommand(layer1) - mgr.executeCmd(flattenCmd) + UsdLayerEditorTest._executeCmd(flattenCmd) self.assertEqual(len(layer1.subLayerPaths), 0) self.assertIsNotNone(layer1.GetPrimAtPath("/Ball"), diff --git a/test/lib/usdLayerEditor/mayaLayerEditorTestSetup.py b/test/lib/usdLayerEditor/mayaLayerEditorTestSetup.py new file mode 100644 index 0000000000..2aa2be8837 --- /dev/null +++ b/test/lib/usdLayerEditor/mayaLayerEditorTestSetup.py @@ -0,0 +1,164 @@ +# +# 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. +# +"""Bind the DCC-agnostic UsdLayerEditorTest static hooks to Maya implementations. + +Mirrors the 3dsmax setup script (max_layer_editor_test_setup.py) so the same +shared layer_editor_test.py suite can run in both DCCs. +""" + +from maya import cmds + +import mayaUsd +import mayaUsd_createStageWithNewLayer + +from layer_editor_test import UsdLayerEditorTest + + +# Per-test undo/redo stacks for UsdLayerEditor commands. resetScene() clears +# them so each test starts with a clean slate. +_undo_stack = [] +_redo_stack = [] + + +def _newScene(): + """Reset the Maya scene and ensure the mayaUsd plugin is loaded.""" + cmds.file(new=True, force=True) + if not cmds.pluginInfo('mayaUsdPlugin', query=True, loaded=True): + cmds.loadPlugin('mayaUsdPlugin') + + +def _stageFromShape(shapePath): + return mayaUsd.lib.GetPrim(shapePath).GetStage() + + +def _createProxyFromFile(filePath): + """Create a mayaUsdProxyShape pointing at filePath; return (shapePath, stage).""" + cmds.createNode('mayaUsdProxyShape', name='stageShape') + shapeNode = cmds.ls(sl=True, l=True)[0] + cmds.setAttr('{}.filePath'.format(shapeNode), filePath, type='string') + # Force Maya to evaluate the proxy shape so the stage and its full layer + # stack are populated before the test accesses them. + cmds.refresh(force=True) + stage = _stageFromShape(shapeNode) + cmds.select(clear=True) + cmds.connectAttr('time1.outTime', '{}.time'.format(shapeNode)) + # Second refresh so UsdStageMap registers the wired proxy shape; without + # this, UsdUfe::stagePath(stage) returns an empty path and + # usdUfe.getStage(objectPath) returns None inside onRefreshSystemLock + # callbacks. + cmds.refresh(force=True) + return shapeNode, stage + + +def createStage(rootFile): + """Create a Maya USD stage backed by ``rootFile`` and return its ``Usd.Stage``. + + If ``rootFile`` is empty, a stage with a fresh anonymous root layer is + returned. Otherwise a ``mayaUsdProxyShape`` is created pointing at the + given USD file path. + """ + _newScene() + if rootFile: + _shapePath, stage = _createProxyFromFile(rootFile) + # Reload from disk so any in-memory modifications from a previous stage + # that shared the same SdfLayer (same file path) don't carry over. + stage.Reload() + else: + shapePath = mayaUsd_createStageWithNewLayer.createStageWithNewLayer() + stage = _stageFromShape(shapePath) + return stage + + +def resetScene(): + _undo_stack.clear() + _redo_stack.clear() + _newScene() + + +def undo(): + if _undo_stack: + cmd = _undo_stack.pop() + cmd.undo() + _redo_stack.append(cmd) + + +def redo(): + if _redo_stack: + cmd = _redo_stack.pop() + cmd.redo() + _undo_stack.append(cmd) + + +def executeCmd(cmd): + """Execute a UsdLayerEditor command and push it onto the local undo stack. + + UsdLayerEditor commands are Boost.Python-bound and cannot be passed to + ufe.UndoableCommandMgr (pybind11). Since the commands expose undo()/redo() + directly, we manage the stack ourselves rather than going through Maya's MEL + undo machinery. + """ + cmd.execute() + _undo_stack.append(cmd) + _redo_stack.clear() + # Process pending Qt timer events (e.g. async layer-tree model rebuilds) + # so the UI is up to date before the test makes assertions. + if cmds.window('mayaUsdLayerEditor', exists=True): + cmds.refresh(force=True) + + +def openStageLayerEditor(rootFile): + """Create a Maya USD stage from ``rootFile`` and open the Layer Editor on it. + + Returns the ``Usd.Stage`` so the shared test can interact with it. + """ + _newScene() + if rootFile: + shapePath, stage = _createProxyFromFile(rootFile) + else: + shapePath = mayaUsd_createStageWithNewLayer.createStageWithNewLayer() + stage = _stageFromShape(shapePath) + + # Show the session layer (some shared tests rely on it being visible). + cmds.optionVar(intValue=('MayaUSDLayerEditor_AutoHideSessionLayer', 0)) + + # Open the Layer Editor window on the proxy shape. + cmds.mayaUsdLayerEditorWindow('mayaUsdLayerEditor', proxyShape=shapePath) + cmds.refresh(force=True) + + return stage + + +def setup(): + """Install the Maya implementations into ``UsdLayerEditorTest``.""" + # usdUfe.getStage() expects a path without the |world prefix, but + # Ufe::Path::string() (used in C++ callbacks) includes it. Patch getStage + # so callbacks that receive objectPath from C++ work correctly in Maya. + import usdUfe + _orig_getStage = usdUfe.getStage + + def _maya_getStage(objectPath): + if objectPath and objectPath.startswith('|world'): + objectPath = objectPath[len('|world'):] + return _orig_getStage(objectPath) + + usdUfe.getStage = _maya_getStage + + UsdLayerEditorTest._createStage = staticmethod(createStage) + UsdLayerEditorTest._resetScene = staticmethod(resetScene) + UsdLayerEditorTest._undo = staticmethod(undo) + UsdLayerEditorTest._redo = staticmethod(redo) + UsdLayerEditorTest._openStageLayerEditor = staticmethod(openStageLayerEditor) + UsdLayerEditorTest._executeCmd = staticmethod(executeCmd) diff --git a/test/lib/usdLayerEditor/testMayaUsdSharedLayerEditor.py b/test/lib/usdLayerEditor/testMayaUsdSharedLayerEditor.py new file mode 100644 index 0000000000..1d4661496d --- /dev/null +++ b/test/lib/usdLayerEditor/testMayaUsdSharedLayerEditor.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# +# 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. +# +"""ctest entry point: runs the shared layer_editor_test.py suite inside Maya. + +Registered in test/lib/CMakeLists.txt and discovered by ctest. This file is a +thin wrapper: it binds the Maya implementations of the DCC hooks (via +mayaLayerEditorTestSetup.setup()) and re-exports UsdLayerEditorTest so +unittest discovery picks up every test_* method from the shared component's +layer_editor_test.py. +""" + +import os +import sys + +# Make sure the shared component's test directory (this file's own dir) is +# importable so layer_editor_test and mayaLayerEditorTestSetup resolve. +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) + +import fixturesUtils # noqa: E402 +import mayaLayerEditorTestSetup # noqa: E402 +mayaLayerEditorTestSetup.setup() + +# Re-export the shared test class so fixturesUtils.runTests(globals()) loads +# every test_* method from the shared component's layer_editor_test.py. +from layer_editor_test import UsdLayerEditorTest # noqa: E402,F401 + + +if __name__ == '__main__': + fixturesUtils.runTests(globals()) diff --git a/lib/usdLayerEditor/test/test_utils.py b/test/lib/usdLayerEditor/test_utils.py similarity index 100% rename from lib/usdLayerEditor/test/test_utils.py rename to test/lib/usdLayerEditor/test_utils.py