diff --git a/src/plugins/qmldesigner/components/edit3d/edit3dwidget.cpp b/src/plugins/qmldesigner/components/edit3d/edit3dwidget.cpp index 91ee59666d3..e2da1c48135 100644 --- a/src/plugins/qmldesigner/components/edit3d/edit3dwidget.cpp +++ b/src/plugins/qmldesigner/components/edit3d/edit3dwidget.cpp @@ -9,6 +9,7 @@ #include "edit3dvisibilitytogglesmenu.h" #include "metainfo.h" #include "modelnodeoperations.h" +#include "nodeabstractproperty.h" #include "qmldesignerconstants.h" #include "qmldesignerplugin.h" #include "qmlvisualnode.h" @@ -217,6 +218,19 @@ void Edit3DWidget::createContextMenu() }); m_contextMenu->addSeparator(); + + m_selectParentAction = m_contextMenu->addAction(tr("Select Parent"), [&] { + ModelNode parentNode = ModelNode::lowestCommonAncestor(view()->selectedModelNodes()); + if (!parentNode.isValid()) + return; + + if (!parentNode.isRootNode() && view()->isSelectedModelNode(parentNode)) + parentNode = parentNode.parentProperty().parentModelNode(); + + view()->setSelectedModelNode(parentNode); + }); + + m_contextMenu->addSeparator(); } bool Edit3DWidget::isPasteAvailable() const @@ -372,19 +386,21 @@ void Edit3DWidget::showContextMenu(const QPoint &pos, const ModelNode &modelNode const bool isValid = modelNode.isValid(); const bool isModel = modelNode.metaInfo().isQtQuick3DModel(); - const bool isNotRoot = isValid && !modelNode.isRootNode(); const bool isCamera = isValid && modelNode.metaInfo().isQtQuick3DCamera(); const bool isSingleComponent = view()->hasSingleSelectedModelNode() && modelNode.isComponent(); + const bool anyNodeSelected = view()->hasSelectedModelNodes(); + const bool selectionExcludingRoot = anyNodeSelected && !view()->rootModelNode().isSelected(); m_editComponentAction->setEnabled(isSingleComponent); m_editMaterialAction->setEnabled(isModel); - m_duplicateAction->setEnabled(isNotRoot); - m_copyAction->setEnabled(isNotRoot); + m_duplicateAction->setEnabled(selectionExcludingRoot); + m_copyAction->setEnabled(selectionExcludingRoot); m_pasteAction->setEnabled(isPasteAvailable()); - m_deleteAction->setEnabled(isNotRoot); - m_fitSelectedAction->setEnabled(isNotRoot); + m_deleteAction->setEnabled(selectionExcludingRoot); + m_fitSelectedAction->setEnabled(anyNodeSelected); m_alignCameraAction->setEnabled(isCamera); m_alignViewAction->setEnabled(isCamera); + m_selectParentAction->setEnabled(selectionExcludingRoot); m_contextMenu->popup(mapToGlobal(pos)); } diff --git a/src/plugins/qmldesigner/components/edit3d/edit3dwidget.h b/src/plugins/qmldesigner/components/edit3d/edit3dwidget.h index 38ff56e67ad..25948d5d9aa 100644 --- a/src/plugins/qmldesigner/components/edit3d/edit3dwidget.h +++ b/src/plugins/qmldesigner/components/edit3d/edit3dwidget.h @@ -71,6 +71,7 @@ private: QPointer m_fitSelectedAction; QPointer m_alignCameraAction; QPointer m_alignViewAction; + QPointer m_selectParentAction; QPointer m_createSubMenu; ModelNode m_contextMenuTarget; QVector3D m_contextMenuPos3d; diff --git a/src/plugins/qmldesigner/designercore/include/modelnode.h b/src/plugins/qmldesigner/designercore/include/modelnode.h index 6c2f424cf1c..c9c78588004 100644 --- a/src/plugins/qmldesigner/designercore/include/modelnode.h +++ b/src/plugins/qmldesigner/designercore/include/modelnode.h @@ -222,6 +222,7 @@ public: void setLocked(bool value); static bool isThisOrAncestorLocked(const ModelNode &node); + static ModelNode lowestCommonAncestor(const QList &nodes); qint32 internalId() const; diff --git a/src/plugins/qmldesigner/designercore/model/modelnode.cpp b/src/plugins/qmldesigner/designercore/model/modelnode.cpp index 564c1203297..ced1a0de44b 100644 --- a/src/plugins/qmldesigner/designercore/model/modelnode.cpp +++ b/src/plugins/qmldesigner/designercore/model/modelnode.cpp @@ -1313,6 +1313,96 @@ bool ModelNode::isThisOrAncestorLocked(const ModelNode &node) return isThisOrAncestorLocked(node.parentProperty().parentModelNode()); } +/*! + * \brief The lowest common ancestor node for node1 and node2. If one of the nodes (Node A) is + * the ancestor of the other node, the return value is Node A and not the parent of Node A. + * \param node1 First node + * \param node2 Second node + * \param depthOfLCA Depth of the return value + * \param depthOfNode1 Depth of node1. Use this parameter for optimization + * \param depthOfNode2 Depth of node2. Use this parameter for optimization + */ +static ModelNode lowestCommonAncestor(const ModelNode &node1, + const ModelNode &node2, + int &depthOfLCA, + const int &depthOfNode1 = -1, + const int &depthOfNode2 = -1) +{ + Q_ASSERT(node1.isValid() && node2.isValid()); + + auto depthOfNode = [] (const ModelNode &node) -> int { + int depth = 0; + ModelNode parentNode = node; + while (!parentNode.isRootNode()) { + depth++; + parentNode = parentNode.parentProperty().parentModelNode(); + } + return depth; + }; + + if (node1 == node2) { + depthOfLCA = (depthOfNode1 < 0) + ? ((depthOfNode2 < 0) ? depthOfNode(node1) : depthOfNode2) + : depthOfNode1; + return node1; + } + + if (node1.isRootNode()) { + depthOfLCA = 0; + return node1; + } + + if (node2.isRootNode()) { + depthOfLCA = 0; + return node2; + } + + ModelNode nodeLower = node1; + ModelNode nodeHigher = node2; + int depthLower = (depthOfNode1 < 0) ? depthOfNode(nodeLower) : depthOfNode1; + int depthHigher = (depthOfNode2 < 0) ? depthOfNode(nodeHigher) :depthOfNode2; + + if (depthLower > depthHigher) { + std::swap(depthLower, depthHigher); + std::swap(nodeLower, nodeHigher); + } + + int depthDiff = depthHigher - depthLower; + while (depthDiff--) + nodeHigher = nodeHigher.parentProperty().parentModelNode(); + + while (nodeLower != nodeHigher) { + nodeLower = nodeLower.parentProperty().parentModelNode(); + nodeHigher = nodeHigher.parentProperty().parentModelNode(); + --depthLower; + } + + depthOfLCA = depthLower; + return nodeLower; +} + +/*! + * \brief The lowest common node containing all nodes. If one of the nodes (Node A) is + * the ancestor of the other nodes, the return value is Node A and not the parent of Node A. + */ +ModelNode ModelNode::lowestCommonAncestor(const QList &nodes) +{ + if (nodes.isEmpty()) + return {}; + + ModelNode accumulatedNode = nodes.first(); + int accumulatedNodeDepth = -1; + Utils::span nodesExceptFirst(nodes.constBegin() + 1, nodes.constEnd()); + for (const ModelNode &node : nodesExceptFirst) { + accumulatedNode = QmlDesigner::lowestCommonAncestor(accumulatedNode, + node, + accumulatedNodeDepth, + accumulatedNodeDepth); + } + + return accumulatedNode; +} + void ModelNode::setScriptFunctions(const QStringList &scriptFunctionList) { model()->d->setScriptFunctions(m_internalNode, scriptFunctionList);