diff --git a/share/qtcreator/qml/qmlpuppet/mockfiles/CameraGizmo.qml b/share/qtcreator/qml/qmlpuppet/mockfiles/CameraGizmo.qml index 3b6badbb281..3646586e1af 100644 --- a/share/qtcreator/qml/qmlpuppet/mockfiles/CameraGizmo.qml +++ b/share/qtcreator/qml/qmlpuppet/mockfiles/CameraGizmo.qml @@ -41,8 +41,7 @@ IconGizmo { materials: [ DefaultMaterial { id: defaultMaterial - emissiveColor: cameraGizmo.targetNode === cameraGizmo.selectedNode ? "#FF0000" - : "#555555" + emissiveColor: cameraGizmo.selected ? "#FF0000" : "#555555" lighting: DefaultMaterial.NoLighting cullingMode: Material.DisableCulling } diff --git a/share/qtcreator/qml/qmlpuppet/mockfiles/EditView3D.qml b/share/qtcreator/qml/qmlpuppet/mockfiles/EditView3D.qml index e97aeec19a7..ea033bea739 100644 --- a/share/qtcreator/qml/qmlpuppet/mockfiles/EditView3D.qml +++ b/share/qtcreator/qml/qmlpuppet/mockfiles/EditView3D.qml @@ -46,28 +46,82 @@ Window { property alias showEditLight: btnEditViewLight.toggled property alias usePerspective: btnPerspective.toggled - property Node selectedNode: null + property Node selectedNode: null // This is non-null only in single selection case + property var selectedNodes: [] // All selected nodes property var lightGizmos: [] property var cameraGizmos: [] + property var selectionBoxes: [] property rect viewPortRect: Qt.rect(0, 0, 1000, 1000) - signal objectClicked(var object) + signal selectionChanged(var selectedNodes) signal commitObjectProperty(var object, var propName) signal changeObjectProperty(var object, var propName) - function selectObject(object) { - selectedNode = object; + function ensureSelectionBoxes(count) { + var needMore = count - selectionBoxes.length + if (needMore > 0) { + var component = Qt.createComponent("SelectionBox.qml"); + if (component.status === Component.Ready) { + for (var i = 0; i < needMore; ++i) { + var geometryName = _generalHelper.generateUniqueName("SelectionBoxGeometry"); + var box = component.createObject(mainSceneHelpers, {"view3D": editView, + "geometryName": geometryName}); + selectionBoxes[selectionBoxes.length] = box; + } + } + } } - function handleObjectClicked(object) { + function selectObjects(objects) { + // Create selection boxes as necessary. One more box than is actually needed is created, so + // that we always have a previously created box to use for new selection. + // This fixes an occasional visual glitch when creating a new box. + ensureSelectionBoxes(objects.length + 1) + + var i; + for (i = 0; i < objects.length; ++i) + selectionBoxes[i].targetNode = objects[i]; + for (i = objects.length; i < selectionBoxes.length; ++i) + selectionBoxes[i].targetNode = null; + + selectedNodes = objects; + if (objects.length === 0 || objects.length > 1) + selectedNode = null; + else + selectedNode = objects[0]; + } + + function handleObjectClicked(object, multi) { var theObject = object; if (btnSelectGroup.selected) { while (theObject && theObject.parent !== scene) theObject = theObject.parent; } - selectObject(theObject); - objectClicked(theObject); + // Object selection logic: + // Regular click: Clear any multiselection, single-selects the clicked object + // Ctrl-click: No objects selected: Act as single select + // One or more objects selected: Multiselect + // Null object always clears entire selection + var newSelection = []; + if (object !== null) { + if (multi && selectedNodes.length > 0) { + var deselect = false; + for (var i = 0; i < selectedNodes.length; ++i) { + // Multiselecting already selected object clears that object from selection + if (selectedNodes[i] !== object) + newSelection[newSelection.length] = selectedNodes[i]; + else + deselect = true; + } + if (!deselect) + newSelection[newSelection.length] = object; + } else { + newSelection[0] = theObject; + } + } + selectObjects(newSelection); + selectionChanged(newSelection); } function addLightGizmo(obj) @@ -76,10 +130,10 @@ Window { if (component.status === Component.Ready) { var gizmo = component.createObject(overlayScene, {"view3D": overlayView, "targetNode": obj, - "selectedNode": selectedNode}); + "selectedNodes": selectedNodes}); lightGizmos[lightGizmos.length] = gizmo; gizmo.clicked.connect(handleObjectClicked); - gizmo.selectedNode = Qt.binding(function() {return selectedNode;}); + gizmo.selectedNodes = Qt.binding(function() {return selectedNodes;}); } } @@ -91,17 +145,20 @@ Window { var gizmo = component.createObject( overlayScene, {"view3D": overlayView, "targetNode": obj, "geometryName": geometryName, - "viewPortRect": viewPortRect, "selectedNode": selectedNode}); + "viewPortRect": viewPortRect, "selectedNodes": selectedNodes}); cameraGizmos[cameraGizmos.length] = gizmo; gizmo.clicked.connect(handleObjectClicked); gizmo.viewPortRect = Qt.binding(function() {return viewPortRect;}); - gizmo.selectedNode = Qt.binding(function() {return selectedNode;}); + gizmo.selectedNodes = Qt.binding(function() {return selectedNodes;}); } } // Work-around the fact that the projection matrix for the camera is not calculated until // the first frame is rendered, so any initial calls to mapFrom3DScene() will fail. - Component.onCompleted: _generalHelper.requestOverlayUpdate(); + Component.onCompleted: { + selectObjects([]); + _generalHelper.requestOverlayUpdate(); + } onWidthChanged: _generalHelper.requestOverlayUpdate(); onHeightChanged: _generalHelper.requestOverlayUpdate(); @@ -187,11 +244,14 @@ Window { GradientStop { position: 0.0; color: "#999999" } } - TapHandler { // check tapping/clicking an object in the scene - onTapped: { - var pickResult = editView.pick(eventPoint.scenePosition.x, - eventPoint.scenePosition.y); - handleObjectClicked(pickResult.objectHit); + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton + onClicked: { + var pickResult = editView.pick(mouse.x, mouse.y); + handleObjectClicked(pickResult.objectHit, mouse.modifiers & Qt.ControlModifier); + if (!pickResult.objectHit) + mouse.accepted = false; } } @@ -213,12 +273,6 @@ Window { step: 50 } - SelectionBox { - id: selectionBox - view3D: editView - targetNode: viewWindow.selectedNode - } - PointLight { id: editLight visible: showEditLight @@ -387,7 +441,8 @@ Window { onSelectedChanged: { if (selected) { - var targetNode = viewWindow.selectedNode ? selectionBox.model : null; + var targetNode = viewWindow.selectedNodes.length > 0 + ? selectionBoxes[0].model : null; cameraControl.fitObject(targetNode, editView.camera.rotation); } } @@ -401,7 +456,7 @@ Window { width: 100 height: width editCameraCtrl: cameraControl - selectedNode : viewWindow.selectedNode ? selectionBox.model : null + selectedNode : viewWindow.selectedNodes.length ? selectionBoxes[0].model : null } Rectangle { // top controls bar diff --git a/share/qtcreator/qml/qmlpuppet/mockfiles/IconGizmo.qml b/share/qtcreator/qml/qmlpuppet/mockfiles/IconGizmo.qml index 3d4183a9dc5..e84de0fbdeb 100644 --- a/share/qtcreator/qml/qmlpuppet/mockfiles/IconGizmo.qml +++ b/share/qtcreator/qml/qmlpuppet/mockfiles/IconGizmo.qml @@ -33,13 +33,20 @@ Node { property View3D view3D property bool highlightOnHover: true property Node targetNode: null - property Node selectedNode: null + property var selectedNodes: null + readonly property bool selected: { + for (var i = 0; i < selectedNodes.length; ++i) { + if (selectedNodes[i] === targetNode) + return true; + } + return false; + } property alias iconSource: iconImage.source property alias overlayColor: colorOverlay.color signal positionCommit() - signal clicked(Node node) + signal clicked(Node node, bool multi) position: targetNode ? targetNode.scenePosition : Qt.vector3d(0, 0, 0) rotation: targetNode ? targetNode.sceneRotation : Qt.vector3d(0, 0, 0) @@ -60,21 +67,26 @@ Node { y: -height / 2 color: "transparent" border.color: "#7777ff" - border.width: iconGizmo.selectedNode !== iconGizmo.targetNode + border.width: !iconGizmo.selected && iconGizmo.highlightOnHover && iconMouseArea.containsMouse ? 2 : 0 radius: 5 - opacity: iconGizmo.selectedNode === iconGizmo.targetNode ? 0.2 : 1 + opacity: iconGizmo.selected ? 0.2 : 1 Image { id: iconImage fillMode: Image.Pad MouseArea { id: iconMouseArea anchors.fill: parent - onClicked: iconGizmo.clicked(iconGizmo.targetNode) - hoverEnabled: iconGizmo.highlightOnHover - && iconGizmo.selectedNode !== iconGizmo.targetNode - acceptedButtons: iconGizmo.selectedNode !== iconGizmo.targetNode - ? Qt.LeftButton : Qt.NoButton + onPressed: { + if (iconGizmo.selected && !(mouse.modifiers & Qt.ControlModifier)) { + mouse.accepted = false; + } + } + + onClicked: iconGizmo.clicked(iconGizmo.targetNode, + mouse.modifiers & Qt.ControlModifier) + hoverEnabled: iconGizmo.highlightOnHover && !iconGizmo.selected + acceptedButtons: Qt.LeftButton } } ColorOverlay { diff --git a/share/qtcreator/qml/qmlpuppet/mockfiles/SelectionBox.qml b/share/qtcreator/qml/qmlpuppet/mockfiles/SelectionBox.qml index 995b4badd80..10c443efce5 100644 --- a/share/qtcreator/qml/qmlpuppet/mockfiles/SelectionBox.qml +++ b/share/qtcreator/qml/qmlpuppet/mockfiles/SelectionBox.qml @@ -33,6 +33,7 @@ Node { property View3D view3D property Node targetNode: null property alias model: selectionBoxModel + property alias geometryName: selectionBoxGeometry.name SelectionBoxGeometry { id: selectionBoxGeometry diff --git a/share/qtcreator/qml/qmlpuppet/qml2puppet/editor3d/selectionboxgeometry.cpp b/share/qtcreator/qml/qmlpuppet/qml2puppet/editor3d/selectionboxgeometry.cpp index 77700619237..1fb4f445a6e 100644 --- a/share/qtcreator/qml/qmlpuppet/qml2puppet/editor3d/selectionboxgeometry.cpp +++ b/share/qtcreator/qml/qmlpuppet/qml2puppet/editor3d/selectionboxgeometry.cpp @@ -41,6 +41,9 @@ namespace QmlDesigner { namespace Internal { +static const float floatMin = std::numeric_limits::lowest(); +static const float floatMax = std::numeric_limits::max(); + SelectionBoxGeometry::SelectionBoxGeometry() : QQuick3DGeometry() { @@ -133,9 +136,6 @@ QSSGRenderGraphObject *SelectionBoxGeometry::updateSpatialNode(QSSGRenderGraphOb QByteArray vertexData; QByteArray indexData; - static const float floatMin = std::numeric_limits::lowest(); - static const float floatMax = std::numeric_limits::max(); - QVector3D minBounds = QVector3D(floatMax, floatMax, floatMax); QVector3D maxBounds = QVector3D(floatMin, floatMin, floatMin); @@ -154,10 +154,18 @@ QSSGRenderGraphObject *SelectionBoxGeometry::updateSpatialNode(QSSGRenderGraphOb rootRN->markDirty(QSSGRenderNode::TransformDirtyFlag::TransformNotDirty); rootRN->calculateGlobalVariables(); } - getBounds(m_targetNode, vertexData, indexData, minBounds, maxBounds, QMatrix4x4()); + getBounds(m_targetNode, vertexData, indexData, minBounds, maxBounds); + appendVertexData(QMatrix4x4(), vertexData, indexData, minBounds, maxBounds); + + // Track changes in ancestors, as they can move node without affecting node properties + auto parentNode = m_targetNode->parentNode(); + while (parentNode) { + trackNodeChanges(parentNode); + parentNode = parentNode->parentNode(); + } } else { // Fill some dummy data so geometry won't get rejected - appendVertexData(vertexData, indexData, minBounds, maxBounds); + appendVertexData(QMatrix4x4(), vertexData, indexData, minBounds, maxBounds); } geometry->addAttribute(QSSGRenderGeometry::Attribute::PositionSemantic, 0, @@ -182,62 +190,73 @@ QSSGRenderGraphObject *SelectionBoxGeometry::updateSpatialNode(QSSGRenderGraphOb return node; } -void SelectionBoxGeometry::getBounds(QQuick3DNode *node, QByteArray &vertexData, - QByteArray &indexData, QVector3D &minBounds, - QVector3D &maxBounds, const QMatrix4x4 &transform) +void SelectionBoxGeometry::getBounds( + QQuick3DNode *node, QByteArray &vertexData, QByteArray &indexData, + QVector3D &minBounds, QVector3D &maxBounds) { - QMatrix4x4 fullTransform; + QMatrix4x4 localTransform; auto nodePriv = QQuick3DObjectPrivate::get(node); auto renderNode = static_cast(nodePriv->spatialNode); - // All transforms are relative to targetNode transform, so its local transform is ignored if (node != m_targetNode) { if (renderNode) { if (renderNode->flags.testFlag(QSSGRenderNode::Flag::TransformDirty)) renderNode->calculateLocalTransform(); - fullTransform = transform * renderNode->localTransform; + localTransform = renderNode->localTransform; } - - m_connections << QObject::connect(node, &QQuick3DNode::scaleChanged, - this, &SelectionBoxGeometry::update, Qt::QueuedConnection); - m_connections << QObject::connect(node, &QQuick3DNode::rotationChanged, - this, &SelectionBoxGeometry::update, Qt::QueuedConnection); - m_connections << QObject::connect(node, &QQuick3DNode::positionChanged, - this, &SelectionBoxGeometry::update, Qt::QueuedConnection); - m_connections << QObject::connect(node, &QQuick3DNode::pivotChanged, - this, &SelectionBoxGeometry::update, Qt::QueuedConnection); - m_connections << QObject::connect(node, &QQuick3DNode::orientationChanged, - this, &SelectionBoxGeometry::update, Qt::QueuedConnection); - m_connections << QObject::connect(node, &QQuick3DNode::rotationOrderChanged, - this, &SelectionBoxGeometry::update, Qt::QueuedConnection); + trackNodeChanges(node); } + QVector3D localMinBounds = QVector3D(floatMax, floatMax, floatMax); + QVector3D localMaxBounds = QVector3D(floatMin, floatMin, floatMin); + + // Find bounds for children QVector minBoundsVec; QVector maxBoundsVec; - - // Check for children const auto children = node->childItems(); for (const auto child : children) { if (auto childNode = qobject_cast(child)) { QVector3D newMinBounds = minBounds; QVector3D newMaxBounds = maxBounds; - getBounds(childNode, vertexData, indexData, newMinBounds, newMaxBounds, fullTransform); + getBounds(childNode, vertexData, indexData, newMinBounds, newMaxBounds); minBoundsVec << newMinBounds; maxBoundsVec << newMaxBounds; } } + auto combineMinBounds = [](QVector3D &target, const QVector3D &source) { + target.setX(qMin(source.x(), target.x())); + target.setY(qMin(source.y(), target.y())); + target.setZ(qMin(source.z(), target.z())); + }; + auto combineMaxBounds = [](QVector3D &target, const QVector3D &source) { + target.setX(qMax(source.x(), target.x())); + target.setY(qMax(source.y(), target.y())); + target.setZ(qMax(source.z(), target.z())); + }; + auto transformCorner = [&](const QMatrix4x4 &m, QVector3D &minTarget, QVector3D &maxTarget, + const QVector3D &corner) { + QVector3D mappedCorner = m.map(corner); + combineMinBounds(minTarget, mappedCorner); + combineMaxBounds(maxTarget, mappedCorner); + }; + auto transformCorners = [&](const QMatrix4x4 &m, QVector3D &minTarget, QVector3D &maxTarget, + const QVector3D &minCorner, const QVector3D &maxCorner) { + transformCorner(m, minTarget, maxTarget, minCorner); + transformCorner(m, minTarget, maxTarget, maxCorner); + transformCorner(m, minTarget, maxTarget, QVector3D(minCorner.x(), minCorner.y(), maxCorner.z())); + transformCorner(m, minTarget, maxTarget, QVector3D(minCorner.x(), maxCorner.y(), minCorner.z())); + transformCorner(m, minTarget, maxTarget, QVector3D(maxCorner.x(), minCorner.y(), minCorner.z())); + transformCorner(m, minTarget, maxTarget, QVector3D(minCorner.x(), maxCorner.y(), maxCorner.z())); + transformCorner(m, minTarget, maxTarget, QVector3D(maxCorner.x(), maxCorner.y(), minCorner.z())); + transformCorner(m, minTarget, maxTarget, QVector3D(maxCorner.x(), minCorner.y(), maxCorner.z())); + }; + // Combine all child bounds - for (const auto &newBounds : qAsConst(minBoundsVec)) { - minBounds.setX(qMin(newBounds.x(), minBounds.x())); - minBounds.setY(qMin(newBounds.y(), minBounds.y())); - minBounds.setZ(qMin(newBounds.z(), minBounds.z())); - } - for (const auto &newBounds : qAsConst(maxBoundsVec)) { - maxBounds.setX(qMax(newBounds.x(), maxBounds.x())); - maxBounds.setY(qMax(newBounds.y(), maxBounds.y())); - maxBounds.setZ(qMax(newBounds.z(), maxBounds.z())); - } + for (const auto &newBounds : qAsConst(minBoundsVec)) + combineMinBounds(localMinBounds, newBounds); + for (const auto &newBounds : qAsConst(maxBoundsVec)) + combineMaxBounds(localMaxBounds, newBounds); if (auto modelNode = qobject_cast(node)) { if (auto renderModel = static_cast(renderNode)) { @@ -253,47 +272,30 @@ void SelectionBoxGeometry::getBounds(QQuick3DNode *node, QByteArray &vertexData, QVector3D localMin = center - extents; QVector3D localMax = center + extents; - // Transform all corners of the local bounding box to find final extent in - // in parent space - - auto checkCorner = [&minBounds, &maxBounds, &fullTransform] - (const QVector3D &corner) { - QVector3D mappedCorner = fullTransform.map(corner); - minBounds.setX(qMin(mappedCorner.x(), minBounds.x())); - minBounds.setY(qMin(mappedCorner.y(), minBounds.y())); - minBounds.setZ(qMin(mappedCorner.z(), minBounds.z())); - maxBounds.setX(qMax(mappedCorner.x(), maxBounds.x())); - maxBounds.setY(qMax(mappedCorner.y(), maxBounds.y())); - maxBounds.setZ(qMax(mappedCorner.z(), maxBounds.z())); - }; - - checkCorner(localMin); - checkCorner(localMax); - checkCorner(QVector3D(localMin.x(), localMin.y(), localMax.z())); - checkCorner(QVector3D(localMin.x(), localMax.y(), localMin.z())); - checkCorner(QVector3D(localMax.x(), localMin.y(), localMin.z())); - checkCorner(QVector3D(localMin.x(), localMax.y(), localMax.z())); - checkCorner(QVector3D(localMax.x(), localMax.y(), localMin.z())); - checkCorner(QVector3D(localMax.x(), localMin.y(), localMax.z())); + combineMinBounds(localMinBounds, localMin); + combineMaxBounds(localMaxBounds, localMax); } } } } - // Target node and immediate children get selection boxes - if (transform.isIdentity()) { - // Adjust bounds to reduce targetNode pixels obscuring the selection box - QVector3D extents = (maxBounds - minBounds) / 1000.f; - QVector3D minAdjBounds = minBounds - extents; - QVector3D maxAdjBounds = maxBounds + extents; + // Transform local space bounding box to parent space + transformCorners(localTransform, minBounds, maxBounds, localMinBounds, localMaxBounds); - appendVertexData(vertexData, indexData, minAdjBounds, maxAdjBounds); - } + // Immediate child boxes + if (node->parentNode() == m_targetNode) + appendVertexData(localTransform, vertexData, indexData, localMinBounds, localMaxBounds); } -void SelectionBoxGeometry::appendVertexData(QByteArray &vertexData, QByteArray &indexData, - const QVector3D &minBounds, const QVector3D &maxBounds) +void SelectionBoxGeometry::appendVertexData(const QMatrix4x4 &m, QByteArray &vertexData, + QByteArray &indexData, const QVector3D &minBounds, + const QVector3D &maxBounds) { + // Adjust bounds to reduce targetNode pixels obscuring the selection box + QVector3D extents = (maxBounds - minBounds) / 1000.f; + QVector3D minAdjBounds = minBounds - extents; + QVector3D maxAdjBounds = maxBounds + extents; + int initialVertexSize = vertexData.size(); int initialIndexSize = indexData.size(); const int vertexSize = int(sizeof(float)) * 8 * 3; // 8 vertices, 3 floats/vert @@ -305,14 +307,20 @@ void SelectionBoxGeometry::appendVertexData(QByteArray &vertexData, QByteArray & auto dataPtr = reinterpret_cast(vertexData.data() + initialVertexSize); auto indexPtr = reinterpret_cast(indexData.data() + initialIndexSize); - *dataPtr++ = maxBounds.x(); *dataPtr++ = maxBounds.y(); *dataPtr++ = maxBounds.z(); - *dataPtr++ = minBounds.x(); *dataPtr++ = maxBounds.y(); *dataPtr++ = maxBounds.z(); - *dataPtr++ = minBounds.x(); *dataPtr++ = minBounds.y(); *dataPtr++ = maxBounds.z(); - *dataPtr++ = maxBounds.x(); *dataPtr++ = minBounds.y(); *dataPtr++ = maxBounds.z(); - *dataPtr++ = maxBounds.x(); *dataPtr++ = maxBounds.y(); *dataPtr++ = minBounds.z(); - *dataPtr++ = minBounds.x(); *dataPtr++ = maxBounds.y(); *dataPtr++ = minBounds.z(); - *dataPtr++ = minBounds.x(); *dataPtr++ = minBounds.y(); *dataPtr++ = minBounds.z(); - *dataPtr++ = maxBounds.x(); *dataPtr++ = minBounds.y(); *dataPtr++ = minBounds.z(); + QVector3D corners[8]; + corners[0] = QVector3D(maxAdjBounds.x(), maxAdjBounds.y(), maxAdjBounds.z()); + corners[1] = QVector3D(minAdjBounds.x(), maxAdjBounds.y(), maxAdjBounds.z()); + corners[2] = QVector3D(minAdjBounds.x(), minAdjBounds.y(), maxAdjBounds.z()); + corners[3] = QVector3D(maxAdjBounds.x(), minAdjBounds.y(), maxAdjBounds.z()); + corners[4] = QVector3D(maxAdjBounds.x(), maxAdjBounds.y(), minAdjBounds.z()); + corners[5] = QVector3D(minAdjBounds.x(), maxAdjBounds.y(), minAdjBounds.z()); + corners[6] = QVector3D(minAdjBounds.x(), minAdjBounds.y(), minAdjBounds.z()); + corners[7] = QVector3D(maxAdjBounds.x(), minAdjBounds.y(), minAdjBounds.z()); + + for (int i = 0; i < 8; ++i) { + corners[i] = m.map(corners[i]); + *dataPtr++ = corners[i].x(); *dataPtr++ = corners[i].y(); *dataPtr++ = corners[i].z(); + } *indexPtr++ = 0 + indexAdd; *indexPtr++ = 1 + indexAdd; *indexPtr++ = 1 + indexAdd; *indexPtr++ = 2 + indexAdd; @@ -330,6 +338,22 @@ void SelectionBoxGeometry::appendVertexData(QByteArray &vertexData, QByteArray & *indexPtr++ = 7 + indexAdd; *indexPtr++ = 4 + indexAdd; } +void SelectionBoxGeometry::trackNodeChanges(QQuick3DNode *node) +{ + m_connections << QObject::connect(node, &QQuick3DNode::sceneScaleChanged, + this, &SelectionBoxGeometry::update, Qt::QueuedConnection); + m_connections << QObject::connect(node, &QQuick3DNode::sceneRotationChanged, + this, &SelectionBoxGeometry::update, Qt::QueuedConnection); + m_connections << QObject::connect(node, &QQuick3DNode::scenePositionChanged, + this, &SelectionBoxGeometry::update, Qt::QueuedConnection); + m_connections << QObject::connect(node, &QQuick3DNode::pivotChanged, + this, &SelectionBoxGeometry::update, Qt::QueuedConnection); + m_connections << QObject::connect(node, &QQuick3DNode::orientationChanged, + this, &SelectionBoxGeometry::update, Qt::QueuedConnection); + m_connections << QObject::connect(node, &QQuick3DNode::rotationOrderChanged, + this, &SelectionBoxGeometry::update, Qt::QueuedConnection); +} + } } diff --git a/share/qtcreator/qml/qmlpuppet/qml2puppet/editor3d/selectionboxgeometry.h b/share/qtcreator/qml/qmlpuppet/qml2puppet/editor3d/selectionboxgeometry.h index 08a28cec06c..a642f2ec58e 100644 --- a/share/qtcreator/qml/qmlpuppet/qml2puppet/editor3d/selectionboxgeometry.h +++ b/share/qtcreator/qml/qmlpuppet/qml2puppet/editor3d/selectionboxgeometry.h @@ -69,10 +69,11 @@ protected: QSSGRenderGraphObject *updateSpatialNode(QSSGRenderGraphObject *node) override; private: - void getBounds(QQuick3DNode *node, QByteArray &vertexData, QByteArray &indexData, - QVector3D &minBounds, QVector3D &maxBounds, const QMatrix4x4 &transform); - void appendVertexData(QByteArray &vertexData, QByteArray &indexData, + void getBounds(QQuick3DNode *node, QByteArray &vertexData, + QByteArray &indexData, QVector3D &minBounds, QVector3D &maxBounds); + void appendVertexData(const QMatrix4x4 &m, QByteArray &vertexData, QByteArray &indexData, const QVector3D &minBounds, const QVector3D &maxBounds); + void trackNodeChanges(QQuick3DNode *node); QQuick3DNode *m_targetNode = nullptr; QQuick3DViewport *m_view3D = nullptr; diff --git a/share/qtcreator/qml/qmlpuppet/qml2puppet/instances/qt5informationnodeinstanceserver.cpp b/share/qtcreator/qml/qmlpuppet/qml2puppet/instances/qt5informationnodeinstanceserver.cpp index 54012f7e4a5..6cdd9357951 100644 --- a/share/qtcreator/qml/qmlpuppet/qml2puppet/instances/qt5informationnodeinstanceserver.cpp +++ b/share/qtcreator/qml/qmlpuppet/qml2puppet/instances/qt5informationnodeinstanceserver.cpp @@ -58,7 +58,6 @@ #include "createscenecommand.h" #include "tokencommand.h" #include "removesharedmemorycommand.h" -#include "changeselectioncommand.h" #include "objectnodeinstance.h" #include @@ -125,7 +124,8 @@ QObject *Qt5InformationNodeInstanceServer::createEditView3D(QQmlEngine *engine) } window->installEventFilter(this); - QObject::connect(window, SIGNAL(objectClicked(QVariant)), this, SLOT(objectClicked(QVariant))); + QObject::connect(window, SIGNAL(selectionChanged(QVariant)), + this, SLOT(handleSelectionChanged(QVariant))); QObject::connect(window, SIGNAL(commitObjectProperty(QVariant, QVariant)), this, SLOT(handleObjectPropertyCommit(QVariant, QVariant))); QObject::connect(window, SIGNAL(changeObjectProperty(QVariant, QVariant)), @@ -134,6 +134,8 @@ QObject *Qt5InformationNodeInstanceServer::createEditView3D(QQmlEngine *engine) this, SLOT(handleActiveChanged())); QObject::connect(&m_propertyChangeTimer, &QTimer::timeout, this, &Qt5InformationNodeInstanceServer::handleObjectPropertyChangeTimeout); + QObject::connect(&m_selectionChangeTimer, &QTimer::timeout, + this, &Qt5InformationNodeInstanceServer::handleSelectionChangeTimeout); //For macOS we have to use the 4.1 core profile QSurfaceFormat surfaceFormat = window->requestedFormat(); @@ -148,14 +150,21 @@ QObject *Qt5InformationNodeInstanceServer::createEditView3D(QQmlEngine *engine) return window; } -// an object is clicked in the 3D edit view. Null object indicates selection clearing. -void Qt5InformationNodeInstanceServer::objectClicked(const QVariant &object) +// The selection has changed in the 3D edit view. Empty list indicates selection is cleared. +void Qt5InformationNodeInstanceServer::handleSelectionChanged(const QVariant &objs) { - auto obj = object.value(); - ServerNodeInstance instance; - if (obj) - instance = instanceForObject(obj); - selectInstance(instance); + QList instanceList; + const QVariantList varObjs = objs.value(); + for (const auto &object : varObjs) { + auto obj = object.value(); + if (obj) { + ServerNodeInstance instance = instanceForObject(obj); + instanceList << instance; + } + } + selectInstances(instanceList); + // Hold selection changes reflected back from designer for a bit + m_selectionChangeTimer.start(500); } QVector @@ -327,6 +336,7 @@ Qt5InformationNodeInstanceServer::Qt5InformationNodeInstanceServer(NodeInstanceC Qt5NodeInstanceServer(nodeInstanceClient) { m_propertyChangeTimer.setInterval(100); + m_selectionChangeTimer.setSingleShot(true); } void Qt5InformationNodeInstanceServer::sendTokenBack() @@ -385,9 +395,9 @@ bool Qt5InformationNodeInstanceServer::isDirtyRecursiveForParentInstances(QQuick } /* This method allows changing the selection from the puppet */ -void Qt5InformationNodeInstanceServer::selectInstance(const ServerNodeInstance &instance) +void Qt5InformationNodeInstanceServer::selectInstances(const QList &instanceList) { - nodeInstanceClient()->selectionChanged(createChangeSelectionCommand({instance})); + nodeInstanceClient()->selectionChanged(createChangeSelectionCommand(instanceList)); } /* This method allows changing property values from the puppet @@ -405,6 +415,11 @@ void Qt5InformationNodeInstanceServer::handleObjectPropertyChangeTimeout() ValuesModifiedCommand::TransactionOption::None); } +void Qt5InformationNodeInstanceServer::handleSelectionChangeTimeout() +{ + changeSelection(m_pendingSelectionChangeCommand); +} + QObject *Qt5InformationNodeInstanceServer::findRootNodeOf3DViewport( const QList &instanceList) const { @@ -459,26 +474,25 @@ void Qt5InformationNodeInstanceServer::setup3DEditView(const QListsetParent(m_editView3D); - sceneProperty.write(objectToVariant(node)); - QQmlProperty parentProperty(node, "parent", context()); + m_rootNode->setParent(m_editView3D); + sceneProperty.write(objectToVariant(m_rootNode)); + QQmlProperty parentProperty(m_rootNode, "parent", context()); parentProperty.write(objectToVariant(m_editView3D)); QQmlProperty showLightProperty(m_editView3D, "showLight", context()); showLightProperty.write(showCustomLight); @@ -680,18 +694,41 @@ void Qt5InformationNodeInstanceServer::changeSelection(const ChangeSelectionComm if (!m_editView3D) return; + if (m_selectionChangeTimer.isActive()) { + // If selection was recently changed by puppet, hold updating the selection for a bit to + // avoid selection flicker, especially in multiselect cases. + m_pendingSelectionChangeCommand = command; + // Add additional time in case more commands are still coming through + m_selectionChangeTimer.start(500); + return; + } + const QVector instanceIds = command.instanceIds(); + QVariantList selectedObjs; for (qint32 id : instanceIds) { if (hasInstanceForId(id)) { ServerNodeInstance instance = instanceForId(id); QObject *object = nullptr; if (instance.isSubclassOf("QQuick3DNode")) object = instance.internalObject(); - QMetaObject::invokeMethod(m_editView3D, "selectObject", Q_ARG(QVariant, - objectToVariant(object))); - return; // TODO: support multi-selection + if (object && object != m_rootNode) + selectedObjs << objectToVariant(object); } } + + // Ensure the UI has enough selection box items. If it doesn't yet have them, which can be the + // case when the first selection processed is a multiselection, we wait a bit as + // using the new boxes immediately leads to visual glitches. + int boxCount = m_editView3D->property("selectionBoxes").value().size(); + if (boxCount < selectedObjs.size()) { + QMetaObject::invokeMethod(m_editView3D, "ensureSelectionBoxes", + Q_ARG(QVariant, QVariant::fromValue(selectedObjs.size()))); + m_pendingSelectionChangeCommand = command; + m_selectionChangeTimer.start(100); + } else { + QMetaObject::invokeMethod(m_editView3D, "selectObjects", + Q_ARG(QVariant, QVariant::fromValue(selectedObjs))); + } } void Qt5InformationNodeInstanceServer::changePropertyValues(const ChangeValuesCommand &command) diff --git a/share/qtcreator/qml/qmlpuppet/qml2puppet/instances/qt5informationnodeinstanceserver.h b/share/qtcreator/qml/qmlpuppet/qml2puppet/instances/qt5informationnodeinstanceserver.h index 439e4da0c74..8a0f9837d54 100644 --- a/share/qtcreator/qml/qmlpuppet/qml2puppet/instances/qt5informationnodeinstanceserver.h +++ b/share/qtcreator/qml/qmlpuppet/qml2puppet/instances/qt5informationnodeinstanceserver.h @@ -28,6 +28,7 @@ #include "qt5nodeinstanceserver.h" #include "tokencommand.h" #include "valueschangedcommand.h" +#include "changeselectioncommand.h" #include #include @@ -51,7 +52,7 @@ public: void changePropertyValues(const ChangeValuesCommand &command) override; private slots: - void objectClicked(const QVariant &object); + void handleSelectionChanged(const QVariant &objs); void handleObjectPropertyCommit(const QVariant &object, const QVariant &propName); void handleObjectPropertyChange(const QVariant &object, const QVariant &propName); void updateViewPortRect(); @@ -64,11 +65,12 @@ protected: void sendTokenBack(); bool isDirtyRecursiveForNonInstanceItems(QQuickItem *item) const; bool isDirtyRecursiveForParentInstances(QQuickItem *item) const; - void selectInstance(const ServerNodeInstance &instance); + void selectInstances(const QList &instanceList); void modifyProperties(const QVector &properties); private: void handleObjectPropertyChangeTimeout(); + void handleSelectionChangeTimeout(); QObject *createEditView3D(QQmlEngine *engine); void setup3DEditView(const QList &instanceList); QObject *findRootNodeOf3DViewport(const QList &instanceList) const; @@ -93,11 +95,13 @@ private: QList m_completedComponentList; QList m_tokenList; QTimer m_propertyChangeTimer; + QTimer m_selectionChangeTimer; QVariant m_changedNode; PropertyName m_changedProperty; ServerNodeInstance m_viewPortInstance; - bool m_blockViewActivate = false; + QObject *m_rootNode = nullptr; + ChangeSelectionCommand m_pendingSelectionChangeCommand; }; } // namespace QmlDesigner