From bd7e8fe110c5d95ef7d3212a522db9e1e1d19aaf Mon Sep 17 00:00:00 2001 From: Luc-Eric Rousseau Date: Tue, 8 Dec 2020 16:10:11 -0500 Subject: [PATCH 1/6] MAYA-107516 USD Layer Editor --- lib/mayaUsd/commands/CMakeLists.txt | 3 + .../commands/abstractLayerEditorWindow.h | 102 ++++ lib/mayaUsd/commands/layerEditorCommand.cpp | 81 ++- .../commands/layerEditorWindowCommand.cpp | 354 +++++++++++++ .../commands/layerEditorWindowCommand.h | 54 ++ lib/mayaUsd/ufe/UsdContextOps.cpp | 19 +- lib/usd/ui/CMakeLists.txt | 4 + lib/usd/ui/initStringResources.cpp | 24 + lib/usd/ui/initStringResources.h | 31 ++ lib/usd/ui/layerEditor/CMakeLists.txt | 54 ++ lib/usd/ui/layerEditor/abstractCommandHook.h | 111 ++++ .../ui/layerEditor/dirtyLayersCountBadge.cpp | 89 ++++ .../ui/layerEditor/dirtyLayersCountBadge.h | 46 ++ .../ui/layerEditor/generatedIconButton.cpp | 169 ++++++ lib/usd/ui/layerEditor/generatedIconButton.h | 50 ++ lib/usd/ui/layerEditor/layerEditorWidget.cpp | 354 +++++++++++++ lib/usd/ui/layerEditor/layerEditorWidget.h | 81 +++ lib/usd/ui/layerEditor/layerTreeItem.cpp | 355 +++++++++++++ lib/usd/ui/layerEditor/layerTreeItem.h | 178 +++++++ .../ui/layerEditor/layerTreeItemDelegate.cpp | 369 +++++++++++++ .../ui/layerEditor/layerTreeItemDelegate.h | 149 ++++++ lib/usd/ui/layerEditor/layerTreeModel.cpp | 491 ++++++++++++++++++ lib/usd/ui/layerEditor/layerTreeModel.h | 141 +++++ lib/usd/ui/layerEditor/layerTreeView.cpp | 385 ++++++++++++++ lib/usd/ui/layerEditor/layerTreeView.h | 113 ++++ lib/usd/ui/layerEditor/layerTreeViewStyle.h | 202 +++++++ lib/usd/ui/layerEditor/loadLayersDialog.cpp | 372 +++++++++++++ lib/usd/ui/layerEditor/loadLayersDialog.h | 106 ++++ lib/usd/ui/layerEditor/mayaCommandHook.cpp | 207 ++++++++ lib/usd/ui/layerEditor/mayaCommandHook.h | 78 +++ .../ui/layerEditor/mayaLayerEditorWindow.cpp | 289 +++++++++++ .../ui/layerEditor/mayaLayerEditorWindow.h | 91 ++++ lib/usd/ui/layerEditor/mayaSessionState.cpp | 366 +++++++++++++ lib/usd/ui/layerEditor/mayaSessionState.h | 105 ++++ lib/usd/ui/layerEditor/pathChecker.cpp | 221 ++++++++ lib/usd/ui/layerEditor/pathChecker.h | 87 ++++ lib/usd/ui/layerEditor/qtUtils.cpp | 65 +++ lib/usd/ui/layerEditor/qtUtils.h | 68 +++ lib/usd/ui/layerEditor/resources.qrc | 34 ++ .../ui/layerEditor/resources/save_all_100.png | Bin 0 -> 207 bytes .../ui/layerEditor/resources/save_all_150.png | Bin 0 -> 236 bytes .../ui/layerEditor/resources/save_all_200.png | Bin 0 -> 245 bytes .../resources/save_all_hover_100.png | Bin 0 -> 208 bytes .../resources/save_all_hover_150.png | Bin 0 -> 238 bytes .../resources/save_all_hover_200.png | Bin 0 -> 246 bytes .../resources/save_all_pressed_100.png | Bin 0 -> 285 bytes .../resources/save_all_pressed_150.png | Bin 0 -> 329 bytes .../resources/save_all_pressed_200.png | Bin 0 -> 345 bytes .../resources/target_hover_100.png | Bin 0 -> 564 bytes .../resources/target_hover_150.png | Bin 0 -> 849 bytes .../resources/target_hover_200.png | Bin 0 -> 1087 bytes .../layerEditor/resources/target_on_100.png | Bin 0 -> 629 bytes .../layerEditor/resources/target_on_150.png | Bin 0 -> 981 bytes .../layerEditor/resources/target_on_200.png | Bin 0 -> 1264 bytes .../resources/target_on_hover_100.png | Bin 0 -> 609 bytes .../resources/target_on_hover_150.png | Bin 0 -> 903 bytes .../resources/target_on_hover_200.png | Bin 0 -> 1190 bytes .../resources/target_on_pressed_100.png | Bin 0 -> 985 bytes .../resources/target_on_pressed_150.png | Bin 0 -> 1437 bytes .../resources/target_on_pressed_200.png | Bin 0 -> 1884 bytes .../resources/target_pressed_100.png | Bin 0 -> 937 bytes .../resources/target_pressed_150.png | Bin 0 -> 1340 bytes .../resources/target_pressed_200.png | Bin 0 -> 1705 bytes .../resources/target_regular_100.png | Bin 0 -> 342 bytes .../resources/target_regular_150.png | Bin 0 -> 485 bytes .../resources/target_regular_200.png | Bin 0 -> 589 bytes lib/usd/ui/layerEditor/sessionState.cpp | 44 ++ lib/usd/ui/layerEditor/sessionState.h | 92 ++++ .../ui/layerEditor/stageSelectorWidget.cpp | 128 +++++ lib/usd/ui/layerEditor/stageSelectorWidget.h | 59 +++ lib/usd/ui/layerEditor/stringResources.cpp | 66 +++ lib/usd/ui/layerEditor/stringResources.h | 119 +++++ lib/usd/ui/layerEditor/warningDialogs.cpp | 96 ++++ lib/usd/ui/layerEditor/warningDialogs.h | 43 ++ plugin/adsk/plugin/plugin.cpp | 21 + plugin/adsk/scripts/CMakeLists.txt | 2 + .../adsk/scripts/mayaUSDRegisterStrings.mel | 65 +++ plugin/adsk/scripts/mayaUsdMenu.mel | 177 ++++++- .../scripts/mayaUsd_createStageFromFile.mel | 38 +- .../mayaUsd_layerEditorFileDialogs.mel | 109 ++++ 80 files changed, 7126 insertions(+), 31 deletions(-) create mode 100644 lib/mayaUsd/commands/abstractLayerEditorWindow.h create mode 100644 lib/mayaUsd/commands/layerEditorWindowCommand.cpp create mode 100644 lib/mayaUsd/commands/layerEditorWindowCommand.h create mode 100644 lib/usd/ui/initStringResources.cpp create mode 100644 lib/usd/ui/initStringResources.h create mode 100644 lib/usd/ui/layerEditor/CMakeLists.txt create mode 100644 lib/usd/ui/layerEditor/abstractCommandHook.h create mode 100644 lib/usd/ui/layerEditor/dirtyLayersCountBadge.cpp create mode 100644 lib/usd/ui/layerEditor/dirtyLayersCountBadge.h create mode 100644 lib/usd/ui/layerEditor/generatedIconButton.cpp create mode 100644 lib/usd/ui/layerEditor/generatedIconButton.h create mode 100644 lib/usd/ui/layerEditor/layerEditorWidget.cpp create mode 100644 lib/usd/ui/layerEditor/layerEditorWidget.h create mode 100644 lib/usd/ui/layerEditor/layerTreeItem.cpp create mode 100644 lib/usd/ui/layerEditor/layerTreeItem.h create mode 100644 lib/usd/ui/layerEditor/layerTreeItemDelegate.cpp create mode 100644 lib/usd/ui/layerEditor/layerTreeItemDelegate.h create mode 100644 lib/usd/ui/layerEditor/layerTreeModel.cpp create mode 100644 lib/usd/ui/layerEditor/layerTreeModel.h create mode 100644 lib/usd/ui/layerEditor/layerTreeView.cpp create mode 100644 lib/usd/ui/layerEditor/layerTreeView.h create mode 100644 lib/usd/ui/layerEditor/layerTreeViewStyle.h create mode 100644 lib/usd/ui/layerEditor/loadLayersDialog.cpp create mode 100644 lib/usd/ui/layerEditor/loadLayersDialog.h create mode 100644 lib/usd/ui/layerEditor/mayaCommandHook.cpp create mode 100644 lib/usd/ui/layerEditor/mayaCommandHook.h create mode 100644 lib/usd/ui/layerEditor/mayaLayerEditorWindow.cpp create mode 100644 lib/usd/ui/layerEditor/mayaLayerEditorWindow.h create mode 100644 lib/usd/ui/layerEditor/mayaSessionState.cpp create mode 100644 lib/usd/ui/layerEditor/mayaSessionState.h create mode 100644 lib/usd/ui/layerEditor/pathChecker.cpp create mode 100644 lib/usd/ui/layerEditor/pathChecker.h create mode 100644 lib/usd/ui/layerEditor/qtUtils.cpp create mode 100644 lib/usd/ui/layerEditor/qtUtils.h create mode 100644 lib/usd/ui/layerEditor/resources.qrc create mode 100644 lib/usd/ui/layerEditor/resources/save_all_100.png create mode 100644 lib/usd/ui/layerEditor/resources/save_all_150.png create mode 100644 lib/usd/ui/layerEditor/resources/save_all_200.png create mode 100644 lib/usd/ui/layerEditor/resources/save_all_hover_100.png create mode 100644 lib/usd/ui/layerEditor/resources/save_all_hover_150.png create mode 100644 lib/usd/ui/layerEditor/resources/save_all_hover_200.png create mode 100644 lib/usd/ui/layerEditor/resources/save_all_pressed_100.png create mode 100644 lib/usd/ui/layerEditor/resources/save_all_pressed_150.png create mode 100644 lib/usd/ui/layerEditor/resources/save_all_pressed_200.png create mode 100644 lib/usd/ui/layerEditor/resources/target_hover_100.png create mode 100644 lib/usd/ui/layerEditor/resources/target_hover_150.png create mode 100644 lib/usd/ui/layerEditor/resources/target_hover_200.png create mode 100644 lib/usd/ui/layerEditor/resources/target_on_100.png create mode 100644 lib/usd/ui/layerEditor/resources/target_on_150.png create mode 100644 lib/usd/ui/layerEditor/resources/target_on_200.png create mode 100644 lib/usd/ui/layerEditor/resources/target_on_hover_100.png create mode 100644 lib/usd/ui/layerEditor/resources/target_on_hover_150.png create mode 100644 lib/usd/ui/layerEditor/resources/target_on_hover_200.png create mode 100644 lib/usd/ui/layerEditor/resources/target_on_pressed_100.png create mode 100644 lib/usd/ui/layerEditor/resources/target_on_pressed_150.png create mode 100644 lib/usd/ui/layerEditor/resources/target_on_pressed_200.png create mode 100644 lib/usd/ui/layerEditor/resources/target_pressed_100.png create mode 100644 lib/usd/ui/layerEditor/resources/target_pressed_150.png create mode 100644 lib/usd/ui/layerEditor/resources/target_pressed_200.png create mode 100644 lib/usd/ui/layerEditor/resources/target_regular_100.png create mode 100644 lib/usd/ui/layerEditor/resources/target_regular_150.png create mode 100644 lib/usd/ui/layerEditor/resources/target_regular_200.png create mode 100644 lib/usd/ui/layerEditor/sessionState.cpp create mode 100644 lib/usd/ui/layerEditor/sessionState.h create mode 100644 lib/usd/ui/layerEditor/stageSelectorWidget.cpp create mode 100644 lib/usd/ui/layerEditor/stageSelectorWidget.h create mode 100644 lib/usd/ui/layerEditor/stringResources.cpp create mode 100644 lib/usd/ui/layerEditor/stringResources.h create mode 100644 lib/usd/ui/layerEditor/warningDialogs.cpp create mode 100644 lib/usd/ui/layerEditor/warningDialogs.h create mode 100644 plugin/adsk/scripts/mayaUSDRegisterStrings.mel create mode 100644 plugin/adsk/scripts/mayaUsd_layerEditorFileDialogs.mel diff --git a/lib/mayaUsd/commands/CMakeLists.txt b/lib/mayaUsd/commands/CMakeLists.txt index dbcb6d39d5..c8ba5b38f7 100644 --- a/lib/mayaUsd/commands/CMakeLists.txt +++ b/lib/mayaUsd/commands/CMakeLists.txt @@ -8,14 +8,17 @@ target_sources(${PROJECT_NAME} baseListShadingModesCommand.cpp editTargetCommand.cpp layerEditorCommand.cpp + layerEditorWindowCommand.cpp ) set(HEADERS + abstractLayerEditorWindow.h baseExportCommand.h baseImportCommand.h baseListShadingModesCommand.h editTargetCommand.h layerEditorCommand.h + layerEditorWindowCommand.h ) # ----------------------------------------------------------------------------- diff --git a/lib/mayaUsd/commands/abstractLayerEditorWindow.h b/lib/mayaUsd/commands/abstractLayerEditorWindow.h new file mode 100644 index 0000000000..5ab5fa9731 --- /dev/null +++ b/lib/mayaUsd/commands/abstractLayerEditorWindow.h @@ -0,0 +1,102 @@ +#ifndef ABSTRACTLAYEREDITORWINDOW_H +#define ABSTRACTLAYEREDITORWINDOW_H + +#include + +#include +#include + +namespace MAYAUSD_NS_DEF { + +class AbstractLayerEditorWindow; + +/** + * @brief abract class used by layer editor window command to create and get + * the layer editor windows. + * this allows breaking the circular dependency between between maya usd lib + * and UI libraries + * + */ +class MAYAUSD_CORE_PUBLIC AbstractLayerEditorCreator +{ +public: + AbstractLayerEditorCreator(); + virtual ~AbstractLayerEditorCreator(); + + typedef std::vector PanelNamesList; + + /* + * @brief returns a pointer to the registered singleton + * + * @return AbstractLayerEditorCreator* + */ + static AbstractLayerEditorCreator* instance(); + + /** + * @brief Create the maya panel with the given name + */ + virtual AbstractLayerEditorWindow* createWindow(const char* panelName) = 0; + + /** + * @brief returns the panel with a given name if it already exists + */ + virtual AbstractLayerEditorWindow* getWindow(const char* panelName) const = 0; + + /** + * @brief Gets an array of all the panels that exists + * + * @return std::vector + */ + virtual PanelNamesList getAllPanelNames() const = 0; + +private: + static AbstractLayerEditorCreator* _instance; +}; + +/** + * @brief Abstract class used to break dependency between core lib and maya UI, + * used to implement the layer editor commands + */ +class MAYAUSD_CORE_PUBLIC AbstractLayerEditorWindow +{ +public: + /** + * @brief Constructor implemented by the maya usd layer editor + * + * @param panelName this is the name of the control in mel, not the title of the window + */ + AbstractLayerEditorWindow(const char* panelName); + + /** + * @brief Virtual Destructor + */ + virtual ~AbstractLayerEditorWindow(); + + // queries about the current selection + virtual int selectionLength() = 0; + virtual bool isInvalidLayer() = 0; + virtual bool isSessionLayer() = 0; + virtual bool isLayerDirty() = 0; + virtual bool isSubLayer() = 0; + virtual bool isAnonymousLayer() = 0; + virtual bool layerNeedsSaving() = 0; + virtual bool layerAppearsMuted() = 0; + virtual bool layerIsMuted() = 0; + + virtual void removeSubLayer() = 0; + virtual void saveEdits() = 0; + virtual void discardEdits() = 0; + virtual void addAnonymousSublayer() = 0; + virtual void addParentLayer() = 0; + virtual void loadSubLayers() = 0; + virtual void muteLayer() = 0; + virtual void printLayer() = 0; + virtual void clearLayer() = 0; + virtual void selectPrimsWithSpec() = 0; + + virtual void selectProxyShape(const char* shapePath) = 0; +}; + +} // namespace MAYAUSD_NS_DEF + +#endif // ABSTRACTLAYEREDITORWINDOW_H diff --git a/lib/mayaUsd/commands/layerEditorCommand.cpp b/lib/mayaUsd/commands/layerEditorCommand.cpp index 84288f82b3..27439c34f7 100644 --- a/lib/mayaUsd/commands/layerEditorCommand.cpp +++ b/lib/mayaUsd/commands/layerEditorCommand.cpp @@ -43,6 +43,8 @@ const char kClearLayerFlag[] = "cl"; const char kClearLayerFlagL[] = "clear"; const char kAddAnonSublayerFlag[] = "aa"; const char kAddAnonSublayerFlagL[] = "addAnonymous"; +const char kMuteLayerFlag[] = "mt"; +const char kMuteLayerFlagL[] = "muteLayer"; } // namespace @@ -57,7 +59,8 @@ enum class CmdId kReplace, kDiscardEdit, kClearLayer, - kAddAnonLayer + kAddAnonLayer, + kMuteLayer }; class BaseCmd @@ -320,6 +323,60 @@ class ClearLayer : public BackupLayerBase } }; +class MuteLayer : public BaseCmd +{ +public: + MuteLayer() + : BaseCmd(CmdId::kMuteLayer) + { + } + + bool doIt(SdfLayerHandle layer) override + { + auto stage = getStage(); + if (!stage) + return false; + if (_muteIt) { + stage->MuteLayer(layer->GetIdentifier()); + } else { + stage->UnmuteLayer(layer->GetIdentifier()); + } + + // we perfer not holding to pointers needlessly, but we need to hold on to the layer if we + // mute it otherwise usd will let go of it and its modifications, and any dirty children + // will also be lost + _mutedLayer = layer; + return true; + } + + bool undoIt(SdfLayerHandle layer) override + { + auto stage = getStage(); + if (!stage) + return false; + if (_muteIt) { + stage->UnmuteLayer(layer->GetIdentifier()); + } else { + stage->MuteLayer(layer->GetIdentifier()); + } + // we can release the pointer + _mutedLayer = nullptr; + return true; + } + + std::string _proxyShapePath; + bool _muteIt = true; + +protected: + UsdStageWeakPtr getStage() + { + auto prim = UsdMayaQuery::GetPrim(_proxyShapePath.c_str()); + auto stage = prim.GetStage(); + return stage; + } + PXR_NS::SdfLayerRefPtr _mutedLayer; +}; + } // namespace Impl const char LayerEditorCommand::commandName[] = "mayaUsdLayerEditor"; @@ -346,8 +403,11 @@ MSyntax LayerEditorCommand::createSyntax() syntax.makeFlagMultiUse(kReplaceSubPathFlag); syntax.addFlag(kDiscardEditsFlag, kDiscardEditsFlagL); syntax.addFlag(kClearLayerFlag, kClearLayerFlagL); + // parameter: new layer name syntax.addFlag(kAddAnonSublayerFlag, kAddAnonSublayerFlagL, MSyntax::kString); syntax.makeFlagMultiUse(kAddAnonSublayerFlag); + // paramter: proxy shape name + syntax.addFlag(kMuteLayerFlag, kMuteLayerFlagL, MSyntax::kBoolean, MSyntax::kString); return syntax; } @@ -434,6 +494,25 @@ MStatus LayerEditorCommand::parseArgs(const MArgList& argList) _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()) + "\""); + return MS::kInvalidParameter; + } + + auto cmd = std::make_shared(); + cmd->_muteIt = muteIt; + cmd->_proxyShapePath = proxyShapeName.asChar(); + _subCommands.push_back(std::move(cmd)); + } } return MS::kSuccess; diff --git a/lib/mayaUsd/commands/layerEditorWindowCommand.cpp b/lib/mayaUsd/commands/layerEditorWindowCommand.cpp new file mode 100644 index 0000000000..f28851a702 --- /dev/null +++ b/lib/mayaUsd/commands/layerEditorWindowCommand.cpp @@ -0,0 +1,354 @@ +// +// 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 "layerEditorWindowCommand.h" + +#include "abstractLayerEditorWindow.h" + +#include +#include + +namespace { +#define DEF_FLAG(short, long) \ + const char k_##long##Flag[] = #short; \ + const char k_##long##FlagLong[] = #long; + +#define FLAG(long) k_##long##FlagLong + +DEF_FLAG(rl, reload) +DEF_FLAG(ps, proxyShape) + +// query flags +DEF_FLAG(se, selectionLength) +DEF_FLAG(il, isInvalidLayer) +DEF_FLAG(sl, isSessionLayer) +DEF_FLAG(dl, isLayerDirty) +DEF_FLAG(su, isSubLayer) +DEF_FLAG(al, isAnonymousLayer) +DEF_FLAG(ns, layerNeedsSaving) +DEF_FLAG(am, layerAppearsMuted) +DEF_FLAG(mu, layerIsMuted) + +// edit flags +DEF_FLAG(rs, removeSubLayer) +DEF_FLAG(sv, saveEdits) +DEF_FLAG(de, discardEdits) +DEF_FLAG(aa, addAnonymousSublayer) +DEF_FLAG(ap, addParentLayer) +DEF_FLAG(lo, loadSubLayers) +DEF_FLAG(ml, muteLayer) +DEF_FLAG(pl, printLayer) +DEF_FLAG(cl, clearLayer) +DEF_FLAG(sp, selectPrimsWithSpec) + +const MString WORKSPACE_CONTROL_NAME = "mayaUsdLayerEditor"; +} // namespace + +namespace MAYAUSD_NS_DEF { + +// AbstractLayerEditorCreator implememtation + +AbstractLayerEditorCreator* AbstractLayerEditorCreator::_instance = nullptr; + +AbstractLayerEditorCreator* AbstractLayerEditorCreator::instance() +{ + // + return AbstractLayerEditorCreator::_instance; +} + +AbstractLayerEditorCreator::AbstractLayerEditorCreator() +{ + AbstractLayerEditorCreator::_instance = this; +} + +AbstractLayerEditorCreator::~AbstractLayerEditorCreator() +{ + AbstractLayerEditorCreator::_instance = nullptr; +} + +// AbstractLayerEditorWindow implementation + +AbstractLayerEditorWindow::AbstractLayerEditorWindow(const char* panelName) +{ + // this empty implementation is necessary for linking +} + +AbstractLayerEditorWindow::~AbstractLayerEditorWindow() +{ + // this empty implementation is necessary for linking +} + +// LayerEditorWindowCommand implementation + +const char LayerEditorWindowCommand::commandName[] = "mayaUsdLayerEditorWindow"; + +// plug-in callback to create the command object +void* LayerEditorWindowCommand::creator() +{ + // + return static_cast(new LayerEditorWindowCommand()); +} + +// plug-in callback to register the command syntax +MSyntax LayerEditorWindowCommand::createSyntax() +{ + MSyntax syntax; + syntax.enableQuery(true); + syntax.enableEdit(true); + +#define ADD_FLAG(name) syntax.addFlag(k_##name##Flag, k_##name##FlagLong) + + ADD_FLAG(reload); + syntax.addFlag(k_proxyShapeFlag, k_proxyShapeFlagLong, MSyntax::kString); + + ADD_FLAG(selectionLength); + ADD_FLAG(isInvalidLayer); + ADD_FLAG(isSessionLayer); + ADD_FLAG(isLayerDirty); + ADD_FLAG(isSubLayer); + ADD_FLAG(isAnonymousLayer); + ADD_FLAG(layerNeedsSaving); + ADD_FLAG(layerAppearsMuted); + ADD_FLAG(layerIsMuted); + + ADD_FLAG(removeSubLayer); + ADD_FLAG(saveEdits); + ADD_FLAG(discardEdits); + ADD_FLAG(addAnonymousSublayer); + ADD_FLAG(addParentLayer); + ADD_FLAG(loadSubLayers); + ADD_FLAG(muteLayer); + ADD_FLAG(printLayer); + ADD_FLAG(clearLayer); + ADD_FLAG(selectPrimsWithSpec); + + // editor name + syntax.addArg(MSyntax::kString); + + return syntax; +} + +LayerEditorWindowCommand::LayerEditorWindowCommand() +{ + // +} +bool LayerEditorWindowCommand::isUndoable() const +{ + // + return false; +} + +MStatus LayerEditorWindowCommand::doIt(const MArgList& argList) +{ + const MString WINDOW_TITLE_NAME = "USD Layer Editor"; + const MString DEAULT_EDITOR_NAME = "mayaUsdLayerEditor"; + + auto handler = AbstractLayerEditorCreator::instance(); + if (!handler) { + return MS::kNotFound; + } + + // get the name of the layer editor use + MArgParser argParser(syntax(), argList); + MString editorName = argParser.commandArgumentString(0); + if (editorName.length() == 0) { + editorName = DEAULT_EDITOR_NAME; + } + + // get the pointer to that control + auto layerEditorWindow = handler->getWindow(editorName.asChar()); + if (argParser.isQuery() || argParser.isEdit()) { + if (!layerEditorWindow) { + MString errorMsg; + errorMsg.format("layer editor named ^1s not found", editorName); + displayError(errorMsg); + return MS::kNotFound; + } + } + + bool createOrShowWindow = false; + if (!argParser.isQuery()) { + if (argParser.isEdit()) { + if (argParser.isFlagSet(k_reloadFlag)) { + createOrShowWindow = true; + } + } else { + // create mode + createOrShowWindow = true; + } + } + + MStatus st; + + // call this always, to check for flags used in wrong mode + st = handleQueries(argParser, layerEditorWindow); + if (st != MS::kSuccess) { + return st; + } + + // call this always, to check for -e mode missing + st = handleEdits(argParser, layerEditorWindow); + if (st != MS::kSuccess) + return st; + + if (createOrShowWindow) { + if (layerEditorWindow) { + // Call -restore on existing workspace control to make it visible + // from whatever previous state it was in + MString restoreCommand; + restoreCommand.format("workspaceControl -e -restore ^1s", WORKSPACE_CONTROL_NAME); + MGlobal::executeCommand(restoreCommand); + } else { + bool doReload = argParser.isFlagSet(k_reloadFlag); + // If we're not reloading a workspace, we need to create a workspace control + if (!doReload) { + + MString cmdString; + cmdString.format( + "workspaceControl" + " -label \"^1s\"" + " -retain false" + " -deleteLater false" + " -loadImmediately true" + " -floating true" + " -initialWidth 400" + " -initialHeight 600" + " -requiredPlugin \"^2s\"" + " \"^3s\"", + WINDOW_TITLE_NAME, + "mayaUsdPlugin", + WORKSPACE_CONTROL_NAME); + MGlobal::executeCommand(cmdString); + } + layerEditorWindow = handler->createWindow(editorName.asChar()); + // Set the -uiScript (used to rebuild UI when reloading workspace) + // after creation so that it doesn't get executed + if (!doReload) { + const MString mCommandName = LayerEditorWindowCommand::commandName; + MString uiScriptCommand; + uiScriptCommand.format( + R"(workspaceControl -e -uiScript "^1s -reload" "^2s")", + mCommandName, + WORKSPACE_CONTROL_NAME); + MGlobal::executeCommand(uiScriptCommand); + } + } + + if (argParser.isFlagSet(k_proxyShapeFlag)) { + MString proxyShapeName; + argParser.getFlagArgument(k_proxyShapeFlag, 0, proxyShapeName); + if (proxyShapeName.length() > 0) { + layerEditorWindow->selectProxyShape(proxyShapeName.asChar()); + } + } + } + + return MS::kSuccess; +} + +void LayerEditorWindowCommand::cleanupOnPluginUnload() +{ + // Close the workspace control, if it still exists. + auto handler = AbstractLayerEditorCreator::instance(); + if (handler) { + MString closeCommand; + auto panels = handler->getAllPanelNames(); + for (auto entry : panels) { + closeCommand.format("workspaceControl -e -close \"^1s\"", MString(entry.c_str())); + MGlobal::executeCommand(closeCommand); + } + } +} + +MStatus LayerEditorWindowCommand::undoIt() +{ + // + return MS::kSuccess; +} + +MStatus LayerEditorWindowCommand::redoIt() +{ + // + return MS::kSuccess; +} + +MStatus LayerEditorWindowCommand::handleQueries( + const MArgParser& argParser, + AbstractLayerEditorWindow* layerEditor) +{ + // this methis is written so that it'll return an error + // if a query flag is used in non-query mode. + bool notQuery = !argParser.isQuery(); + MString errorMsg("Need -query mode for parameter "); + +#define HANDLE_Q_FLAG(name) \ + if (argParser.isFlagSet(FLAG(name))) { \ + if (notQuery) { \ + errorMsg += FLAG(name); \ + displayError(errorMsg); \ + return MS::kInvalidParameter; \ + } \ + setResult(layerEditor->name()); \ + } + + HANDLE_Q_FLAG(selectionLength) + HANDLE_Q_FLAG(isInvalidLayer) + HANDLE_Q_FLAG(isSessionLayer) + HANDLE_Q_FLAG(isLayerDirty) + HANDLE_Q_FLAG(isSubLayer) + HANDLE_Q_FLAG(isAnonymousLayer) + HANDLE_Q_FLAG(layerNeedsSaving) + HANDLE_Q_FLAG(layerAppearsMuted) + HANDLE_Q_FLAG(layerIsMuted) + + return MS::kSuccess; +} + +MStatus LayerEditorWindowCommand::handleEdits( + const MArgParser& argParser, + AbstractLayerEditorWindow* layerEditor) +{ + // + // this methis is written so that it'll return an error + // if a query flag is used in non-query mode. + bool notEdit = !argParser.isEdit(); + MString errorMsg("Need -edit mode for parameter "); + +#define HANDLE_E_FLAG(name) \ + if (argParser.isFlagSet(FLAG(name))) { \ + if (notEdit) { \ + errorMsg += FLAG(name); \ + displayError(errorMsg); \ + return MS::kInvalidParameter; \ + } \ + layerEditor->name(); \ + } + + HANDLE_E_FLAG(removeSubLayer) + HANDLE_E_FLAG(saveEdits) + HANDLE_E_FLAG(discardEdits) + HANDLE_E_FLAG(addAnonymousSublayer) + HANDLE_E_FLAG(addParentLayer) + HANDLE_E_FLAG(loadSubLayers) + HANDLE_E_FLAG(muteLayer) + HANDLE_E_FLAG(printLayer) + HANDLE_E_FLAG(clearLayer) + HANDLE_E_FLAG(selectPrimsWithSpec) + + return MS::kSuccess; +} + +} // namespace MAYAUSD_NS_DEF diff --git a/lib/mayaUsd/commands/layerEditorWindowCommand.h b/lib/mayaUsd/commands/layerEditorWindowCommand.h new file mode 100644 index 0000000000..409a7a936b --- /dev/null +++ b/lib/mayaUsd/commands/layerEditorWindowCommand.h @@ -0,0 +1,54 @@ +// +// 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. +// + +#ifndef LAYEREDITORWINDOWCOMMAND_H +#define LAYEREDITORWINDOWCOMMAND_H + +#include + +#include +#include + +namespace MAYAUSD_NS_DEF { + +class AbstractLayerEditorWindow; + +class MAYAUSD_CORE_PUBLIC LayerEditorWindowCommand : public MPxCommand +{ +public: + LayerEditorWindowCommand(); + // plugin registration requirements + static const char commandName[]; + static void* creator(); + static MSyntax createSyntax(); + + // cleanup function to be called to delete the windows if the plugin is unloaded + static void cleanupOnPluginUnload(); + + // MPxCommand callbacks + MStatus doIt(const MArgList& argList) override; + MStatus undoIt() override; + MStatus redoIt() override; + bool isUndoable() const override; + +protected: + MStatus handleQueries(const MArgParser& argParser, AbstractLayerEditorWindow* layerEditor); + MStatus handleEdits(const MArgParser& argParser, AbstractLayerEditorWindow* layerEditor); +}; + +} // namespace MAYAUSD_NS_DEF + +#endif // LAYEREDITORWINDOWCOMMAND_H diff --git a/lib/mayaUsd/ufe/UsdContextOps.cpp b/lib/mayaUsd/ufe/UsdContextOps.cpp index f538010c5c..dd07294854 100644 --- a/lib/mayaUsd/ufe/UsdContextOps.cpp +++ b/lib/mayaUsd/ufe/UsdContextOps.cpp @@ -426,16 +426,12 @@ Ufe::ContextOps::Items UsdContextOps::getItems(const Ufe::ContextOps::ItemPath& Ufe::ContextOps::Items items; if (itemPath.empty()) { // Top-level item - USD Layer editor (for all context op types). - int hasLayerEditorCmd { 0 }; - MGlobal::executeCommand("runTimeCommand -exists UsdLayerEditor", hasLayerEditorCmd); - if (hasLayerEditorCmd) { #if UFE_PREVIEW_VERSION_NUM >= 2023 - items.emplace_back(kUSDLayerEditorItem, kUSDLayerEditorLabel, kUSDLayerEditorImage); + items.emplace_back(kUSDLayerEditorItem, kUSDLayerEditorLabel, kUSDLayerEditorImage); #else - items.emplace_back(kUSDLayerEditorItem, kUSDLayerEditorLabel); + items.emplace_back(kUSDLayerEditorItem, kUSDLayerEditorLabel); #endif - items.emplace_back(Ufe::ContextItem::kSeparator); - } + items.emplace_back(Ufe::ContextItem::kSeparator); // Top-level items (do not add for gateway type node): if (!fIsAGatewayType) { @@ -596,7 +592,14 @@ Ufe::UndoableCommand::Ptr UsdContextOps::doOpCmd(const ItemPath& itemPath) return UsdUndoAddNewPrimCommand::create(fItem, itemPath[1], itemPath[1]); } else if (itemPath[0] == kUSDLayerEditorItem) { // Just open the editor directly and return null so we don't have undo. - MGlobal::executeCommand("UsdLayerEditor"); + auto ufePath = ufe::stagePath(prim().GetStage()); + auto noWorld = ufePath.popHead().string(); + auto dagPath = UsdMayaUtil::nameToDagPath(noWorld); + auto shapePath = dagPath.fullPathName(); + + MString script; + script.format("mayaUsdLayerEditorWindow -proxyShape ^1s mayaUsdLayerEditor", shapePath); + MGlobal::executeCommand(script); return nullptr; } else if (itemPath[0] == AddReferenceUndoableCommand::commandName) { MString fileRef = MGlobal::executeCommandStringResult(selectUSDFileScript); diff --git a/lib/usd/ui/CMakeLists.txt b/lib/usd/ui/CMakeLists.txt index a03002bdac..fe3a7c7efd 100644 --- a/lib/usd/ui/CMakeLists.txt +++ b/lib/usd/ui/CMakeLists.txt @@ -22,11 +22,14 @@ set(CMAKE_AUTOUIC ON) add_library(${PROJECT_NAME} SHARED) +add_subdirectory(layerEditor) + # ----------------------------------------------------------------------------- # sources # ----------------------------------------------------------------------------- target_sources(${PROJECT_NAME} PRIVATE + initStringResources.cpp ItemDelegate.cpp TreeItem.cpp TreeModel.cpp @@ -95,6 +98,7 @@ endif() set(HEADERS api.h IMayaMQtUtil.h + initStringResources.h ItemDelegate.h IUSDImportView.h TreeItem.h diff --git a/lib/usd/ui/initStringResources.cpp b/lib/usd/ui/initStringResources.cpp new file mode 100644 index 0000000000..7631a882fd --- /dev/null +++ b/lib/usd/ui/initStringResources.cpp @@ -0,0 +1,24 @@ +// +// 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 "initStringResources.h" + +#include "layerEditor/stringResources.h" + +namespace MAYAUSD_NS_DEF { + +MStatus initStringResources() { return UsdLayerEditor::StringResources::registerAll(); } + +} // namespace MAYAUSD_NS_DEF diff --git a/lib/usd/ui/initStringResources.h b/lib/usd/ui/initStringResources.h new file mode 100644 index 0000000000..cf62b1bd02 --- /dev/null +++ b/lib/usd/ui/initStringResources.h @@ -0,0 +1,31 @@ +// +// 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. +// +#ifndef INIT_STRING_RESOURCES_H +#define INIT_STRING_RESOURCES_H + +#include +#include + +#include + +namespace MAYAUSD_NS_DEF { + +// register all string +MAYAUSD_UI_PUBLIC MStatus initStringResources(); + +} // namespace MAYAUSD_NS_DEF + +#endif // INIT_STRING_RESOURCES_H diff --git a/lib/usd/ui/layerEditor/CMakeLists.txt b/lib/usd/ui/layerEditor/CMakeLists.txt new file mode 100644 index 0000000000..47c76c70d7 --- /dev/null +++ b/lib/usd/ui/layerEditor/CMakeLists.txt @@ -0,0 +1,54 @@ + +# +# 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. +# + +target_sources(${PROJECT_NAME} + PRIVATE + dirtyLayersCountBadge.cpp + generatedIconButton.cpp + layerEditorWidget.cpp + layerEditorWidget.h + layerTreeItem.cpp + layerTreeItem.h + layerTreeItemDelegate.cpp + layerTreeItemDelegate.h + layerTreeModel.cpp + layerTreeModel.h + layerTreeView.cpp + layerTreeView.h + loadLayersDialog.cpp + loadLayersDialog.h + mayaCommandHook.cpp + mayaCommandHook.h + mayaLayerEditorWindow.cpp + mayaLayerEditorWindow.h + mayaSessionState.cpp + mayaSessionState.h + pathChecker.cpp + pathChecker.h + qtUtils.cpp + qtUtils.h + resources.qrc + sessionState.cpp + sessionState.h + stageSelectorWidget.cpp + stageSelectorWidget.h + stringResources.cpp + stringResources.h + warningDialogs.cpp + warningDialogs.h +) + diff --git a/lib/usd/ui/layerEditor/abstractCommandHook.h b/lib/usd/ui/layerEditor/abstractCommandHook.h new file mode 100644 index 0000000000..489db5432e --- /dev/null +++ b/lib/usd/ui/layerEditor/abstractCommandHook.h @@ -0,0 +1,111 @@ +// +// 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. +// + +#ifndef ABSTRACTCOMMANDHOOK_H +#define ABSTRACTCOMMANDHOOK_H + +#include "pxr/usd/sdf/layer.h" + +#include + +#include + +#include + +namespace UsdLayerEditor { + +typedef pxr::SdfLayerRefPtr UsdLayer; +typedef pxr::UsdStageRefPtr UsdStage; + +class SessionState; + +/** + * @brief The Abstact Command Hook contains all the methods which are used to modify USD layers and + * stages. These methods will be "hooked" by the MayaCommandHook derived class to call maya mel + * commands to do the work. + * + */ + +class AbstractCommandHook +{ +public: + typedef std::string const& Path; + + AbstractCommandHook(SessionState* in_sessionState) + : _sessionState(in_sessionState) + { + } + virtual ~AbstractCommandHook() { } + + // set stage edit target + virtual void setEditTarget(UsdLayer usdLayer) = 0; + + // insert a sub layer path at a given index + virtual void insertSubLayerPath(UsdLayer usdLayer, Path path, int index) = 0; + + // remove a sub layer by path + virtual void removeSubLayerPath(UsdLayer usdLayer, Path path) = 0; + + // replaces a path in the layer stack + virtual void replaceSubLayerPath(UsdLayer usdLayer, Path oldPath, Path newPath) = 0; + + // discard edit on a layer + virtual void discardEdits(UsdLayer usdLayer) = 0; + + // erases everything on a layer + virtual void clearLayer(UsdLayer usdLayer) = 0; + + // add an anon layer at the top of the stack, returns it + virtual UsdLayer addAnonymousSubLayer(UsdLayer usdLayer, std::string newName) = 0; + + // mute or unmute the given layer + virtual void muteSubLayer(UsdLayer usdLayer, bool muteIt) = 0; + + // starts a complex undo operation in the host app. Please use UndoContext class to safely + // open/close + virtual void openUndoBracket(const QString& name) = 0; + // closes a complex undo operation in the host app. Please use UndoContext class to safely + // open/close + virtual void closeUndoBracket() = 0; + + // Help menu callback + virtual void showLayerEditorHelp() = 0; + + // this method is used to select the prims with spec in a layer + virtual void selectPrimsWithSpec(UsdLayer usdLayer) = 0; + +protected: + SessionState* _sessionState; +}; + +class UndoContext +{ +public: + UndoContext(AbstractCommandHook* in_parent, const QString& in_name) + : _parent(in_parent) + { + in_parent->openUndoBracket(in_name); + } + ~UndoContext() { _parent->closeUndoBracket(); } + AbstractCommandHook* hook() { return _parent; } + +protected: + AbstractCommandHook* _parent; +}; + +}; // namespace UsdLayerEditor + +#endif // ABSTRACTCOMMANDHOOK_H diff --git a/lib/usd/ui/layerEditor/dirtyLayersCountBadge.cpp b/lib/usd/ui/layerEditor/dirtyLayersCountBadge.cpp new file mode 100644 index 0000000000..4602baf2d8 --- /dev/null +++ b/lib/usd/ui/layerEditor/dirtyLayersCountBadge.cpp @@ -0,0 +1,89 @@ +// +// 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 "dirtyLayersCountBadge.h" + +#include "qtUtils.h" + +#include + +namespace { +const char HIG_YELLOW[] = "#fbb549"; +} + +namespace UsdLayerEditor { + +DirtyLayersCountBadge::DirtyLayersCountBadge(QWidget* in_parent) + : QWidget(in_parent) +{ + // + setAttribute(Qt::WA_TransparentForMouseEvents); +} + +void DirtyLayersCountBadge::updateCount(int newCount) +{ + if (newCount != _dirtyCount) { + _dirtyCount = newCount; + update(); + } +} + +void DirtyLayersCountBadge::paintEvent(QPaintEvent* event) +{ + QWidget::paintEvent(event); + if (_dirtyCount > 0) { + auto thisRect = rect(); + QPainter painter(this); + auto oldPen = painter.pen(); + QString textToDraw; + if (_dirtyCount > 99) { + textToDraw = "99+"; + } else { + textToDraw = QString::number(_dirtyCount); + } + + painter.setRenderHint(QPainter::Antialiasing, true); + painter.setPen(Qt::NoPen); + painter.setBrush(QBrush(QColor(HIG_YELLOW))); + + int size = DPIScale(14); + int buttonRightEdge = DPIScale(16); + QRect drawRect(0, 0, size, size); + int charLen = textToDraw.length(); + int extraWidth = (charLen - 1) * DPIScale(6); + drawRect.adjust(0, 0, extraWidth, 0); + + drawRect.moveTopLeft(QPoint(buttonRightEdge, 0)); + if (drawRect.right() >= thisRect.right()) { + drawRect.moveTopRight(thisRect.topRight()); + } + + painter.drawRoundedRect(drawRect, size / 2.0, size / 2.0); + + painter.setPen(QColor(0, 0, 0)); + QFont font; + font.setPixelSize(DPIScale(11)); + font.setBold(true); + painter.setFont(font); + int nudge = DPIScale(-1); + drawRect.adjust(0, 0, 1, nudge); + painter.drawText(drawRect, Qt::AlignCenter, textToDraw); + + painter.setPen(oldPen); + } +} + +} // namespace UsdLayerEditor \ No newline at end of file diff --git a/lib/usd/ui/layerEditor/dirtyLayersCountBadge.h b/lib/usd/ui/layerEditor/dirtyLayersCountBadge.h new file mode 100644 index 0000000000..1ae52eaa17 --- /dev/null +++ b/lib/usd/ui/layerEditor/dirtyLayersCountBadge.h @@ -0,0 +1,46 @@ +// +// 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. +// + +#ifndef DIRTYLAYERSCOUNTBADGE_H +#define DIRTYLAYERSCOUNTBADGE_H + +#include + +namespace UsdLayerEditor { +/** + * @brief Widget that apears on top of the Save Layer button, to show how many layers need to be + *saved + * + **/ + +class DirtyLayersCountBadge : public QWidget +{ + Q_OBJECT +public: + DirtyLayersCountBadge(QWidget* in_parent); + // API for parent widget + void updateCount(int newCount); + + // QWidgets overrides + virtual void paintEvent(QPaintEvent* event) override; + +protected: + int _dirtyCount = 0; +}; + +} // namespace UsdLayerEditor + +#endif // DIRTYLAYERSCOUNTBADGE_H diff --git a/lib/usd/ui/layerEditor/generatedIconButton.cpp b/lib/usd/ui/layerEditor/generatedIconButton.cpp new file mode 100644 index 0000000000..906c90721d --- /dev/null +++ b/lib/usd/ui/layerEditor/generatedIconButton.cpp @@ -0,0 +1,169 @@ +// +// 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 "generatedIconButton.h" + +#include "qtUtils.h" + +#include +#include +#include +#include +#include + +namespace { + +enum class PixmapType +{ + DISABLED, + HOVER +}; + +struct LUTS +{ + int valueLut[256]; // look up table to apply to HS[V] value + int alphaLut[256]; // look up table to apply to alpha +}; + +// look up table to apply to HS[V] value +void generateVLut(LUTS& luts, PixmapType pixmapType) +{ + { + const int HIGH_LIMIT = 205, LOW_LIMIT = 30, MAX_VALUE = 255; + const int ADJUSTEMENT_VALUE = MAX_VALUE - HIGH_LIMIT; + + int newValue; + for (int v = 0; v < 256; v++) { + newValue = v; + if (v > LOW_LIMIT) { // value below this limit will not be adjusted + if (v < HIGH_LIMIT) { // value above this limit will just max up to 255 + if (pixmapType != PixmapType::DISABLED) { + newValue = v + ADJUSTEMENT_VALUE; + } + } else { + newValue = MAX_VALUE; + } + } + luts.valueLut[v] = newValue; + } + } +} + +// look up table to apply to alpha +void generateAlphaLut(LUTS& luts, PixmapType pixmapType) +{ + if (pixmapType != PixmapType::DISABLED) { + for (int a = 0; a < 256; a++) { + luts.alphaLut[a] = a; + } + } else { + for (int a = 0; a < 256; a++) { + luts.alphaLut[a] = static_cast(a * 0.4); + } + } +} + +// generates a hover or disabled bitmap from a source bitmatp +QPixmap generateIconPixmap(const QPixmap& pixmap, PixmapType pixmapType) +{ + LUTS luts; + generateVLut(luts, pixmapType); + generateAlphaLut(luts, pixmapType); + + int h, s, v, a; + auto img = pixmap.toImage().convertToFormat(QImage::Format_ARGB32); + int height = img.height(); + int width = img.width(); + + QColor color; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + color = img.pixelColor(x, y); + color.getHsv(&h, &s, &v, &a); + color.setHsv(h, s, luts.valueLut[v], luts.alphaLut[a]); + img.setPixelColor(x, y, color); + } + } + return QPixmap::fromImage(img); +} + +} // namespace + +namespace UsdLayerEditor { + +GeneratedIconButton::GeneratedIconButton(QWidget* in_parent, const QIcon& in_icon, int in_size) +{ + if (in_size == -1) { + _size = DPIScale(20); + } else { + _size = in_size; + } + setIcon(in_icon); + _noIcons = in_icon.availableSizes().size() == 0; + _inHover = false; + + if (!_noIcons) { + _basePixmap = in_icon.pixmap(_size, _size); + _hoverPixmap = generateIconPixmap(_basePixmap, PixmapType::HOVER); + _disabledPixmap = generateIconPixmap(_basePixmap, PixmapType::DISABLED); + } +} + +bool GeneratedIconButton::event(QEvent* in_event) +{ + switch (in_event->type()) { + case QEvent::Enter: { + _inHover = true; + repaint(); + } break; + case QEvent::Leave: { + _inHover = false; + repaint(); + } break; + case QEvent::ToolTip: { + QToolTip::showText(dynamic_cast(in_event)->globalPos(), toolTip()); + } break; + default: { + return QAbstractButton::event(in_event); + } + } + return true; +} + +void GeneratedIconButton::paintEvent(QPaintEvent* in_event) +{ + QPainter painter(this); + if (_noIcons) { + painter.drawRect(rect()); + } else { + QPixmap pixmap; + + if (!isEnabled()) { + pixmap = _disabledPixmap; + } else if (_inHover) { + pixmap = _hoverPixmap; + } else { + pixmap = _basePixmap; + } + QStyleOptionButton option; + option.initFrom(this); + painter.drawPixmap(option.rect, pixmap, pixmap.rect()); + } +} + +QSize GeneratedIconButton::sizeHint() const { return QSize(_size, _size); } + +} // namespace UsdLayerEditor diff --git a/lib/usd/ui/layerEditor/generatedIconButton.h b/lib/usd/ui/layerEditor/generatedIconButton.h new file mode 100644 index 0000000000..e89eabfbba --- /dev/null +++ b/lib/usd/ui/layerEditor/generatedIconButton.h @@ -0,0 +1,50 @@ +// +// 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. +// + +#ifndef GENERATEDBUTTON_H +#define GENERATEDBUTTON_H + +#include +#include + +namespace UsdLayerEditor { + +/** + * @brief button class that generates a hover and disabled bitmap from a base QIcon + * + */ + +class GeneratedIconButton : public QAbstractButton +{ +public: + GeneratedIconButton(QWidget* in_parent, const QIcon& in_icon, int in_size = -1); + + QSize sizeHint() const override; + +protected: + int _size; + bool _noIcons; + bool _inHover; + QPixmap _basePixmap; //!< normal icon + QPixmap _hoverPixmap; //!< brighter icon + QPixmap _disabledPixmap; //!< darker icon + + bool event(QEvent* in_event) override; + void paintEvent(QPaintEvent* in_event) override; +}; + +} // namespace UsdLayerEditor +#endif // GENERATEDBUTTON_H diff --git a/lib/usd/ui/layerEditor/layerEditorWidget.cpp b/lib/usd/ui/layerEditor/layerEditorWidget.cpp new file mode 100644 index 0000000000..3d4272bf8f --- /dev/null +++ b/lib/usd/ui/layerEditor/layerEditorWidget.cpp @@ -0,0 +1,354 @@ +// +// 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 "abstractCommandHook.h" +#include "dirtyLayersCountBadge.h" +#include "generatedIconButton.h" +#include "layerTreeModel.h" +#include "layerTreeView.h" +#include "qtUtils.h" +#include "stageSelectorWidget.h" +#include "stringResources.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace { + +using namespace UsdLayerEditor; + +// returns image_100.png" when you pass "image", +// using the DPI setting and also returns always 100 on mac, because Qt doesn't +// properly support high dpi with style sheets +QString getDPIPixmapName(QString baseName) +{ + QString suffix(QString::number(IS_MAC_OS ? 100 : DPIScale(100))); + return baseName + "_" + suffix + ".png"; +} + +// setup a push button with DPI-appropriate regular, hover and pressed png in the +// autodesk human interface guideline style +static void setupButtonWithHIGBitmaps(QPushButton* button, QString baseName) +{ + button->setFlat(true); + + // regular size: 16px, pressed:24px + // therefore, border is 4 + int padding = DPIScale(4); + QString cssTemplate(R"CSS( + QPushButton { + padding : %1px; + background-image: url(%2); + border: 0px; + background-origin: content; + } + QPushButton::hover { + background-image: url(%3); + } + QPushButton::pressed { + background-image: url(%4); + border: 0px; + padding: 0px; + background-origin: content; + })CSS"); + + QString css = cssTemplate.arg(padding) + .arg(getDPIPixmapName(baseName)) + .arg(getDPIPixmapName(baseName + "_hover")) + .arg(getDPIPixmapName(baseName + "_pressed")); + + button->setStyleSheet(css); + + // overkill, but used to generate the grayed out version + auto effect = new QGraphicsOpacityEffect(button); + button->setGraphicsEffect(effect); +} + +void disableHIGButton(QPushButton* button, bool disable = true) +{ + button->setDisabled(disable); + auto effect = dynamic_cast(button->graphicsEffect()); + effect->setOpacity(disable ? 0.4 : 1.0); +} + +// 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); + + 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) +{ + setupLayout(); + ::setupDefaultMenu(&in_sessionState, in_parent); +} + +// helper for setupLayout +QLayout* LayerEditorWidget::setupLayout_toolbar() +{ + auto buttonSize = DPIScale(24); + auto margin = DPIScale(12); + auto toolbar = new QHBoxLayout(); + toolbar->setContentsMargins(margin, 0, 0, 0); + auto buttonAlignment = Qt::AlignLeft | Qt::AlignRight; + + auto addButton = [toolbar, buttonAlignment](const char* iconName, const QString& tooltip) { + auto icon = utils->createIcon(iconName); + auto button = new GeneratedIconButton(nullptr, icon); + button->setToolTip(tooltip); + toolbar->addWidget(button, 0, buttonAlignment); + return button; + }; + + _buttons._newLayer = addButton( + ":/RS_create_layer.png", 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); + + _buttons._loadLayer = addButton( + ":/RS_import_layer.png", + StringResources::getAsQString(StringResources::kLoadExistingLayer)); + // clicked callback + connect( + _buttons._loadLayer, + &QAbstractButton::clicked, + this, + &LayerEditorWidget::onLoadLayersButtonClicked); + + toolbar->addStretch(); + + { // save stage button: contains a push button and a "badge" widget + auto saveButtonParent = new QWidget(); + auto saveButtonYOffset = DPIScale(4); + auto saveButtonSize = QSize(buttonSize + DPIScale(12), buttonSize + saveButtonYOffset); + saveButtonParent->setFixedSize(saveButtonSize); + auto saveStageBtn = new QPushButton(saveButtonParent); + saveStageBtn->move(0, saveButtonYOffset); + setupButtonWithHIGBitmaps(saveStageBtn, ":/UsdLayerEditor/LE_save_all"); + saveStageBtn->setFixedSize(buttonSize, buttonSize); + saveStageBtn->setToolTip( + StringResources::getAsQString(StringResources::kSaveAllEditsInLayerStack)); + connect( + saveStageBtn, + &QAbstractButton::clicked, + this, + &LayerEditorWidget::onSaveStageButtonClicked); + + auto dirtyCountBadge = new DirtyLayersCountBadge(saveButtonParent); + dirtyCountBadge->setFixedSize(saveButtonSize); + toolbar->addWidget(saveButtonParent, 0, buttonAlignment); + + _buttons._saveStageButton = saveStageBtn; + _buttons._dirtyCountBadge = dirtyCountBadge; + + // update dirty count on stage change + connect( + _treeView->model(), + &QAbstractItemModel::modelReset, + this, + &LayerEditorWidget::updateDirtyCountBadgeOnIdle); + // update dirty count on dirty notfication + connect( + _treeView->model(), + &QAbstractItemModel::dataChanged, + this, + &LayerEditorWidget::updateDirtyCountBadgeOnIdle); + } + + 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(); + updateDirtyCountBadge(); +} + +void LayerEditorWidget::updateDirtyCountBadgeOnIdle() +{ + if (!_updateDirtyCountOnIdle) { + _updateDirtyCountOnIdle = true; + QTimer::singleShot(0, this, &LayerEditorWidget::updateDirtyCountBadge); + } +} + +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 item = _treeView->currentLayerItem(); + if (item) { + disabled = item->isInvalidLayer() || item->appearsMuted(); + } + } + } + _buttons._newLayer->setDisabled(disabled); + _buttons._loadLayer->setDisabled(disabled); +} + +void LayerEditorWidget::updateDirtyCountBadge() +{ + const auto layers = _treeView->layerTreeModel()->getAllNeedsSavingLayers(); + int count = static_cast(layers.size()); + _buttons._dirtyCountBadge->updateCount(count); + + bool disable = count == 0; + disableHIGButton(_buttons._saveStageButton, disable); + _updateDirtyCountOnIdle = 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 { + // add a sibling to the selection + UndoContext context(_sessionState.commandHook(), "Add Anonymous Layer"); + auto parentItem = layerTreeItem->parentLayerItem(); + int rowToInsert = layerTreeItem->row(); + auto newLayer = parentItem->addAnonymousSublayerAndReturn(); + // move it to the right place, if it's not top + if (rowToInsert > 0) { + context.hook()->removeSubLayerPath(parentItem->layer(), newLayer->GetIdentifier()); + context.hook()->insertSubLayerPath( + parentItem->layer(), newLayer->GetIdentifier(), rowToInsert); + model->selectUsdLayerOnIdle(newLayer); + } + } +} + +void LayerEditorWidget::onLoadLayersButtonClicked() +{ + auto model = _treeView->layerTreeModel(); + auto selectionModel = _treeView->selectionModel(); + 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(); } + +} // namespace UsdLayerEditor diff --git a/lib/usd/ui/layerEditor/layerEditorWidget.h b/lib/usd/ui/layerEditor/layerEditorWidget.h new file mode 100644 index 0000000000..eaaebd1e7f --- /dev/null +++ b/lib/usd/ui/layerEditor/layerEditorWidget.h @@ -0,0 +1,81 @@ +// +// 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. +// + +#ifndef LAYEREDITORWIDGET_H +#define LAYEREDITORWIDGET_H + +#include "generatedIconButton.h" +#include "layerTreeItem.h" +#include "layerTreeView.h" + +#include +#include + +class QMainWindow; +class QLayout; +class QPushButton; + +namespace UsdLayerEditor { +class DirtyLayersCountBadge; +class GeneratedIconButton; +class LayerTreeView; +class SessionState; + +/** + * @brief Widget that manages a menu, a combo box to select a USD stage, and USD Layer Tree view + * + * This widget is meant to be hosted by a parent QMainWindow, where the menu will be created + **/ + +class LayerEditorWidget : public QWidget +{ + Q_OBJECT +public: + explicit LayerEditorWidget(SessionState& in_sessionState, QMainWindow* in_parent = nullptr); + +Q_SIGNALS: + +public Q_SLOTS: + void onNewLayerButtonClicked(); + void onLoadLayersButtonClicked(); + void onSaveStageButtonClicked(); + void updateDirtyCountBadgeOnIdle(); + +public: + LayerTreeView* layerTree() { return _treeView.data(); } + +protected: + void setupLayout(); + QLayout* setupLayout_toolbar(); + SessionState& _sessionState; + struct + { + GeneratedIconButton* _newLayer; + GeneratedIconButton* _loadLayer; + QPushButton* _saveStageButton; + DirtyLayersCountBadge* _dirtyCountBadge; + } _buttons; + void updateNewLayerButton(); + void updateDirtyCountBadge(); + + QPointer _treeView; + + bool _updateDirtyCountOnIdle = false; // true if request to update on idle is pending +}; + +} // namespace UsdLayerEditor + +#endif // LAYEREDITORWIDGET_H diff --git a/lib/usd/ui/layerEditor/layerTreeItem.cpp b/lib/usd/ui/layerEditor/layerTreeItem.cpp new file mode 100644 index 0000000000..62cecfbcc1 --- /dev/null +++ b/lib/usd/ui/layerEditor/layerTreeItem.cpp @@ -0,0 +1,355 @@ +#include "layerTreeItem.h" + +#include "abstractCommandHook.h" +#include "layerTreeModel.h" +#include "loadLayersDialog.h" +#include "pathChecker.h" +#include "qtUtils.h" +#include "sessionState.h" +#include "stringResources.h" +#include "warningDialogs.h" + +#include +#include +#include + +#include + +#include + +PXR_NAMESPACE_USING_DIRECTIVE + +namespace UsdLayerEditor { + +// delegate Action API for command buttons +std::vector LayerTreeItem::_actionButtons; + +const std::vector& LayerTreeItem::actionButtonsDefinition() +{ + if (_actionButtons.size() == 0) { + LayerActionInfo info; + info._name = "Mute Action"; + info._tooltip = StringResources::getAsQString(StringResources::kMuteUnmuteLayer); + info._pixmap = utils->createPNGResPixmap("RS_disable"); + _actionButtons.push_back(info); + } + return _actionButtons; +} + +LayerTreeItem::LayerTreeItem( + SdfLayerRefPtr in_usdLayer, + LayerType in_layerType, + std::string in_subLayerPath, + RecursionDetector* in_recursionDetector) + : _layer(std::move(in_usdLayer)) + , _isTargetLayer(false) + , _layerType(in_layerType) + , _subLayerPath(in_subLayerPath) +{ + fetchData(RebuildChildren::Yes, in_recursionDetector); +} + +// QStandardItem API +int LayerTreeItem::type() const { return QStandardItem::UserType; } + +// used by draw delegate: returns how deep in the hierarchy we are +int LayerTreeItem::depth() const +{ + auto parent = parentLayerItem(); + return (parent == nullptr) ? 0 : 1 + parent->depth(); +} + +// this algorithm works with muted layers +void LayerTreeItem::populateChildren(RecursionDetector* recursionDetector) +{ + removeRows(0, rowCount()); + if (isInvalidLayer()) + return; + + auto subPaths = _layer->GetSubLayerPaths(); + auto& resolver = ArGetResolver(); + auto anchor = toForwardSlashes(_layer->GetRealPath()); + + RecursionDetector defaultDetector; + if (!recursionDetector) { + recursionDetector = &defaultDetector; + } + recursionDetector->push(_layer->GetRealPath()); + + for (auto const path : subPaths) { + std::string actualPath = computePathToLoadSublayer(path, anchor, resolver); + auto subLayer = SdfLayer::FindOrOpen(actualPath); + if (subLayer) { + if (recursionDetector->contains(subLayer->GetRealPath())) { + MString msg; + msg.format( + StringResources::getAsMString(StringResources::kErrorRecursionDetected), + subLayer->GetRealPath().c_str()); + puts(msg.asChar()); + } else { + auto item + = new LayerTreeItem(subLayer, LayerType::SubLayer, path, recursionDetector); + appendRow(item); + } + } else { + MString msg; + msg.format( + StringResources::getAsMString(StringResources::kErrorDidNotFind), + std::string(path).c_str()); + puts(msg.asChar()); + auto item = new LayerTreeItem(subLayer, LayerType::SubLayer, path); + appendRow(item); + } + } + + recursionDetector->pop(); +} + +LayerItemVector LayerTreeItem::childrenVector() const +{ + LayerItemVector result; + result.reserve(rowCount()); + for (int i = 0; i < rowCount(); i++) { + result.push_back(dynamic_cast(child(i, 0))); + } + return result; +} + +// recursively update the target layer data member. Meant to be called from invisibleRoot +void LayerTreeItem::updateTargetLayerRecursive(const pxr::SdfLayerRefPtr& newTargetLayer) +{ + if (!_layer) + return; + bool thisLayerIsNowTarget = (_layer == newTargetLayer); + if (thisLayerIsNowTarget != _isTargetLayer) { + _isTargetLayer = thisLayerIsNowTarget; + emitDataChanged(); + } + for (auto child : childrenVector()) { + child->updateTargetLayerRecursive(newTargetLayer); + } +} + +void LayerTreeItem::fetchData(RebuildChildren in_rebuild, RecursionDetector* in_recursionDetector) +{ + std::string name; + if (isSessionLayer()) { + name = "sessionLayer"; + } else { + if (isInvalidLayer()) { + name = _subLayerPath; + } else { + name = _layer->GetDisplayName(); + if (name.empty()) { + name = _layer->GetIdentifier(); + } + } + } + _displayName = name; + setText(name.c_str()); + if (in_rebuild == RebuildChildren::Yes) { + populateChildren(in_recursionDetector); + } + emitDataChanged(); +} + +QVariant LayerTreeItem::data(int role) const +{ + switch (role) { + case Qt::TextColorRole: return QColor(200, 200, 200); + case Qt::BackgroundRole: return QColor(71, 71, 71); + case Qt::TextAlignmentRole: return Qt::AlignLeft + Qt::AlignVCenter; + case Qt::SizeHintRole: return QSize(0, DPIScale(30)); + default: return QStandardItem::data(role); + } +} + +LayerTreeModel* LayerTreeItem::parentModel() const +{ + return dynamic_cast(model()); +} + +AbstractCommandHook* LayerTreeItem::commandHook() const +{ + return parentModel()->sessionState()->commandHook(); +} + +pxr::UsdStageRefPtr const& LayerTreeItem::stage() const +{ + return parentModel()->sessionState()->stage(); +} + +bool LayerTreeItem::isMuted() const +{ + return isInvalidLayer() ? false : stage()->IsLayerMuted(_layer->GetIdentifier()); +} + +bool LayerTreeItem::appearsMuted() const +{ + if (isMuted()) { + return true; + } + auto item = parentLayerItem(); + while (item != nullptr) { + if (item->isMuted()) { + return true; + } + item = item->parentLayerItem(); + } + return false; +} + +bool LayerTreeItem::isMovable() const +{ + // Dragging the root layer, session and muted layer is not allowed. + return !isSessionLayer() && !isRootLayer() && !appearsMuted(); +} + +bool LayerTreeItem::needsSaving() const +{ + if (_layer) + if (!isSessionLayer()) + return isDirty() || isAnonymous(); + return false; +} + +// delegate Action API for command buttons +void LayerTreeItem::getActionButton(int index, LayerActionInfo* out_info) const +{ + *out_info = actionButtonsDefinition()[0]; + out_info->_checked = isMuted(); +} + +void LayerTreeItem::removeSubLayer() +{ + if (isSublayer()) { // can't remove session or root layer + if (_isTargetLayer) { + commandHook()->setEditTarget(stage()->GetRootLayer()); + } + commandHook()->removeSubLayerPath(parentLayerItem()->layer(), subLayerPath()); + } +} + +void LayerTreeItem::saveEdits() +{ + if (isAnonymous()) { + if (!isSessionLayer()) + saveAnonymousLayer(); + } else { + layer()->Save(); + } +} + +// helper to save anon layers called by saveEdits() +void LayerTreeItem::saveAnonymousLayer() +{ + auto sessionState = parentModel()->sessionState(); + + std::string fileName, formatTag; + if (sessionState->saveLayerUI(nullptr, &fileName, &formatTag)) { + // the path we has is an absolute path + const QString dialogTitle = StringResources::getAsQString(StringResources::kSaveLayer); + if (saveSubLayer(dialogTitle, parentLayerItem(), layer(), fileName, formatTag)) { + printf("USD Layer written to %s\n", fileName.c_str()); + + // now replace the layer in the parent + if (isRootLayer()) { + sessionState->rootLayerPathChanged(fileName); + } else { + // now replace the layer in the parent + auto parentItem = parentLayerItem(); + auto newLayer = SdfLayer::FindOrOpen(fileName); + if (newLayer) { + bool setTarget = _isTargetLayer; + auto model = parentModel(); + parentItem->layer()->GetSubLayerPaths().Replace( + layer()->GetIdentifier(), newLayer->GetIdentifier()); + if (setTarget) { + sessionState->stage()->SetEditTarget(newLayer); + } + model->selectUsdLayerOnIdle(newLayer); + } else { + QMessageBox::critical( + nullptr, + dialogTitle, + StringResources::getAsQString(StringResources::kErrorFailedToReloadLayer)); + } + } + } + } +} +void LayerTreeItem::discardEdits() +{ + if (isAnonymous()) { + // according to MAYA-104336, we don't prompt for confirmation for anonymous layers + commandHook()->discardEdits(layer()); + } else { + MString title; + title.format( + StringResources::getAsMString(StringResources::kDiscardEditsTitle), + MQtUtil::toMString(text())); + + MString desc; + desc.format( + StringResources::getAsMString(StringResources::kDiscardEditsMsg), + MQtUtil::toMString(text())); + + if (confirmDialog(MQtUtil::toQString(title), MQtUtil::toQString(desc))) { + commandHook()->discardEdits(layer()); + } + } +} + +void LayerTreeItem::addAnonymousSublayer() +{ + // + addAnonymousSublayerAndReturn(); +} + +pxr::SdfLayerRefPtr LayerTreeItem::addAnonymousSublayerAndReturn() +{ + auto model = parentModel(); + auto newLayer + = commandHook()->addAnonymousSubLayer(layer(), model->findNameForNewAnonymousLayer()); + model->selectUsdLayerOnIdle(newLayer); + return newLayer; +} + +void LayerTreeItem::loadSubLayers(QWidget* in_parent) +{ + LoadLayersDialog dlg(this, in_parent); + dlg.exec(); + if (dlg.pathsToLoad().size() > 0) { + const int index = 0; + UndoContext context(commandHook(), "Load Layers"); + for (auto path : dlg.pathsToLoad()) { + context.hook()->insertSubLayerPath(layer(), path, index); + } + } +} + +void LayerTreeItem::printLayer() +{ + if (!isInvalidLayer()) { + parentModel()->sessionState()->printLayer(layer()); + } +} + +void LayerTreeItem::clearLayer() +{ + MString title; + title.format( + StringResources::getAsMString(StringResources::kClearLayerTitle), + MQtUtil::toMString(text())); + + MString desc; + desc.format( + StringResources::getAsMString(StringResources::kClearLayerConfirmMessage), + MQtUtil::toMString(text())); + + if (confirmDialog(MQtUtil::toQString(title), MQtUtil::toQString(desc))) { + commandHook()->clearLayer(layer()); + } +} + +} // namespace UsdLayerEditor diff --git a/lib/usd/ui/layerEditor/layerTreeItem.h b/lib/usd/ui/layerEditor/layerTreeItem.h new file mode 100644 index 0000000000..25ac8b5a49 --- /dev/null +++ b/lib/usd/ui/layerEditor/layerTreeItem.h @@ -0,0 +1,178 @@ +// +// 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. +// + +#ifndef LAYERTREEITEM_H +#define LAYERTREEITEM_H + +#include "pxr/usd/sdf/layer.h" + +#include +#include + +#include + +#include +#include +#include + +namespace UsdLayerEditor { +class AbstractCommandHook; +class LayerTreeModel; +class LayerTreeItem; +class RecursionDetector; + +enum class LayerType +{ + SessionLayer, + RootLayer, + SubLayer +}; +enum class RebuildChildren +{ + Yes, + No +}; + +struct LayerActionInfo +{ + QString _name; + QString _tooltip; + QPixmap _pixmap; + int extraPadding = 0; + QColor _borderColor; + bool _checked = false; +}; + +typedef std::vector recursionDetection; +typedef std::vector LayerItemVector; + +/** + * @brief Implements one USD layer item in the treeview + * + */ + +class LayerTreeItem : public QStandardItem +{ +public: + LayerTreeItem( + pxr::SdfLayerRefPtr in_usdLayer, + LayerType in_layerType = LayerType::SubLayer, + std::string in_subLayerPath = "", + RecursionDetector* in_recursionDetector = nullptr); + + // refresh our data from the USD Layer + void fetchData(RebuildChildren in_rebuild, RecursionDetector* in_recursionDetector = nullptr); + + // QStandardItem API + int type() const override; + QVariant data(int role) const override; + void emitDataChanged() { QStandardItem::emitDataChanged(); } + + // parent(), properly typed + LayerTreeItem* parentLayerItem() const { return dynamic_cast(parent()); } + // model, properly typed + LayerTreeModel* parentModel() const; + // get the display name + const std::string& displayName() const { return _displayName; } + // if a sublayer, get the path we were saved with in the parent + const std::string& subLayerPath() const { return _subLayerPath; } + // shortcut to get stage from model + pxr::UsdStageRefPtr const& stage() const; + + // is the layer muted at the stage level? + bool isMuted() const; + // check if this layer is muted, or any of its parent + bool appearsMuted() const; + // true if dirty, but look at needsSaving for UI feedback + bool isDirty() const { return _layer ? _layer->IsDirty() : false; } + // need to indicate visually that layer has something to save + bool needsSaving() const; + // for drag and drop + bool isMovable() const; + + // used by draw delegate: returns how deep in the hierarchy we are + int depth() const; + // is this sublayer with a path that doesn't load? + bool isInvalidLayer() const { return !_layer; } + // update the target layer flags -- meant to be called from invisible root + void updateTargetLayerRecursive(const pxr::SdfLayerRefPtr& newTargetLayer); + + // USD Layer type query + 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 isRootLayer() const { return _layerType == LayerType::RootLayer; } + pxr::SdfLayerRefPtr layer() const { return _layer; } + + // allows c++ iteration of children + LayerItemVector childrenVector() const; + + // menu callbacks + void removeSubLayer(); + void saveEdits(); + void discardEdits(); + + // there are two addAnonymousSubLayer , because the menu needs all method to be void + void addAnonymousSublayer(); + pxr::SdfLayerRefPtr addAnonymousSublayerAndReturn(); + void loadSubLayers(QWidget* in_parent); + void printLayer(); + void clearLayer(); + + // delegate Action API for command buttons + int getActionButtonCount() const { return (isSublayer() && !isInvalidLayer()) ? 1 : 0; } + // delegate Action API for command buttons + void getActionButton(int index, LayerActionInfo* out_info) const; + static const std::vector& actionButtonsDefinition(); + +protected: + pxr::SdfLayerRefPtr _layer; + std::string _displayName; + bool _isTargetLayer = false; + LayerType _layerType = LayerType::SubLayer; + std::string _subLayerPath; // name of the layer as it was found in the parent's stack + + static std::vector _actionButtons; + +protected: + AbstractCommandHook* commandHook() const; + +protected: + void populateChildren(RecursionDetector* in_recursionDetector); + // helper to save anon layers called by saveEdits() + void saveAnonymousLayer(); +}; + +class RecursionDetector +{ +public: + RecursionDetector() { } + void push(const std::string& path) { _paths.push_back(path); } + + void pop() { _paths.pop_back(); } + bool contains(const std::string& in_path) const + { + return !in_path.empty() + && std::find(_paths.cbegin(), _paths.cend(), in_path) != _paths.cend(); + } + + std::vector _paths; +}; + +} // namespace UsdLayerEditor + +#endif // LAYERTREEITEM_H diff --git a/lib/usd/ui/layerEditor/layerTreeItemDelegate.cpp b/lib/usd/ui/layerEditor/layerTreeItemDelegate.cpp new file mode 100644 index 0000000000..236a223340 --- /dev/null +++ b/lib/usd/ui/layerEditor/layerTreeItemDelegate.cpp @@ -0,0 +1,369 @@ +// +// 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 "layerTreeItemDelegate.h" + +#include "qtUtils.h" + +#include + +namespace { +} + +namespace UsdLayerEditor { + +LayerTreeItemDelegate::LayerTreeItemDelegate(LayerTreeView* in_parent) + : QStyledItemDelegate(in_parent) + , _treeView(in_parent) +{ + DISABLED_BACKGROUND_IMAGE = utils->createPNGResPixmap("RS_disabled_tile"); + DISABLED_HIGHLIGHT_IMAGE = utils->createPNGResPixmap("RS_disabled_tile_highlight"); + WARNING_IMAGE = utils->createPNGResPixmap("RS_warning"); + + const char* targetOnPixmaps[3] { "target_on", "target_on_hover", "target_on_pressed" }; + const char* targetOffPixmaps[3] { "target_regular", "target_hover", "target_pressed" }; + for (int i = 0; i < 3; i++) { + TARGET_ON_IMAGES[i] + = utils->createPNGResPixmap(QString(":/UsdLayerEditor/") + targetOnPixmaps[i]); + TARGET_OFF_IMAGES[i] + = utils->createPNGResPixmap(QString(":/UsdLayerEditor/") + targetOffPixmaps[i]); + } +} + +QRect LayerTreeItemDelegate::getAdjustedItemRect(LayerTreeItem const* item, QRect const& optionRect) + const +{ + int indent = item->depth() * _treeView->indentation(); + QRect rect(optionRect); + rect.setLeft(indent); + rect.setBottom(rect.bottom() - BOTTOM_GAP_OFFSET); + return rect; +} + +QRect LayerTreeItemDelegate::getTextRect(QRect const& itemRect) const +{ + QRect textRect(itemRect); + textRect.setLeft(textRect.left() + TEXT_LEFT_OFFSET); + textRect.setRight(textRect.right() - TEXT_RIGHT_OFFSET); + return textRect; +} + +QRect LayerTreeItemDelegate::getTargetIconRect(QRect const& itemRect) const +{ + QRect rect(itemRect); + rect.translate(ARROW_AREA_WIDTH, 0); + rect.setWidth(CHECK_MARK_AREA_WIDTH); + return rect; +} + +void LayerTreeItemDelegate::paint_drawTarget( + QPainter* painter, + QRectC rect, + Item item, + Options option) const +{ + if (item->isInvalidLayer()) { + return; + } + auto targetRect = getTargetIconRect(rect); + + bool pressed = (_pressedTarget == item); + bool muted = item->appearsMuted(); + + bool hover = !muted && (option.state & QStyle::State_MouseOver) + && QtUtils::isMouseInRectangle(_treeView, targetRect); + + QPixmap icon; + if (item->isTargetLayer()) { + icon = pressed ? TARGET_ON_IMAGES[2] : TARGET_ON_IMAGES[hover ? 1 : 0]; + } else { + icon = pressed ? TARGET_OFF_IMAGES[2] : TARGET_OFF_IMAGES[hover ? 1 : 0]; + } + + auto iconRect = icon.rect(); + // got to be careful. the icon rect is correct with DPI on windows/linux + // but on mac, the icon is twice the resolution that we will actually draw in + // so we need to scale that back, because we draw in logical pixels vs physical + double deviceRatio = icon.devicePixelRatio(); + if (deviceRatio != 1.0) { + iconRect.setWidth(static_cast(iconRect.width() / deviceRatio)); + iconRect.setHeight(static_cast(iconRect.height() / deviceRatio)); + } + + iconRect.moveCenter(targetRect.center()); + if (muted) { + painter->setOpacity(DISABLED_OPACITY); + } + painter->drawPixmap(iconRect, icon); + if (muted) { + painter->setOpacity(1.0); + } +} + +void LayerTreeItemDelegate::paint_drawFill( + QPainter* painter, + QRectC rect, + Item item, + bool isHighlighted, + const QColor& highlightColor) const +{ + QRect rect2(rect); + auto oldPen = painter->pen(); + bool muted = item->appearsMuted(); + + if (isHighlighted) { + rect2.setLeft(rect2.left() + HIGHLIGHTED_FILL_OFFSET); + if (muted) { + painter->drawTiledPixmap(rect2, DISABLED_HIGHLIGHT_IMAGE); + } else { + painter->fillRect(rect2, highlightColor); + } + rect2.setLeft(rect2.left() - HIGHLIGHTED_FILL_OFFSET); + } else { + painter->fillRect(rect2, item->data(Qt::BackgroundRole).value()); + if (muted) { + painter->drawTiledPixmap(rect2, DISABLED_BACKGROUND_IMAGE); + } + } + painter->setPen(oldPen); +} + +void LayerTreeItemDelegate::paint_drawArrow(QPainter* painter, QRectC rect, Item item) const +{ + painter->save(); + if (item->rowCount() != 0) { + auto left = rect.left() + ARROW_OFFSET + 1; + const QPointF* arrow = nullptr; + if (_treeView->isExpanded(item->index())) { + painter->translate(left + EXPANDED_ARROW_OFFSET, rect.top()); + arrow = &EXPANDED_ARROW[0]; + } else { + painter->translate(left + COLLAPSED_ARROW_OFFSET, rect.top()); + arrow = &COLLAPSED_ARROW[0]; + } + auto const oldBrush = painter->brush(); + painter->setBrush(ARROW_COLOR); + painter->setPen(Qt::NoPen); + painter->drawPolygon(arrow, 3); + painter->setBrush(oldBrush); + } + painter->restore(); +} + +void LayerTreeItemDelegate::paint_drawText(QPainter* painter, QRectC rect, Item item) const +{ + const auto oldPen = painter->pen(); + painter->setPen(QPen(item->data(Qt::TextColorRole).value(), 1)); + const auto textRect = getTextRect(rect); + bool muted = item->appearsMuted(); + if (muted) + painter->setOpacity(DISABLED_OPACITY); + + QString text = item->data(Qt::DisplayRole).value(); + + // draw a * for dirty layers + if (item->needsSaving() || item->isDirty()) { + // item.needsSaving returns false for sessionLayer, but I think we should show the dirty + // flag for it + text += "*"; + } + + QRect boundingRect; + painter->drawText( + textRect, item->data(Qt::TextAlignmentRole).value(), text, &boundingRect); + if (item->isInvalidLayer()) { + int x = boundingRect.right() + DPIScale(4); + int y = boundingRect.top(); + painter->drawPixmap(x, y, WARNING_IMAGE); + } + + if (muted) + painter->setOpacity(1.0); +} +void LayerTreeItemDelegate::paint_drawToolbarFrame(QPainter* painter, QRectC rect, int iconCount) + const +{ + if (iconCount > 0) { + int top = rect.top() + ICON_TOP_OFFSET; + QColor backgroundColor(55, 55, 55); + int toolbarLength = iconCount * ACTION_WIDTH + (2 * ACTION_BORDER); + int left = rect.right() - (toolbarLength + ACTION_BORDER); + left = left > 0 ? left : 0; + painter->setOpacity(0.8); + painter->fillRect(left, top - ACTION_BORDER, toolbarLength, ACTION_WIDTH, backgroundColor); + painter->setOpacity(1.0); + } +} + +void LayerTreeItemDelegate::drawStdIcon(QPainter* painter, int left, int top, const QPixmap& pixmap) + const +{ + painter->drawPixmap(QRect(left, top, ICON_WIDTH, ICON_WIDTH), pixmap); +} + +void LayerTreeItemDelegate::paint_drawOneAction( + QPainter* painter, + int left, + int top, + const LayerActionInfo& actionInfo, + const QColor& highlightColor) const +{ + QRect iconRect(left, top, ICON_WIDTH, ICON_WIDTH); + // MAYA 84884: Created a backround rectangle underneath the icon to extend the mouse coverage + // region + int BACKGROUND_RECT_LENGTH = DPIScale(28); + int BACKGROUND_RECT_LEFT_OFFSET = DPIScale(4); + QRect backgroundRect( + left - BACKGROUND_RECT_LEFT_OFFSET, + top - ACTION_BORDER, + BACKGROUND_RECT_LENGTH, + ACTION_WIDTH); + + if (highlightColor.isValid()) + painter->fillRect(iconRect, highlightColor); + + // draw the icon. Its opacity depends on mouse over. + { + if (!QtUtils::isMouseInRectangle(_treeView, backgroundRect)) { + painter->setOpacity(0.7); + } else { + painter->setOpacity(1.0); + const_cast(this)->_lastHitAction = actionInfo._name; + } + drawStdIcon(painter, left, top, actionInfo._pixmap); + painter->setOpacity(1.0); + } + + if (actionInfo._borderColor.isValid()) { + auto oldPen = painter->pen(); + painter->setPen(QPen(actionInfo._borderColor, 1)); + painter->drawRect(iconRect); + painter->setPen(oldPen); + } +} + +void LayerTreeItemDelegate::paint_ActionIcons( + QPainter* painter, + QRectC rect, + Item item, + const QColor& highlightColor) const +{ + int top = rect.top() + ICON_TOP_OFFSET; + + int start = ACTION_BORDER; + int iconCount = item->getActionButtonCount(); + + QString tooltip; + + // draw the darkened toolbar frame + paint_drawToolbarFrame(painter, rect, iconCount); + + for (int iconIndex = 0; iconIndex < iconCount; iconIndex++) { + + LayerActionInfo action; + item->getActionButton(iconIndex, &action); + start += ACTION_WIDTH + action.extraPadding; + paint_drawOneAction( + painter, + rect.right() - start, + top, + action, + action._checked ? highlightColor : QColor()); + if (_lastHitAction == action._name) { + tooltip = action._tooltip; + } + } + + const_cast(item)->setToolTip(tooltip); +} + +void LayerTreeItemDelegate::paint( + QPainter* painter, + const QStyleOptionViewItem& option, + const QModelIndex& index) const +{ + if (index.isValid()) { + auto item = _treeView->layerItemFromIndex(index); + + QRect rect = getAdjustedItemRect(item, option.rect); + bool isHighlighted + = option.showDecorationSelected && (option.state & QStyle::State_Selected); + auto highlightedColor = option.palette.color(QPalette::Highlight); + + paint_drawFill(painter, rect, item, isHighlighted, highlightedColor); + paint_drawTarget(painter, rect, item, option); + paint_drawArrow(painter, rect, item); + paint_drawText(painter, rect, item); + paint_ActionIcons(painter, rect, item, highlightedColor); + } +} + +bool LayerTreeItemDelegate::editorEvent( + QEvent* event, + QAbstractItemModel* model, + const QStyleOptionViewItem& option, + const QModelIndex& index) +{ + LayerTreeItem* item; + if (index.isValid()) { + item = _treeView->layerItemFromIndex(index); + if (item->isInvalidLayer()) + return false; + } else { + return false; + } + + auto handlePressed = [this, item, index, event, option]() -> bool { + if (!item->appearsMuted()) { + auto mouseEvent = dynamic_cast(event); + if (event->type() == QEvent::MouseButtonRelease) { + // if we're release the mouse in the same button we pressed, fire the command + bool fireCommand = (this->_pressedTarget == item); + this->_pressedTarget = nullptr; + if (fireCommand) { + item->parentModel()->setEditTarget(item); + return true; + } + } else if (mouseEvent->button() == Qt::LeftButton) { + auto rect = this->getAdjustedItemRect(item, option.rect); + auto targetRect = this->getTargetIconRect(rect); + auto mousePos = mouseEvent->pos(); + if (targetRect.contains(mousePos)) { + setPressedTarget(index, item); + return true; + } else { + setPressedTarget(QModelIndex(), nullptr); + } + } + } + return false; + }; + + switch (event->type()) { + case QEvent::MouseButtonPress: + case QEvent::MouseButtonRelease: + if (handlePressed()) + return true; + break; + case QEvent::MouseMove: + _treeView->update(); // force redraw to property reflect hover + break; + default: break; + } + return false; +} + +} // namespace UsdLayerEditor \ No newline at end of file diff --git a/lib/usd/ui/layerEditor/layerTreeItemDelegate.h b/lib/usd/ui/layerEditor/layerTreeItemDelegate.h new file mode 100644 index 0000000000..b5fd3c570c --- /dev/null +++ b/lib/usd/ui/layerEditor/layerTreeItemDelegate.h @@ -0,0 +1,149 @@ +// +// 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. +// + +#ifndef LAYERTREEITEMDELEGATE_H +#define LAYERTREEITEMDELEGATE_H + +#include "layerTreeItem.h" +#include "layerTreeModel.h" +#include "layerTreeView.h" +#include "qtUtils.h" + +#include +#include + +class QTreeView; + +namespace UsdLayerEditor { +class LayerTreeItem; +class LayerTreeView; +struct LayerActionInfo; + +/** + * @brief Overrides one the drawing and mouse click for individual items in the tree view. + * Only one instance of this class exists per tree. + * + */ +class LayerTreeItemDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + LayerTreeItemDelegate(LayerTreeView* in_parent); + + // API for LayerTreeView + // gets the rectangle of the item, adjusted for the tree indentation + QRect getAdjustedItemRect(LayerTreeItem const* item, QRect const& optionRect) const; + // gets the rectangle where the text is drawn + QRect getTextRect(QRect const& itemRect) const; + // get the rectangle for the "set current target" icon + QRect getTargetIconRect(QRect const& itemRect) const; + + // QStyledItemDelegate API + void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) + const override; + bool editorEvent( + QEvent* event, + QAbstractItemModel* model, + const QStyleOptionViewItem& option, + const QModelIndex& index) override; + +public: + // slot: + void onModelReset() + { + _lastHitAction.clear(); + _pressedTarget = nullptr; + } + + bool isTargetPressed() const { return _pressedTarget != nullptr; } + void clearPressedTarget() { _pressedTarget = nullptr; } + QString lastHitAction() const { return _lastHitAction; } + void clearLastHitAction() { _lastHitAction = ""; } + +protected: + QPointer _treeView; + QString _lastHitAction; + + // used to implement target column + const LayerTreeItem* _pressedTarget; + + void setPressedTarget(QModelIndex index, LayerTreeItem* item) + { + _pressedTarget = item; + if (item != nullptr) + item->parentModel()->dataChanged(index, index); + } + +protected: + typedef QStyleOptionViewItem const& Options; + typedef QRect const& QRectC; + typedef LayerTreeItem const* Item; + + const int BOTTOM_GAP_OFFSET = DPIScale(2); + + const QColor ARROW_COLOR = QColor(189, 189, 189); + const int ARROW_OFFSET = DPIScale(6); + const int ARROW_AREA_WIDTH = DPIScale(24); + const int EXPANDED_ARROW_OFFSET = DPIScale(3.0); + const int COLLAPSED_ARROW_OFFSET = DPIScale(6.0); + QPointF EXPANDED_ARROW[3] = { DPIScale(QPointF(0.0, 11.0)), + DPIScale(QPointF(10.0, 11.0)), + DPIScale(QPointF(5.0, 16.0)) }; + QPointF COLLAPSED_ARROW[3] = { DPIScale(QPointF(0.0, 8.0)), + DPIScale(QPointF(5.0, 13.0)), + DPIScale(QPointF(0.0, 18.0)) }; + // action icon area + const int ACTION_BORDER = DPIScale(2); + const int ICON_WIDTH = DPIScale(20); + const int ACTION_WIDTH = ICON_WIDTH + (2 * ACTION_BORDER); + const int WARNING_ICON_WIDTH = DPIScale(11); + const int ICON_TOP_OFFSET = DPIScale(4); + + const int CHECK_MARK_AREA_WIDTH = DPIScale(28); + const int TEXT_LEFT_OFFSET = (ARROW_AREA_WIDTH + CHECK_MARK_AREA_WIDTH); + const int TEXT_RIGHT_OFFSET = DPIScale(36); + const int HIGHLIGHTED_FILL_OFFSET = DPIScale(1); + const double DISABLED_OPACITY = 0.6; + QPixmap DISABLED_BACKGROUND_IMAGE; + QPixmap DISABLED_HIGHLIGHT_IMAGE; + QPixmap TARGET_ON_IMAGES[3]; + QPixmap TARGET_OFF_IMAGES[3]; + QPixmap WARNING_IMAGE; + + void paint_drawTarget(QPainter* painter, QRectC rect, Item item, Options option) const; + void paint_drawFill( + QPainter* painter, + QRectC rect, + Item item, + bool isHighlighted, + const QColor& highlightColor) const; + void paint_drawArrow(QPainter* painter, QRectC rect, Item item) const; + void paint_drawText(QPainter* painter, QRectC rect, Item item) const; + void paint_ActionIcons(QPainter* painter, QRectC rect, Item item, const QColor& highlightColor) + const; + void paint_drawToolbarFrame(QPainter* painter, QRectC rect, int iconCount) const; + void paint_drawOneAction( + QPainter* painter, + int left, + int top, + const LayerActionInfo& action, + const QColor& highlightColor) const; + void drawStdIcon(QPainter* painter, int left, int top, const QPixmap& pixmap) const; +}; + +} // namespace UsdLayerEditor + +#endif // LAYERTREEITEMDELEGATE_H diff --git a/lib/usd/ui/layerEditor/layerTreeModel.cpp b/lib/usd/ui/layerEditor/layerTreeModel.cpp new file mode 100644 index 0000000000..54f82c600e --- /dev/null +++ b/lib/usd/ui/layerEditor/layerTreeModel.cpp @@ -0,0 +1,491 @@ +// +// 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 "layerTreeModel.h" + +#include "layerEditorWidget.h" +#include "layerTreeItem.h" +#include "stringResources.h" +#include "warningDialogs.h" + +#include + +#include + +#include + +#include +#include + +PXR_NAMESPACE_USING_DIRECTIVE + +namespace { +// drag and drop support +// For now just use the plain text type to store the layer identifiers. +const QString LAYER_EDITOR_MIME_TYPE = QStringLiteral("text/plain"); +const QString LAYED_EDITOR_MIME_SEP = QStringLiteral(";"); + +QStringList getLayerListAsQStringList(const UsdLayerEditor::LayerItemVector& layerItems) +{ + QStringList result; + for (auto item : layerItems) { + result.append(item->data(Qt::DisplayRole).value()); + } + return result; +} + +} // namespace + +namespace UsdLayerEditor { + +bool LayerTreeModel::_blockUsdNotices = false; + +void LayerTreeModel::suspendUsdNotices(bool suspend) +{ + // + _blockUsdNotices = suspend; +} + +LayerTreeModel::LayerTreeModel(SessionState* in_sessionState, QObject* in_parent) + : QStandardItemModel(in_parent) +{ + setSessionState(in_sessionState); + registerUsdNotifications(true); +} + +LayerTreeModel::~LayerTreeModel() +{ + // + registerUsdNotifications(false); +} + +void LayerTreeModel::registerUsdNotifications(bool in_register) +{ + if (in_register) { + TfWeakPtr me(this); + _noticeKeys.push_back(TfNotice::Register(me, &LayerTreeModel::usd_layerChanged)); + _noticeKeys.push_back(TfNotice::Register(me, &LayerTreeModel::usd_editTargetChanged)); + _noticeKeys.push_back(TfNotice::Register( + me, &LayerTreeModel::usd_layerDirtinessChanged, TfWeakPtr(nullptr))); + + } else { + TfNotice::Revoke(&_noticeKeys); + } +} + +Qt::ItemFlags LayerTreeModel::flags(const QModelIndex& index) const +{ + // set flags for drag and drop support + auto item = layerItemFromIndex(index); + if (!item || item->isInvalidLayer()) { + return Qt::ItemIsSelectable | Qt::ItemIsEnabled; + } + + Qt::ItemFlags defaultFlags = QStandardItemModel::flags(index); + if (index.isValid() && item->isMovable()) { + return Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags; + } else { + defaultFlags &= ~Qt::ItemIsDragEnabled; + return Qt::ItemIsDropEnabled | defaultFlags; + } +} + +Qt::DropActions LayerTreeModel::supportedDropActions() const +{ + // We support only moving layers around to reorder or re-parent. + return Qt::MoveAction; +} + +QStringList LayerTreeModel::mimeTypes() const +{ + // Just return our supported type (i.e. not appending it to the current) + // list of supported types. This way our base class(es) won't perform + // any drop actions. + return QStringList(LAYER_EDITOR_MIME_TYPE); +} + +QMimeData* LayerTreeModel::mimeData(const QModelIndexList& indexes) const +{ + // Returns an object that contains serialized items of data corresponding + // to the list of indexes specified. + + // Prepare the entries to move. For now we just store the layer identifiers + // as a string separated by special character. + auto mimeData = new QMimeData(); + QStringList identifiers; + for (auto index : indexes) { + auto item = layerItemFromIndex(index); + if (item->isInvalidLayer()) { + identifiers += item->subLayerPath().c_str(); + } else { + identifiers += item->layer()->GetIdentifier().c_str(); + } + } + mimeData->setText(identifiers.join(LAYED_EDITOR_MIME_SEP)); + return mimeData; +} + +bool LayerTreeModel::dropMimeData( + const QMimeData* in_mimeData, + Qt::DropAction action, + int row, + int column, + const QModelIndex& parentIndex) +{ + // Handles the data supplied by a drag and drop operation that ended with the given action. + + // Check if the action is supported? + if (!in_mimeData || (action != Qt::MoveAction)) { + return false; + } + if (!in_mimeData->hasFormat(LAYER_EDITOR_MIME_TYPE)) { + return false; + } + + auto parentItem = layerItemFromIndex(parentIndex); + if (!parentItem) { + return false; + } + + // row is -1 when dropped on a parent item and not between rows. + // In that case we want to insert at row 0 (first child). + if (row == -1) { + row = 0; + } + + // Parse the mime data that was passed in to get the list of layers. + // We parse it in reversed order since when we insert the original + // order is maintained. + QStringList identifiers = in_mimeData->text().split(LAYED_EDITOR_MIME_SEP); + std::reverse(identifiers.begin(), identifiers.end()); + UndoContext context(_sessionState->commandHook(), "Drop USD Layers"); + for (QString const& ident : identifiers) { + auto layer = SdfLayer::FindOrOpen(ident.toStdString()); + if (layer) { + auto layerItem = findUSDLayerItem(layer); + if (layerItem) { + auto oldParent = layerItem->parentLayerItem()->layer(); + int index = (int)oldParent->GetSubLayerPaths().Find(layerItem->subLayerPath()); + auto itemSubLayerPath = layerItem->subLayerPath(); + context.hook()->removeSubLayerPath(oldParent, itemSubLayerPath); + // When we are moving an item (underneath the same parent) + // to a new location higher up we have to adjust the row + // (new location) to account for the remove we just did. + if (oldParent == parentItem->layer() && (index < row)) { + row -= 1; + } + context.hook()->insertSubLayerPath(parentItem->layer(), itemSubLayerPath, row); + } + } + } + return true; +} + +void LayerTreeModel::setEditTarget(LayerTreeItem* item) +{ + if (!item->appearsMuted()) { + UndoContext context(_sessionState->commandHook(), "Set USD Edit Target Layer"); + context.hook()->setEditTarget(item->layer()); + } +} + +void LayerTreeModel::selectUsdLayerOnIdle(const SdfLayerRefPtr& usdLayer) +{ + QTimer::singleShot(0, this, [this, usdLayer]() { + auto item = findUSDLayerItem(usdLayer); + if (item != nullptr) { + auto index = indexFromItem(item); + Q_EMIT selectLayerSignal(index); + } + }); +} + +QModelIndex LayerTreeModel::rootLayerIndex() +{ + auto root = invisibleRootItem(); + for (int row = 0; row < root->rowCount(); row++) { + auto child = dynamic_cast(root->child(row)); + if (child->isRootLayer()) { + return index(row, 0); + } + } + return QModelIndex(); +} + +void LayerTreeModel::setSessionState(SessionState* in_sessionState) +{ + _sessionState = in_sessionState; + connect( + in_sessionState, + &SessionState::currentStageChangedSignal, + this, + &LayerTreeModel::sessionStageChanged); + + connect( + in_sessionState, + &SessionState::autoHideSessionLayerSignal, + this, + &LayerTreeModel::sessionStageChanged); +} + +void LayerTreeModel::rebuildModelOnIdle() +{ + if (!_rebuildOnIdlePending) { + _rebuildOnIdlePending = true; + QTimer::singleShot(0, this, &LayerTreeModel::rebuildModel); + } +} + +void LayerTreeModel::rebuildModel() +{ + _rebuildOnIdlePending = false; + _lastAskedAnonLayerNameSinceRebuild = 0; + + beginResetModel(); + clear(); + + if (_sessionState->isValid()) { + bool showSessionLayer = true; + auto sessionLayer = _sessionState->stage()->GetSessionLayer(); + if (_sessionState->autoHideSessionLayer()) { + showSessionLayer + = sessionLayer->IsDirty() || sessionLayer == _sessionState->targetLayer(); + } + if (showSessionLayer) { + appendRow(new LayerTreeItem(sessionLayer, LayerType::SessionLayer)); + } + + auto rootLayer = _sessionState->stage()->GetRootLayer(); + appendRow(new LayerTreeItem(rootLayer, LayerType::RootLayer)); + + updateTargetLayer(InRebuildModel::Yes); + } + + endResetModel(); +} + +LayerTreeItem* LayerTreeModel::findUSDLayerItem(const SdfLayerRefPtr& usdLayer) const +{ + const auto allItems = getAllItems(); + for (auto item : allItems) { + if (item->layer() == usdLayer) + return item; + } + return nullptr; +} + +void LayerTreeModel::updateTargetLayer(InRebuildModel inRebuild) +{ + if (rowCount() == 0) { + return; + } + + auto editTarget = _sessionState->targetLayer(); + auto root = invisibleRootItem(); + + // if session layer is in auto-hide handle case where it is the target + if (inRebuild == InRebuildModel::No && _sessionState->autoHideSessionLayer()) { + bool needToRebuild = false; + auto firstLayerItem = dynamic_cast(root->child(0)); + // if session layer is no longer the target layer, we need to rebuild to hide it + if (firstLayerItem->isSessionLayer() && firstLayerItem->isTargetLayer()) { + needToRebuild = firstLayerItem->layer() != editTarget; + } + // if the new target is the session layer + if (editTarget == _sessionState->stage()->GetSessionLayer()) { + needToRebuild = true; + } + if (needToRebuild) { + rebuildModelOnIdle(); + return; + } + } + + // all other cases, just update the icon + for (int i = 0, count = root->rowCount(); i < count; i++) { + auto child = dynamic_cast(root->child(i)); + child->updateTargetLayerRecursive(editTarget); + } +} + +// notification from USD +void LayerTreeModel::usd_layerChanged(SdfNotice::LayersDidChangeSentPerLayer const& notice) +{ + // experienced crashes in python prototype For now, rebuild everything + if (!_blockUsdNotices) + rebuildModelOnIdle(); +} + +// notification from USD +void LayerTreeModel::usd_editTargetChanged(UsdNotice::StageEditTargetChanged const& notice) +{ + if (!_blockUsdNotices) { + QTimer::singleShot( + 0, dynamic_cast(this), [this]() { updateTargetLayer(InRebuildModel::No); }); + } +} + +// notification from USD +void LayerTreeModel::usd_layerDirtinessChanged( + SdfNotice::LayerDirtinessChanged const& notice, + const TfWeakPtr& layer) +{ + if (!_blockUsdNotices) { + auto layerItem = findUSDLayerItem(layer); + if (layerItem) { + layerItem->fetchData(RebuildChildren::No); + } + } +} + +// called from SessionState::currentStageChangedSignal +void LayerTreeModel::sessionStageChanged() { rebuildModel(); } + +// called from SessionState::autoHideSessionLayerSignal +void LayerTreeModel::autoHideSessionLayerChanged() { rebuildModelOnIdle(); } + +LayerTreeItem* LayerTreeModel::layerItemFromIndex(const QModelIndex& index) const +{ + return dynamic_cast(itemFromIndex(index)); +} + +void layerItemVectorRecurs( + LayerTreeItem* parent, + LayerTreeModel::ConditionFunc filter, + std::vector& result) +{ + if (filter(parent)) { + result.push_back(parent); + } + if (parent->rowCount() > 0) { + const auto& children = parent->childrenVector(); + for (auto const& child : children) + layerItemVectorRecurs(child, filter, result); + } +} + +LayerItemVector LayerTreeModel::getAllItems(ConditionFunc filter) const +{ + + LayerItemVector result; + auto root = invisibleRootItem(); + for (int i = 0, count = root->rowCount(); i < count; i++) { + auto child = dynamic_cast(root->child(i)); + layerItemVectorRecurs(child, filter, result); + } + return result; +} + +LayerItemVector LayerTreeModel::getAllNeedsSavingLayers() const +{ + auto filter = [](const LayerTreeItem* item) { return item->needsSaving(); }; + return getAllItems(filter); +} + +LayerItemVector LayerTreeModel::getAllAnonymousLayers() const +{ + auto filter + = [](const LayerTreeItem* item) { return item->isAnonymous() && !item->isSessionLayer(); }; + return getAllItems(filter); +} + +void LayerTreeModel::saveStage() +{ + const auto anonLayers = getLayerListAsQStringList(getAllAnonymousLayers()); + QString dialogTitle = StringResources::getAsQString(StringResources::kSaveStage); + QString message; + if (anonLayers.size()) { + if (anonLayers.size() == 1) + message = StringResources::getAsQString(StringResources::kToSaveTheStageSaveAnonym); + else { + MString msg; + MString size; + size = anonLayers.size(); + msg.format( + StringResources::getAsMString(StringResources::kToSaveTheStageSaveAnonyms), size); + message = MQtUtil::toQString(msg); + } + warningDialog(dialogTitle, message, &anonLayers); + } else { + QString buttonText; + const auto layers = getAllNeedsSavingLayers(); + const auto layersQStringList = getLayerListAsQStringList(layers); + if (layers.size()) { + if (layers.size() == 1) { + message + = StringResources::getAsQString(StringResources::kToSaveTheStageFileWillBeSave); + buttonText = StringResources::getAsQString(StringResources::kSave); + } else { + MString msg; + MString size; + size = layers.size(); + msg.format( + StringResources::getAsMString(StringResources::kToSaveTheStageFilesWillBeSave), + size); + message = MQtUtil::toQString(msg); + buttonText = StringResources::getAsQString(StringResources::kSaveAll); + } + + message += " "; + message += StringResources::getAsQString(StringResources::kNotUndoable); + + if (confirmDialog(dialogTitle, message, &layersQStringList, &buttonText)) { + for (auto layer : layers) { + layer->saveEdits(); + } + } + } + } +} + +std::string LayerTreeModel::findNameForNewAnonymousLayer() const +{ + const std::string prefix = "anonymousLayer"; + size_t prefix_len = prefix.size(); + int largest = _lastAskedAnonLayerNameSinceRebuild; + int suffix_int; + std::string name, suffix_string; + + // find the largest number we have + const auto allItems = getAllItems(); + for (const auto& item : allItems) { + name = item->displayName(); + if (name.compare(0, prefix_len, prefix) == 0) { + suffix_string = name.substr(prefix_len); + suffix_int = std::stoi(suffix_string); + if (suffix_int > largest) + largest = suffix_int; + } + } + + _lastAskedAnonLayerNameSinceRebuild = largest + 1; + return prefix + std::to_string(largest + 1); +} + +void LayerTreeModel::toggleMuteLayer(LayerTreeItem* item, bool* forcedState) +{ + if (item->isInvalidLayer() || !item->isSublayer()) + return; + + if (forcedState) { + if (*forcedState == item->isMuted()) + return; + } + + _sessionState->commandHook()->muteSubLayer(item->layer(), !item->isMuted()); +} + +} // namespace UsdLayerEditor diff --git a/lib/usd/ui/layerEditor/layerTreeModel.h b/lib/usd/ui/layerEditor/layerTreeModel.h new file mode 100644 index 0000000000..43e387a009 --- /dev/null +++ b/lib/usd/ui/layerEditor/layerTreeModel.h @@ -0,0 +1,141 @@ +// +// 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. +// + +#ifndef LAYERTREEMODEL_H +#define LAYERTREEMODEL_H + +#include "sessionState.h" + +#include +#include +#include +#include + +#include + +#include +#include + +namespace UsdLayerEditor { + +class LayerTreeItem; +class SessionState; + +typedef std::vector LayerItemVector; + +enum class InRebuildModel +{ + Yes, + No, +}; + +/** + * @brief Implements the Qt data model for the usd layer tree view + * + */ +class LayerTreeModel + : public QStandardItemModel + , public pxr::TfWeakBase +{ + Q_OBJECT +public: + LayerTreeModel(SessionState* in_sessionState, QObject* in_parent); + ~LayerTreeModel(); + SessionState* sessionState() { return _sessionState; } + + // API to suspend reacting to USD notifications + static void suspendUsdNotices(bool suspend); + + // get propertly typed item + LayerTreeItem* layerItemFromIndex(const QModelIndex& index) const; + // gets everything recursivly as an array : used to simplify iteration + typedef bool (*ConditionFunc)(const LayerTreeItem*); + LayerItemVector getAllItems(ConditionFunc filter = [](const LayerTreeItem*) { + return true; + }) const; + // get all the layers that need saving + LayerItemVector getAllNeedsSavingLayers() const; + // get all anonymous layers except the session layer + LayerItemVector getAllAnonymousLayers() const; + + // get an appropriate name for a new anonymous layer + std::string findNameForNewAnonymousLayer() const; + + // save stage UI + void saveStage(); + + // return the index of the root layer + QModelIndex rootLayerIndex(); + + // target button callbacks + void setEditTarget(LayerTreeItem* item); + + // mute layer management + void toggleMuteLayer(LayerTreeItem* item, bool* forcedState = nullptr); + + // ask to select a layer in the near future + void selectUsdLayerOnIdle(const pxr::SdfLayerRefPtr& usdLayer); + + // drag and drop support + Qt::ItemFlags flags(const QModelIndex& index) const override; + Qt::DropActions supportedDropActions() const override; + QStringList mimeTypes() const override; + QMimeData* mimeData(const QModelIndexList& indexes) const override; + bool dropMimeData( + const QMimeData* data, + Qt::DropAction action, + int row, + int column, + const QModelIndex& parent) override; + + // for debugging + void forceRefresh() { rebuildModelOnIdle(); } + +Q_SIGNALS: + void selectLayerSignal(const QModelIndex&); + +protected: + // slots + void sessionStageChanged(); + void autoHideSessionLayerChanged(); + + void setSessionState(SessionState* in_sessionState); + SessionState* _sessionState = nullptr; + + void registerUsdNotifications(bool in_register); + void usd_layerChanged(pxr::SdfNotice::LayersDidChangeSentPerLayer const& notice); + void usd_editTargetChanged(pxr::UsdNotice::StageEditTargetChanged const& notice); + void usd_layerDirtinessChanged( + pxr::SdfNotice::LayerDirtinessChanged const& notice, + const pxr::TfWeakPtr& layer); + + pxr::TfNotice::Keys _noticeKeys; + static bool _blockUsdNotices; + ; + + mutable int _lastAskedAnonLayerNameSinceRebuild = 0; + + void rebuildModelOnIdle(); + bool _rebuildOnIdlePending = false; + void rebuildModel(); + + void updateTargetLayer(InRebuildModel inRebuild); + + LayerTreeItem* findUSDLayerItem(const pxr::SdfLayerRefPtr& usdLayer) const; +}; + +} // namespace UsdLayerEditor +#endif // LAYERTREEMODEL_H diff --git a/lib/usd/ui/layerEditor/layerTreeView.cpp b/lib/usd/ui/layerEditor/layerTreeView.cpp new file mode 100644 index 0000000000..fe87e04967 --- /dev/null +++ b/lib/usd/ui/layerEditor/layerTreeView.cpp @@ -0,0 +1,385 @@ +// +// 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 "layerTreeView.h" + +#include "abstractCommandHook.h" +#include "layerTreeItem.h" +#include "layerTreeItemDelegate.h" +#include "layerTreeModel.h" +#include "stringResources.h" + +#include +#include + +using namespace UsdLayerEditor; + +struct CallMethodParams +{ + const LayerItemVector* selection; + QString name; + AbstractCommandHook* commandHook; +}; + +namespace { +QColor BLACK_BACKGROUND(43, 43, 43); + +typedef void (LayerTreeItem::*simpleLayerMethod)(); + +void doCallMethodOnSelection(const CallMethodParams& params, simpleLayerMethod method) +{ + if (params.selection->size() > 0) { + UndoContext context(params.commandHook, params.name); + + for (auto item : *params.selection) { + (item->*method)(); + } + } +} + +} // namespace + +namespace UsdLayerEditor { + +LayerTreeView::LayerTreeView(SessionState* in_sessionState, QWidget* in_parent) + : QTreeView(in_parent) +{ + _model = new LayerTreeModel(in_sessionState, this); + setModel(_model); + connect(_model, &LayerTreeModel::selectLayerSignal, this, &LayerTreeView::selectLayerRquest); + + // clang-format off + QString styleSheet = + "QTreeView { " + "background: "+ BLACK_BACKGROUND.name() + ";" + "show-decoration-selected: 0;" + "}"; + // clang-format on + setStyleSheet(styleSheet); + setStyle(&_treeViewStyle); + setHeaderHidden(true); + setUniformRowHeights(true); + setIndentation(16); + setEditTriggers(QAbstractItemView::NoEditTriggers); + setSelectionMode(QAbstractItemView::ExtendedSelection); + setMouseTracking(true); + setExpandsOnDoubleClick(false); + setDragEnabled(true); + setAcceptDrops(true); + setDropIndicatorShown(true); + setDragDropMode(QAbstractItemView::InternalMove); + + // custom row drawing + _delegate = new LayerTreeItemDelegate(this); + setItemDelegate(_delegate); + connect( + _model, &QAbstractItemModel::modelReset, _delegate, &LayerTreeItemDelegate::onModelReset); + + // context menu + setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu); + + // updates + connect(_model, &QAbstractItemModel::modelReset, this, &LayerTreeView::onModelReset); + + // signals + connect(this, &QAbstractItemView::doubleClicked, this, &LayerTreeView::onItemDoubleClicked); + + // renderSetuplike API + auto actionButtons = LayerTreeItem::actionButtonsDefinition(); + for (auto actionInfo : actionButtons) { + auto action = new QAction(actionInfo._name, this); + connect(action, &QAction::triggered, this, &LayerTreeView::onMuteLayerButtonPushed); + _actionButtons._staticActions.push_back(action); + } +} + +LayerTreeItem* LayerTreeView::layerItemFromIndex(const QModelIndex& index) const +{ + return _model->layerItemFromIndex(index); +} + +LayerTreeModel* LayerTreeView::layerTreeModel() const { return _model.data(); } + +void LayerTreeView::selectLayerRquest(const QModelIndex& index) +{ + // slot called when the user manually adds a sublayer with the UI + // we use this to select the new layer + setCurrentIndex(index); + scrollTo(index); +} + +void LayerTreeView::onItemDoubleClicked(const QModelIndex& index) +{ + if (index.isValid()) { + auto layerTreeItem = layerItemFromIndex(index); + if (layerTreeItem->isAnonymous()) { + layerTreeItem->saveEdits(); + } + } +} + +void LayerTreeView::onModelReset() { expandAll(); } + +LayerItemVector LayerTreeView::getSelectedLayerItems() const +{ + auto selection = selectionModel()->selectedRows(); + + LayerItemVector result; + result.reserve(selection.size()); + for (const auto& index : selection) { + result.push_back(layerItemFromIndex(index)); + } + + // with the context menu, + // you need to hold down ctrl/cmd and click a non selected item to get in this code + // you then have a current item that is not in the selection + auto clickedIndex = currentIndex(); + if (clickedIndex.isValid() && selection.indexOf(clickedIndex) == -1) { + result.push_back(currentLayerItem()); + } + return result; +} + +void LayerTreeView::onAddParentLayer(const QString& undoName) const +{ + auto selection = getSelectedLayerItems(); + + CallMethodParams params; + params.selection = &selection; + params.commandHook = _model->sessionState()->commandHook(); + params.name = undoName; + + // we add one new parent to each item in the selection + // for undo, it's ok to directly create the anon layer with the API + // because the mel command to add the path will hold to that anon layer if we undo + UndoContext context(params.commandHook, params.name); + for (auto item : *params.selection) { + auto oldParent = item->parentLayerItem()->layer(); + // create an anon layer as the new parent + auto anonLayer + = pxr::SdfLayer::CreateAnonymous(item->parentModel()->findNameForNewAnonymousLayer()); + // insert this selected item under it + anonLayer->InsertSubLayerPath(item->layer()->GetIdentifier()); + // replace this selected item in its parent with the anon layer + params.commandHook->replaceSubLayerPath( + oldParent, item->subLayerPath(), anonLayer->GetIdentifier()); + // if there is only one, a common case, select it + if (params.selection->size() == 1) + item->parentModel()->selectUsdLayerOnIdle(anonLayer); + } +} + +void LayerTreeView::onMuteLayer(const QString& undoName) const +{ + auto selection = getSelectedLayerItems(); + + CallMethodParams params; + params.selection = &selection; + params.commandHook = _model->sessionState()->commandHook(); + params.name = undoName; + + bool mute = !currentLayerItem()->isMuted(); + + UndoContext context(params.commandHook, params.name); + for (auto item : *params.selection) { + item->parentModel()->toggleMuteLayer(item, &mute); + } +} + +void LayerTreeView::callMethodOnSelection(const QString& undoName, simpleLayerMethod method) +{ + CallMethodParams params; + auto selection = getSelectedLayerItems(); + params.selection = &selection; + params.commandHook = _model->sessionState()->commandHook(); + params.name = undoName; + doCallMethodOnSelection(params, method); +} + +void LayerTreeView::paintEvent(QPaintEvent* event) +{ + PARENT_CLASS::paintEvent(event); + // Overrides the paint event to make it so that place holder text is displayed when the list is + // empty. + if (model()->rowCount() == 0) { + int NO_LAYERS_IMAGE_SIZE = DPIScale(62); + int HALF_FONT_HEIGHT = DPIScale(7); + auto NO_LAYERS_IMAGE = utils->createPixmap(":/RS_no_layer.png"); + QPen PLACEHOLDER_TEXT_PEN(QColor(128, 128, 128)); + + QPainter painter(viewport()); + int sz = NO_LAYERS_IMAGE_SIZE / 2; + auto pos = contentsRect().center() - QPoint(sz, sz + HALF_FONT_HEIGHT); + painter.drawPixmap(pos, NO_LAYERS_IMAGE); + + auto oldPen = painter.pen(); + painter.setPen(PLACEHOLDER_TEXT_PEN); + auto textRect = contentsRect(); + textRect.translate(0, sz); + painter.drawText( + textRect, Qt::AlignCenter, StringResources::getAsQString(StringResources::kNoLayers)); + painter.setPen(oldPen); + } +} + +bool LayerTreeView::event(QEvent* event) +{ + // override for dynamic tooltips + if (event->type() == QEvent::ToolTip) { + handleTooltips(dynamic_cast(event)); + return true; + } else { + return PARENT_CLASS::event(event); + } +} + +void LayerTreeView::handleTooltips(QHelpEvent* event) +{ + auto index = indexAt(event->pos()); + if (index.isValid()) { + auto itemRect = visualRect(index); + auto layerTreeItem = _model->layerItemFromIndex(index); + itemRect = _delegate->getAdjustedItemRect(layerTreeItem, itemRect); + auto targetRect = _delegate->getTargetIconRect(itemRect); + auto textRect = _delegate->getTextRect(itemRect); + if (targetRect.contains(event->pos())) { + QString tip + = StringResources::getAsQString(StringResources::kSetLayerAsTargetLayerTooltip); + QToolTip::showText(event->globalPos(), tip); + return; + } else if (textRect.contains(event->pos())) { + QString tip; + if (layerTreeItem->isInvalidLayer()) { + tip = StringResources::getAsQString(StringResources::kPathNotFound) + + layerTreeItem->subLayerPath().c_str(); + } else { + tip = layerTreeItem->layer()->GetRealPath().c_str(); + } + QToolTip::showText(event->globalPos(), tip); + return; + } + } + QToolTip::hideText(); + event->ignore(); +} + +void LayerTreeView::mousePressEvent(QMouseEvent* event) +{ + if (event->button() == Qt::LeftButton) { + auto index = indexAt(event->pos()); + // get the action button under the mouse if there is one + auto action = getCurrentAction(event->modifiers(), event->pos(), index); + _actionButtons._mouseReleaseAction = action; + if (action) { + _actionButtons._actionButtonPressed = true; + event->accept(); + return; + } + } + PARENT_CLASS::mousePressEvent(event); +} + +// support for renderSetup-like action button API +void LayerTreeView::mouseMoveEvent(QMouseEvent* event) +{ + + // dirty the tree view so it will repaint when mouse is over it + // this is needed to change the icons when hovered over them + _delegate->clearLastHitAction(); + auto region = childrenRegion(); + setDirtyRegion(region); + // don't trigger D&D if button pressed + if (!_actionButtons._actionButtonPressed && !_delegate->isTargetPressed()) { + PARENT_CLASS::mouseMoveEvent(event); + } +} + +void LayerTreeView::mouseReleaseEvent(QMouseEvent* event) +{ + if (event->button() == Qt::LeftButton && _actionButtons._mouseReleaseAction) { + auto index = indexAt(event->pos()); + auto action = getCurrentAction(event->modifiers(), event->pos(), index); + _actionButtons._actionButtonPressed = false; + if (action == _actionButtons._mouseReleaseAction) { + // set the currently clicked on element active without selecting it + selectionModel()->setCurrentIndex(index, QItemSelectionModel::NoUpdate); + // trigger the action to be executed + action->trigger(); + event->accept(); + _actionButtons._mouseReleaseAction = nullptr; + return; + } + } + + PARENT_CLASS::mouseReleaseEvent(event); + _delegate->clearPressedTarget(); +} + +void LayerTreeView::keyPressEvent(QKeyEvent* event) +{ + if (event->type() == QEvent::KeyPress) { + if (event->key() == Qt::Key_Delete) { + CallMethodParams params; + auto selection = getSelectedLayerItems(); + params.selection = &selection; + params.commandHook = _model->sessionState()->commandHook(); + params.name = "Remove"; + doCallMethodOnSelection(params, &LayerTreeItem::removeSubLayer); + return; + } else if (event->key() == Qt::Key_R) { + _model->forceRefresh(); + return; + } + } + return PARENT_CLASS::keyPressEvent(event); +} + +// support for renderSetup-like action button API +QAction* LayerTreeView::getCurrentAction( + QFlags modifiers, + QPoint pos, + QModelIndex index) const +{ + auto item = _model->itemFromIndex(index); + if (item) { + auto actionName = _delegate->lastHitAction(); + if (!actionName.isEmpty()) { + for (auto a : _actionButtons._staticActions) { + if (a->text() == actionName) + return a; + } + } + } + return nullptr; +} + +void LayerTreeView::leaveEvent(QEvent* event) +{ + // + _delegate->clearLastHitAction(); +} + +void LayerTreeView::onMuteLayerButtonPushed() +{ + auto item = currentLayerItem(); + if (item) { + item->parentModel()->toggleMuteLayer(item); + } + // need to force redraw of everything otherwise redraw isn't right + update(); +} + +} // namespace UsdLayerEditor diff --git a/lib/usd/ui/layerEditor/layerTreeView.h b/lib/usd/ui/layerEditor/layerTreeView.h new file mode 100644 index 0000000000..a506e08a12 --- /dev/null +++ b/lib/usd/ui/layerEditor/layerTreeView.h @@ -0,0 +1,113 @@ +// +// 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. +// + +#ifndef LAYERTREEVIEW_H +#define LAYERTREEVIEW_H + +#include "layerTreeViewStyle.h" +#include "sessionState.h" + +#include +#include + +class QHelpEvent; + +struct CallMethodParams; +namespace UsdLayerEditor { + +class LayerTreeItem; +class LayerTreeItemDelegate; +class LayerTreeModel; +class SessionState; + +typedef std::vector LayerItemVector; +typedef void (LayerTreeItem::*simpleLayerMethod)(); + +/** + * @brief Implements the Qt TreeView for USD layers. This widget is owned by the LayerEditorWidget. + * + */ +class LayerTreeView : public QTreeView +{ + Q_OBJECT +public: + typedef QTreeView PARENT_CLASS; + LayerTreeView(SessionState* in_sessionState, QWidget* in_parent); + + // get properly typed item + LayerTreeItem* layerItemFromIndex(const QModelIndex& index) const; + + // QTreeWidget-like method that returns the current item when one is selected + LayerTreeItem* currentLayerItem() const + { + auto index = currentIndex(); + return index.isValid() ? layerItemFromIndex(index) : nullptr; + } + + // returns array of selected item, including the current item + LayerItemVector getSelectedLayerItems() const; + + // return property typed model + LayerTreeModel* layerTreeModel() const; + + // calls a given method on all items in the selection, with the given string as the undo chunk + // name + void callMethodOnSelection(const QString& undoName, simpleLayerMethod method); + + // menu callbacks + void onAddParentLayer(const QString& undoName) const; + void onMuteLayer(const QString& undoName) const; + + // QWidgets overrides + virtual void paintEvent(QPaintEvent* event) override; + virtual bool event(QEvent* event) override; + virtual void keyPressEvent(QKeyEvent* event) override; + virtual void mousePressEvent(QMouseEvent* event) override; + virtual void mouseMoveEvent(QMouseEvent* event) override; + virtual void mouseReleaseEvent(QMouseEvent* event) override; + virtual void leaveEvent(QEvent* event) override; + +protected: + // slot: + void onModelReset(); + void onItemDoubleClicked(const QModelIndex& index); + void onMuteLayerButtonPushed(); + + // delayed signal to select a layer on idle + void selectLayerRquest(const QModelIndex& index); + + LayerTreeViewStyle _treeViewStyle; + QPointer _model; + LayerTreeItemDelegate* _delegate; + + void handleTooltips(QHelpEvent* event); + + // the mute button area has a different implementation than + // the target button. It is based on Maya's renderSetup + struct ActionButtons + { + std::vector _staticActions; + QAction* _mouseReleaseAction = nullptr; + bool _actionButtonPressed = false; + } _actionButtons; + // returns the action button the mouse was last over + QAction* + getCurrentAction(QFlags modifiers, QPoint pos, QModelIndex index) const; +}; + +} // namespace UsdLayerEditor + +#endif // LAYERTREEVIEW_H diff --git a/lib/usd/ui/layerEditor/layerTreeViewStyle.h b/lib/usd/ui/layerEditor/layerTreeViewStyle.h new file mode 100644 index 0000000000..5962d4beb7 --- /dev/null +++ b/lib/usd/ui/layerEditor/layerTreeViewStyle.h @@ -0,0 +1,202 @@ +// +// 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. +// + +#ifndef LAYERTREEVIEWSTYLE_H +#define LAYERTREEVIEWSTYLE_H + +#include "qtUtils.h" + +#include +#include +#include +#include +#include +#include + +namespace UsdLayerEditor { + +/** + * @brief overrides drawing of the treeview, mostly for the drag and drop indicator + * + */ +class LayerTreeViewStyle : public QCommonStyle +{ +public: + LayerTreeViewStyle() { _appStyle = QApplication::style(); } + +protected: + QPointer _appStyle; + + QColor DROP_INDICATOR_COLOR = QColor(255, 255, 255); + int DROP_INDICATOR_WIDTH = DPIScale(3); + int ARROW_AREA_WIDTH = DPIScale(24); + + // overrides + void drawComplexControl( + ComplexControl cc, + const QStyleOptionComplex* opt, + QPainter* p, + const QWidget* w = Q_NULLPTR) const override + { + _appStyle->drawComplexControl(cc, opt, p, w); + } + + void drawControl( + ControlElement element, + const QStyleOption* opt, + QPainter* p, + const QWidget* w = Q_NULLPTR) const override + { + _appStyle->drawControl(element, opt, p, w); + } + + void drawItemPixmap(QPainter* painter, const QRect& rect, int alignment, const QPixmap& pixmap) + const override + { + _appStyle->drawItemPixmap(painter, rect, alignment, pixmap); + } + + void drawItemText( + QPainter* painter, + const QRect& rect, + int flags, + const QPalette& pal, + bool enabled, + const QString& text, + QPalette::ColorRole textRole = QPalette::NoRole) const override + { + _appStyle->drawItemText(painter, rect, flags, pal, enabled, text, textRole); + } + + void drawPrimitive( + PrimitiveElement element, + const QStyleOption* option, + QPainter* painter, + const QWidget* widget) const override + { + // Changes the way the drop indicator is drawn + if (element == QStyle::PE_IndicatorItemViewItemDrop && !option->rect.isNull()) { + painter->save(); + painter->setRenderHint(QPainter::Antialiasing, true); + auto oldPen = painter->pen(); + painter->setPen(QPen(DROP_INDICATOR_COLOR, DROP_INDICATOR_WIDTH)); + auto rect = option->rect; + rect.setLeft(DROP_INDICATOR_WIDTH); + rect.setRight(widget->width() - DROP_INDICATOR_WIDTH * 2); + if (option->rect.height() == 0) { + painter->drawLine(rect.topLeft(), option->rect.topRight()); + } else { + painter->drawRect(rect); + } + painter->setPen(oldPen); + painter->restore(); + } + } + + QPixmap generatedIconPixmap( + QIcon::Mode iconMode, + const QPixmap& pixmap, + const QStyleOption* opt) const override + { + return _appStyle->generatedIconPixmap(iconMode, pixmap, opt); + } + + SubControl hitTestComplexControl( + ComplexControl cc, + const QStyleOptionComplex* opt, + const QPoint& pt, + const QWidget* w = Q_NULLPTR) const override + { + return _appStyle->hitTestComplexControl(cc, opt, pt, w); + } + + QRect itemPixmapRect(const QRect& r, int flags, const QPixmap& pixmap) const override + { + return _appStyle->itemPixmapRect(r, flags, pixmap); + } + + QRect itemTextRect( + const QFontMetrics& fm, + const QRect& r, + int flags, + bool enabled, + const QString& text) const override + { + return _appStyle->itemTextRect(fm, r, flags, enabled, text); + } + + int pixelMetric( + PixelMetric m, + const QStyleOption* opt = Q_NULLPTR, + const QWidget* widget = Q_NULLPTR) const override + { + return _appStyle->pixelMetric(m, opt, widget); + } + + void polish(QPalette& pal) override { _appStyle->polish(pal); } + void polish(QApplication* app) override { _appStyle->polish(app); } + void polish(QWidget* widget) override { _appStyle->polish(widget); } + void unpolish(QWidget* widget) override { _appStyle->unpolish(widget); } + void unpolish(QApplication* application) override { _appStyle->unpolish(application); } + + int + styleHint(StyleHint hint, const QStyleOption* opt, const QWidget* w, QStyleHintReturn* shret) + const override + { + if (hint == QStyle::SH_Slider_AbsoluteSetButtons) { + return Qt::LeftButton | Qt::MidButton | Qt::RightButton; + } else if (hint == QStyle::SH_ItemView_ShowDecorationSelected) { + return 0; + } else { + return _appStyle->styleHint(hint, opt, w, shret); + } + } + + QRect subControlRect( + ComplexControl cc, + const QStyleOptionComplex* opt, + SubControl sc, + const QWidget* w = Q_NULLPTR) const override + { + return _appStyle->subControlRect(cc, opt, sc, w); + } + + QRect subElementRect( + SubElement element, + const QStyleOption* option, + const QWidget* widget = Q_NULLPTR) const override + { + if (element == QStyle::SE_TreeViewDisclosureItem) { + auto rect = option->rect; + rect.setRight(rect.left() + ARROW_AREA_WIDTH); + return rect; + } else { + return _appStyle->subElementRect(element, option, widget); + } + } + QSize sizeFromContents( + ContentsType ct, + const QStyleOption* opt, + const QSize& contentsSize, + const QWidget* widget = Q_NULLPTR) const override + { + return _appStyle->sizeFromContents(ct, opt, contentsSize, widget); + } +}; + +} // namespace UsdLayerEditor + +#endif // LAYERTREEVIEWSTYLE_H diff --git a/lib/usd/ui/layerEditor/loadLayersDialog.cpp b/lib/usd/ui/layerEditor/loadLayersDialog.cpp new file mode 100644 index 0000000000..a085f05c7c --- /dev/null +++ b/lib/usd/ui/layerEditor/loadLayersDialog.cpp @@ -0,0 +1,372 @@ +// +// 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 "loadLayersDialog.h" + +#include "generatedIconButton.h" +#include "layerTreeItem.h" +#include "layerTreeModel.h" +#include "layerTreeView.h" +#include "pathChecker.h" +#include "qtUtils.h" +#include "stringResources.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace { +using namespace UsdLayerEditor; + +bool isAbsolutePath(const std::string& in_path) +{ + QFileInfo fileInfo(QString::fromStdString(in_path)); + return fileInfo.isAbsolute(); +} + +class MyLineEdit : public QLineEdit +{ +public: + MyLineEdit(QWidget* in_parent) + : QLineEdit(in_parent) + { + } + QSize sizeHint() const override + { + auto hint = QLineEdit::sizeHint(); + hint.setWidth(DPIScale(300)); + return hint; + } +}; + +} // namespace + +namespace UsdLayerEditor { + +LayerPathRow::LayerPathRow(LoadLayersDialog* in_parent) + : QWidget(in_parent) + , _parent(in_parent) +{ + auto gridLayout = new QGridLayout(); + QtUtils::initLayoutMargins(gridLayout); + _absolutePath = ""; + _parentPath = _parent->findDirectoryToUse(""); + + _label = new QLabel(StringResources::getAsQString(StringResources::kLayerPath)); + gridLayout->addWidget(_label, 0, 0); + + _pathEdit = new MyLineEdit(this); + connect(_pathEdit, &QLineEdit::textChanged, this, &LayerPathRow::onTextChanged); + gridLayout->addWidget(_pathEdit, 0, 1); + + QIcon icon; + + icon = utils->createIcon(":/fileOpen.png"); + _openBrowser = new GeneratedIconButton(this, icon); + gridLayout->addWidget(_openBrowser, 0, 2); + connect(_openBrowser, &QAbstractButton::clicked, in_parent, &LoadLayersDialog::onOpenBrowser); + + icon = utils->createIcon(":/trash.png"); + _trashIcon = new GeneratedIconButton(this, icon); + connect(_trashIcon, &QAbstractButton::clicked, in_parent, &LoadLayersDialog::onDeleteRow); + _trashIcon->setToolTip(StringResources::getAsQString(StringResources::kRemoveSublayer)); + gridLayout->addWidget(_trashIcon, 0, 3); + + icon = utils->createIcon(":/addCreateGeneric.png"); + _addPathIcon = new GeneratedIconButton(this, icon); + _addPathIcon->setVisible(false); + _addPathIcon->setToolTip(StringResources::getAsQString(StringResources::kAddSublayer)); + connect(_addPathIcon, &QAbstractButton::clicked, in_parent, &LoadLayersDialog::onAddRow); + gridLayout->addWidget(_addPathIcon, 0, 3); + + _convertToRel + = new QCheckBox(StringResources::getAsQString(StringResources::kConvertToRelativePath)); + connect(_convertToRel, &QAbstractButton::clicked, this, &LayerPathRow::onRelativeButtonChecked); + _convertToRel->setEnabled(false); + gridLayout->addWidget(_convertToRel, 1, 1); + + setLayout(gridLayout); + + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum); +} + +void LayerPathRow::onTextChanged(const QString& text) +{ + if (!_convertToRel->isChecked()) { + _absolutePath = text.toStdString(); + _convertToRel->setEnabled(isAbsolutePath(_absolutePath)); + } +} + +void LayerPathRow::onRelativeButtonChecked(bool checked) +{ + if (checked) { + QDir dir(_parentPath.c_str()); + + QString relativePath = dir.relativeFilePath(_absolutePath.c_str()); + _pathEdit->setText(relativePath); + _pathEdit->setEnabled(false); + } else { + _pathEdit->setEnabled(true); + _pathEdit->setText(_absolutePath.c_str()); + } +} + +std::string LayerPathRow::pathToUse() const +{ + if (_convertToRel->isChecked()) + return _pathEdit->text().toStdString(); + else + return _absolutePath; +} + +void LayerPathRow::setAbsolutePath(const std::string& path) +{ + + _absolutePath = path; + _pathEdit->setText(path.c_str()); + _pathEdit->setEnabled(true); + + _convertToRel->setChecked(false); + _convertToRel->setEnabled(isAbsolutePath(_absolutePath)); +} + +void LayerPathRow::setAsRowInserter(bool setIt) +{ + bool enabled = !setIt; + _label->setEnabled(enabled); + _pathEdit->setEnabled(enabled); + _openBrowser->setEnabled(enabled); + _convertToRel->setEnabled(enabled); + + _trashIcon->setVisible(!setIt); + _addPathIcon->setVisible(setIt); +} + +LoadLayersDialog::LoadLayersDialog(LayerTreeItem* in_treeItem, QWidget* in_parent) + : QDialog(in_parent) + , _treeItem(in_treeItem) +{ + MString title; + title.format( + StringResources::getAsMString(StringResources::kLoadSublayersTo), + MQtUtil::toMString(_treeItem->text())); + setWindowTitle(MQtUtil::toQString(title)); + auto rowsLayout = new QVBoxLayout(); + int margin = DPIScale(5) + DPIScale(20); + rowsLayout->setContentsMargins(margin, margin, margin, 0); + rowsLayout->setSpacing(DPIScale(8)); + + auto firstRow = new LayerPathRow(this); + rowsLayout->addWidget(firstRow); + auto inserter = new LayerPathRow(this); + inserter->setAsRowInserter(true); + rowsLayout->addWidget(inserter); + + _rowsLayout = rowsLayout; + + // Ok/Cancel button area + auto buttonsLayout = new QHBoxLayout(); + QtUtils::initLayoutMargins(buttonsLayout, DPIScale(20)); + buttonsLayout->addStretch(); + auto okButton + = new QPushButton(StringResources::getAsQString(StringResources::kLoadSublayers), this); + connect(okButton, &QPushButton::clicked, this, &LoadLayersDialog::onOk); + okButton->setDefault(true); + auto cancelButton + = new QPushButton(StringResources::getAsQString(StringResources::kCancel), this); + connect(cancelButton, &QPushButton::clicked, this, &LoadLayersDialog::onCancel); + buttonsLayout->addWidget(okButton); + buttonsLayout->addWidget(cancelButton); + + auto mainVLayout = new QVBoxLayout(); + QtUtils::initLayoutMargins(mainVLayout); + mainVLayout->setAlignment(Qt::AlignTop); + mainVLayout->addLayout(rowsLayout); + + // setup a scroll area + auto dialogContentParent = new QWidget(); + dialogContentParent->setLayout(mainVLayout); + + auto scrollArea = new QScrollArea(); + scrollArea->setFrameShape(QFrame::NoFrame); + scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scrollArea->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + scrollArea->setWidget(dialogContentParent); + scrollArea->setWidgetResizable(true); + _scrollArea = scrollArea; + + // add that scroll area as our single child + auto topLayout = new QVBoxLayout(); + QtUtils::initLayoutMargins(topLayout); + topLayout->setSpacing(0); + topLayout->addWidget(scrollArea); + + // then add the buttons + auto buttonArea = new QWidget(this); + buttonArea->setLayout(buttonsLayout); + buttonArea->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + topLayout->addWidget(buttonArea); + + setLayout(topLayout); + setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); +} + +void LoadLayersDialog::onOk() +{ + + for (int i = 0, count = _rowsLayout->count(); i < count; ++i) { + auto row = dynamic_cast(_rowsLayout->itemAt(i)->widget()); + if (!row->pathToUse().empty()) { + if (!checkIfPathIsSafeToAdd( + StringResources::getAsQString(StringResources::kLoadSublayersError), + _treeItem, + row->pathToUse())) { + return; // abort the OK + } + _pathToLoad.push_back(row->pathToUse()); + } + } + accept(); +} + +void LoadLayersDialog::onCancel() { reject(); } + +LayerPathRow* LoadLayersDialog::getLastRow() const +{ + return dynamic_cast(_rowsLayout->itemAt(_rowsLayout->count() - 1)->widget()); +} + +void LoadLayersDialog::appendInserterRow() +{ + auto inserter = new LayerPathRow(this); + inserter->setAsRowInserter(true); + _rowsLayout->addWidget(inserter); + adjustScrollArea(); + scrollToEnd(); +} + +void LoadLayersDialog::scrollToEnd() +{ + QTimer::singleShot(0, this, [this]() { _scrollArea->ensureWidgetVisible(getLastRow()); }); +} + +void LoadLayersDialog::adjustScrollArea() +{ + auto lastRow = getLastRow(); + auto rowCount = _rowsLayout->count(); + auto maxRows = 8; + if (rowCount > maxRows) + rowCount = maxRows; + + auto height = lastRow->sizeHint().height(); + auto spacing = _rowsLayout->spacing(); + height = height * rowCount + (rowCount - 1) * spacing; + + auto margins = _rowsLayout->contentsMargins(); + height = height + margins.top() + margins.bottom(); + + _scrollArea->setMinimumHeight(height); +} + +void LoadLayersDialog::onAddRow() +{ + auto lastRow = getLastRow(); + lastRow->setAsRowInserter(false); + appendInserterRow(); +} + +void LoadLayersDialog::onDeleteRow() +{ + auto row = dynamic_cast(sender()->parent()); + _rowsLayout->removeWidget(row); + row->deleteLater(); + adjustScrollArea(); +} + +std::string LoadLayersDialog::findDirectoryToUse(const std::string& rowText) const +{ + auto path = rowText; + if (path.empty()) { + auto item = _treeItem; + while (item != nullptr) { + if (!item->isAnonymous()) { + path = item->layer()->GetRealPath(); + break; + } + item = item->parentLayerItem(); + } + } + if (path.empty()) { + path = _treeItem->parentModel()->sessionState()->defaultLoadPath(); + } + + if (!path.empty()) { + QFileInfo fileInfo(QString::fromStdString(path)); + path = fileInfo.path().toStdString(); + } + return path; +} + +void LoadLayersDialog::onOpenBrowser() +{ + auto row = dynamic_cast(sender()->parent()); + auto defaultPath = findDirectoryToUse(row->absolutePath()); + + auto files = _treeItem->parentModel()->sessionState()->loadLayersUI(windowTitle(), defaultPath); + if (files.size() == 0) + return; + + row->setAbsolutePath(files[0]); + + // insert new rows if more than one file selected + if (files.size() > 1) { + // find where to insert new rows + int index = 0; + int rowCount = _rowsLayout->count(); + for (int i = 0; i < rowCount; i++) { + auto thisRow = dynamic_cast(_rowsLayout->itemAt(i)->widget()); + if (thisRow == row) { + index = i + 1; + break; + } + } + assert(index != 0); + + // note: starts at 1 + for (size_t i = 1; i < files.size(); i++) { + auto newRow = new LayerPathRow(this); + _rowsLayout->insertWidget(index, newRow); + newRow->setAbsolutePath(files[i]); + ++index; + } + + adjustScrollArea(); + scrollToEnd(); + } +} + +} // namespace UsdLayerEditor diff --git a/lib/usd/ui/layerEditor/loadLayersDialog.h b/lib/usd/ui/layerEditor/loadLayersDialog.h new file mode 100644 index 0000000000..122081d067 --- /dev/null +++ b/lib/usd/ui/layerEditor/loadLayersDialog.h @@ -0,0 +1,106 @@ +// +// 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. +// + +#ifndef LOADLAYERSDIALOG_H +#define LOADLAYERSDIALOG_H + +#include +#include + +#include +#include + +class QCheckBox; +class QLabel; +class QLineEdit; +class QScrollArea; +class QVBoxLayout; + +namespace UsdLayerEditor { + +class LayerTreeItem; +class LayerTreeView; +class LayerPathRow; + +/** + * @brief Dialog to load multiple USD sublayers at once onto a parent layer. + * + */ +class LoadLayersDialog : public QDialog +{ +public: + typedef std::list PathList; + + LoadLayersDialog(LayerTreeItem* in_treeItem, QWidget* in_parent); + const PathList& pathsToLoad() { return _pathToLoad; } + + std::string findDirectoryToUse(const std::string& rowText) const; + +protected: + // slots + void onOk(); + void onCancel(); + +public: + // slots connected by LayerPathRow class + void onOpenBrowser(); + void onDeleteRow(); + void onAddRow(); + +protected: + PathList _pathToLoad; + LayerTreeItem* _treeItem; + QVBoxLayout* _rowsLayout; + QScrollArea* _scrollArea; + + LayerPathRow* getLastRow() const; + void appendInserterRow(); + void adjustScrollArea(); + void scrollToEnd(); +}; + +class LayerPathRow : public QWidget +{ +public: + LayerPathRow(LoadLayersDialog* in_parent); + void setAsRowInserter(bool setIt); + + // returns the path to store, either the rel or abs path + std::string pathToUse() const; + // returns the absolute path, always + std::string absolutePath() const { return _absolutePath; } + // sets the absolute path + void setAbsolutePath(const std::string& path); + +protected: + void onTextChanged(const QString& text); + void onRelativeButtonChecked(bool checked); + +protected: + std::string _absolutePath; + std::string _parentPath; + LoadLayersDialog* _parent; + QLabel* _label; + QLineEdit* _pathEdit; + QAbstractButton* _openBrowser; + QAbstractButton* _trashIcon; + QAbstractButton* _addPathIcon; + QCheckBox* _convertToRel; +}; + +}; // namespace UsdLayerEditor + +#endif // LOADLAYERSDIALOG_H diff --git a/lib/usd/ui/layerEditor/mayaCommandHook.cpp b/lib/usd/ui/layerEditor/mayaCommandHook.cpp new file mode 100644 index 0000000000..c251c57573 --- /dev/null +++ b/lib/usd/ui/layerEditor/mayaCommandHook.cpp @@ -0,0 +1,207 @@ +// +// 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 "mayaCommandHook.h" + +#include "abstractCommandHook.h" +#include "mayaSessionState.h" + +#include +#include +#include + +#include +#include + +#include + +#include +#include + +#define STR(x) std::string(x) + +namespace { +std::string quote(const std::string& string) { return STR(" \"") + string + STR("\""); } + +MString executeMel(const std::string& commandString) +{ + // executes maya command with display and undo set to true so that it logs + MStringArray result; + MGlobal::executeCommand( + MString(commandString.c_str()), + result, + /*display*/ true, + /*undo*/ true); + if (result.length() > 0) + return result[0]; + else + return ""; +} + +// maya doesn't support spaces in undo chunk names... +MString cleanChunkName(QString name) { return quote(name.replace(" ", "_").toStdString()).c_str(); } + +} // namespace +namespace UsdLayerEditor { +std::string MayaCommandHook::proxyShapePath() +{ + return dynamic_cast(_sessionState)->proxyShapePath(); +} + +void MayaCommandHook::setEditTarget(UsdLayer usdLayer) +{ + std::string cmd; + cmd = STR("mayaUsdEditTarget -edit -editTarget ") + quote(usdLayer->GetIdentifier()); + cmd += " " + quote(proxyShapePath()); + executeMel(cmd); +} + +// starts a complex undo operation in the host app. Please use UndoContext class to safely +// open/close +void MayaCommandHook::openUndoBracket(const QString& name) +{ + MGlobal::executeCommand( + MString("undoInfo -openChunk -chunkName ") + cleanChunkName(name), false, false); +} + +// closes a complex undo operation in the host app. Please use UndoContext class to safely +// open/close +void MayaCommandHook::closeUndoBracket() +{ + MGlobal::executeCommand("undoInfo -closeChunk", false, false); +} + +// insert a sub layer path at a given index +void MayaCommandHook::insertSubLayerPath(UsdLayer usdLayer, Path path, int index) +{ + std::string cmd; + cmd = "mayaUsdLayerEditor -edit -insertSubPath "; + cmd += std::to_string(index); + cmd += quote(path); + cmd += quote(usdLayer->GetIdentifier()); + executeMel(cmd); +} + +// remove a sub layer by path +void MayaCommandHook::removeSubLayerPath(UsdLayer usdLayer, Path path) +{ + size_t index = usdLayer->GetSubLayerPaths().Find(path); + assert(index != -1); + std::string cmd; + cmd = "mayaUsdLayerEditor -edit -removeSubPath "; + cmd += std::to_string(index); + cmd += quote(usdLayer->GetIdentifier()); + executeMel(cmd); +} + +// replaces a path in the layer stack +void MayaCommandHook::replaceSubLayerPath(UsdLayer usdLayer, Path oldPath, Path newPath) +{ + std::string cmd; + cmd = "mayaUsdLayerEditor -edit -replaceSubPath "; + cmd += quote(oldPath); + cmd += quote(newPath); + cmd += quote(usdLayer->GetIdentifier()); + executeMel(cmd); +} + +// discard edit on a layer +void MayaCommandHook::discardEdits(UsdLayer usdLayer) +{ + std::string cmd; + cmd = "mayaUsdLayerEditor -edit -discardEdits "; + cmd += quote(usdLayer->GetIdentifier()); + executeMel(cmd); +} + +// erases everything on a layer +void MayaCommandHook::clearLayer(UsdLayer usdLayer) +{ + std::string cmd; + cmd = "mayaUsdLayerEditor -edit -clear "; + cmd += quote(usdLayer->GetIdentifier()); + executeMel(cmd); +} + +// add an anon layer at the top of the stack, returns it +UsdLayer MayaCommandHook::addAnonymousSubLayer(UsdLayer usdLayer, std::string newName) +{ + std::string cmd; + cmd = "mayaUsdLayerEditor -edit -addAnonymous "; + cmd += quote(newName); + cmd += quote(usdLayer->GetIdentifier()); + std::string result = executeMel(cmd).asChar(); + return pxr::SdfLayer::FindOrOpen(result); +} + +// mute or unmute the given layer +void MayaCommandHook::muteSubLayer(UsdLayer usdLayer, bool muteIt) +{ + std::string cmd; + cmd = "mayaUsdLayerEditor -edit -muteLayer "; + cmd += muteIt ? "1" : "0"; + cmd += quote(proxyShapePath()); + cmd += quote(usdLayer->GetIdentifier()); + executeMel(cmd).asChar(); +} + +// Help menu callback +void MayaCommandHook::showLayerEditorHelp() { executeMel("showHelp UsdLayerEditor"); } + +// this method is used to select the prims with spec in a layer +void MayaCommandHook::selectPrimsWithSpec(UsdLayer usdLayer) +{ + std::string script; + script = "proxyShapePath = '" + proxyShapePath() + "'\n"; + script += "primPathList = ["; + + QStringList array; + for (auto prim : _sessionState->stage()->Traverse()) { + auto primSpec = usdLayer->GetPrimAtPath(prim.GetPath()); + if (primSpec) { + array.append(prim.GetPath().GetString().c_str()); + } + } + if (array.size() == 0) + return; + script += ("'"); + script += array.join("','").toStdString(); + script += ("']\n"); + + script += R"PYTHON( +# Ufe +import ufe +try: + from maya.internal.ufeSupport import ufeSelectCmd +except ImportError: + # Maya 2019 and 2020 don't have ufeSupport plugin, so use fallback. + from ufeScripts import ufeSelectCmd + +# create a selection list +sn = ufe.Selection() + +for primPath in primPathList: + ufePath = ufe.PathString.path(proxyShapePath + ',' + primPath) + ufeSceneItem = ufe.Hierarchy.createItem(ufePath) + sn.append(ufeSceneItem) + +ufeSelectCmd.replaceWith(sn) +)PYTHON"; + + MGlobal::executePythonCommand(script.c_str(), true, false); +} + +} // namespace UsdLayerEditor diff --git a/lib/usd/ui/layerEditor/mayaCommandHook.h b/lib/usd/ui/layerEditor/mayaCommandHook.h new file mode 100644 index 0000000000..0e97a2d066 --- /dev/null +++ b/lib/usd/ui/layerEditor/mayaCommandHook.h @@ -0,0 +1,78 @@ +// +// 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. +// + +#ifndef MAYACOMMANDHOOK_H +#define MAYACOMMANDHOOK_H + +#include "abstractCommandHook.h" + +namespace UsdLayerEditor { + +/** + * @brief "hook" all the commands of the layer editor to execute them with mel commands + * + */ +class MayaCommandHook : public AbstractCommandHook +{ +public: + MayaCommandHook(SessionState* in_sessionState) + : AbstractCommandHook(in_sessionState) + { + } + + void setEditTarget(UsdLayer usdLayer) override; + + // insert a sub layer path at a given index + void insertSubLayerPath(UsdLayer usdLayer, Path path, int index) override; + + // remove a sub layer by path + void removeSubLayerPath(UsdLayer usdLayer, Path path) override; + + // replaces a path in the layer stack + void replaceSubLayerPath(UsdLayer usdLayer, Path oldPath, Path newPath) override; + + // discard edit on a layer + void discardEdits(UsdLayer usdLayer) override; + + // erases everything on a layer + void clearLayer(UsdLayer usdLayer) override; + + // add an anon layer at the top of the stack, returns it + UsdLayer addAnonymousSubLayer(UsdLayer usdLayer, std::string newName) override; + + // mute or unmute the given layer + void muteSubLayer(UsdLayer usdLayer, bool muteIt) override; + + // starts a complex undo operation in the host app. Please use UndoContext class to safely + // open/close + void openUndoBracket(const QString& name) override; + // closes a complex undo operation in the host app. Please use UndoContext class to safely + // open/close + void closeUndoBracket() override; + + // Help menu callback + void showLayerEditorHelp() override; + + // this method is used to select the prims with spec in a layer + void selectPrimsWithSpec(UsdLayer usdLayer) override; + +protected: + std::string proxyShapePath(); +}; + +} // namespace UsdLayerEditor + +#endif // MAYACOMMANDHOOK_H diff --git a/lib/usd/ui/layerEditor/mayaLayerEditorWindow.cpp b/lib/usd/ui/layerEditor/mayaLayerEditorWindow.cpp new file mode 100644 index 0000000000..107e522730 --- /dev/null +++ b/lib/usd/ui/layerEditor/mayaLayerEditorWindow.cpp @@ -0,0 +1,289 @@ + + +// +// 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 "mayaLayerEditorWindow.h" + +#include "layerEditorWidget.h" +#include "layerTreeModel.h" +#include "layerTreeView.h" +#include "mayaSessionState.h" +#include "qtUtils.h" +#include "sessionState.h" + +#include + +#include +#include + +#include + +#include +#include + +using namespace MAYAUSD_NS_DEF; + +namespace { +using namespace UsdLayerEditor; + +class LayerEditorWindowCreator : public AbstractLayerEditorCreator +{ +public: + LayerEditorWindowCreator() { ; }; + virtual ~LayerEditorWindowCreator() { } + + AbstractLayerEditorWindow* createWindow(const char* panelName) override; + AbstractLayerEditorWindow* getWindow(const char* panelName) const override; + std::vector getAllPanelNames() const override; + +private: + // it's very important that this be a QPointer, so that it gets + // automatically nulled if the window gets closed + typedef std::map> EditorsMap; + + static EditorsMap _editors; +} g_layerEditorWindowCreator; + +LayerEditorWindowCreator::EditorsMap LayerEditorWindowCreator::_editors; + +AbstractLayerEditorWindow* LayerEditorWindowCreator::createWindow(const char* panelName) +{ + auto _workspaceControl = MQtUtil::getCurrentParent(); + auto editorWindow = new MayaLayerEditorWindow(panelName, nullptr); + _editors[panelName] = editorWindow; + + // Add UI as a child of the workspace control + MQtUtil::addWidgetToMayaLayout(editorWindow, _workspaceControl); + return editorWindow; +} + +AbstractLayerEditorWindow* LayerEditorWindowCreator::getWindow(const char* panelName) const +{ + auto window = _editors.find(panelName); + return (window == _editors.end()) ? nullptr : window->second; +} + +AbstractLayerEditorCreator::PanelNamesList LayerEditorWindowCreator::getAllPanelNames() const +{ + PanelNamesList result; + for (auto entry : _editors) { + result.push_back(entry.first); + } + return result; +} + +class MayaQtUtils : public QtUtils +{ + + double dpiScale() override { return MQtUtil::dpiScale(1.0f); } + QIcon createIcon(const char* iconName) override + { + QIcon* icon = MQtUtil::createIcon(iconName); + QIcon copy(*icon); + delete icon; + return copy; + } + + QPixmap createPixmap(QString const& pixmapName, int width, int height) override + { + QPixmap* pixmap = MQtUtil::createPixmap(pixmapName.toStdString().c_str()); + if (pixmap != nullptr) { + QPixmap copy(*pixmap); + delete pixmap; + return copy; + } + return QPixmap(); + } +} g_mayaQtUtils; + +} // namespace + +namespace UsdLayerEditor { + +MayaLayerEditorWindow::MayaLayerEditorWindow(const char* panelName, QWidget* parent) + : PARENT_CLASS(parent) + , AbstractLayerEditorWindow(panelName) + , _panelName(panelName) +{ + if (!UsdLayerEditor::utils) { + UsdLayerEditor::utils = &g_mayaQtUtils; + } + + onCreateUI(); + + connect( + &_sessionState, + &MayaSessionState::clearUIOnSceneResetSignal, + this, + &MayaLayerEditorWindow::onClearUIOnSceneReset); + + connect( + treeView(), + &QWidget::customContextMenuRequested, + this, + &MayaLayerEditorWindow::onShowContextMenu); +} + +MayaLayerEditorWindow::~MayaLayerEditorWindow() { _sessionState.unregisterNotifications(); } + +void MayaLayerEditorWindow::onClearUIOnSceneReset() +{ + // I'm not sure if I need this, but in earlier prototypes it was + // safer to delete the entire UI and re-recreate it on scene changes + // to release all the proxies + LayerTreeModel::suspendUsdNotices(true); + _sessionState.unregisterNotifications(); + setCentralWidget(nullptr); + delete _layerEditor; + + QTimer::singleShot(2000, this, &MayaLayerEditorWindow::onCreateUI); +} + +void MayaLayerEditorWindow::onCreateUI() +{ + LayerTreeModel::suspendUsdNotices(false); + _layerEditor = new LayerEditorWidget(_sessionState, this); + setCentralWidget(_layerEditor); + _layerEditor->show(); + _sessionState.registerNotifications(); +} + +LayerTreeView* MayaLayerEditorWindow::treeView() { return _layerEditor->layerTree(); } + +int MayaLayerEditorWindow::selectionLength() +{ + auto selection = treeView()->getSelectedLayerItems(); + return static_cast(selection.size()); +} + +#define CALL_CURRENT_ITEM(method) \ + auto item = treeView()->currentLayerItem(); \ + return (item == nullptr) ? false : item->method() + +bool MayaLayerEditorWindow::isInvalidLayer() { CALL_CURRENT_ITEM(isInvalidLayer); } +bool MayaLayerEditorWindow::isSessionLayer() { CALL_CURRENT_ITEM(isSessionLayer); } +bool MayaLayerEditorWindow::isLayerDirty() { CALL_CURRENT_ITEM(isDirty); } +bool MayaLayerEditorWindow::isSubLayer() { CALL_CURRENT_ITEM(isSublayer); } +bool MayaLayerEditorWindow::isAnonymousLayer() { CALL_CURRENT_ITEM(isAnonymous); } +bool MayaLayerEditorWindow::layerNeedsSaving() { CALL_CURRENT_ITEM(needsSaving); } +bool MayaLayerEditorWindow::layerAppearsMuted() { CALL_CURRENT_ITEM(appearsMuted); } +bool MayaLayerEditorWindow::layerIsMuted() { CALL_CURRENT_ITEM(isMuted); } + +void MayaLayerEditorWindow::removeSubLayer() +{ + QString name = "Remove"; + treeView()->callMethodOnSelection(name, &LayerTreeItem::removeSubLayer); +} + +void MayaLayerEditorWindow::saveEdits() +{ + auto item = treeView()->currentLayerItem(); + if (item) { + QString name = item->isAnonymous() ? "Save As..." : "Save Edits"; + treeView()->callMethodOnSelection(name, &LayerTreeItem::saveEdits); + } +} + +void MayaLayerEditorWindow::discardEdits() +{ + QString name = "Discard Edits"; + treeView()->callMethodOnSelection(name, &LayerTreeItem::discardEdits); +} + +void MayaLayerEditorWindow::addAnonymousSublayer() +{ + QString name = "Add Sublayer"; + treeView()->callMethodOnSelection(name, &LayerTreeItem::addAnonymousSublayer); +} + +void MayaLayerEditorWindow::addParentLayer() +{ + QString name = "Add Parent Layer"; + treeView()->onAddParentLayer(name); +} + +void MayaLayerEditorWindow::loadSubLayers() +{ + auto item = treeView()->currentLayerItem(); + if (item) { + item->loadSubLayers(this); + } +} + +void MayaLayerEditorWindow::muteLayer() +{ + auto item = treeView()->currentLayerItem(); + if (item != nullptr) { + QString name = item->isMuted() ? "Unmute" : "Mute"; + treeView()->onMuteLayer(name); + } +} + +void MayaLayerEditorWindow::printLayer() +{ + QString name = "Print to Script Editor"; + treeView()->callMethodOnSelection(name, &LayerTreeItem::printLayer); +} + +void MayaLayerEditorWindow::clearLayer() +{ + QString name = "Clear"; + treeView()->callMethodOnSelection(name, &LayerTreeItem::clearLayer); +} + +void MayaLayerEditorWindow::selectPrimsWithSpec() +{ + auto item = treeView()->currentLayerItem(); + if (item != nullptr) { + _sessionState.commandHook()->selectPrimsWithSpec(item->layer()); + } +} + +void MayaLayerEditorWindow::selectProxyShape(const char* shapePath) +{ + auto prim = UsdMayaQuery::GetPrim(shapePath); + if (prim) { + auto stage = prim.GetStage(); + if (stage != nullptr) { + _sessionState.setStage(stage); + } + } +} + +void MayaLayerEditorWindow::onShowContextMenu(const QPoint& pos) +{ + QMenu contextMenu; + + MString menuName = "UsdLayerEditorContextMenu"; + contextMenu.setObjectName(menuName.asChar()); + contextMenu.setSeparatorsCollapsible(false); // elimitates a maya gltich with divider + + MString setParent; + setParent.format("setParent -menu ^1s;", menuName); + + MString command; + command.format("mayaUsdMenu_layerEditorContextMenu(\"^1s\");", _panelName.c_str()); + + MGlobal::executeCommand( + setParent + command, + /*display*/ false, + /*undo*/ false); + + contextMenu.exec(treeView()->mapToGlobal(pos)); +} + +} // namespace UsdLayerEditor diff --git a/lib/usd/ui/layerEditor/mayaLayerEditorWindow.h b/lib/usd/ui/layerEditor/mayaLayerEditorWindow.h new file mode 100644 index 0000000000..c26d2761fb --- /dev/null +++ b/lib/usd/ui/layerEditor/mayaLayerEditorWindow.h @@ -0,0 +1,91 @@ +// +// 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. +// + +#ifndef MAYALAYEREDITORWINDOW_H +#define MAYALAYEREDITORWINDOW_H + +#include "layerTreeView.h" +#include "mayaSessionState.h" + +#include +#include + +#include +#include + +using namespace MAYAUSD_NS_DEF; +namespace UsdLayerEditor { + +class LayerEditorWidget; +class LayerTreeView; + +/** + * @brief implements the maya panel that contains the USD layer editor + * + */ +class MayaLayerEditorWindow + : public QMainWindow + , public MayaUsd::AbstractLayerEditorWindow +{ + Q_OBJECT +public: + typedef QMainWindow PARENT_CLASS; + + MayaLayerEditorWindow(const char* panelName, QWidget* parent = nullptr); + ~MayaLayerEditorWindow(); + + // tree commands + int selectionLength() override; + bool isInvalidLayer() override; + bool isSessionLayer() override; + bool isLayerDirty() override; + bool isSubLayer() override; + bool isAnonymousLayer() override; + bool layerNeedsSaving() override; + bool layerAppearsMuted() override; + bool layerIsMuted() override; + + void removeSubLayer() override; + void saveEdits() override; + void discardEdits() override; + void addAnonymousSublayer() override; + void addParentLayer() override; + void loadSubLayers() override; + void muteLayer() override; + void printLayer() override; + void clearLayer() override; + void selectPrimsWithSpec() override; + + void selectProxyShape(const char* shapePath) override; + +public: + void onClearUIOnSceneReset(); + void onCreateUI(); + + // slots: + void onShowContextMenu(const QPoint& pos); + +protected: + MayaSessionState _sessionState; + QPointer _layerEditor; + std::string _panelName; + + LayerTreeView* treeView(); +}; + +} // namespace UsdLayerEditor + +#endif // MAYALAYEREDITORWINDOW_H diff --git a/lib/usd/ui/layerEditor/mayaSessionState.cpp b/lib/usd/ui/layerEditor/mayaSessionState.cpp new file mode 100644 index 0000000000..1c211b9986 --- /dev/null +++ b/lib/usd/ui/layerEditor/mayaSessionState.cpp @@ -0,0 +1,366 @@ +// +// 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 "mayaSessionState.h" + +#include "stringResources.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#ifdef THIS +#undef THIS +#endif + +PXR_NAMESPACE_USING_DIRECTIVE + +namespace { +MString PROXY_NODE_TYPE = "mayaUsdProxyShapeBase"; +MString AUTO_HIDE_OPTION_VAR = "MayaUSDLayerEditor_AutoHideSessionLayer"; +} // namespace + +namespace UsdLayerEditor { +using namespace MAYAUSD_NS_DEF; + +MayaSessionState::MayaSessionState() + : _mayaCommandHook(this) +{ + if (MGlobal::optionVarExists(AUTO_HIDE_OPTION_VAR)) { + _autoHideSessionLayer = MGlobal::optionVarIntValue(AUTO_HIDE_OPTION_VAR) != 0; + } +} + +MayaSessionState::~MayaSessionState() +{ + // +} + +void MayaSessionState::setStage(pxr::UsdStageRefPtr const& in_stage) +{ + PARENT_CLASS::setStage(in_stage); + if (in_stage) { + auto stageList = allStages(); + for (auto const& entry : stageList) { + if (entry._stage == in_stage) { + _currentProxyShapePath = entry._proxyShapePath; + break; + } + } + } else { + _currentProxyShapePath.clear(); + } +} + +bool MayaSessionState::getStageEntry(StageEntry* out_stageEntry, const MString& shapePath) +{ + auto prim = UsdMayaQuery::GetPrim(shapePath.asChar()); + if (prim) { + auto stage = prim.GetStage(); + // debatable, but we remove the path|to|shape + auto tokenList = QString(shapePath.asChar()).split("|"); + QString niceName; + + if (tokenList.length() > 1) { + niceName = tokenList[tokenList.length() - 1]; + } else { + niceName = tokenList[0]; + } + out_stageEntry->_stage = stage; + out_stageEntry->_displayName = niceName.toStdString(); + out_stageEntry->_proxyShapePath = shapePath.asChar(); + return true; + } + return false; +} + +std::vector MayaSessionState::allStages() const +{ + std::vector stages; + MStringArray shapes; + MGlobal::executeCommand( + MString("ls -long -type ") + PROXY_NODE_TYPE, shapes, /*display*/ false, /*undo*/ false); + StageEntry entry; + for (unsigned i = 0; i < shapes.length(); ++i) { + if (getStageEntry(&entry, shapes[i])) { + stages.push_back(entry); + } + } + return stages; +} + +// API implementation +AbstractCommandHook* MayaSessionState::commandHook() { return &_mayaCommandHook; } + +void MayaSessionState::registerNotifications() +{ + MCallbackId id; + + id = MDGMessage::addNodeAddedCallback( + MayaSessionState::proxyShapeAddedCB, PROXY_NODE_TYPE, this); + _callbackIds.push_back(id); + + id = MDGMessage::addNodeRemovedCallback( + MayaSessionState::proxyShapeRemovedCB, PROXY_NODE_TYPE, this); + _callbackIds.push_back(id); + + id = MNodeMessage::addNameChangedCallback( + MObject::kNullObj, MayaSessionState::nodeRenamedCB, this); + _callbackIds.push_back(id); + + id = MSceneMessage::addCallback( + MSceneMessage::kBeforeOpen, MayaSessionState::sceneClosingCB, this); + _callbackIds.push_back(id); + + id = MSceneMessage::addCallback( + MSceneMessage::kBeforeNew, MayaSessionState::sceneClosingCB, this); + _callbackIds.push_back(id); + + TfWeakPtr me(this); + _stageResetNoticeKey = TfNotice::Register(me, &MayaSessionState::mayaUsdStageReset); +} + +void MayaSessionState::unregisterNotifications() +{ + for (auto id : _callbackIds) { + MMessage::removeCallback(id); + } + _callbackIds.clear(); + + TfNotice::Revoke(_stageResetNoticeKey); +} + +void MayaSessionState::mayaUsdStageReset(const MayaUsdProxyStageSetNotice& notice) +{ + auto shapePath = notice.GetShapePath(); + if (shapePath == _currentProxyShapePath) { + auto stage = notice.GetStage(); + QTimer::singleShot(0, this, [this, stage]() { setStage(stage); }); + } +} + +/* static */ +void MayaSessionState::proxyShapeAddedCB(MObject& node, void* clientData) +{ + auto THIS = static_cast(clientData); + + // doing it on idle give time to the Load Stage to set a file name + QTimer::singleShot(0, [THIS, node]() { THIS->proxyShapeAddedCBOnIdle(node); }); +} + +/* static */ +void MayaSessionState::proxyShapeAddedCBOnIdle(const MObject& obj) +{ + // doing it on idle give time to the Load Stage to set a file name + // but we don't do a second idle because we could get a delete right after a Add + MDagPath dagPath; + MFnDagNode(obj).getPath(dagPath); + auto shapePath = dagPath.fullPathName(); + StageEntry entry; + if (getStageEntry(&entry, shapePath)) { + Q_EMIT stageListChangedSignal(entry._stage); + } +} + +/* static */ +void MayaSessionState::proxyShapeRemovedCB(MObject& node, void* clientData) +{ + auto THIS = static_cast(clientData); + QTimer::singleShot(0, [THIS]() { THIS->stageListChangedSignal(); }); +} + +/* static */ +void MayaSessionState::nodeRenamedCB(MObject& obj, const MString& oldName, void* clientData) +{ + if (oldName.length() != 0) { + auto THIS = static_cast(clientData); + // this does not work: + // if OpenMaya.MFnDependencyNode(obj).typeName == PROXY_NODE_TYPE + if (obj.hasFn(MFn::kShape)) { + MDagPath dagPath; + MFnDagNode(obj).getPath(dagPath); + auto shapePath = dagPath.fullPathName(); + + StageEntry entry; + if (getStageEntry(&entry, shapePath)) { + QTimer::singleShot(0, [THIS, entry]() { + Q_EMIT THIS->stageRenamedSignal(entry._displayName, entry._stage); + }); + } + } + } +} + +/* static */ +void MayaSessionState::sceneClosingCB(void* clientData) +{ + auto THIS = static_cast(clientData); + Q_EMIT THIS->clearUIOnSceneResetSignal(); +} + +bool MayaSessionState::saveLayerUI( + QWidget* in_parent, + std::string* out_filePath, + std::string* out_pFormat) const +{ + MString fileSelected; + MGlobal::executeCommand( + MString("UsdLayerEditor_SaveLayerFileDialog"), + fileSelected, + /*display*/ true, + /*undo*/ false); + if (fileSelected.length() == 0) + return false; + + int binary = 0; + MGlobal::executeCommand( + MString("UsdLayerEditor_SaveLayerFileDialog_binary"), + binary, + /*display*/ false, + /*undo*/ false); + *out_filePath = fileSelected.asChar(); + + // figure out format + QFileInfo fileInfo(fileSelected.asChar()); + QString extension = fileInfo.suffix().toLower(); + + // unambiguous formats + if (extension == "usda" || extension == "usdc") { + *out_pFormat = extension.toStdString(); + } else { + *out_pFormat = binary ? "usdc" : "usda"; + } + + return true; +} + +std::vector +MayaSessionState::loadLayersUI(const QString& in_title, const std::string& in_default_path) const +{ + // opens a dialog to return a list of paths to load ui that returns a list of paths to load + QString defaultPath(in_default_path.c_str()); + defaultPath.replace("\\", "\\\\"); + MString mayaDefaultPath(defaultPath.toStdString().c_str()); + + MString mayaTitle(in_title.toStdString().c_str()); + MString script; + script.format( + MString("UsdLayerEditor_LoadLayersFileDialog(\"^1s\", \"^2s\")"), + mayaTitle, + mayaDefaultPath); + + MStringArray files; + MGlobal::executeCommand( + script, + files, + /*display*/ true, + /*undo*/ false); + if (files.length() == 0) + return std::vector(); + else { + std::vector results; + for (const auto& file : files) { + results.push_back(file.asChar()); + } + return results; + } +} + +void MayaSessionState::setupCreateMenu(QMenu* in_menu) +{ + MString menuName = "UsdLayerEditorCreateMenu"; + in_menu->setObjectName(menuName.asChar()); + + MString script; + script.format("setParent -menu ^1s;", menuName); + script += "menuItem -runTimeCommand mayaUsdCreateStageWithNewLayer;"; + script += "menuItem -runTimeCommand mayaUsdCreateStageFromFile;"; + script += "menuItem -runTimeCommand mayaUsdCreateStageFromFileOptions -optionBox true;"; + MGlobal::executeCommand( + script, + /*display*/ false, + /*undo*/ false); +} + +// path to default load layer dialogs to +std::string MayaSessionState::defaultLoadPath() const +{ + MString sceneName; + MGlobal::executeCommand( + MString("file -q -sceneName"), + sceneName, + /*display*/ false, + /*undo*/ false); + return sceneName.asChar(); +} + +// called when an anonymous root layer has been saved to a file +// in this case, the stage needs to be re-created on the new file +void MayaSessionState::rootLayerPathChanged(std::string const& in_path) +{ + if (!_currentProxyShapePath.empty()) { + + MString script; + MString proxyShape(_currentProxyShapePath.c_str()); + MString newValue(in_path.c_str()); + script.format("setAttr -type \"string\" ^1s.filePath \"^2s\"", proxyShape, newValue); + MGlobal::executeCommand( + script, + /*display*/ true, + /*undo*/ false); + } +} + +void MayaSessionState::setAutoHideSessionLayer(bool hideIt) +{ + int value = hideIt ? 1 : 0; + MGlobal::setOptionVarValue(AUTO_HIDE_OPTION_VAR, value); + PARENT_CLASS::setAutoHideSessionLayer(hideIt); +} + +void MayaSessionState::printLayer(const pxr::SdfLayerRefPtr& layer) const +{ + MString result, temp; + + temp.format( + StringResources::getAsMString(StringResources::kUsdLayerIdentifier), + layer->GetIdentifier().c_str()); + result += temp; + result += "\n"; + if (layer->GetRealPath() != layer->GetIdentifier()) { + temp.format( + StringResources::getAsMString(StringResources::kRealPath), + layer->GetRealPath().c_str()); + result += temp; + result += "\n"; + } + std::string text; + layer->ExportToString(&text); + result += text.c_str(); + MGlobal::displayInfo(result); +} + +} // namespace UsdLayerEditor diff --git a/lib/usd/ui/layerEditor/mayaSessionState.h b/lib/usd/ui/layerEditor/mayaSessionState.h new file mode 100644 index 0000000000..4d1bf19111 --- /dev/null +++ b/lib/usd/ui/layerEditor/mayaSessionState.h @@ -0,0 +1,105 @@ +// +// 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. +// + +#ifndef MAYASESSIONSTATE_H +#define MAYASESSIONSTATE_H + +#include "mayaCommandHook.h" +#include "sessionState.h" + +#include + +#include +#include + +#include + +#include + +namespace UsdLayerEditor { +PXR_NAMESPACE_USING_DIRECTIVE + +/** + * @brief Implements the SessionState virtual class to wraps the Maya stage and Maya-specific UI + * for the Layer Editor + * + */ +class MayaSessionState + : public SessionState + , public pxr::TfWeakBase + +{ + Q_OBJECT +public: + typedef SessionState PARENT_CLASS; + + MayaSessionState(); + ~MayaSessionState(); + + // API implementation + void setStage(pxr::UsdStageRefPtr const& in_stage) override; + void setAutoHideSessionLayer(bool hide) override; + AbstractCommandHook* commandHook() override; + std::vector allStages() const override; + // path to default load layer dialogs to + std::string defaultLoadPath() const override; + // ui that returns a list of paths to load + std::vector + loadLayersUI(const QString& title, const std::string& default_path) const override; + // ui to save a layer. returns the path and the file format (ex: "usda") + bool saveLayerUI(QWidget* in_parent, std::string* out_filePath, std::string* out_pFormat) + const override; + void printLayer(const pxr::SdfLayerRefPtr& layer) const override; + + // main API + void setupCreateMenu(QMenu* in_menu) override; + // called when an anonymous root layer has been saved to a file + // in this case, the stage needs to be re-created on the new file + void rootLayerPathChanged(std::string const& in_path) override; + + std::string proxyShapePath() { return _currentProxyShapePath; } + +Q_SIGNALS: + void clearUIOnSceneResetSignal(); + +public: + void registerNotifications(); + void unregisterNotifications(); + +protected: + // get the stage and proxy name for a path + static bool getStageEntry(StageEntry* out_stageEntry, const MString& shapePath); + + // maya callback handers + static void proxyShapeAddedCB(MObject& node, void* clientData); + static void proxyShapeRemovedCB(MObject& node, void* clientData); + static void nodeRenamedCB(MObject& node, const MString& oldName, void* clientData); + static void sceneClosingCB(void* clientData); + + void proxyShapeAddedCBOnIdle(const MObject& node); + + // Notice listener method for proxy stage set + void mayaUsdStageReset(const MayaUsdProxyStageSetNotice& notice); + + std::vector _callbackIds; + TfNotice::Key _stageResetNoticeKey; + std::string _currentProxyShapePath; + MayaCommandHook _mayaCommandHook; +}; + +} // namespace UsdLayerEditor + +#endif // MAYASESSIONSTATE_H diff --git a/lib/usd/ui/layerEditor/pathChecker.cpp b/lib/usd/ui/layerEditor/pathChecker.cpp new file mode 100644 index 0000000000..2b7350bba5 --- /dev/null +++ b/lib/usd/ui/layerEditor/pathChecker.cpp @@ -0,0 +1,221 @@ +// +// 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 "pathChecker.h" + +#include "layerTreeItem.h" +#include "stringResources.h" +#include "warningDialogs.h" + +#include +#include + +#include + +#include +#include + +#include + +namespace { +using namespace UsdLayerEditor; + +typedef pxr::SdfLayerRefPtr UsdLayer; +typedef std::vector UsdLayerVector; + +// helper for checkIfPathIsSafeToAdd +UsdLayerVector getAllParentHandles(LayerTreeItem* parentItem) +{ + UsdLayerVector result; + while (parentItem != nullptr) { + result.push_back(parentItem->layer()); + parentItem = parentItem->parentLayerItem(); + } + return result; +} + +// helper for checkIfPathIsSafeToAdd +// 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( + const QString& in_errorTitle, + UsdLayer parentLayer, + UsdLayerVector parentHandles, + UsdLayer testLayer, + const std::string& pathToCheck, + const std::string& topPathToAdd) +{ + if (std::find(parentHandles.begin(), parentHandles.end(), testLayer) != parentHandles.end()) { + QString message; + + if (pathToCheck != topPathToAdd) { + MString tmp; + tmp.format( + StringResources::getAsMString( + StringResources::kErrorCannotAddPathInHierarchyThrough), + pathToCheck.c_str(), + topPathToAdd.c_str()); + message = MQtUtil::toQString(tmp); + } else { + MString tmp; + tmp.format( + StringResources::getAsMString(StringResources::kErrorCannotAddPathInHierarchy), + pathToCheck.c_str()); + message = MQtUtil::toQString(tmp); + } + warningDialog(in_errorTitle, message); + return false; + } + + auto& resolver = pxr::ArGetResolver(); + auto anchor = toForwardSlashes(testLayer->GetRealPath()); + + // now check all children of the testLayer recursivly down for conflicts with any of the parents + parentHandles.push_back(testLayer); + + auto proxy = testLayer->GetSubLayerPaths(); + for (const auto path : proxy) { + auto actualpath = computePathToLoadSublayer(path, anchor, resolver); + + auto childLayer = pxr::SdfLayer::FindOrOpen(actualpath); + if (childLayer != nullptr) { + if (!checkPathRecursive( + in_errorTitle, + testLayer, + parentHandles, + childLayer, + actualpath, + topPathToAdd)) { + return false; + } + } + } + parentHandles.pop_back(); + + return true; +} + +} // namespace + +namespace UsdLayerEditor { + +bool checkIfPathIsSafeToAdd( + const QString& in_errorTitle, + LayerTreeItem* in_parentItem, + const std::string& in_pathToAdd) +{ + // We can't allow the user to add a sublayer path that is the same as the item or one of its + // parent. At this point I think it's safe to go the route of actually loading the layer and + // checking if the handles were already loaded + + auto parentLayer = in_parentItem->layer(); + auto& resolver = pxr::ArGetResolver(); + + // first check if the path is already in the stack + auto proxy = parentLayer->GetSubLayerPaths(); + if (proxy.Find(in_pathToAdd) == size_t(-1)) { + + std::string anchor = toForwardSlashes(in_parentItem->layer()->GetRealPath()); + auto pathToAdd = computePathToLoadSublayer(in_pathToAdd, anchor, resolver); + + // now we're going to check if the layer is already in the stack, through + // another path + bool foundLayerInStack = false; + auto subLayer = pxr::SdfLayer::FindOrOpen(pathToAdd); + if (subLayer == nullptr) { + return true; // always safe to add a bad path, unless it's already in the stack + } else { + // check the layer stack again, this time comparing handles + for (const auto path : proxy) { + std::string actualpath = computePathToLoadSublayer(path, anchor, resolver); + + auto childLayer = pxr::SdfLayer::FindOrOpen(actualpath); + if (childLayer == subLayer) { + foundLayerInStack = true; + break; + } + } + } + if (!foundLayerInStack) { + UsdLayerVector parentHandles = getAllParentHandles(in_parentItem); + return checkPathRecursive( + in_errorTitle, parentLayer, parentHandles, subLayer, pathToAdd, pathToAdd); + } + } + + MString msg; + msg.format( + StringResources::getAsMString(StringResources::kErrorCannotAddPathTwice), + in_pathToAdd.c_str()); + warningDialog(in_errorTitle, MQtUtil::toQString(msg)); + return false; +} + +bool saveSubLayer( + const QString& in_errorTitle, + LayerTreeItem* in_parentItem, + pxr::SdfLayerRefPtr in_layer, + const std::string& in_absolutePath, + const std::string& in_formatTag) +{ + std::string backupFileName; + QFileInfo fileInfo(QString::fromStdString(in_absolutePath)); + bool fileError = false; + bool success = false; + if (fileInfo.exists()) { + // backup existing file + backupFileName = in_absolutePath + ".backup"; + remove(backupFileName.c_str()); + if (rename(in_absolutePath.c_str(), backupFileName.c_str()) != 0) { + fileError = true; + } + } + + if (!fileError) { + pxr::SdfFileFormat::FileFormatArguments formatArgs; + formatArgs["format"] = in_formatTag; + + if (!in_layer->Export(in_absolutePath, "", formatArgs)) { + fileError = true; + } else { + // parent item is null if we're saving the root later + if (in_parentItem != nullptr) { + success = checkIfPathIsSafeToAdd(in_errorTitle, in_parentItem, in_absolutePath); + } else { + success = true; + } + } + } + + // place back the file on failure + if (!success && !backupFileName.empty()) { + remove(in_absolutePath.c_str()); + rename(backupFileName.c_str(), in_absolutePath.c_str()); + } + + if (fileError) { + MString msg; + msg.format( + StringResources::getAsMString(StringResources::kErrorFailedToSaveFile), + in_absolutePath.c_str()); + + warningDialog(in_errorTitle, MQtUtil::toQString(msg)); + return false; + } + + return success; +} + +} // namespace UsdLayerEditor diff --git a/lib/usd/ui/layerEditor/pathChecker.h b/lib/usd/ui/layerEditor/pathChecker.h new file mode 100644 index 0000000000..45440b1e06 --- /dev/null +++ b/lib/usd/ui/layerEditor/pathChecker.h @@ -0,0 +1,87 @@ +// +// 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. +// +#ifndef PATHCHECKER_H +#define PATHCHECKER_H + +#include +#include +#include + +#include +#include + +class QString; +namespace UsdLayerEditor { + +class LayerTreeItem; + +// check if it's safe to add a sub path on a layer by +// verifying that it would not be creating a recursion +// bad paths are always allowed, because they could be custom URIs or future paths +// used for Load Layers +bool checkIfPathIsSafeToAdd( + const QString& in_errorTitle, + LayerTreeItem* in_parentItem, + const std::string& in_pathToAdd); + +// check if it's safe to save an anon sublayer to the given path +// and then does it. +// stategy: +// save the layer, then use the same logic as loadLayers to +// see if it actually can add this path without creating a recursion +// if that fails, delete the file we created. +// for now, assumes an absolute input path +bool saveSubLayer( + const QString& in_errorTitle, + LayerTreeItem* in_parentItem, + pxr::SdfLayerRefPtr in_layer, + const std::string& in_absolutePath, + const std::string& in_formatTag); + +// convert path string to use forward slashes +inline std::string toForwardSlashes(const std::string& in_path) +{ + // it works better on windows if all the paths have forward slashes + auto path = in_path; + std::replace(path.begin(), path.end(), '\\', '/'); + return path; +} + +// contains the logic to get the right path to use for SdfLayer:::FindOrOpen +// from a sublayerpath. +// this path could be absolute, relative, or be an anon layer +inline std::string computePathToLoadSublayer( + const std::string& subLayerPath, + const std::string& anchor, + pxr::ArResolver& resolver) +{ + std::string actualPath = subLayerPath; + if (resolver.IsRelativePath(subLayerPath)) { + auto subLayer = pxr::SdfLayer::Find(subLayerPath); // note: finds in the cache + if (subLayer) { + if (!resolver.IsRelativePath(subLayer->GetIdentifier())) { + actualPath = toForwardSlashes(subLayer->GetRealPath()); + } + } else { + actualPath = resolver.AnchorRelativePath(anchor, subLayerPath); + } + } + return actualPath; +} + +} // namespace UsdLayerEditor + +#endif // PATHCHECKER_H diff --git a/lib/usd/ui/layerEditor/qtUtils.cpp b/lib/usd/ui/layerEditor/qtUtils.cpp new file mode 100644 index 0000000000..4d1b332815 --- /dev/null +++ b/lib/usd/ui/layerEditor/qtUtils.cpp @@ -0,0 +1,65 @@ +// +// 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 "qtUtils.h" + +#include +#include +#include + +namespace UsdLayerEditor { + +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"; + } + + const QString resourcePrefix(":/"); + if (pixmapName.left(2) != resourcePrefix) { + pixmapName = resourcePrefix + pixmapName; + } + return createPixmap(pixmapName, width, height); +} + +QPixmap QtUtils::createPixmap(QString const& in_pixmapName, int width, int height) +{ + + QPixmap pixmap(in_pixmapName); + if (width != 0 && height != 0) { + return pixmap.scaled(width, height); + } + return pixmap; +} + +UsdLayerEditor::QtUtils* utils; + +void QtUtils::initLayoutMargins(QLayout* layout, int margin) +{ + layout->setContentsMargins(margin, margin, margin, margin); +} + +// returns the widget after setting it fixed-size +QWidget* QtUtils::fixedWidget(QWidget* widget) +{ + widget->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + return widget; +} + +} // namespace UsdLayerEditor diff --git a/lib/usd/ui/layerEditor/qtUtils.h b/lib/usd/ui/layerEditor/qtUtils.h new file mode 100644 index 0000000000..85a19d53bc --- /dev/null +++ b/lib/usd/ui/layerEditor/qtUtils.h @@ -0,0 +1,68 @@ +// +// 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. +// +#ifndef LAYEREDITOR_QTUTILS_H +#define LAYEREDITOR_QTUTILS_H + +class QPushButton; +class QSize; +class QString; +#include +#include +#include +#include + +namespace UsdLayerEditor { + +/** + * @brief QT helpers the layer editor needs to load bitmaps and handle DPI scaling + * + */ +class QtUtils +{ +public: + virtual double dpiScale() { return 1.0; } + virtual QIcon createIcon(const char* iconName); + virtual QPixmap createPixmap(QString const& pixmapName, int width = 0, int height = 0); + + virtual QPixmap createPNGResPixmap(QString const& pixmapName, int width = 0, int height = 0); + + // shortcut to setting the margins + static void initLayoutMargins(QLayout* layout, int margin = 0); + // returns the widget after setting it fixed-size + static QWidget* fixedWidget(QWidget* widget); + + // tests if the mouse is the given rectangle + bool static inline isMouseInRectangle(QWidget* widget, QRect const& rect) + { + auto globalPos = QCursor::pos(); + auto localPos = widget->mapFromGlobal(globalPos); + return rect.contains(localPos); + } +}; + +#ifdef Q_OS_DARWIN +const bool IS_MAC_OS = true; +#else +const bool IS_MAC_OS = false; +#endif + +extern QtUtils* utils; + +template inline T DPIScale(T pixel) { return static_cast(pixel * utils->dpiScale()); } + +} // namespace UsdLayerEditor + +#endif // LAYEREDITOR_QTUTILS_H diff --git a/lib/usd/ui/layerEditor/resources.qrc b/lib/usd/ui/layerEditor/resources.qrc new file mode 100644 index 0000000000..e912d27477 --- /dev/null +++ b/lib/usd/ui/layerEditor/resources.qrc @@ -0,0 +1,34 @@ + + + + resources/save_all_100.png + resources/save_all_150.png + resources/save_all_200.png + resources/save_all_hover_100.png + resources/save_all_hover_150.png + resources/save_all_hover_200.png + resources/save_all_pressed_100.png + resources/save_all_pressed_150.png + resources/save_all_pressed_200.png + + resources/target_hover_100.png + resources/target_hover_150.png + resources/target_hover_200.png + resources/target_on_100.png + resources/target_on_150.png + resources/target_on_200.png + resources/target_on_hover_100.png + resources/target_on_hover_150.png + resources/target_on_hover_200.png + resources/target_on_pressed_100.png + resources/target_on_pressed_150.png + resources/target_on_pressed_200.png + resources/target_pressed_100.png + resources/target_pressed_150.png + resources/target_pressed_200.png + resources/target_regular_100.png + resources/target_regular_150.png + resources/target_regular_200.png + + + diff --git a/lib/usd/ui/layerEditor/resources/save_all_100.png b/lib/usd/ui/layerEditor/resources/save_all_100.png new file mode 100644 index 0000000000000000000000000000000000000000..6e5ced20b7dc97fb3d9938eda012e9fe40e7562a GIT binary patch literal 207 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBeu}4yV@L(#)k)rb4GKIi%a1T$67E#b%8yXC z_I1?cJ{-Ma#sTiX43jUEu2`k$aBk)#@o#cAn_E2piRZ-Kczt+vLWxrM*8|$;MJuL9 yUy5p)Da#SWdM0Mkq}$tFmIz$z>+gBvJz2OeO0I;{h<`oMJ_b)$KbLh*2~7Y^OhWwt literal 0 HcmV?d00001 diff --git a/lib/usd/ui/layerEditor/resources/save_all_150.png b/lib/usd/ui/layerEditor/resources/save_all_150.png new file mode 100644 index 0000000000000000000000000000000000000000..f5a9401d0fb8f2a718ceaee158c604721f6f8986 GIT binary patch literal 236 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjoCO|{#S9GG!XV7ZFl!D-1!HlL zyA#8@b22Z19F}xPUq=Rpjs4tz5?O)#Mo$;VkP61PQx5VrDDb#OKVs;6#FfV|`4HQ> z2O4<+Q(g3rDQ*1E^4`His-mS%f$8w`2|+?k5B8=n7hRdOc>646gV+lV2^YFd1J{a0 z*zUXcgpEbb@T$;qF0bzWGLLpGn(C0!*l|X~Q>;b6Y|0ZqL9b640X>B&&z{ck`y$+Z ce$xp)-b%Hr+p`@e0v*HP>FVdQ&MBb@01L)YfdBvi literal 0 HcmV?d00001 diff --git a/lib/usd/ui/layerEditor/resources/save_all_200.png b/lib/usd/ui/layerEditor/resources/save_all_200.png new file mode 100644 index 0000000000000000000000000000000000000000..a6eee73c353431785763a648dced1299dbe8f267 GIT binary patch literal 245 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz&H|6fVg?3oVGw3ym^DWND9BhG zKVR65r(>%$rxICE&SMXM51wmI)nY0uRJCT(jcrJTB(pvBxMN`jPR* ld)4h7(jrPfjVGNtQX|9oPT%!RViw4i44$rjF6*2UngCKlS4RK< literal 0 HcmV?d00001 diff --git a/lib/usd/ui/layerEditor/resources/save_all_hover_100.png b/lib/usd/ui/layerEditor/resources/save_all_hover_100.png new file mode 100644 index 0000000000000000000000000000000000000000..7be004080bd5ec3cfcefb6ba0b4c24397a177322 GIT binary patch literal 208 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBeyXR7V@L(#)k)rb4GKIi?ZpQ=)8N7$Z}l=xXPPm5$3YA*1;0b0o5>FVdQ&MBb@0Fxa@ A3;+NC literal 0 HcmV?d00001 diff --git a/lib/usd/ui/layerEditor/resources/save_all_hover_150.png b/lib/usd/ui/layerEditor/resources/save_all_hover_150.png new file mode 100644 index 0000000000000000000000000000000000000000..d314a9b89187bde810ce5a07b04b09e93dd3c55b GIT binary patch literal 238 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjoCO|{#S9GG!XV7ZFl!D-1!HlL zyA#8@b22Z19F}xPUq=Rpjs4tz5?O)#W=|K#kP61PQ>^(K6a-w{lUD3y5?veq;Yr-V zXLHw+HTtl6L6VPm>>%g@s_CFe_6u)<{)QI4vWGlZ|^ zIIcW?eO6540uP_1M>?CXJkoF9u`YwLQ{zIEzmBJfL~_`)Cw_w7pE4$Vuj5cWbG+n; ezgNvP&$1txa{pQldpm&6VeoYIb6Mw<&;$TRGE(^f literal 0 HcmV?d00001 diff --git a/lib/usd/ui/layerEditor/resources/save_all_hover_200.png b/lib/usd/ui/layerEditor/resources/save_all_hover_200.png new file mode 100644 index 0000000000000000000000000000000000000000..727d5c3fd136d723b5565213b96bcf08993669c4 GIT binary patch literal 246 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz&H|6fVg?3oVGw3ym^DWND9BhG zo4>kX>Jbgn!ZuP zpo1fQLG-ToC)tml|GApOz$!SaMZheh!|>T0>BgCB4kXUX@m|n3Ii688pz$4RTGny> zaz@slqAUKoWpIQpxL@5+RrN2p(Iqc+>C{QWha?_z@x(A)i#o1Yc$h}VLjb17BZZR_@`ehyFYiH?9e$Nhc7K5j&pUXO@geCx_%~Jya literal 0 HcmV?d00001 diff --git a/lib/usd/ui/layerEditor/resources/save_all_pressed_100.png b/lib/usd/ui/layerEditor/resources/save_all_pressed_100.png new file mode 100644 index 0000000000000000000000000000000000000000..4ecea6ca43812a14b9d816bb1d9dd39e8e8da73c GIT binary patch literal 285 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjoCO|{#S9GG!XV7ZFl!D-1!HlL zyA#8@b22Z19F}xPUq=Rpjs4tz5?O)#O`a}}Ar*{IryBAdGT?Eo=PpTn#NlJf5Uh9b z66Z1*UE%2S6Gb~U*;h_TI4BvR#T>R_`ZrH|=QTgn%chtxd|Set$sFr;Y3;&?9=8s) zxo6MVb78B&u{#N8eV@(IUva>)eBQagHjLXngoLI4oc5V*;~MljtNE7n*R+SW6Q=m5 z98jHXHcvOuE3-jpjUz|m1tU%Qb1#Dpj@_t!U(NCG)VHe+&6PG~{x=NQW$c(-wuEoj cl-~IG1>I`PCp^5@1av)vr>mdKI;Vst0269y+yDRo literal 0 HcmV?d00001 diff --git a/lib/usd/ui/layerEditor/resources/save_all_pressed_150.png b/lib/usd/ui/layerEditor/resources/save_all_pressed_150.png new file mode 100644 index 0000000000000000000000000000000000000000..196d695221dca15a3e2d3fbcdb49073f95dae594 GIT binary patch literal 329 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbBI14-?iy0WWg+Z8+Vb&Z8pde#$ zkh>GZx^prwfgF}}M_)$E)e-c@N{Cl1*jv*C{Z>JmbH5rJs)eA_L9F)#*I>flA zeRq+l`}(HmhJ{Km9|-PR(5bhNujkuTAur+k)BnWYIxzjyJ#P<&HyYKagg##ie8;KF zeoCtFgP&l3kD|Yzq|y<=xdCanb8dg@;#{%n)tBu254<Y%8>%Uxlnh-f{^|H)!u7Np*LeU4Uxm%soOo66KEaZQ*F#5u$ML8Ol&yL2NdCSco XY}q(t>)GExpE7v5`njxgN@xNAN0xwy literal 0 HcmV?d00001 diff --git a/lib/usd/ui/layerEditor/resources/save_all_pressed_200.png b/lib/usd/ui/layerEditor/resources/save_all_pressed_200.png new file mode 100644 index 0000000000000000000000000000000000000000..e38808c8d55b660924d05b3aa2c3da5d3554d1ec GIT binary patch literal 345 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC&H|6fVg?3oVGw3ym^DWND9BhG z{Ot%6Yq`5LSZ=ZXk^e!{^q~|~HrpZm8aB@oSwi+Ar7SRiBAQiLU zmOL`7ne$R_1C#gtiERbK+0Kl)6QpsT?i!JH(pRVqrXgAX2t7WI$ev7_?-EdsyD^UB3wbFV&-Lt>7g7og8F~`+O{9@ gRC11)am2oUV}scvjVUwzfIesNboFyt=akR{048aK-~a#s literal 0 HcmV?d00001 diff --git a/lib/usd/ui/layerEditor/resources/target_hover_100.png b/lib/usd/ui/layerEditor/resources/target_hover_100.png new file mode 100644 index 0000000000000000000000000000000000000000..4e4c19376437fda86ccd0fc2bdf8d58260f52bf6 GIT binary patch literal 564 zcmV-40?Yl0P)|y5JFjL zG&?(QX6Nn9D&uagR;!*;Y5?iOY{+p4JG@@6Uc23n^LV8BxApkZ7{o z?L6DI6WXQ@uGMM_2~KXen+JJswOWdTWy78kOAN7$`Afi?p*oNlKpT2lC=|x)^?D0Q zVSk&Zxqz=p46zbpC~$7T8POo1SS${q-ACmE=Uy(C8FT>CVwENjr8FO^r-Mmjzu&)5 z;B(}Rz6SJE-eW)tZ<2kAvWJq)DV0hQxKaWDw@gSMsga^2=qur9@Uirsw=yu-#-v;> zH&3V2mV01dG}gSMndJ7?KhCoRF*h&Bbfy%HiVsd1#Tj|eLZZ@A=}tvkL4!Lzy}i;50*TnpKrT~*R9y1mt1~-V)rEFslsN|;Xmn+O8pIwB2REan%ZsAOIv$TM zIDQ#V7H?*-{!Ho~z|n?saT^D$BSA6L9Qucp^Y~weWFC)GX&H?G0000?BS1s|L|{(@0}+5DfE(KZO$6q`S_RC1n%7KQ zAld#^O{-gfdw&zQ6+y${(22+6wbSWTl$DUH1Ya#}+aAf@Jsb|bTrTH@>9DU|j>qG~ z&CSg-6kDGlG4>*cn%4}e;Kgh`Jh zxYDqoT$H_N<@fjZeJTfYQ`V!I10()*@Ir=OccBYK;yXa^7dRl1uqxqdQMm`jVP#0! zK0*g1>2&&Ou~<0!{T})p$>BGJ_|g-2??Fh1MJvah80}A|(>GZsS-)|Ici^%9Ra+hT zN+y$d_R-zl-POebRiZ;(BV6wc8fEK{w~RZ(NQe0-b01)rFnw1dn5W`D6n@~j!@XrF zHt8H^p#c?_htl*)G{^dYV8FuzJJK@F zMeH1ES4j}`uuB2tc{K{0m#89-iXKcMp(1{yTdN@`mb%)E{TzFJR-)jQ0*}1Z)l@U8 z1jDUsC?4oeCGrYo^$gN^>)C9KHwa2&B-!$9K~%H8&}f*t(#iE;<0>=;>lWj|FT2~@ zTbJ?lnI)#rMqK^;VDfT)CBVDcY+NFDiN^1vi(#iHA&~8MJC{g837$hakQ?LvJnYWn z+L(mzX1X!&Q3QD^^$=g=OZPz7xVHl_JzCK4ENv>}u( z%2{_eLz>5YlKTpIQC&^#d8mOvSSr>h2xTj)dJU}|CCGmlC4r4kW?rLW4m{akx7%$y b>bw0HvjsrRW+ZkT00000NkvXXu0mjf6mNP~ literal 0 HcmV?d00001 diff --git a/lib/usd/ui/layerEditor/resources/target_hover_200.png b/lib/usd/ui/layerEditor/resources/target_hover_200.png new file mode 100644 index 0000000000000000000000000000000000000000..3e037b40d0095f2dd90abfa5176ab3a64e0a5136 GIT binary patch literal 1087 zcmV-F1i<@=P)IAwBFu=Qima;qS7vr#azeM;Ei5f9Rp;jBc80@Y38)C8Aln0sBnW~Y_Bsy_4;STf zd60*0FzvwA)l~$-4UBz<;dH`5aQgcC+T7mWP9~nq+Mvd(tE-35sA2vAAjt8?#>S7# zeIJrqhhcbW-p%|!AbERx`|h(ws1difx1|Zp2O!iWF^uBo=4QYFpRW-IYn_mp=U%E1 zY3q$K3rx_Q8q}dd$K(Lese#U62AwfWj0B+1S5{WK`VzHze0+=<)e;x`z*VqSa?bY5 zyXI|J^78UW-LKJv5=&W9D32+VyU#A=F=vCPQ` zT3?bN#t`G2IPUR`d9Mu?3d_sOHMxJ{(t{gz7z5vw0Bz0?Y**n=EpzO3C@$ zj4=XKWbP;dN}<;B>FKE_%P~9{oXC7+{7lTpSxUJ2sJ_aQI?6qS2(lj$RcI~*i&@UJACpAKM0M-FYYA= zD*VBFuN%J=1~y^o#D)LAIU~NbU`dFG{pDkXOFvyCg73hI@-k3uhjE8E`$~WtOGvQV zxy)51SkIS;21rC}k|CLhT(ixRh$bbrT8KuInv5k5`ny;x5?5C|(xL?)w_HE-JZlDX zf%(`2#B!#l_t?Cq)zDL|Mm5`4$?}3N=e}Npi4E1Z9?G9%bvI%|$^}a@37Jm<%en0( zgjS^XQ<|v&^r`f7?tuo)D*G`Dhw=A&8tA$`+e`S$xd@kVJ?e-g=sx(6-OK&`ebZ)0 z3EEvbR}zBhM+rs;XV0AW&DTr#TQVV@*_0Zm&6);M`&;HSO{oP&H>5r{{T9v(fzg{0H;*5ST z3z(kI_`GNSIMrZ3&#`2HvB8Lp`aEZDXAMDowDW$R{{_4kq6g6y)tUeR002ovPDHLk FV1jW@`7!_i literal 0 HcmV?d00001 diff --git a/lib/usd/ui/layerEditor/resources/target_on_100.png b/lib/usd/ui/layerEditor/resources/target_on_100.png new file mode 100644 index 0000000000000000000000000000000000000000..285c2e110acf31126c12a625dd90282e2ae78e26 GIT binary patch literal 629 zcmV-*0*d{KP)N!*2i&p~LLkARgBRtPX!eu~z0JXJ30eHiKOEiWUoyBC~7Dd>Blh9J(g&Fry> zJ0ae{6}spN6D7VVaKz_i9PMFZRkl^NKsDPw;dU1yYUJ3M{p&J1sjz%7qHWD?RW`{# zKh^wIg0&gl>F?MU#_Y$pZlFO!ulUv$=xEPyBF?oM zZ*Y37ttfK$)ETq?$yuJ6z$V~TrfXOfqbG3?PFPQ9q* z>FED(w0Eg4$5*^^tg;1HKNB4Sx<7Qa14(VcivOeJu_m}%U7VjrxsLw}j2-F<%Oodk P00000NkvXXu0mjfKFS!( literal 0 HcmV?d00001 diff --git a/lib/usd/ui/layerEditor/resources/target_on_150.png b/lib/usd/ui/layerEditor/resources/target_on_150.png new file mode 100644 index 0000000000000000000000000000000000000000..c1af7638999bb206ffaa669d493a9bf7acd0b2e6 GIT binary patch literal 981 zcmV;`11kK9P)6=FEG$3-T{iqbB;Xi4Xk|H)^NiBzO-)Dejf``oS{*3?yO0C_a?cm^#2Pl4 zr}ZZ=kUo-5>C1KfvF~)`y_+V03Rnl>ul4kMXw>h;r*d*ts1~VP7aV(ZIK8;{V#z`0@2=aEi1}^_3 zGkpQWayQIhAZqR`vSpeCgzcD+9Yt8k1AkKLg>8;p!vtb9e30FSCjT&`)DaR)Yomz4 zt)M+}vU&$E1sbd4q0Q01_p4bBUE89OA}IkTp=M zCGp1&=Tdupmo8iWV`tp8tuewKRv{k!BN?IKh*h+#@PDaCY(vU-lA@w-rQ;hG5@Lhi zu~Ukn?F^?WooAe;k#Q_JSUK~?R;(bM^4EQTY`dcP;H;qni#v#AvG5~jkk9uuD@b^P zY!_ux7P5IF2NU|>jxc1-z3*jfWV}#Y=>fgoo`sAA(T*RSF}TEm&UqU~NDYj#WCxh% zY8DFSDZxXiW}4qZ8V6Av7hG@>6r3{@J=Bwwrn~u(@a9t<$jk`o!eb)F&+C)3ozZAla2)GB79}&XHMmscCeZkVigNfkm>A zC~`7R$lbcn!-9^68K{_p|Yw#%= z(Y$1>b+Jr~i~mgSm@Ct)YW_j@gx~=^$DQ9qz literal 0 HcmV?d00001 diff --git a/lib/usd/ui/layerEditor/resources/target_on_200.png b/lib/usd/ui/layerEditor/resources/target_on_200.png new file mode 100644 index 0000000000000000000000000000000000000000..292ee74db8d5bc31c9f6fdc58511bd98417d0d29 GIT binary patch literal 1264 zcmVA=Luw=irN7rKS7jJ6!Z}F z+!R!vAO>Q?4kYeoX7~f{Y8lB+1`w87?#?$mJ2T%blY2MwbO)sOSZHV}fItA; zqjwtMLITXRfGPeRH#|5Wx5JBd^omsrTEip|bJbHq?iYY-iGXA9pg(Dcv#&3!5jyep z0uFIO557UrJ4bIj;WN060d#6uz$<(V;QPo7eAsq*L)sI+o+iyYaJp+8`uN*Mh2f+V z3i>rJ;U}6CTD`zUGguZr5bL(Y@IdX!NvVJ?f{kGfX`LnqI2_`hznzY+u!eZl=KE`WCD3BE(~d%B{6%WBGtfT19I0V_Q~JuJ?PaNw^Cxf zvOe#B`Tx<9Cn$#SDzYG)>ljQEXXN(m1Rw62iZK~Z51&2MWd$eI79Q!Y8VmSnY!&ig zvU~reb^87Qgf}p4g9|k11wM|8prq*bK=(k1ecKA5()OD_ejedfMELhrRnOKXgklK< zEuHLzVRrh2t|h3xgA}9qPj_`|{e@TQAxI_q3m^(Q^vFjehIHEXu@ppx#h}hv85w4$ zV-B9j%z!e16e2f_^aebmJ4GY5wGZzKAR4c?6zMt=3Oy$)0g1dKg%H0~F|<%4-YC5% zU|M!ufa@$>qmqmS?9d#&qcsdv-6}+s0m9h8QqYvjI%VV1pXRm0RKKTNe8{gdno0Nd1_>&1qA*gU?5=uDH-v&fUe66Jf{Q~ z7d6*L;SySZpb_g$)7mtXTIqjXKuW`FW37}=%P3#KLyY`U35=mbb7hL!1)v)H$=^qf=xnVX#%n*-j=~ zTQWL#_8L^!cEc{Gp-APCwKKT>F)NvGcbSs=a7!UNFM0`TZo&4u2(zVvcF{|?=$wtqi&EJUF)h#0O;#UA-3zi=zo?MZ?_ulp1Pe$| zR?Dm%n^I71eNrc7`{ruvac$$u(UP%p2c70(v{|bp=YHaazy`PJeCfL&Yc62uFoF$1 z_ni}CHhh0te17HWN9!M>S9;EpjNmDzZow(F2C%|gf0Cgh@uL6OHLz2fVfcg+v1^CO=Rwpd;#{YCq3a`nqxDjfyB= aC0_vA`B4}#5<52l0000 z&^MyQlUN_KNx!qSZ)u7vj`O;BiL4CW`e*4bn?7sQQ+1c2qYfX}oe}E7yhUNp z(Yj!!(Q&bOfa(w4oQg<}SVxg#xhr1->nL9&!Zf!JOAw($AQ*p%pK0tIT_XPmUu{DP z+cn8EX!=@=F-o}DiGze_d*keF687rNP^5>=YCsG5W+sEbn;HR?pRrd+;_>x)16>7$<{ zY}E*~NMV;|SJWB+!LW|`vsSVb#v>|@^ZRbyrW97a4hwN9pk~1@Biy?k>r)R)>>&6D zi}531>obt4CYzRa#uU!nIvEEr(?8%J(N|&w!?iw~-7iGd&eFCYSpow84QeyL*>-b_ zacE|zn%cYYOKj0<93BjLQbG6Dx*66~1`pO52)1Vg3J*-nP_y-zDvHNA^a*_@=xoK! vW8&?&QQO!V2o^YE*I?aTJBqH}alp$9!wdIt2+A|800000NkvXXu0mjfK8Fhm literal 0 HcmV?d00001 diff --git a/lib/usd/ui/layerEditor/resources/target_on_hover_150.png b/lib/usd/ui/layerEditor/resources/target_on_hover_150.png new file mode 100644 index 0000000000000000000000000000000000000000..99f79d87ae7b02c447cf5aa10161f44e855d5cd5 GIT binary patch literal 903 zcmV;219<$2P)S}`D=15w+86P;Xy zJ8cK>D=>g{!qUM!2b8dCk_}5E9z~3Hx~z}h3!%uLWp=&Q2`iB#c!Jxf1k@~Iz)aiB zv?WX>M4b3D2`wAn6XV8NNh1r+$@1V&xEU+>Sd6klxRiryr%O%wS9HW{39VpBk=S$f z{#FD@q*(pdzduv(p#bNVp;lWgp%W$-!MhXZ+?L>#H|XIFW@vTUyQ6gmGi6&7Me$PN z?9Y67)tP=HzH0*_=QM3`{_w!MV=OfvE?M0yfR=4p!%~+$IkW62J+}}rt-HKX-@ei8 z8}$k%Q}fd*BYvZ$q=w$;hucAgP!wbvs&>G5z#8ciGIZ!#tQ%;F`@@~YxYiVJe}O@!=S`aqrJp?qp>mMtl=>i6*^gFvH96 zeKKb?sUetv$HSriJ7&*Y-t$}oi(%1g@P#EsBK@+CEfy)-Hm d5AC9R`ycLe@jEY7D53xW002ovPDHLkV1j_Tl>Yz# literal 0 HcmV?d00001 diff --git a/lib/usd/ui/layerEditor/resources/target_on_hover_200.png b/lib/usd/ui/layerEditor/resources/target_on_hover_200.png new file mode 100644 index 0000000000000000000000000000000000000000..19480092c137cad2a2beb8782ab9ecba8601cb8b GIT binary patch literal 1190 zcmV;X1X=ruP)N*b5fNB_6b5xfP8|G6IiMI zkjf_P0(Q`(KWPS}v1GtWRk{kK8qIX~^z_&L4bz&^8D7?=bg!kjv`eISh}e_in)y7D zu~5cF%oN;9Aw21@QwS;mgA3gdt5=u0hdC^erz@y=pom-4gWJ@PdCco{|MxF87($haM zMta`UVk~E!r$i@oyB{(UxMkA9o4n)s!fu2kx77pOdXIy=AW0BUqEt>hr zYRtJ(_xH7?29U6vt4Eqts8|pF->sipeOFZRiTR1^ghcFtMajZ}|ANoYZX}$9`?Oj2 z@p?G?*kV?FT8)HIcJEt9j)r4#f{wFlmx;+HWH_Tscju?gzdMD_trBtKbhmzzIPt0L zMglp6qfp7*$ai;vu*Zup#&?6FCjU?z$18NX$r;xc*pAPNt6wwDGaVp=NG6F)-%$-D z^dh3bCMB}FOxGPbqWv3w=xFbG^Php}Y5)moJ(3ICoMR*uXl>)=Fc@DtNm+Nw=6^Frs5HP+AobAM;F&cp5d&Ns)(DesVGQ5m=)h1)bTU_Tt01eA zca-UB3#rm{WL`7l*Sgd*jy|FK4X1T!e0ib6M zOH=}kEg_mMNF~Lu65S;yfApvkDPH21EQ)ATiQ9R?B|vU-%6hCb zb|5LAI#WIc1s#MnaiZ>nk`?ehS8#NDmP_QebFpQM(n?AO_EOQ`PHcrt_#Toe@@%zC z97`@GThX_3OWz7;eLf{K$_q(pnr5vMBqejXzY!_55|ay_zbc^RA=-6BdQheBqf}I6 z?sreA@~(sfJI*V~p)|C;Wlt$^z$*N07ZV63R^*)A#JrP$){Cwp7PZ#p6r?qe@7*H8 z8|UXjZn}5AoVt=+&#Ns^xP(QqM>*GX3H1f4>$=zTAL4FKYJ!&zlmGw#07*qoM6N<$ Ef~j#e3jhEB literal 0 HcmV?d00001 diff --git a/lib/usd/ui/layerEditor/resources/target_on_pressed_100.png b/lib/usd/ui/layerEditor/resources/target_on_pressed_100.png new file mode 100644 index 0000000000000000000000000000000000000000..d02e2909536b5e56ca2bddf445a847e89d3a5e2c GIT binary patch literal 985 zcmV;~119{5P)KIS$xG`Fdgo0?BhOD(g^S=nP1S&JJ$k15GH1UnBkGl zg$A=P7T6^YGZZlo7I0=v4j|wVGs9}F2LcmrvY4TVg6tT(f(Tnt8pP&jCREz-(Z1&5 zJVAd;t=iQ4um|z0P1w^tNC(m$aUEFx98(EK6m7z~2PuLH)pzZ>2_X52$8R%N^9DItqR}gAX(>4`m^WFEyMOZsJvvs=P@}Ul? z+9Uxdr=2X*cLap#Yy6{8#Mh_?$Ig`26>y70_ZKzO+CiB_ z0CV1PJnleyiyVA1(=%{+z7+&S8!`jN?kc_tr8=>}|M)boo)Lb83-!li^>&XunII`h z7wVt4r7#SekN+!UlLW>91sQxx{1%eI*DWm=#qlIlIaUxXV9J3pFyR(!eoCzZKRopw z=bN~K4|71Oln4dES2OhGn^g}fypCOT1%G~KQne<*R9{akj4pm5?5;|_zRu18;ap5P z=$qu2+piKMQ&FlTjk))*e)E6IB=tV067`2sGd{dFfl^wlCvfr!;f(ckj)8jM-YqPK zckY>_1Pllv26H{)cXE5C=RUZXVy$C&z7;YB9n*uONT6$Ml|hY#+2uN&PDQjOqXd#Q z4N9b)80l?$Sya^9P%V$nD$2CP6VOq)Oe8JNl`bbR(JCr442qgicGtHHN<`gN&-EpM z2I%6qhwZdGGzOHl;Z|WFSx>Wrm5U#yzTQx?GlpwO6fHFA4j07^AT&RSEof)eQ+;1s zl`Y~=hBNRltV!^{F#8`32o@3ZK?5?K!;Fj>0oh!@!rT7<$Vg=HpE@pW00000NkvXX Hu0mjf){VLu literal 0 HcmV?d00001 diff --git a/lib/usd/ui/layerEditor/resources/target_on_pressed_150.png b/lib/usd/ui/layerEditor/resources/target_on_pressed_150.png new file mode 100644 index 0000000000000000000000000000000000000000..40c2d16ac08ae1058d48647ab4014a52454f40fa GIT binary patch literal 1437 zcmV;O1!DS%P)!vPB%B|>57?d{j2CduhET1tqhsxP%8%vgv+5;m^=eyPh6>Gh?iue$BwK5 z1z+qhbK^ZP;=1!hn1A%}q4~j#Nk8#hZ}*M!mEDHU1{39^Aj;l!bh9`zPPpsH=ZAPg zpUsZ!pB@=(ZZAEdat4^gfD|WA?-6f}Rl2yk@?eRno4u{ZfonGmSDW?Z!oc_xiBP&p zxIQc@!dohf#@r*T)uQieRH<{*>K(bqAwUU5)ynl@JCyjI`9t5W{wTR*T7S#3)ERMP ztY};MQ6&L3-qWh8&3;2=?=HK5U>RxDtEu=NZc|qfO~X^)lS#L|BIo-B$cYa ziPrhMxN-j>R@dj;`R)Rt-=?_z3i^3H?p*J>rL8XM6Z!|61>%jNp6ew3lSBROvtz@S zWmzbx>`JuhH>-&Z0xn~b_MiCgm7?&K_G9e}}i2mJ)-V{(d>0ArmqPuAzY^Kor< zqKbfl4AxcX0lY3JIcuKYgT1@xGgyP)K?y`C6muF-(7t3jdRWKM**FP+Yh+s#n7ep9 zJ)g|IU2`orz2Wz74maIC3oBp_YnSLf0I-%m6P}tagO#R+`de@w9Og8jH36Xvpm9xG zH~N?9h*=C!kzaNa8iNY3h9^1quksqyE3eRW(-5j`}=GTV*reFG%$dCf+H0qioiw08US+N zbbA1EBnlVv*oLs}jNeTR_I9(~%gWRsucR}r-@?2$3qS!RQ^RsdA%wNr1eO69 zC`QT`JS%xpwsT8n``NJ(3(eS8WNI|ukgb%qy0lMsp{noBpti;PMH!Fd;=*Kk?4P&X zHz7S-KVS8IV?jpa-0ZYNx7@ z6IU315`qvg0wD;<5Tb>02y)5c0V6opC>|ruv{pqO$3m?IWt@%&9Y&FZA`E~&S>ny+60Yl zn2mJ>&`SkgG)&P5!B+GN&u`VcLH2IGo;-oQE0@Q&~~U<%q%R0^mpLvE!AbRrphaFbzQZZa1U55al{glI5cvGwb&z2 z*SFe-ed^-Rcc<6n9#tVdpqBz*`S~ds@6!c<$t!;RBIEjzbOoDnB6l7NpE5@hqidY} z-SK<$i3`>WfEAZ#$oA4FZIf5XE_{@j13nCWLf%Raw-I}I@oelwMBjn#4%I+!kGkzv zS$V;SiABVCx?%{Fkj5|A!8RsXE3o3rGi&IR{aNw5^tYrHQh1)dIW@?8+4?x9OauZd zi%&}lMV6-L#7qvD6+`X=fh~P|;Zei_tb!O^$A9If1acc#{+EPh$I!5HQz90{nwXfF zF^$FHnpp~_dIvfAhM?N~pA@zEClsQPD4r!2%tRju?ADs9LN-CosuItP+eNVuSP|R? zo|mSdFesHy4ww@YgDDQr7qB$?2pg09J~(#ELN?tPtbga?uSaj^W;fu$ zI9n1+eg~PTxw7PxBJa5L2xi46HywyZi?K!Dx9LH4U z>_01lcxP@cbW#?bDtOq{p?#iwj4R-`Dlv%s8>~yS(IeJ0UK}83|J3@%51z zx!YGK(k<=1bX`0c3HpQbL_3GeTvll%NZ0JH-<>Qd4EAoZN0H za1bG572xtFP^_#hJc(CIGb?L5a(k8Za30*l6H;S!rBbl;+-)Dn>l75L8E{wERZ$8U z*TU*w6M#++~CN^U180N+XcOusdsD8Rp=S8VREE5hnf=Ejsp3>fhZ4cWix zU?Y&eU{ruEkV|M3ZY_E?35-p`{dnMk=9y&d!F^RZ+`SjCb|JS9ba&$4KsKVxt<@S| z$h7@E6hVdV({bM)=xn11>g*SaXN-&cw`J;D1!%i`a2HMwe3oyZ6uyDpZhQ`LL^LI6 zwGVsM$XEsX+FKft4nfx>0=ReoZLn( zjMlF1)YM#mQ;rzGNF}-&>(!o%W1|S7X2RicpFDXgzgu*=*+ZgxR?VC)vp;I&B+{wp`fbH|%olg9^Ia<^Efg2Fh-!;i8u=ThS{ zn5YCRI!&k2WX`UOX0ruB$93&BpfCdcU>bu}8om1c0ZQeByge7&jKFga#@j&E9m zO~+f{{{E=HSJR_zmN!?I6`W1kkO}k|+-A|fAKst~5AJzp$Ld2&zE42UO?{MbeoAJt zuFW`ko5%zRY6xiEoBAO}i&$brk5xc`&U>}x%93;C%EDX~!J8m`3f-p(o&9EdIrxSx z&_(wClVcSE&sqsXVTk1k7<4iRL4EJ?nId}+e=a$&4FLo!_s`nIjKJY;lrRk2 z^pmSK6Ce|{*Hsl?J@QGOAyy~DXaxui9j_+_3EjNCEU{gjuH*H-_Er^8lgg}5N6+#4 zi^j`;F}-N~FBD)^vPUaGfetiw(F6*Vp|QCNAaez0_yPC_|s$!1Z58y>BDSf1X=4XQgOpS!TiI{5$+Imx+o7EzjM!bJ-_pv^E=;V5d2lE>o-0)c?f zVzC&GkB@B&3kzqs-__M6=;`T^mzS3}Feg$fmG2XQBm`JnTT@}?e!t%_JUpzftgPfi z00jbaI2`Ks_SSZIczCe2wRJW>KRErMBy)VZ#%PLY+BMYXrLzin%4dy|)!M>jP!MVfzfbYxViRFPqXKBWfO z+uMr(h{a<0>FKG1X(q5qWiS{%qt@2eD$s{}Fbg)gMx^*WH#et`jTc8#+E6HT4b7zB6f~B|;c(a&7Z*Q4scbN4lh}N#)oK7Tm|_7EPBLUf0!=3vMnTz( zTCLWA%))2vPfbmY2u>-(;P<{N7!a7!l$RZ5K0ZGFq|x9solo0%E-m!t%EsHFXih5!K~ed0r!-huK!xy0iNCjRXYKfPrcAki0A00000 LNkvXXu0mjfL)EZn literal 0 HcmV?d00001 diff --git a/lib/usd/ui/layerEditor/resources/target_pressed_150.png b/lib/usd/ui/layerEditor/resources/target_pressed_150.png new file mode 100644 index 0000000000000000000000000000000000000000..6ffa1d37cc394a731deb2007a208afa5bf895127 GIT binary patch literal 1340 zcmV-C1;hG@P)YzrDJ;vhMEg8fRx`jcaRbFF6klSytoj=7NF(iB6|`78VvJb7%j- z!NFQ@Z?E1HU~U5%8X8h5l}e4TudhEmVK$rBO(xTS>+9=Qc41Xj)f1^yD#^^uR3s!M zD6w{TcV{yg4EpBg=0$hzaSPDI#Dpe0J6nV2lamu;V`Jm+@$s?kzBA;_(AL&g4Fk5d zwWY7Guh+Y}%OxOqrntEHDV|qWR;JjOHZ?UR78Mm`#KpyBFb;t~7Z(@*JBDd|d|Z>0 zlcRAyFJwM6QaItSl+eXr`}p{XHa9o*hkjPFT%sar3fM0@8{R&g?htkwb~x!p@oHougC)h`}_Nbva+&afSH+@ zc^(uL^aMPj3zxRHx6R~53-*IUwY9Zs7AAPCrl#fv!n?e@{9R;ZWX9RqnK?f{U*|C( zcxPl}BuL;L&twFo57G zGd!wNslF8AprA0)*JfabcW^NP0u0s|7_3p2Y4`T_Mrq&g@B-HG8}`79I0L?s1lTMV zivj05J3G}v_X+|^NlD=W5qFH3+uGVR&Hx7o2YaANj7JE`z`($d3=~3&^}@nJ1?HWd z9W&DvV^~c>J(pCi9^4>%}ZD#4o%^? zuC9*HCej`r5T!oHvr`H^wfei zmW2>i{MOgk$Ja0_Aw2I$CX7P&3JN2HjFkE}xR+G=8wi<`Fuo9KD*%Rs@j1v#slHTM zSvmPOq4fw5459~}ffq_jN`3-JN_#EKBJ?|xlaos<^T4mHtSlLMQH3mZEFI2ei1XA# zZ@x5?#P0|E=d4l#R8LQjhN-rY!87gc?SB9aYhMsn7E+;e_f~sU<>cgKJ1RAXLZRTs z#>Pw(Iuo_2hWPk+f9eGIDv$k?2rp3{$YV34?0;WhU)$d_cKk;Z4`=Cphr7qzYS0{e z8h6`LbOywkXG}oRzBDv6ynBPj%&~=Kz&18E{-ODtaR|iC%gc*OOG`^6Q?hoK!2kJP zTUch!ZK*4R40LpK4By9@cLL-HjM@`O&;~H4;hHOv3|< zp(&w5cp>azdmyD#b{GgGEHN$1uo#vH2&Dfva?&%`3-``I3)A=|mwV^jd(Zv<|NP7M zA2zTL@TMc04nI0P+4{#jIvVJh-oQQxuV?NdxQJOgiapprmUlNmcE64ECxE#xd^a-p zhL^i5z&+}zys)YO!1bab?@ zt*y;!GMOyBy}dJR6@WSr$5w#Y*jVk28#nYJAt5?0up0FA^i)54_RL&eU2S74fXfQN z;v|6K;K74>wgR-Yv=rUHf8Xrtil7*TF((15XJlmj5D*X$&Q^#WJ9Y%0Iddkav$NCE z-{0@F5>Btc`Sa(49zJ|{TcgqVOYgzgEf!00eSLjLdwcr`cMq(VUat>2cI;SGaBy&h z+`BU~Gb1S}DXF=+x&6!uIHdqA|MKO_@1+9ZUaiLlPSTR38KUwGD$94rJY!fkM!m6hdEDvgeg zHb8M)CnO}8F{gO{`t|EyshLMn0r)B%vEmq906vOfgdpNQWFoj$T3XtZy?gh5jpOm- z#|d3sT{Y6Vp*UQRirNg4fOrpu5i!LSpcn%X^7QG`M*bU?I&|pJx43R>Y)lh_dNGS6 zQ5G4In~Pki3;?Ac>C5(=O=PICmYl`7!v z?d@RYqM{;urS$Rf(Td|aIXOi*CnhFdl1rtqbW&21QBF{9B&y!F<-YBb0#adkf${bA z-6@W93IJ;_H5Y$JTCuq+N@E8~GX<=ALnHdP6lw%=7K#c;1wtmlS(>-6UAt!YW6*}B z<{B6nu;6^*!i7L_oWFyVEsi5YF$E|pFg-o(An5-6`|ZAOv)LR3J#yrTJ!4P|{3N#u z7^4`l(}Vczp4uKdE5uxWE`D^Ret$&7xhkwqW zg)%2ko}}JIubrQtw^3Srk8#8Y_06$F2#uRIZSoc8FDxv4TV7t?kHQ!h7Ur-rWDL{9 z#Khck@~NWJQYtNSb~&1@7MzaD^%^LQL@`WCbErzAR7D*sCq9IR9@pUQ5QI4i&~e^N zU@j>wdGX?fsyS6D0G~#aA&$Z7pwhU8ARu1S)G7xAb#``kD!FAge#_BuD7O?LltOh! zh8#`q@=;4E-4=l_gC}j-vc=!e&(EF~v8gpSHr9vC_5Ats8s2`&ZOi;?EhQjFhxtdv zHc<%(>Z@0;8kcD5lhj%tSk-M62tdt=rLwZp0@BB=i1bMX(EsSsqoI_Bzmy|;|H-xr z$5JcdDqOKVhf6ziXtpaVDrzWidy37tD?q@KJN?+y)byUYfYsRC^}loHjzO%}N?tBY8%0vvpM9c>^wDY@-+a2wErb+x^|*7)=``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eB{%cPc$B+ufqmv#AHW~1=-u7#de!ynYXumUn;u4Lm+g@%AxL%slqxox}|Hm1d z?qsc9t#f)^J)=h7#2*LlN1VLISW|GWG|s?&%fcVCJ2r2;mQ~>{byTB$*Bl0gXO18F z@-qwWi8Sw@_)O8_DGNiwUGYoiqIR5m_EO*20 zy|>5SE@pT1!1X!ocDu+qx56}ng?gRQ%Cg)qm&^1j0T72E{uFT+atO^KlwVp5Jmgc5 zA4Prw!5i!`j$=o|dD;2 zXb@BfuiBe>1(2u%1{ycK+dzmsgyoCHVqaftRWf^wTE#j|)Biry)hZOo+Rm+F-V*n0 zoK`J^QL*q?8`bsYB?y9G1;Ru=Cw0_pkU&KukddLrl?ITu|S2raW@m6`e b<9hr7Aq=)99#J%|00000NkvXXu0mjfGgQR- literal 0 HcmV?d00001 diff --git a/lib/usd/ui/layerEditor/resources/target_regular_200.png b/lib/usd/ui/layerEditor/resources/target_regular_200.png new file mode 100644 index 0000000000000000000000000000000000000000..954479e7c7155bd7efd660b9f5f3f5d7475b467c GIT binary patch literal 589 zcmV-T0)w_Xafjv-HYJz(yf-9E9FkLF(2Gypb+y-UyDsV^o%9%wuY9%9BBEW@(& z^D-`&m>_oGc)*6U$C4I0V4D67z;e z0w->Tu6v3R0PgGwGTSGO$4<-(QV_6%k6n47k?=%)K?(wY3Lk;qp75)nk-*+Nv2>(V z0_47m6i5PxC;>$7yFHh$A_XQR+Pd2)0YJX_Qd89=xR#I`bh{`4lc_p;jZfma&mdUV zQ^+>zT3{-h%;kHHCrtmaCr=^s$p`xZUoJQ3IXiNKy5tCcDQMLy7~y~|$vsf8mJBu| zP~6BsN`)!mP3U{HCk)hg4OIPux+%pEd*hLG=s1ovO=)UdZr?S(ii=lz9wvvZWzE0h biXnUhGqLiSkpMD#00000NkvXXu0mjf=E?dR literal 0 HcmV?d00001 diff --git a/lib/usd/ui/layerEditor/sessionState.cpp b/lib/usd/ui/layerEditor/sessionState.cpp new file mode 100644 index 0000000000..29b3e593c6 --- /dev/null +++ b/lib/usd/ui/layerEditor/sessionState.cpp @@ -0,0 +1,44 @@ +// +// 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 "sessionState.h" + +namespace UsdLayerEditor { + +void SessionState::setAutoHideSessionLayer(bool hideIt) +{ + _autoHideSessionLayer = hideIt; + Q_EMIT autoHideSessionLayerSignal(_autoHideSessionLayer); +} + +void SessionState::setStage(pxr::UsdStageRefPtr const& in_stage) +{ + if (in_stage != _stage) { + _stage = in_stage; + Q_EMIT currentStageChangedSignal(); + } +} + +pxr::SdfLayerRefPtr SessionState::targetLayer() const +{ + if (_stage != nullptr) { + const auto& target = _stage->GetEditTarget(); + return target.GetLayer(); + } else { + return nullptr; + } +} + +} // namespace UsdLayerEditor \ No newline at end of file diff --git a/lib/usd/ui/layerEditor/sessionState.h b/lib/usd/ui/layerEditor/sessionState.h new file mode 100644 index 0000000000..e3f3f92532 --- /dev/null +++ b/lib/usd/ui/layerEditor/sessionState.h @@ -0,0 +1,92 @@ +// +// 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. +// +#ifndef SESSIONSTATE_H +#define SESSIONSTATE_H + +#include "abstractCommandHook.h" + +#include +#include + +#include + +#include +#include +#include + +class QMenu; +class QWidget; + +namespace UsdLayerEditor { + +/** + * @brief Abstract class that wraps the editing session stage, including the stage list, the current + * stage, and app-specific UI + * + */ +class SessionState : public QObject +{ + Q_OBJECT +public: + virtual ~SessionState() { } + + struct StageEntry + { + pxr::UsdStageRefPtr _stage; + std::string _displayName; + std::string _proxyShapePath; + }; + + // properties + virtual bool autoHideSessionLayer() const { return _autoHideSessionLayer; } + virtual void setAutoHideSessionLayer(bool hide); + pxr::UsdStageRefPtr const& stage() const { return _stage; } + pxr::SdfLayerRefPtr targetLayer() const; + virtual void setStage(pxr::UsdStageRefPtr const& in_stage); + virtual AbstractCommandHook* commandHook() = 0; + virtual std::vector allStages() const = 0; + // path to default load layer dialogs to + virtual std::string defaultLoadPath() const = 0; + // ui that returns a list of paths to load + virtual std::vector + loadLayersUI(const QString& title, const std::string& default_path) const = 0; + // ui to save a layer. returns the path and the file format (ex: "usda") + virtual bool + saveLayerUI(QWidget* in_parent, std::string* out_filePath, std::string* out_pFormat) const = 0; + virtual void printLayer(const pxr::SdfLayerRefPtr& layer) const = 0; + + // main API + virtual void setupCreateMenu(QMenu* in_menu) = 0; + // called when an anonymous root layer has been saved to a file + // in this case, the stage needs to be re-created on the new file + virtual void rootLayerPathChanged(std::string const& in_path) = 0; + + bool isValid() { return _stage && _stage->GetRootLayer(); } + +Q_SIGNALS: + void currentStageChangedSignal(); + void stageListChangedSignal(pxr::UsdStageRefPtr const& toSelect = pxr::UsdStageRefPtr()); + void stageRenamedSignal(std::string const& name, pxr::UsdStageRefPtr const& stage); + void autoHideSessionLayerSignal(bool hideIt); + +protected: + pxr::UsdStageRefPtr _stage; + bool _autoHideSessionLayer = true; +}; + +} // namespace UsdLayerEditor + +#endif // SESSIONSTATE_H diff --git a/lib/usd/ui/layerEditor/stageSelectorWidget.cpp b/lib/usd/ui/layerEditor/stageSelectorWidget.cpp new file mode 100644 index 0000000000..e6b7391079 --- /dev/null +++ b/lib/usd/ui/layerEditor/stageSelectorWidget.cpp @@ -0,0 +1,128 @@ +// +// 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 "stageSelectorWidget.h" + +#include "qtUtils.h" +#include "stringResources.h" + +#include + +#include +#include +#include + +Q_DECLARE_METATYPE(pxr::UsdStageRefPtr); + +namespace UsdLayerEditor { + +StageSelectorWidget::StageSelectorWidget(SessionState* in_sessionState, QWidget* in_parent) + : QWidget(in_parent) +{ + auto mainHLayout = new QHBoxLayout(); + QtUtils::initLayoutMargins(mainHLayout); + mainHLayout->setSpacing(DPIScale(4)); + mainHLayout->addWidget(QtUtils::fixedWidget( + new QLabel(StringResources::getAsQString(StringResources::kUsdStage)))); + + _dropDown = new QComboBox(); + mainHLayout->addWidget(_dropDown); + connect( + _dropDown, + static_cast(&QComboBox::currentIndexChanged), + this, + &StageSelectorWidget::selectedIndexChanged); + + setLayout(mainHLayout); + + setSessionState(in_sessionState); +} + +void StageSelectorWidget::setSessionState(SessionState* in_sessionState) +{ + _sessionState = in_sessionState; + connect( + _sessionState, + &SessionState::stageListChangedSignal, + this, + &StageSelectorWidget::updateFromSessionState); + updateFromSessionState(); + connect( + _sessionState, + &SessionState::currentStageChangedSignal, + this, + &StageSelectorWidget::sessionStageChanged); + connect( + _sessionState, &SessionState::stageRenamedSignal, this, &StageSelectorWidget::stageRenamed); + updateFromSessionState(); +} + +pxr::UsdStageRefPtr StageSelectorWidget::selectedStage() +{ + if (_dropDown->currentIndex() != -1) { + auto const& data = _dropDown->currentData(); + return data.value(); + } + + return pxr::UsdStageRefPtr(); +} +// repopulates the combo based on the session stage list +void StageSelectorWidget::updateFromSessionState(pxr::UsdStageRefPtr const& stageToSelect) +{ + QSignalBlocker blocker(_dropDown); + _dropDown->clear(); + auto allStages = _sessionState->allStages(); + for (auto const& stage : allStages) { + _dropDown->addItem(QString(stage._displayName.c_str()), QVariant::fromValue(stage._stage)); + } + if (!stageToSelect) { + const auto& newStage = selectedStage(); + if (newStage != _sessionState->stage()) { + _sessionState->setStage(newStage); + } + } else { + _sessionState->setStage(stageToSelect); + } +} + +// called when combo value is changed by user +void StageSelectorWidget::selectedIndexChanged(int index) +{ + _internalChange = true; + _sessionState->setStage(selectedStage()); + _internalChange = false; +} + +// handles when someone else changes the current stage +// but also called when when do it ourselves +void StageSelectorWidget::sessionStageChanged() +{ + if (!_internalChange) { + auto index = _dropDown->findData(QVariant::fromValue(_sessionState->stage())); + if (index != -1) { + QSignalBlocker blocker(_dropDown); + _dropDown->setCurrentIndex(index); + } + } +} + +void StageSelectorWidget::stageRenamed(std::string const& name, pxr::UsdStageRefPtr const& stage) +{ + auto index = _dropDown->findData(QVariant::fromValue(stage)); + if (index != -1) { + _dropDown->setItemText(index, name.c_str()); + } +} +} // namespace UsdLayerEditor diff --git a/lib/usd/ui/layerEditor/stageSelectorWidget.h b/lib/usd/ui/layerEditor/stageSelectorWidget.h new file mode 100644 index 0000000000..5260f5da88 --- /dev/null +++ b/lib/usd/ui/layerEditor/stageSelectorWidget.h @@ -0,0 +1,59 @@ +// +// 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. +// +#ifndef STAGESELECTORWIDGET_H +#define STAGESELECTORWIDGET_H + +#include "sessionState.h" + +#include +#include + +#include +#include + +namespace UsdLayerEditor { + +class SessionState; + +/** + * @brief Drop down list that allows selecting a stage. Owned by the LayerEditorWidget + * + */ +class StageSelectorWidget : public QWidget +{ + Q_OBJECT +public: + StageSelectorWidget(SessionState* in_sessionState, QWidget* in_parent); + +protected: + void setSessionState(SessionState* in_sessionState); + pxr::UsdStageRefPtr selectedStage(); + + // slot: + void updateFromSessionState(pxr::UsdStageRefPtr const& stageToSelect = pxr::UsdStageRefPtr()); + void stageRenamed(std::string const& name, pxr::UsdStageRefPtr const& stage); + void sessionStageChanged(); + void selectedIndexChanged(int index); + +private: + SessionState* _sessionState = nullptr; + QComboBox* _dropDown = nullptr; + bool _internalChange = false; // for notifications +}; + +} // namespace UsdLayerEditor + +#endif // STAGESELECTORWIDGET_H diff --git a/lib/usd/ui/layerEditor/stringResources.cpp b/lib/usd/ui/layerEditor/stringResources.cpp new file mode 100644 index 0000000000..f531d8c2de --- /dev/null +++ b/lib/usd/ui/layerEditor/stringResources.cpp @@ -0,0 +1,66 @@ +// +// 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 "stringResources.h" + +#include + +#include + +#include + +namespace UsdLayerEditor { + +namespace StringResources { + +std::vector& get_stringResourceIDRegister() +{ + static std::vector s_stringResourceIDRegister {}; + return s_stringResourceIDRegister; +} + +MStatus registerAll() +{ + MStatus status { MStatus::MStatusCode::kSuccess }; + for (auto& stringResourceID : get_stringResourceIDRegister()) { + status = MStringResource::registerString(stringResourceID); + } + return status; +} + +MStringResourceId create(const char* key, const char* value) +{ + MStringResourceId stringResourceID { "mayaUsdPlugin", key, value }; + get_stringResourceIDRegister().push_back(stringResourceID); + return stringResourceID; +} + +MString getAsMString(const MStringResourceId& stringResourceID) +{ + MStatus status { MStatus::MStatusCode::kSuccess }; + MString string = MStringResource::getString(stringResourceID, status); + return string; +} + +QString getAsQString(const MStringResourceId& stringResourceID) +{ + MString str = getAsMString(stringResourceID); + return MQtUtil::toQString(str); +} + +} // namespace StringResources + +} // namespace UsdLayerEditor diff --git a/lib/usd/ui/layerEditor/stringResources.h b/lib/usd/ui/layerEditor/stringResources.h new file mode 100644 index 0000000000..e20ac696e8 --- /dev/null +++ b/lib/usd/ui/layerEditor/stringResources.h @@ -0,0 +1,119 @@ +// +// 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. +// + +#ifndef STRING_RESOURCES_H +#define STRING_RESOURCES_H + +#include +#include +#include +#include + +class QString; + +namespace UsdLayerEditor { + +namespace StringResources { + +// register all strings +MStatus registerAll(); + +// Retrive a string resource from the given MStringResourceId. +MString getAsMString(const MStringResourceId& strResID); + +// Retrive a string resource from the given MStringResourceId. +QString getAsQString(const MStringResourceId& strResID); + +// create a MStringResourceId, must be called before registerAll() +MStringResourceId create(const char* key, const char* value); + +// ------------------------------------------------------------- + +// Same keys definied in MEL file will have priority over C++ keys/values. +// Those with higher priority will end up showing in the UI. + +// clang-format off + +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 kConvertToRelativePath { create("kConvertToRelativePath", "Convert to Relative Path") }; +const auto kCancel { create("kCancel", "Cancel") }; +const auto kClearLayerTitle { create("kclearLayerTitle", "Clear \"^1s\"") }; +const auto kClearLayerConfirmMessage { create("kClearLayerConfirmMessage", + "Are you sure you want to clear this layer?" + " \"^1s\" will remain in the Layer Editor" + " but all contents will be cleared, including sublayer paths.") }; +const auto kCreate { create("kCreate", "Create") }; +const auto kDiscardEditsTitle { create("kDiscardEditsTitle", "Discard Edits \"^1s\"") }; +const auto kDiscardEditsMsg { create("kDiscardEditsMsg", "Are you sure you want to revert these edits?" + " \"^1s\" will revert to its state on disk") }; +const auto kHelp { create("kHelp", "Help") }; +const auto kHelpOnUSDLayerEditor { create("kHelpOnUSDLayerEditor", "Help on USD Layer Editor") }; +const auto kLoadExistingLayer { create("kLoadExistingLayer", "Load an Existing Layer") }; +const auto kLoadSublayersError { create("kLoadSublayersError", "Load Sublayers Error") }; +const auto kLoadSublayersTo { create("kLoadSublayersTo", "Load Sublayers to ^1s") }; +const auto kLoadSublayers { create("kLoadSublayers", "Load Sublayers") }; +const auto kLayerPath { create("kLayerPath", "Layer Path:") }; +const auto kMuteUnmuteLayer { create("kMuteUnmuteLayer", "Mute/unmute the layer. Muted layers are ignored by the stage.")}; +const auto kNoLayers { create("kNoLayers", "No Layers") }; +const auto kNotUndoable { create("kNotUndoable", "You can not undo this action.") }; +const auto kOption { create("kOption", "Option") }; +const auto kPathNotFound { create("kPathNotFound", "Path not found: ") }; +const auto kRealPath { create("kRealPath", "Real Path: ^1s") }; +const auto kRemoveSublayer { create("kRemoveSublayer", "Remove sublayer") }; +const auto kSave { create("kSave", "Save") }; +const auto kSaveAll { create("kSaveAll", "Save All") }; +const auto kSaveAllEditsInLayerStack { create("kSaveAllEditsInLayerStack", "Save all edits in the Layer Stack")}; +const auto kSaveLayer { create("kSaveLayer", "Save Layer") }; +const auto kSaveStage { create("kSaveStage", "Save Stage") }; +const auto kToSaveTheStageSaveAnonym { create("kToSaveTheStageSaveAnonym", "To save the stage, you must save your anonymous layer")}; +const auto kToSaveTheStageSaveAnonyms{ create("kToSaveTheStageSaveAnonyms", "To save the stage, you must save your ^1s anonymous layers")}; +const auto kToSaveTheStageFileWillBeSave { create("kToSaveTheStageFileWillBeSave", + "To save the stage, edits to the following file will be saved.") }; +const auto kToSaveTheStageFilesWillBeSave { create("kToSaveTheStageFilesWillBeSave", + "To save the stage, edits to the following ^1s files will be saved.") }; +const auto kSetLayerAsTargetLayerTooltip { create("kSetLayerAsTargetLayerTooltip", "Set layer as target layer. Edits are added to the target layer.") }; +const auto kUsdLayerIdentifier { create("kUsdLayerIdentifier", "USD Layer identifier: ^1s") }; +const auto kUsdStage { create("kUsdStage", "USD Stage:") }; + +// ------------------------------------------------------------- +// Errors +// ------------------------------------------------------------- + +const auto kErrorCannotAddPathInHierarchy { create("kErrorCannotAddPathInHierarchy", + "Cannot add path \"^1s\" again in the layer hierarchy") }; +const auto kErrorCannotAddPathInHierarchyThrough { create("kErrorCannotAddPathInHierarchyThrough", + "Cannot add path \"^1s\" again in the layer hierarchy through \"^2s\"") }; +const auto kErrorCannotAddPathTwice { create("kErrorCannotAddPathTwice", + "Cannot add path \"^1s\" twice to the layer stack") }; +const auto kErrorFailedToSaveFile { create("kErrorFailedToSaveFile", + "Failed to save file to \"^1s\"") }; +const auto kErrorRecursionDetected { create("kErrorRecursionDetected", + "Recursion detected. Found \"^1s\" multiple times.\n" + "Only added the first instance to the tree view.") }; +const auto kErrorDidNotFind { create("kErrorDidNotFind", + "USD Layer Editor: did not find \"^1s\"\n") }; +const auto kErrorFailedToReloadLayer { create("kErrorFailedToReloadLayer", + "Failed to Reload Layer") }; + +// clang-format on + +} // namespace StringResources + +} // namespace UsdLayerEditor + +#endif // STRING_RESOURCES_H diff --git a/lib/usd/ui/layerEditor/warningDialogs.cpp b/lib/usd/ui/layerEditor/warningDialogs.cpp new file mode 100644 index 0000000000..a41cd77501 --- /dev/null +++ b/lib/usd/ui/layerEditor/warningDialogs.cpp @@ -0,0 +1,96 @@ +// +// 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 "warningDialogs.h" + +#include "qtUtils.h" + +#include + +namespace { + +// 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) +{ + QString bullet = "         -"; + QString text = ""; + if (in_list != nullptr && !in_list->isEmpty()) { + text += "
"; + for (const auto& item : *in_list) + text += bullet + item + "
"; + text += "
"; + } + return text; +} + +} // namespace + +namespace UsdLayerEditor { + +bool confirmDialog_internal( + bool okCancel, + const QString& title, + const QString& message, + const QStringList* bulletList, + const QString* okButtonText) +{ + QMessageBox msgBox; + // there is no title bar text on mac, instead it's bold text + if (IS_MAC_OS) + msgBox.setText(title); + else + msgBox.setWindowTitle(title); + + QString text(message); + text += getLayerBulletList(bulletList); + msgBox.setInformativeText(text); + + if (!IS_MAC_OS) { + auto margins = msgBox.layout()->contentsMargins(); + margins.setTop(0); + msgBox.layout()->setContentsMargins(margins); + } + + if (okCancel) { + msgBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); + msgBox.setDefaultButton(QMessageBox::Cancel); + } else { + msgBox.setStandardButtons(QMessageBox::Ok); + } + msgBox.setStyleSheet(QString("QLabel{min-width: %1px;}").arg(DPIScale(400))); + + if (okButtonText != nullptr) + msgBox.button(QMessageBox::Ok)->setText(*okButtonText); + + return msgBox.exec() == QMessageBox::Ok; +} + +bool confirmDialog( + const QString& title, + const QString& message, + const QStringList* bulletList, + const QString* okButtonText) +{ + return confirmDialog_internal(true, title, message, bulletList, okButtonText); +} + +// create a warning dialog, with an optional bullet list +void warningDialog(const QString& title, const QString& message, const QStringList* bulletList) +{ + confirmDialog_internal(false, title, message, bulletList, nullptr); +} + +} // namespace UsdLayerEditor diff --git a/lib/usd/ui/layerEditor/warningDialogs.h b/lib/usd/ui/layerEditor/warningDialogs.h new file mode 100644 index 0000000000..2c7b7e66cd --- /dev/null +++ b/lib/usd/ui/layerEditor/warningDialogs.h @@ -0,0 +1,43 @@ +// +// 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. +// +#ifndef WARNINGDIALOGS_H +#define WARNINGDIALOGS_H + +#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( + const QString& title, + const QString& message, + const QStringList* bulletList = nullptr, + const QString* okButtonText = nullptr); + +// create a dialog with a single OK button, with an optional bullet list +void warningDialog( + const QString& title, + const QString& message, + const QStringList* bulletList = nullptr); + +} // namespace UsdLayerEditor + +#endif // WARNINGDIALOGS_H diff --git a/plugin/adsk/plugin/plugin.cpp b/plugin/adsk/plugin/plugin.cpp index b6b1b23f6d..dfc348d5f6 100644 --- a/plugin/adsk/plugin/plugin.cpp +++ b/plugin/adsk/plugin/plugin.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -46,7 +47,9 @@ #include #include + #if defined(WANT_QT_BUILD) +#include #include #endif @@ -146,6 +149,15 @@ template void deregisterCommandCheck(MFnPlugin& plugin) } } +MStatus registerStringResources() +{ + MStatus status { MStatus::MStatusCode::kSuccess }; +#if defined(WANT_QT_BUILD) + status = MayaUsd::initStringResources(); +#endif + return status; +} + } // namespace TF_REGISTRY_FUNCTION(UsdMayaShaderReaderRegistry) @@ -163,6 +175,12 @@ MStatus initializePlugin(MObject obj) MStatus status; MFnPlugin plugin(obj, "Autodesk", TOSTRING(MAYAUSD_VERSION), "Any"); + // register string resources + status = plugin.registerUIStrings(registerStringResources, "mayaUSDRegisterStrings"); + if (!status) { + status.perror("mayaUsdPlugin: unable to register string resources."); + } + status = plugin.registerFileTranslator( "USD Import", "", @@ -189,6 +207,7 @@ MStatus initializePlugin(MObject obj) registerCommandCheck(plugin); registerCommandCheck(plugin); registerCommandCheck(plugin); + registerCommandCheck(plugin); status = plugin.registerCommand( MayaUsd::UsdUndoBlockCmd::commandName, MayaUsd::UsdUndoBlockCmd::creator); @@ -341,6 +360,8 @@ MStatus uninitializePlugin(MObject obj) deregisterCommandCheck(plugin); deregisterCommandCheck(plugin); deregisterCommandCheck(plugin); + deregisterCommandCheck(plugin); + MayaUsd::LayerEditorWindowCommand::cleanupOnPluginUnload(); status = plugin.deregisterNode(MayaUsd::ProxyShape::typeId); CHECK_MSTATUS(status); diff --git a/plugin/adsk/scripts/CMakeLists.txt b/plugin/adsk/scripts/CMakeLists.txt index e9b9663d16..6f92c61190 100644 --- a/plugin/adsk/scripts/CMakeLists.txt +++ b/plugin/adsk/scripts/CMakeLists.txt @@ -1,12 +1,14 @@ list(APPEND scripts_src mayaUsdTranslatorImport.mel mayaUsdTranslatorExport.mel + mayaUSDRegisterStrings.mel AEmayaUsdProxyShapeTemplate.mel AETemplateHelpers.py mayaUsdMenu.mel mayaUsd_createStageFromFile.mel mayaUsd_createStageWithNewLayer.py mayaUsd_createStageFromAsset.mel + mayaUsd_layerEditorFileDialogs.mel mayaUsd_pluginUICreation.mel mayaUsd_pluginUIDeletion.mel mayaUsd_pluginBatchLoad.mel diff --git a/plugin/adsk/scripts/mayaUSDRegisterStrings.mel b/plugin/adsk/scripts/mayaUSDRegisterStrings.mel new file mode 100644 index 0000000000..a102c234bc --- /dev/null +++ b/plugin/adsk/scripts/mayaUSDRegisterStrings.mel @@ -0,0 +1,65 @@ +// 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. +// + +proc register(string $key, string $value) { + registerPluginResource("mayaUsdPlugin", $key, $value); +} + +global proc string getMayaUsdString(string $key) { + return getPluginResource("mayaUsdPlugin", $key); +} + +global proc mayaUSDRegisterStrings() { + + register("kAllUsdFiles", "All USD Files"); + register("kCreateUsdStageFromFile", "Create USD Stage from File"); + register("kCreateUsdStageFromFileOptions", "Create USD Stage from File Options"); + register("kCreateStageFromFile", "Create Stage from File"); + register("kExcludePrimPaths", "Exclude Prim Paths:"); + register("kExcludePrimPathsAnn", "Excludes the specified prim paths from the loaded USD data. Multiple prim paths must be separated by a comma."); + register("kExcludePrimPathsSbm", "Excludes the specified prim paths from the loaded USD data"); + register("kLoad", "Load"); + register("kLoadPayloads", "Load Payloads:"); + register("kLoadPayloadsAnn", "When on, loads all prims marked as payloads. When off, all prims marked as payloads and their children are not loaded."); + register("kLoadPayloadsSbm", "Loads prims marked as payloads"); + register("kMenuAddSublayer", "Add Sublayer"); + register("kMenuAddParentLayer", "Add Parent Layer"); + register("kMenuClear", "Clear"); + register("kMenuDiscardEdits", "Discard Edits"); + register("kMenuLoadSublayers", "Load Sublayers..."); + register("kMenuMute", "Mute"); + register("kMenuPrintToScriptEditor", "Print to Script Editor"); + register("kMenuRemove", "Remove"); + register("kMenuSelectPrimsWithSpec", "Select Prims With Spec"); + register("kMenuStageWithNewLayer", "Stage with New Layer"); + register("kMenuStageWithNewLayerAnn", "Create a new, empty USD Stage"); + register("kMenuStageFromFile", "Stage From File..."); + register("kMenuStageFromFileAnn", "Create a USD Stage from an existing USD file"); + register("kMenuSaveAs", "Save As..."); + register("kMenuSaveEdits", "Save Edits"); + register("kMenuUnmute", "Unmute"); + register("kPrimPath", "Prim Path:"); + register("kPrimPathAnn", "Loads the specified prim path. If a matching prim path is not found, all prims in the file are loaded."); + register("kPrimPathSbm", "Loads the specified prim path"); + register("kSaveAndClose", "Save and Close"); + register("kSaveLayerUsdFileFormatAnn", "Select whether the .usd file is written out in binary or ASCII. You can save a file in .usdc (binary) or .usda (ASCII) format. Manually entering a file name with an extension overrides the selection in this drop-down menu."); + register("kSaveLayerUsdFileFormatSbm", "Select whether the .usd file is written out in binary or ASCII"); + register("kTipYouCanChooseMultipleFiles", "Tip: You can choose multiple files."); + register("kUsdFileOptions", "USD File Options"); + register("kUsdFileFormat", ".usd File Format: "); + + // load any localized resources + loadPluginLanguageResources("mayaUsdPlugin", "mayaUsdPlugin.pres.mel"); +} diff --git a/plugin/adsk/scripts/mayaUsdMenu.mel b/plugin/adsk/scripts/mayaUsdMenu.mel index f3cf4bfaca..ea3ef5b3a7 100644 --- a/plugin/adsk/scripts/mayaUsdMenu.mel +++ b/plugin/adsk/scripts/mayaUsdMenu.mel @@ -17,6 +17,7 @@ // globals // variable used to keep track of created menus global string $gMayaUsdCreateSubMenu = ""; +global string $gMayaUsdOpenUsdLayerEditorMenuItem = ""; /////////////////////////////////////////////////////////////////////////////// // findDividerByLabel @@ -33,6 +34,20 @@ proc string findDividerByLabel(string $menuName, string $label) { return ""; } +/////////////////////////////////////////////////////////////////////////////// +// findMenuByName +// search for a menu item by its (localized) label +// see findMenuByName for dividers +proc string findMenuByName(string $menuName, string $label) { + $allMenuItems = `menu -q -itemArray $menuName`; + for ($menuItem in $allMenuItems) { + if ($label == `menuItem -q -label $menuItem`) { + return $menuItem; + } + } + return ""; +} + /////////////////////////////////////////////////////////////////////////////// // addMenuCallback // safely add a post menu callback to a menu @@ -60,8 +75,8 @@ proc removeMenuCallback(string $menuName, string $cmd) { proc initRuntimeCommands() { if (!`runTimeCommand -exists mayaUsdCreateStageWithNewLayer`) { runTimeCommand -default true - -label "Stage with New Layer" - -annotation "Create a new, empty USD Stage" + -label `getMayaUsdString("kMenuStageWithNewLayer")` + -annotation `getMayaUsdString("kMenuStageWithNewLayerAnn")` -category "Menu items.Maya USD" -command "python(\"import mayaUsd_createStageWithNewLayer; mayaUsd_createStageWithNewLayer.createStageWithNewLayer()\")" -image "USD_stage.png" @@ -70,8 +85,8 @@ proc initRuntimeCommands() { if (!`runTimeCommand -exists mayaUsdCreateStageFromFile`) { runTimeCommand -default true - -label "Stage From File..." - -annotation "Create a USD Stage from an existing USD file" + -label `getMayaUsdString("kMenuStageFromFile")` + -annotation `getMayaUsdString("kMenuStageFromFileAnn")` -category "Menu items.Maya USD" -command "mayaUsd_createStageFromFile" -image "USD_stage.png" @@ -86,7 +101,18 @@ proc initRuntimeCommands() { mayaUsdCreateStageFromFileOptions; } + if (!`runTimeCommand -exists mayaUsdOpenUsdLayerEditor`) { + runTimeCommand -default true + -label "USD Layer Editor" + -annotation "Organize and edit USD data in layers" + -category "Menu items.Common.Windows.General Editors" + -command "mayaUsdLayerEditorWindow mayaUsdLayerEditor" + -image "USD_generic.png" + mayaUsdOpenUsdLayerEditor; + } + source "mayaUsd_createStageFromFile.mel"; + source "mayaUsd_layerEditorFileDialogs.mel"; } /////////////////////////////////////////////////////////////////////////////// @@ -124,12 +150,42 @@ global proc mayaUsdMenu_createMenuCallback() { } } +/////////////////////////////////////////////////////////////////////////////// +// mayaUsdMenu_windowMenuCallback +// setup the items in Maya's "Window" menu +global proc mayaUsdMenu_windowMenuCallback() { + global string $gMainWindowMenu; + string $GeneralEditorSubMenu = findMenuByName($gMainWindowMenu, uiRes("m_WindowMenu.kGeneralEditorsLabel")); + addMenuCallback($GeneralEditorSubMenu, "mayaUsdMenu_generalEditorsMenuCallback"); + + removeMenuCallback(`setParent -q -menu`, "mayaUsdMenu_windowMenuCallback"); +} + +/////////////////////////////////////////////////////////////////////////////// +// mayaUsdMenu_windowMenuCallback +// setup the items in Maya's "Window->General Editors" menu +global proc mayaUsdMenu_generalEditorsMenuCallback() { + if (!(`menuItem -query -exists wmUsdLayerEditorMenuitem`)) + { + global string $gMayaUsdOpenUsdLayerEditorMenuItem; + $gMayaUsdOpenUsdLayerEditorMenuItem = `menuItem + -insertAfter wmNamespaceEditor + -enableCommandRepeat false + -version "2021" + -runTimeCommand mayaUsdOpenUsdLayerEditor + wmUsdLayerEditorMenuitem`; + } + removeMenuCallback(`setParent -q -menu`, "mayaUsdMenu_generalEditorsMenuCallback"); +} + /////////////////////////////////////////////////////////////////////////////// // initCreateMenu // setup the items in Maya's "Create" menu proc initCreateMenu() { global string $gMainCreateMenu; // maya's create menu addMenuCallback($gMainCreateMenu, "mayaUsdMenu_createMenuCallback()"); + global string $gMainWindowMenu; + addMenuCallback($gMainWindowMenu, "mayaUsdMenu_windowMenuCallback"); } /////////////////////////////////////////////////////////////////////////////// @@ -142,7 +198,10 @@ proc termCreateMenu() { deleteUI -mi $gMayaUsdCreateSubMenu; $gMayaUsdCreateSubMenu = ""; } - removeMenuCallback($gMainCreateMenu, "mayaUsdMenu_createMenuCallback()"); + global string $gMayaUsdOpenUsdLayerEditorMenuItem; + if ($gMayaUsdOpenUsdLayerEditorMenuItem != "") { + deleteUI -mi $gMayaUsdOpenUsdLayerEditorMenuItem; + } } @@ -162,3 +221,111 @@ global proc mayaUsdMenu_unloadui() { termCreateMenu(); } +/////////////////////////////////////////////////////////////////////////////// +// menu for layer editor +// +proc string makeCommand(string $panelName, string $command) { + return "mayaUsdLayerEditorWindow -edit -" + $command + " " + $panelName + ";"; +} + +global proc mayaUsdMenu_layerEditorContextMenu(string $panelName) { + + int $invalidLayer = `mayaUsdLayerEditorWindow -q -isInvalidLayer $panelName`; + string $cmd; + + if ($invalidLayer) { + $cmd = makeCommand($panelName, "removeSubLayer"); + menuItem -label "Remove" -c $cmd; + return; // that's all we can support on invalid layers + } + + int $isSessionLayer = `mayaUsdLayerEditorWindow -q -isSessionLayer $panelName`; + int $isAnonymousLayer = `mayaUsdLayerEditorWindow -q -isAnonymousLayer $panelName`; + int $needsSaving = `mayaUsdLayerEditorWindow -q -layerNeedsSaving $panelName`; + int $singleSelect = `mayaUsdLayerEditorWindow -q -selectionLength $panelName` == 1; + int $isDirty = `mayaUsdLayerEditorWindow -q -isLayerDirty $panelName`; + int $appearsMuted = `mayaUsdLayerEditorWindow -q -layerAppearsMuted $panelName`; + int $isSubLayer = `mayaUsdLayerEditorWindow -q -isSubLayer $panelName`; + int $isMuted = `mayaUsdLayerEditorWindow -q -layerIsMuted $panelName`; + + string $label; + int $enabled; + + if (!$isSessionLayer) { + if ($isAnonymousLayer) + $label = getMayaUsdString("kMenuSaveAs"); + else + $label = getMayaUsdString("kMenuSaveEdits"); + $enabled = $singleSelect && $needsSaving; + $cmd = makeCommand($panelName, "saveEdits"); + menuItem -label $label -enable $enabled -c $cmd; + + + } + + $label = getMayaUsdString("kMenuDiscardEdits"); + $cmd = makeCommand($panelName, "discardEdits"); + $enabled = $isDirty; + menuItem -label $label -enable $enabled -c $cmd; + + menuItem -divider 1; + + $label = getMayaUsdString("kMenuAddSublayer"); + $cmd = makeCommand($panelName, "addAnonymousSublayer"); + $enabled = $appearsMuted == 0; + menuItem -label $label -enable $enabled -c $cmd; + + $label = getMayaUsdString("kMenuAddParentLayer"); + $cmd = makeCommand($panelName, "addParentLayer"); + $enabled = $isSubLayer && $appearsMuted == 0; + menuItem -label $label -enable $enabled -c $cmd; + + $label = getMayaUsdString("kMenuLoadSublayers"); + $cmd = makeCommand($panelName, "loadSubLayers"); + $enabled = $singleSelect && !$appearsMuted; + menuItem -label $label -enable $enabled -c $cmd; + + menuItem -divider 1; + + if ($isSubLayer) { + if ($isMuted) + $label = getMayaUsdString("kMenuUnmute"); + else + $label = getMayaUsdString("kMenuMute"); + $cmd = makeCommand($panelName, "muteLayer"); + $enabled = 1; + menuItem -label $label -enable $enabled -c $cmd; + } + + $label = getMayaUsdString("kMenuPrintToScriptEditor"); + $cmd = makeCommand($panelName, "printLayer"); + $enabled = 1; + menuItem -label $label -enable $enabled -c $cmd; + + menuItem -divider 1; + + $label = getMayaUsdString("kMenuSelectPrimsWithSpec"); + $cmd = makeCommand($panelName, "selectPrimsWithSpec"); + $enabled = 1; + menuItem -label $label -enable $enabled -c $cmd; + + // if we add any more menus, add a divider + if ($isSubLayer || !$isAnonymousLayer) + menuItem -divider 1; + + if ($isSubLayer) { + $label = getMayaUsdString("kMenuRemove"); + $cmd = makeCommand($panelName, "removeSubLayer"); + $enabled = 1; + menuItem -label $label -enable $enabled -c $cmd; + } + + + if (!$isAnonymousLayer) { + $label = getMayaUsdString("kMenuClear"); + $cmd = makeCommand($panelName, "clearLayer"); + $enabled = 1; + menuItem -label $label -enable $enabled -c $cmd; + } +} + diff --git a/plugin/adsk/scripts/mayaUsd_createStageFromFile.mel b/plugin/adsk/scripts/mayaUsd_createStageFromFile.mel index 934398a87c..1640d9531a 100644 --- a/plugin/adsk/scripts/mayaUsd_createStageFromFile.mel +++ b/plugin/adsk/scripts/mayaUsd_createStageFromFile.mel @@ -34,18 +34,18 @@ global proc string stageFromFile_UISetup(string $parent) { setParent $parent; string $layout = `scrollLayout -childResizable true`; - frameLayout -label "USD File Options" -collapsable false; - textFieldGrp -l "Prim Path:" - -ann "Loads the specified prim path. If a matching prim path is not found, all prims in the file are loaded." - -sbm "Loads the specified prim path" + frameLayout -label `getMayaUsdString("kUsdFileOptions")` -collapsable false; + textFieldGrp -l `getMayaUsdString("kPrimPath")` + -ann `getMayaUsdString("kPrimPathAnn")` + -sbm `getMayaUsdString("kPrimPathSbm")` primPathField; - textFieldGrp -l "Exclude Prim Paths:" - -ann "Excludes the specified prim paths from the loaded USD data. Multiple prim paths must be separated by a comma." - -sbm "Excludes the specified prim paths from the loaded USD data" + textFieldGrp -l `getMayaUsdString("kExcludePrimPaths")` + -ann `getMayaUsdString("kExcludePrimPathsAnn")` + -sbm `getMayaUsdString("kExcludePrimPathsSbm")` excludePrimPathField; - checkBoxGrp -l "Load Payloads:" - -ann "When on, loads all prims marked as payloads. When off, all prims marked as payloads and their children are not loaded." - -sbm "Loads prims marked as payloads" + checkBoxGrp -l `getMayaUsdString("kLoadPayloads")` + -ann `getMayaUsdString("kLoadPayloadsAnn")` + -sbm `getMayaUsdString("kLoadPayloadsSbm")` loadPayloadsCheckBox; return $layout; @@ -144,17 +144,17 @@ global proc mayaUsd_createStageFromFileOptions() $parent; string $applyCloseBtn = getOptionBoxApplyAndCloseBtn(); - button -edit -label "Create Stage from File" + button -edit -label `getMayaUsdString("kCreateStageFromFile")` -command ($callback + " " + $parent + " " + 1 + "; hideOptionBox;") $applyCloseBtn; string $applyBtn = getOptionBoxApplyBtn(); - button -edit -label "Save and Close" + button -edit -label `getMayaUsdString("kSaveAndClose")` -command ($callback + " " + $parent + " " + 0 + "; hideOptionBox;") $applyBtn; string $cancelBtn = getOptionBoxCloseBtn(); - button -edit -label "Cancel" + button -edit -label `getMayaUsdString("kCancel")` -command ("hideOptionBox;") $cancelBtn; @@ -168,7 +168,7 @@ global proc mayaUsd_createStageFromFileOptions() -command ($callback + " " + $parent + " " + 0) $saveBtn; - setOptionBoxTitle("Create USD Stage from File Options"); + setOptionBoxTitle(getMayaUsdString("kCreateUsdStageFromFileOptions")); setOptionBoxHelpTag( "" ); @@ -180,11 +180,15 @@ global proc mayaUsd_createStageFromFileOptions() global proc mayaUsd_createStageFromFile() { setOptionVars(false); + $caption = getMayaUsdString("kCreateUsdStageFromFile"); + $fileFilter = getMayaUsdString("kAllUsdFiles") + " (*.usd *.usda *.usdc *.usdz);;*.usd;;*.usda;;*.usdc;;*.usdz"; + $okCaption = getMayaUsdString("kCreate"); + string $result[] = `fileDialog2 -fileMode 1 - -caption "Create USD Stage from File" - -fileFilter "All USD Files (*.usd *.usda *.usdc *.usdz);;*.usd;;*.usda;;*.usdc;;*.usdz" - -okCaption "Create" + -caption $caption + -fileFilter $fileFilter + -okCaption $okCaption -optionsUICreate "stageFromFile_UISetup" -optionsUIInit "stageFromFile_UIInit" -optionsUICommit "stageFromFile_UICommit"`; diff --git a/plugin/adsk/scripts/mayaUsd_layerEditorFileDialogs.mel b/plugin/adsk/scripts/mayaUsd_layerEditorFileDialogs.mel new file mode 100644 index 0000000000..eb18342008 --- /dev/null +++ b/plugin/adsk/scripts/mayaUsd_layerEditorFileDialogs.mel @@ -0,0 +1,109 @@ +// 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. +// + +global proc string UsdLayerEditor_SaveLayerFileDialogOptions_Create(string $parent) { + setParent $parent; + + string $layout = `scrollLayout -childResizable true`; + + frameLayout -label `getMayaUsdString("kUsdFileOptions")` -collapsable false; + + radioButtonGrp -numberOfRadioButtons 2 + // adding some space at the end of label, otherwise it's too tight + -label `getMayaUsdString("kUsdFileFormat")` -labelArray2 "Binary" "ASCII" + -vertical + -ann `getMayaUsdString("kSaveLayerUsdFileFormatAnn")` + -sbm `getMayaUsdString("kSaveLayerUsdFileFormatSbm")` + formatSelector; + + return $layout; +} + +global proc int UsdLayerEditor_SaveLayerFileDialog_binary() { + string $varName = "UsdLayerEditor_SaveLayerFileDialogOptions_binary"; + if (!`optionVar -exists $varName`) { + optionVar -intValue $varName 1; + } + + return `optionVar -q $varName`; +} + +global proc UsdLayerEditor_SaveLayerFileDialogOptions_UIInit(string $parent, string $filterType) { + setParent $parent; + + int $binary = UsdLayerEditor_SaveLayerFileDialog_binary(); + if ($binary) { + radioButtonGrp -e -select 1 formatSelector; + } else { + radioButtonGrp -e -select 2 formatSelector; + } +} + +global proc UsdLayerEditor_SaveLayerFileDialogOptions_UICommit(string $parent) { + setParent $parent; + + string $varName = "UsdLayerEditor_SaveLayerFileDialogOptions_binary"; + optionVar -intValue $varName (`radioButtonGrp -q -select formatSelector` == 1); + + +} +global proc string UsdLayerEditor_SaveLayerFileDialog() { + + string $fileFilter = getMayaUsdString("kAllUsdFiles") + " (*.usd *.usda *.usdc );;*.usd;;*.usda;;*.usdc"; + + + // force non-native style, because otherwise you can't chose between ascii and binary for .usd + string $result[] = `fileDialog2 + -fileMode 0 + -fileFilter $fileFilter -dialogStyle 2 + -optionsUICreate "UsdLayerEditor_SaveLayerFileDialogOptions_Create" + -optionsUIInit "UsdLayerEditor_SaveLayerFileDialogOptions_UIInit" + -optionsUICommit "UsdLayerEditor_SaveLayerFileDialogOptions_UICommit" + `; + + if (size($result) > 0) { + return $result[0]; + } else { + return ""; + } +} + +global proc string UsdLayerEditor_LoadLayersFileDialogOptions_Create(string $parent) { + setParent $parent; + + string $layout = `scrollLayout -childResizable true`; + + frameLayout -collapsable false -labelVisible false -marginHeight 20 -marginWidth 20; + + text -label `getMayaUsdString("kTipYouCanChooseMultipleFiles")` -align "left"; + return $layout; +} + + +global proc string[] UsdLayerEditor_LoadLayersFileDialog(string $title, string $folder) { + string $fileFilter = getMayaUsdString("kAllUsdFiles") + " (*.usd *.usda *.usdc );;*.usd;;*.usda;;*.usdc"; + $okCaption = getMayaUsdString("kLoad"); + + string $result[] = `fileDialog2 + -caption $title + -fileMode 4 + -okCaption $okCaption + -fileFilter $fileFilter -dialogStyle 2 + -optionsUICreate "UsdLayerEditor_LoadLayersFileDialogOptions_Create" + -startingDirectory $folder + `; + + return $result; +} From c9a7e1330d41c4a76da363c73952240fd08eef2f Mon Sep 17 00:00:00 2001 From: Luc-Eric Rousseau Date: Tue, 8 Dec 2020 21:09:52 -0500 Subject: [PATCH 2/6] clang-format --- plugin/adsk/plugin/plugin.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/adsk/plugin/plugin.cpp b/plugin/adsk/plugin/plugin.cpp index dfc348d5f6..99f9794459 100644 --- a/plugin/adsk/plugin/plugin.cpp +++ b/plugin/adsk/plugin/plugin.cpp @@ -49,8 +49,8 @@ #include #if defined(WANT_QT_BUILD) -#include #include +#include #endif #if defined(WANT_UFE_BUILD) From efefccc42b338cb6a009ae03393b5d61c1f264e0 Mon Sep 17 00:00:00 2001 From: Luc-Eric Rousseau Date: Tue, 8 Dec 2020 23:46:04 -0500 Subject: [PATCH 3/6] fix linux build --- lib/usd/ui/layerEditor/mayaCommandHook.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/usd/ui/layerEditor/mayaCommandHook.cpp b/lib/usd/ui/layerEditor/mayaCommandHook.cpp index c251c57573..70fbcb4c37 100644 --- a/lib/usd/ui/layerEditor/mayaCommandHook.cpp +++ b/lib/usd/ui/layerEditor/mayaCommandHook.cpp @@ -99,7 +99,7 @@ void MayaCommandHook::insertSubLayerPath(UsdLayer usdLayer, Path path, int index void MayaCommandHook::removeSubLayerPath(UsdLayer usdLayer, Path path) { size_t index = usdLayer->GetSubLayerPaths().Find(path); - assert(index != -1); + assert(index != static_cast(-1)); std::string cmd; cmd = "mayaUsdLayerEditor -edit -removeSubPath "; cmd += std::to_string(index); From 7f99cb27c049549a6a7e0ec92299c0fb88153bbb Mon Sep 17 00:00:00 2001 From: Luc-Eric Rousseau Date: Wed, 9 Dec 2020 14:55:50 -0500 Subject: [PATCH 4/6] fix for target icon on windows --- lib/usd/ui/layerEditor/resources.qrc | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/usd/ui/layerEditor/resources.qrc b/lib/usd/ui/layerEditor/resources.qrc index e912d27477..0cb7f9c906 100644 --- a/lib/usd/ui/layerEditor/resources.qrc +++ b/lib/usd/ui/layerEditor/resources.qrc @@ -11,22 +11,22 @@ resources/save_all_pressed_150.png resources/save_all_pressed_200.png - resources/target_hover_100.png + resources/target_hover_100.png resources/target_hover_150.png resources/target_hover_200.png - resources/target_on_100.png + resources/target_on_100.png resources/target_on_150.png resources/target_on_200.png - resources/target_on_hover_100.png + resources/target_on_hover_100.png resources/target_on_hover_150.png resources/target_on_hover_200.png - resources/target_on_pressed_100.png + resources/target_on_pressed_100.png resources/target_on_pressed_150.png resources/target_on_pressed_200.png - resources/target_pressed_100.png + resources/target_pressed_100.png resources/target_pressed_150.png resources/target_pressed_200.png - resources/target_regular_100.png + resources/target_regular_100.png resources/target_regular_150.png resources/target_regular_200.png From aaacf1a331a52cd7d83354b36b84f30502fba2d4 Mon Sep 17 00:00:00 2001 From: Luc-Eric Rousseau Date: Wed, 9 Dec 2020 15:49:54 -0500 Subject: [PATCH 5/6] code review feedback --- .../commands/abstractLayerEditorWindow.h | 6 ++-- .../commands/layerEditorWindowCommand.cpp | 4 +-- .../ui/layerEditor/dirtyLayersCountBadge.cpp | 2 +- .../ui/layerEditor/dirtyLayersCountBadge.h | 2 +- .../ui/layerEditor/generatedIconButton.cpp | 28 ++++++++--------- lib/usd/ui/layerEditor/generatedIconButton.h | 6 ++-- lib/usd/ui/layerEditor/layerTreeItem.cpp | 6 ++-- .../ui/layerEditor/layerTreeItemDelegate.cpp | 3 -- plugin/adsk/scripts/mayaUsdMenu.mel | 30 +++++++++---------- 9 files changed, 41 insertions(+), 46 deletions(-) diff --git a/lib/mayaUsd/commands/abstractLayerEditorWindow.h b/lib/mayaUsd/commands/abstractLayerEditorWindow.h index 5ab5fa9731..6402137909 100644 --- a/lib/mayaUsd/commands/abstractLayerEditorWindow.h +++ b/lib/mayaUsd/commands/abstractLayerEditorWindow.h @@ -11,10 +11,10 @@ namespace MAYAUSD_NS_DEF { class AbstractLayerEditorWindow; /** - * @brief abract class used by layer editor window command to create and get + * @brief Abstract class used by layer editor window command to create and get * the layer editor windows. - * this allows breaking the circular dependency between between maya usd lib - * and UI libraries + * This allows breaking the circular dependency between between maya usd lib + * and UI libraries. * */ class MAYAUSD_CORE_PUBLIC AbstractLayerEditorCreator diff --git a/lib/mayaUsd/commands/layerEditorWindowCommand.cpp b/lib/mayaUsd/commands/layerEditorWindowCommand.cpp index f28851a702..160531f62b 100644 --- a/lib/mayaUsd/commands/layerEditorWindowCommand.cpp +++ b/lib/mayaUsd/commands/layerEditorWindowCommand.cpp @@ -65,7 +65,6 @@ AbstractLayerEditorCreator* AbstractLayerEditorCreator::_instance = nullptr; AbstractLayerEditorCreator* AbstractLayerEditorCreator::instance() { - // return AbstractLayerEditorCreator::_instance; } @@ -98,7 +97,6 @@ const char LayerEditorWindowCommand::commandName[] = "mayaUsdLayerEditorWindow"; // plug-in callback to create the command object void* LayerEditorWindowCommand::creator() { - // return static_cast(new LayerEditorWindowCommand()); } @@ -322,7 +320,7 @@ MStatus LayerEditorWindowCommand::handleEdits( AbstractLayerEditorWindow* layerEditor) { // - // this methis is written so that it'll return an error + // this method is written so that it'll return an error // if a query flag is used in non-query mode. bool notEdit = !argParser.isEdit(); MString errorMsg("Need -edit mode for parameter "); diff --git a/lib/usd/ui/layerEditor/dirtyLayersCountBadge.cpp b/lib/usd/ui/layerEditor/dirtyLayersCountBadge.cpp index 4602baf2d8..5c239dc6d3 100644 --- a/lib/usd/ui/layerEditor/dirtyLayersCountBadge.cpp +++ b/lib/usd/ui/layerEditor/dirtyLayersCountBadge.cpp @@ -86,4 +86,4 @@ void DirtyLayersCountBadge::paintEvent(QPaintEvent* event) } } -} // namespace UsdLayerEditor \ No newline at end of file +} // namespace UsdLayerEditor diff --git a/lib/usd/ui/layerEditor/dirtyLayersCountBadge.h b/lib/usd/ui/layerEditor/dirtyLayersCountBadge.h index 1ae52eaa17..4198736077 100644 --- a/lib/usd/ui/layerEditor/dirtyLayersCountBadge.h +++ b/lib/usd/ui/layerEditor/dirtyLayersCountBadge.h @@ -21,7 +21,7 @@ namespace UsdLayerEditor { /** - * @brief Widget that apears on top of the Save Layer button, to show how many layers need to be + * @brief Widget that appears on top of the Save Layer button, to show how many layers need to be *saved * **/ diff --git a/lib/usd/ui/layerEditor/generatedIconButton.cpp b/lib/usd/ui/layerEditor/generatedIconButton.cpp index 906c90721d..64a7abe182 100644 --- a/lib/usd/ui/layerEditor/generatedIconButton.cpp +++ b/lib/usd/ui/layerEditor/generatedIconButton.cpp @@ -41,24 +41,22 @@ struct LUTS // look up table to apply to HS[V] value void generateVLut(LUTS& luts, PixmapType pixmapType) { - { - const int HIGH_LIMIT = 205, LOW_LIMIT = 30, MAX_VALUE = 255; - const int ADJUSTEMENT_VALUE = MAX_VALUE - HIGH_LIMIT; - - int newValue; - for (int v = 0; v < 256; v++) { - newValue = v; - if (v > LOW_LIMIT) { // value below this limit will not be adjusted - if (v < HIGH_LIMIT) { // value above this limit will just max up to 255 - if (pixmapType != PixmapType::DISABLED) { - newValue = v + ADJUSTEMENT_VALUE; - } - } else { - newValue = MAX_VALUE; + const int HIGH_LIMIT = 205, LOW_LIMIT = 30, MAX_VALUE = 255; + const int ADJUSTEMENT_VALUE = MAX_VALUE - HIGH_LIMIT; + + int newValue; + for (int v = 0; v < 256; v++) { + newValue = v; + if (v > LOW_LIMIT) { // value below this limit will not be adjusted + if (v < HIGH_LIMIT) { // value above this limit will just max up to 255 + if (pixmapType != PixmapType::DISABLED) { + newValue = v + ADJUSTEMENT_VALUE; } + } else { + newValue = MAX_VALUE; } - luts.valueLut[v] = newValue; } + luts.valueLut[v] = newValue; } } diff --git a/lib/usd/ui/layerEditor/generatedIconButton.h b/lib/usd/ui/layerEditor/generatedIconButton.h index e89eabfbba..749f336b06 100644 --- a/lib/usd/ui/layerEditor/generatedIconButton.h +++ b/lib/usd/ui/layerEditor/generatedIconButton.h @@ -14,8 +14,8 @@ // limitations under the License. // -#ifndef GENERATEDBUTTON_H -#define GENERATEDBUTTON_H +#ifndef GENERATEDICONBUTTON_H +#define GENERATEDICONBUTTON_H #include #include @@ -47,4 +47,4 @@ class GeneratedIconButton : public QAbstractButton }; } // namespace UsdLayerEditor -#endif // GENERATEDBUTTON_H +#endif // GENERATEDICONBUTTON_H diff --git a/lib/usd/ui/layerEditor/layerTreeItem.cpp b/lib/usd/ui/layerEditor/layerTreeItem.cpp index 62cecfbcc1..9bea0bb2b0 100644 --- a/lib/usd/ui/layerEditor/layerTreeItem.cpp +++ b/lib/usd/ui/layerEditor/layerTreeItem.cpp @@ -207,9 +207,11 @@ bool LayerTreeItem::isMovable() const bool LayerTreeItem::needsSaving() const { - if (_layer) - if (!isSessionLayer()) + if (_layer) { + if (!isSessionLayer()) { return isDirty() || isAnonymous(); + } + } return false; } diff --git a/lib/usd/ui/layerEditor/layerTreeItemDelegate.cpp b/lib/usd/ui/layerEditor/layerTreeItemDelegate.cpp index 236a223340..cf6032aa8e 100644 --- a/lib/usd/ui/layerEditor/layerTreeItemDelegate.cpp +++ b/lib/usd/ui/layerEditor/layerTreeItemDelegate.cpp @@ -20,9 +20,6 @@ #include -namespace { -} - namespace UsdLayerEditor { LayerTreeItemDelegate::LayerTreeItemDelegate(LayerTreeView* in_parent) diff --git a/plugin/adsk/scripts/mayaUsdMenu.mel b/plugin/adsk/scripts/mayaUsdMenu.mel index ea3ef5b3a7..56a4518167 100644 --- a/plugin/adsk/scripts/mayaUsdMenu.mel +++ b/plugin/adsk/scripts/mayaUsdMenu.mel @@ -288,13 +288,13 @@ global proc mayaUsdMenu_layerEditorContextMenu(string $panelName) { menuItem -divider 1; if ($isSubLayer) { - if ($isMuted) - $label = getMayaUsdString("kMenuUnmute"); - else - $label = getMayaUsdString("kMenuMute"); - $cmd = makeCommand($panelName, "muteLayer"); - $enabled = 1; - menuItem -label $label -enable $enabled -c $cmd; + if ($isMuted) + $label = getMayaUsdString("kMenuUnmute"); + else + $label = getMayaUsdString("kMenuMute"); + $cmd = makeCommand($panelName, "muteLayer"); + $enabled = 1; + menuItem -label $label -enable $enabled -c $cmd; } $label = getMayaUsdString("kMenuPrintToScriptEditor"); @@ -315,17 +315,17 @@ global proc mayaUsdMenu_layerEditorContextMenu(string $panelName) { if ($isSubLayer) { $label = getMayaUsdString("kMenuRemove"); - $cmd = makeCommand($panelName, "removeSubLayer"); - $enabled = 1; - menuItem -label $label -enable $enabled -c $cmd; - } + $cmd = makeCommand($panelName, "removeSubLayer"); + $enabled = 1; + menuItem -label $label -enable $enabled -c $cmd; + } if (!$isAnonymousLayer) { $label = getMayaUsdString("kMenuClear"); - $cmd = makeCommand($panelName, "clearLayer"); - $enabled = 1; - menuItem -label $label -enable $enabled -c $cmd; - } + $cmd = makeCommand($panelName, "clearLayer"); + $enabled = 1; + menuItem -label $label -enable $enabled -c $cmd; + } } From 398a8ccac82065e497d4884b93378e3520a7cd05 Mon Sep 17 00:00:00 2001 From: Luc-Eric Rousseau Date: Wed, 9 Dec 2020 16:04:00 -0500 Subject: [PATCH 6/6] fix for menu not working after new scene --- lib/usd/ui/layerEditor/mayaLayerEditorWindow.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/usd/ui/layerEditor/mayaLayerEditorWindow.cpp b/lib/usd/ui/layerEditor/mayaLayerEditorWindow.cpp index 107e522730..c5aafc86f2 100644 --- a/lib/usd/ui/layerEditor/mayaLayerEditorWindow.cpp +++ b/lib/usd/ui/layerEditor/mayaLayerEditorWindow.cpp @@ -130,12 +130,6 @@ MayaLayerEditorWindow::MayaLayerEditorWindow(const char* panelName, QWidget* par &MayaSessionState::clearUIOnSceneResetSignal, this, &MayaLayerEditorWindow::onClearUIOnSceneReset); - - connect( - treeView(), - &QWidget::customContextMenuRequested, - this, - &MayaLayerEditorWindow::onShowContextMenu); } MayaLayerEditorWindow::~MayaLayerEditorWindow() { _sessionState.unregisterNotifications(); } @@ -160,6 +154,12 @@ void MayaLayerEditorWindow::onCreateUI() setCentralWidget(_layerEditor); _layerEditor->show(); _sessionState.registerNotifications(); + + connect( + treeView(), + &QWidget::customContextMenuRequested, + this, + &MayaLayerEditorWindow::onShowContextMenu); } LayerTreeView* MayaLayerEditorWindow::treeView() { return _layerEditor->layerTree(); }