/* * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or * its licensors. * * For complete copyright and license terms please see the LICENSE at the root of this * distribution (the "License"). All use of this software is governed by the License, * or, if provided, by the license below or the license accompanying this file. Do not * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * */ #include #include #include #include #include #include #include #include #include namespace EMotionFX { int SkeletonOutlinerPlugin::s_iconSize = 16; SkeletonOutlinerPlugin::SkeletonOutlinerPlugin() : EMStudio::DockWidgetPlugin() , m_mainWidget(nullptr) , m_noSelectionLabel(nullptr) { } SkeletonOutlinerPlugin::~SkeletonOutlinerPlugin() { for (MCore::Command::Callback* callback : m_commandCallbacks) { CommandSystem::GetCommandManager()->RemoveCommandCallback(callback, true); } m_commandCallbacks.clear(); SkeletonOutlinerNotificationBus::Broadcast(&SkeletonOutlinerNotifications::SingleNodeSelectionChanged, nullptr, nullptr); EMotionFX::SkeletonOutlinerRequestBus::Handler::BusDisconnect(); } bool SkeletonOutlinerPlugin::Init() { m_mainWidget = new QWidget(mDock); QVBoxLayout* mainLayout = new QVBoxLayout(); m_mainWidget->setLayout(mainLayout); m_noSelectionLabel = new QLabel("Select an actor instance"); m_noSelectionLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); mainLayout->addWidget(m_noSelectionLabel, 0, Qt::AlignCenter); m_searchWidget = new AzQtComponents::FilteredSearchWidget(m_mainWidget); mainLayout->addWidget(m_searchWidget); m_skeletonModel = AZStd::make_unique(); m_treeView = new ReselectingTreeView(); m_treeView->setObjectName("EMFX.SkeletonOutlinerPlugin.SkeletonOutlinerTreeView"); m_filterProxyModel = new SkeletonSortFilterProxyModel(m_skeletonModel.get(), &m_skeletonModel->GetSelectionModel(), m_treeView); m_filterProxyModel->setFilterKeyColumn(-1); m_filterProxyModel->setFilterCaseSensitivity(Qt::CaseSensitivity::CaseInsensitive); m_treeView->setModel(m_filterProxyModel); m_treeView->setSelectionModel(m_filterProxyModel->GetSelectionProxyModel()); m_filterProxyModel->ConnectFilterWidget(m_searchWidget); m_treeView->setSelectionBehavior(QAbstractItemView::SelectionBehavior::SelectRows); m_treeView->setSelectionMode(QAbstractItemView::ExtendedSelection); m_treeView->setContextMenuPolicy(Qt::DefaultContextMenu); m_treeView->setExpandsOnDoubleClick(false); QHeaderView* header = m_treeView->header(); header->setStretchLastSection(false); header->resizeSection(1, s_iconSize); header->resizeSection(2, s_iconSize); header->resizeSection(3, s_iconSize); header->resizeSection(4, s_iconSize); header->setSectionResizeMode(0, QHeaderView::Stretch); header->hide(); m_treeView->setContextMenuPolicy(Qt::CustomContextMenu); connect(m_treeView, &QTreeView::customContextMenuRequested, this, &SkeletonOutlinerPlugin::OnContextMenu); connect(m_treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &SkeletonOutlinerPlugin::OnSelectionChanged); // Connect after the tree view connected to the model. connect(m_skeletonModel.get(), &QAbstractItemModel::modelReset, this, &SkeletonOutlinerPlugin::Reinit); connect(m_searchWidget, &AzQtComponents::FilteredSearchWidget::TextFilterChanged, this, &SkeletonOutlinerPlugin::OnTextFilterChanged); connect(m_searchWidget, &AzQtComponents::FilteredSearchWidget::TypeFilterChanged, this, &SkeletonOutlinerPlugin::OnTypeFilterChanged); mainLayout->addWidget(m_treeView); mDock->setWidget(m_mainWidget); EMotionFX::SkeletonOutlinerRequestBus::Handler::BusConnect(); Reinit(); m_commandCallbacks.emplace_back(new DataChangedCallback(/*executePreUndo*/ false)); CommandSystem::GetCommandManager()->RegisterCommandCallback(CommandAddCollider::s_commandName, m_commandCallbacks.back()); m_commandCallbacks.emplace_back(new DataChangedCallback(/*executePreUndo*/ false)); CommandSystem::GetCommandManager()->RegisterCommandCallback(CommandRemoveCollider::s_commandName, m_commandCallbacks.back()); m_commandCallbacks.emplace_back(new DataChangedCallback(/*executePreUndo*/ false)); CommandSystem::GetCommandManager()->RegisterCommandCallback(CommandAddRagdollJoint::s_commandName, m_commandCallbacks.back()); m_commandCallbacks.emplace_back(new DataChangedCallback(/*executePreUndo*/ false)); CommandSystem::GetCommandManager()->RegisterCommandCallback(CommandRemoveRagdollJoint::s_commandName, m_commandCallbacks.back()); return true; } void SkeletonOutlinerPlugin::Reinit() { ActorInstance* actorInstance = m_skeletonModel ? m_skeletonModel->GetActorInstance() : nullptr; if (actorInstance) { m_treeView->setVisible(true); m_searchWidget->setVisible(true); m_noSelectionLabel->setVisible(false); } else { m_treeView->setVisible(false); m_searchWidget->setVisible(false); m_noSelectionLabel->setVisible(true); } m_treeView->expandAll(); } void SkeletonOutlinerPlugin::OnTextFilterChanged(const QString& text) { m_treeView->expandAll(); } void SkeletonOutlinerPlugin::OnTypeFilterChanged(const AzQtComponents::SearchTypeFilterList& activeTypeFilters) { m_treeView->expandAll(); } Node* SkeletonOutlinerPlugin::GetSingleSelectedNode() { const QModelIndexList selectedIndices = m_skeletonModel->GetSelectionModel().selectedRows(); if (selectedIndices.size() == 1) { Node* selectedNode = selectedIndices[0].data(SkeletonModel::ROLE_POINTER).value(); return selectedNode; } return nullptr; } QModelIndex SkeletonOutlinerPlugin::GetSingleSelectedModelIndex() { const QModelIndexList selectedIndices = m_skeletonModel->GetSelectionModel().selectedRows(); if (selectedIndices.size() == 1) { return selectedIndices[0]; } return QModelIndex(); } SkeletonModel* SkeletonOutlinerPlugin::GetModel() { return m_skeletonModel.get(); } void SkeletonOutlinerPlugin::DataChanged(const QModelIndex& modelIndex) { const QModelIndex proxyModelIndex = m_filterProxyModel->mapFromSource(modelIndex); const QModelIndex lastColumnProxyModelIndex = proxyModelIndex.sibling(proxyModelIndex.row(), m_filterProxyModel->columnCount() - 1); m_filterProxyModel->dataChanged(proxyModelIndex, lastColumnProxyModelIndex); const QModelIndex lastColumnModelIndex = modelIndex.sibling(modelIndex.row(), m_skeletonModel->columnCount() - 1); m_skeletonModel->dataChanged(modelIndex, lastColumnModelIndex); } void SkeletonOutlinerPlugin::DataListChanged(const QModelIndexList& modelIndexList) { for (const QModelIndex& modelIndex : modelIndexList) { DataChanged(modelIndex); } } AZ::Outcome SkeletonOutlinerPlugin::GetSelectedRowIndices() { return AZ::Success(m_selectedRows); } void SkeletonOutlinerPlugin::OnSelectionChanged(const QItemSelection& selected, const QItemSelection& deselected) { m_selectedRows = m_treeView->selectionModel()->selectedRows(); if (m_selectedRows.size() == 1) { const QModelIndex& modelIndex = m_selectedRows[0]; Node* selectedNode = modelIndex.data(SkeletonModel::ROLE_POINTER).value(); Actor* selectedActor = modelIndex.data(SkeletonModel::ROLE_ACTOR_POINTER).value(); SkeletonOutlinerNotificationBus::Broadcast(&SkeletonOutlinerNotifications::SingleNodeSelectionChanged, selectedActor, selectedNode); } else { SkeletonOutlinerNotificationBus::Broadcast(&SkeletonOutlinerNotifications::SingleNodeSelectionChanged, m_skeletonModel->GetActor(), nullptr); } SkeletonOutlinerNotificationBus::Broadcast(&SkeletonOutlinerNotifications::JointSelectionChanged); } void SkeletonOutlinerPlugin::OnContextMenu(const QPoint& position) { const QModelIndexList selectedRowIndices = m_skeletonModel->GetSelectionModel().selectedRows(); if (selectedRowIndices.empty()) { return; } QMenu* contextMenu = new QMenu(m_mainWidget); contextMenu->setObjectName("EMFX.SkeletonOutlinerPlugin.ContextMenu"); contextMenu; // Allow all external places to plug into the context menu. SkeletonOutlinerNotificationBus::Broadcast(&SkeletonOutlinerNotifications::OnContextMenu, contextMenu, selectedRowIndices); // Zoom to selected joints if (!selectedRowIndices.empty()) { AZStd::vector selectedJoints; selectedJoints.reserve(selectedRowIndices.size()); for (const QModelIndex& modelIndex : selectedRowIndices) { Node* selectedJoint = modelIndex.data(SkeletonModel::ROLE_POINTER).value(); selectedJoints.push_back(selectedJoint); } ActorInstance* selectedActorInstance = selectedRowIndices[0].data(SkeletonModel::ROLE_ACTOR_INSTANCE_POINTER).value(); QAction* zoomToJointAction = contextMenu->addAction("Zoom to selected joints"); connect(zoomToJointAction, &QAction::triggered, this, [=] { SkeletonOutlinerNotificationBus::Broadcast(&SkeletonOutlinerNotifications::ZoomToJoints, selectedActorInstance, selectedJoints); }); } if (!contextMenu->isEmpty()) { contextMenu->popup(m_treeView->mapToGlobal(position)); } connect(contextMenu, &QMenu::triggered, contextMenu, &QMenu::deleteLater); } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool SkeletonOutlinerPlugin::DataChanged(AZ::u32 actorId, const AZStd::string& jointName) { Actor* actor = GetEMotionFX().GetActorManager()->FindActorByID(actorId); if (!actor) { return false; } const Skeleton* skeleton = actor->GetSkeleton(); Node* joint = skeleton->FindNodeByName(jointName); if (!joint) { return false; } SkeletonModel* skeletonModel = nullptr; SkeletonOutlinerRequestBus::BroadcastResult(skeletonModel, &SkeletonOutlinerRequests::GetModel); if (!skeletonModel || skeletonModel->GetActor() != actor) { return false; } QModelIndexList modelIndices; modelIndices.push_back(skeletonModel->GetModelIndex(joint)); SkeletonOutlinerRequestBus::Broadcast(&SkeletonOutlinerRequests::DataListChanged, modelIndices); return true; } bool SkeletonOutlinerPlugin::DataChangedCallback::Execute(MCore::Command* command, const MCore::CommandLine& commandLine) { AZ_UNUSED(commandLine); ParameterMixinActorId* actorIdMixin = azdynamic_cast(command); ParameterMixinJointName* jointNameMixin = azdynamic_cast(command); const bool firstLastCommand = commandLine.GetValueAsBool("updateUI", true); if (actorIdMixin && jointNameMixin && firstLastCommand) { DataChanged(actorIdMixin->GetActorId(), jointNameMixin->GetJointName()); } return true; } bool SkeletonOutlinerPlugin::DataChangedCallback::Undo(MCore::Command* command, const MCore::CommandLine& commandLine) { AZ_UNUSED(commandLine); ParameterMixinActorId* actorIdMixin = azdynamic_cast(command); ParameterMixinJointName* jointNameMixin = azdynamic_cast(command); if (actorIdMixin && jointNameMixin) { DataChanged(actorIdMixin->GetActorId(), jointNameMixin->GetJointName()); } return true; } } // namespace EMotionFX