Revision control

++ // Copyright (C) 2022 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++
++ #include "nodeview.h"
++ #include "effectmanager.h"
++
++ NodeView::NodeView(QQuickItem *parent)
++ : QQuickItem(parent)
++ {
++ setAcceptedMouseButtons(Qt::AllButtons);
++ setAcceptHoverEvents(true);
++ m_nodesModel = new NodesModel();
++ m_arrowsModel = new ArrowsModel();
++
++ // Create main and output nodes
++ // These always exist
++ NodesModel::Node n1{0, 0, 220, 50, 80, 80, "Main"};
++ NodesModel::Node n2{1, 1, 220, 400, 80, 80, "Output"};
++ initializeNodeSize(n1);
++ initializeNodeSize(n2);
++ n1.nextNodeId = 1;
++ m_nodesModel->m_nodesList << n1;
++ m_nodesModel->m_nodesList << n2;
++
++ // Connect source & output by default
++ ArrowsModel::Arrow a1{0, 0, 0, 0, 0, 1};
++ m_arrowsModel->m_arrowsList << a1;
++
++ updateArrowsPositions();
++ m_activeArrow.startNodeId = -1;
++ m_activeArrow.endNodeId = -1;
++
++ connect(m_nodesModel, &QAbstractItemModel::modelReset,
++ this, &NodeView::updateCodeSelectorModel);
++ }
++
++ void NodeView::mouseMoveEvent(QMouseEvent *event)
++ {
++ // Nodes not moved with right button
++ if (event->buttons() & Qt::RightButton)
++ return;
++
++ m_nodesModel->beginResetModel();
++ QPointF movement = event->position() - m_pressPosition;
++ for (auto &node : m_nodesModel->m_nodesList) {
++ if (node.selected) {
++ float newX = node.startX + movement.x();
++ float newY = node.startY + movement.y();
++ // Don't allow dragging outside the visible nodeview area.
++ newX = std::max(newX, float(-node.width / 2.0f));
++ newX = std::min(newX, float(width() - node.width / 2.0f));
++ newY = std::max(newY, float(-node.height / 2.0f));
++ newY = std::min(newY, float(height() - node.height / 2.0f));
++ node.x = newX;
++ node.y = newY;
++ }
++ }
++ m_nodesModel->endResetModel();
++
++ updateArrowsPositions();
++ }
++
++ void NodeView::mousePressEvent(QMouseEvent *event)
++ {
++ m_mousePressed = true;
++
++ setFocus(true);
++
++ QPointF p = event->position();
++ m_pressPosition = p;
++ bool shiftPressed = (event->modifiers() & Qt::ShiftModifier);
++
++ m_nodesModel->beginResetModel();
++ m_arrowsModel->beginResetModel();
++
++ // Reset the nodes starting positions
++ for (auto &node : m_nodesModel->m_nodesList) {
++ if (node.selected) {
++ node.startX = node.x;
++ node.startY = node.y;
++ }
++ }
++
++ bool connectorPressed = false;
++ if (!m_activeArrowEnabled) {
++ m_activeArrow.startNodeId = -1;
++ m_activeArrow.endNodeId = -1;
++ }
++ // Check node connectors first
++ for (auto &node : m_nodesModel->m_nodesList) {
++ float size = 20;
++ // Start is bottom side and end top side of the node.
++ QPointF startConnectorCenter = { node.x + node.width / 2, node.y + node.height};
++ QPointF endConnectorCenter = { node.x + node.width / 2, node.y };
++ QRectF startConnector(startConnectorCenter.x() - size / 2, startConnectorCenter.y() - size / 2, size, size);
++ QRectF endConnector(endConnectorCenter.x() - size / 2, endConnectorCenter.y() - size / 2, size, size);
++ if (startConnector.contains(p)) {
++ m_activeArrowStartPoint = startConnectorCenter;
++ emit activeArrowStartPointChanged();
++ m_activeArrowEndPoint = startConnectorCenter;
++ emit activeArrowEndPointChanged();
++ connectorPressed = true;
++ m_activeArrow.startNodeId = node.nodeId;
++ // Remove possible already existing arrow
++ for (auto &arrow : m_arrowsModel->m_arrowsList) {
++ if (arrow.startNodeId == node.nodeId) {
++ // Update nextNode
++ node.nextNodeId = -1;
++ m_arrowsModel->m_arrowsList.removeAll(arrow);
++ }
++ }
++ } else if (endConnector.contains(p)) {
++ m_activeArrowStartPoint = endConnectorCenter;
++ emit activeArrowStartPointChanged();
++ m_activeArrowEndPoint = endConnectorCenter;
++ emit activeArrowEndPointChanged();
++ connectorPressed = true;
++ m_activeArrow.endNodeId = node.nodeId;
++ // Remove possible already existing arrow
++ for (auto &arrow : m_arrowsModel->m_arrowsList) {
++ if (arrow.endNodeId == node.nodeId) {
++ // Update nextNode
++ if (auto n = m_nodesModel->getNodeWithId(arrow.startNodeId))
++ n->nextNodeId = -1;
++ m_arrowsModel->m_arrowsList.removeAll(arrow);
++ }
++ }
++ }
++ // Don't allow connecting same node out & in together
++ if (m_activeArrow.startNodeId != -1 && m_activeArrow.startNodeId == m_activeArrow.endNodeId) {
++ m_activeArrow.startNodeId = -1;
++ m_activeArrow.endNodeId = -1;
++ }
++ }
++
++ // True when pressing a node that was already selected
++ bool selectedNodeUnderMouse = false;
++ NodesModel::Node newlySelectedNode;
++ if (!connectorPressed) {
++ for (auto &node : m_nodesModel->m_nodesList) {
++ node.startX = node.x;
++ node.startY = node.y;
++ QRectF nodeArea(node.x, node.y, node.width, node.height);
++ if (nodeArea.contains(p)) {
++ if (node.selected)
++ selectedNodeUnderMouse = true;
++ if (shiftPressed)
++ node.selected = !node.selected;
++ else
++ node.selected = true;
++ if (node.selected) {
++ newlySelectedNode = node;
++ break;
++ }
++ }
++ }
++ }
++
++ // Deselected previous nodes
++ if (!shiftPressed && !selectedNodeUnderMouse) {
++ for (auto &node : m_nodesModel->m_nodesList) {
++ if (newlySelectedNode != node)
++ node.selected = false;
++ }
++ }
++
++ // Check if both arrow ends are connected
++ if (m_activeArrow.startNodeId >= 0 && m_activeArrow.endNodeId >= 0) {
++ m_arrowsModel->m_arrowsList.append(m_activeArrow);
++ connectorPressed = false;
++ updateArrowsPositions();
++ // Update nextNode
++ auto n1 = m_nodesModel->getNodeWithId(m_activeArrow.startNodeId);
++ if (n1)
++ n1->nextNodeId = m_activeArrow.endNodeId;
++ }
++
++ if (m_activeArrowEnabled != connectorPressed) {
++ m_activeArrowEnabled = connectorPressed;
++ emit activeArrowEnabledChanged();
++ }
++
++ // Check first selected node
++ NodesModel::Node *selectedNode = nullptr;
++ for (auto &node : m_nodesModel->m_nodesList) {
++ if (node.selected) {
++ selectedNode = &node;
++ break;
++ }
++ }
++ setSelectedNode(selectedNode);
++
++ m_nodesModel->endResetModel();
++ m_arrowsModel->endResetModel();
++
++ // Right button opens the context menu
++ if (m_effectNodeSelected && event->button() == Qt::RightButton)
++ Q_EMIT openNodeContextMenu();
++
++ // Check if active (connected) nodes have changed
++ updateActiveNodesList();
++ }
++
++ void NodeView::mouseReleaseEvent(QMouseEvent *event)
++ {
++ Q_UNUSED(event)
++ m_mousePressed = false;
++ }
++
++ void NodeView::mouseDoubleClickEvent(QMouseEvent *event)
++ {
++ Q_UNUSED(event)
++ // When double clicking custom nodes or main node
++ if (m_selectedNodeId != -1 && m_selectedNodeId != 1)
++ Q_EMIT doubleClickNode();
++ }
++
++ void NodeView::hoverMoveEvent(QHoverEvent *event)
++ {
++ // If start/end is connected, other side follows mouse
++ if (m_activeArrow.startNodeId >= 0) {
++ m_activeArrowEndPoint = event->position();
++ emit activeArrowEndPointChanged();
++ } else if (m_activeArrow.endNodeId >= 0) {
++ m_activeArrowStartPoint = event->position();
++ emit activeArrowStartPointChanged();
++ }
++
++ // Pass event forward, to e.g. keep SplitView mouse cursor
++ // changes functioning correctly.
++ event->setAccepted(false);
++ }
++
++ void NodeView::keyPressEvent(QKeyEvent *event)
++ {
++ Q_UNUSED(event)
++ }
++
++ void NodeView::keyReleaseEvent(QKeyEvent *event)
++ {
++ Q_UNUSED(event)
++ }
++
++ // Selects the node with \a nodeId and deselects other nodes.
++ void NodeView::selectSingleNode(int nodeId)
++ {
++ m_nodesModel->beginResetModel();
++ // Check first selected node
++ NodesModel::Node *selectedNode = nullptr;
++ for (auto &node : m_nodesModel->m_nodesList) {
++ if (node.nodeId == nodeId) {
++ node.selected = true;
++ selectedNode = &node;
++ } else {
++ node.selected = false;
++ }
++ }
++ setSelectedNode(selectedNode);
++ m_nodesModel->endResetModel();
++ }
++
++ // Selects the Main node and deselects other nodes.
++ void NodeView::selectMainNode()
++ {
++ m_nodesModel->beginResetModel();
++ NodesModel::Node *selectedNode = nullptr;
++ for (auto &node : m_nodesModel->m_nodesList) {
++ if (node.type == 0) {
++ node.selected = true;
++ selectedNode = &node;
++ } else {
++ node.selected = false;
++ }
++ }
++ setSelectedNode(selectedNode);
++ m_nodesModel->endResetModel();
++ }
++
++ void NodeView::setSelectedNode(NodesModel::Node *selectedNode)
++ {
++ if (m_nodesModel->m_selectedNode == selectedNode)
++ return;
++
++ m_nodesModel->setSelectedNode(selectedNode);
++ bool effectNodeSelected = (selectedNode != nullptr && selectedNode->type == 2);
++ if (effectNodeSelected != m_effectNodeSelected) {
++ m_effectNodeSelected = effectNodeSelected;
++ Q_EMIT effectNodeSelectedChanged();
++ }
++
++ bool mainNodeSelected = (selectedNode != nullptr && selectedNode->type == 0);
++ if (mainNodeSelected != m_mainNodeSelected) {
++ m_mainNodeSelected = mainNodeSelected;
++ Q_EMIT mainNodeSelectedChanged();
++ }
++
++ m_selectedNodeId = selectedNode ? selectedNode->nodeId : -1;
++ Q_EMIT selectedNodeIdChanged();
++
++ updateCodeSelectorIndex();
++
++ Q_EMIT selectedNodeFragmentCodeChanged();
++ Q_EMIT selectedNodeVertexCodeChanged();
++ Q_EMIT selectedNodeQmlCodeChanged();
++ Q_EMIT selectedNodeNameChanged();
++ Q_EMIT selectedNodeDescriptionChanged();
++ }
++
++ NodesModel *NodeView::nodesModel() const
++ {
++ return m_nodesModel;
++ }
++
++ ArrowsModel *NodeView::arrowsModel() const
++ {
++ return m_arrowsModel;
++ }
++
++ QPointF NodeView::activeArrowStartPoint() const
++ {
++ return m_activeArrowStartPoint;
++ }
++
++ QPointF NodeView::activeArrowEndPoint() const
++ {
++ return m_activeArrowEndPoint;
++ }
++
++ bool NodeView::activeArrowEnabled() const
++ {
++ return m_activeArrowEnabled;
++ }
++
++ void NodeView::updateArrowsPositions()
++ {
++ m_arrowsModel->beginResetModel();
++ for (auto &arrow : m_arrowsModel->m_arrowsList) {
++ auto node = m_nodesModel->getNodeWithId(arrow.startNodeId);
++ auto node2 = m_nodesModel->getNodeWithId(arrow.endNodeId);
++ if (node && node2) {
++ arrow.startX = node->x + node->width / 2;
++ arrow.startY = node->y + node->height;
++ arrow.endX = node2->x + node2->width / 2;
++ arrow.endY = node2->y;
++ }
++ }
++ m_arrowsModel->endResetModel();
++ }
++
++ void NodeView::updateActiveNodesList()
++ {
++ QList<NodesModel::Node *> nodes;
++ QList<int> nodesIds;
++ auto node = m_nodesModel->getNodeWithId(0);
++ if (!node)
++ return;
++ nodes << node;
++ int nodeId = node ? node->nextNodeId : -1;
++ while (nodeId > 0) {
++ auto n = m_nodesModel->getNodeWithId(nodeId);
++ if (n) {
++ nodes << n;
++ if (!n->disabled)
++ nodesIds << n->nodeId;
++ nodeId = n->nextNodeId;
++ } else {
++ break;
++ }
++ }
++ m_activeNodesIds = nodesIds;
++
++ // Update info of graph being complete (source -> output)
++ bool nodeGraphComplete = false;
++ if (!nodes.isEmpty() && nodes.last()->type == 1)
++ nodeGraphComplete = true;
++
++ if (nodeGraphComplete != m_nodeGraphComplete) {
++ m_nodeGraphComplete = nodeGraphComplete;
++ Q_EMIT nodeGraphCompleteChanged();
++ }
++
++ if (m_activeNodesList != nodes) {
++ m_activeNodesList = nodes;
++ // Don't emit until fully initialized to avoid shader errors
++ if (m_initialized)
++ Q_EMIT activeNodesListChanged();
++ }
++ }
++
++ void NodeView::deleteSelectedNodes()
++ {
++ QList<int> nodes;
++ for (auto &node : m_nodesModel->m_nodesList) {
++ if (node.selected && node.type == 2)
++ nodes << node.nodeId;
++ }
++ m_effectManager->deleteEffectNodes(nodes);
++ }
++
++ QString NodeView::selectedNodeName() const {
++ if (auto selectedNode = m_nodesModel->m_selectedNode)
++ return selectedNode->name;
++ return QString();
++ }
++
++ void NodeView::setSelectedNodeName(const QString &name) {
++ if (auto selectedNode = m_nodesModel->m_selectedNode) {
++ if (selectedNode->name == name)
++ return;
++
++ // Make sure the name is unique
++ QString newName = getUniqueNodeName(name);
++ m_nodesModel->beginResetModel();
++ selectedNode->name = newName;
++ m_nodesModel->endResetModel();
++ Q_EMIT selectedNodeNameChanged();
++ }
++ }
++
++ QString NodeView::selectedNodeDescription() const {
++ if (auto selectedNode = m_nodesModel->m_selectedNode)
++ return selectedNode->description;
++ return QString();
++ }
++
++ void NodeView::setSelectedNodeDescription(const QString &description) {
++ if (auto selectedNode = m_nodesModel->m_selectedNode) {
++ if (selectedNode->description == description)
++ return;
++
++ m_nodesModel->beginResetModel();
++ selectedNode->description = description;
++ m_nodesModel->endResetModel();
++ Q_EMIT selectedNodeDescriptionChanged();
++ }
++ }
++
++ QString NodeView::selectedNodeFragmentCode() const
++ {
++ if (auto selectedNode = m_nodesModel->m_selectedNode)
++ return selectedNode->fragmentCode;
++ return QString();
++ }
++
++ void NodeView::setSelectedNodeFragmentCode(const QString &newSelectedNodeFragmentCode)
++ {
++ if (auto selectedNode = m_nodesModel->m_selectedNode) {
++ if (selectedNode->fragmentCode == newSelectedNodeFragmentCode)
++ return;
++ selectedNode->fragmentCode = newSelectedNodeFragmentCode;
++ Q_EMIT selectedNodeFragmentCodeChanged();
++ }
++ }
++
++ QString NodeView::selectedNodeVertexCode() const
++ {
++ if (auto selectedNode = m_nodesModel->m_selectedNode)
++ return selectedNode->vertexCode;
++ return QString();
++ }
++
++ void NodeView::setSelectedNodeVertexCode(const QString &newSelectedNodeVertexCode)
++ {
++ if (auto selectedNode = m_nodesModel->m_selectedNode) {
++ if (selectedNode->vertexCode == newSelectedNodeVertexCode)
++ return;
++ selectedNode->vertexCode = newSelectedNodeVertexCode;
++ Q_EMIT selectedNodeVertexCodeChanged();
++ }
++ }
++
++ QString NodeView::selectedNodeQmlCode() const
++ {
++ if (auto selectedNode = m_nodesModel->m_selectedNode)
++ return selectedNode->qmlCode;
++ return QString();
++ }
++
++ void NodeView::setSelectedNodeQmlCode(const QString &newSelectedNodeQmlCode)
++ {
++ if (auto selectedNode = m_nodesModel->m_selectedNode) {
++ if (selectedNode->qmlCode == newSelectedNodeQmlCode)
++ return;
++ selectedNode->qmlCode = newSelectedNodeQmlCode;
++ Q_EMIT selectedNodeQmlCodeChanged();
++ }
++ }
++
++ bool NodeView::nodeGraphComplete() const
++ {
++ return m_nodeGraphComplete;
++ }
++
++ bool NodeView::effectNodeSelected() const
++ {
++ return m_effectNodeSelected;
++ }
++
++ bool NodeView::mainNodeSelected() const
++ {
++ return m_mainNodeSelected;
++ }
++
++ int NodeView::selectedNodeId() const
++ {
++ return m_selectedNodeId;
++ }
++
++ // Layout the nodes currently in active list
++ void NodeView::layoutNodes(bool distribute)
++ {
++ if (m_activeNodesList.size() < 2)
++ return;
++
++ // Make sure that also non-connected nodes
++ // are inside the visible nodeview area.
++ if (!distribute) {
++ for (auto &node : m_nodesModel->m_nodesList) {
++ float newX = node.x;
++ float newY = node.y;
++ newX = std::max(newX, float(-node.width / 2.0f));
++ newX = std::min(newX, float(width() - node.width / 2.0f));
++ newY = std::max(newY, float(-node.height / 2.0f));
++ newY = std::min(newY, float(height() - node.height / 2.0f));
++ node.x = newX;
++ node.y = newY;
++ }
++ }
++
++ const int nodeCount = m_activeNodesList.size();
++ const float areaHeight = height();
++ const float sideMargin = areaHeight * 0.05f;
++ const float marginY = areaHeight * 0.02f;
++ const float areaWCenter = width() / 2.0f;
++ float origFullHeight = m_activeNodesList.last()->y - m_activeNodesList.first()->y;
++ origFullHeight = std::max(origFullHeight, 100.0f);
++ const float firstY = sideMargin;
++ const float lastY = areaHeight - firstY - m_activeNodesList.last()->height;
++ const float newFullHeight = lastY - firstY;
++ const float yScaling = newFullHeight / origFullHeight;
++ int i = 0;
++ m_nodesModel->beginResetModel();
++ for (const auto &n : m_activeNodesList) {
++ if (!distribute)
++ n->x = areaWCenter - n->width / 2.0f;
++
++ if (i == 0) {
++ n->y = firstY;
++ } else if (i == nodeCount - 1) {
++ n->y = lastY;
++ } else {
++ if (distribute) {
++ n->y = sideMargin + (float(i) / (nodeCount - 1)) * (areaHeight - sideMargin * 2.0f - n->height);
++ } else {
++ n->y = yScaling * n->y;
++ // Make sure that middle nodes Y are between some limits
++ if (auto previousNode = m_activeNodesList.at(i - 1))
++ n->y = std::max(n->y, previousNode->y + previousNode->height + marginY);
++ n->y = std::max(n->y, firstY + marginY + m_activeNodesList.first()->height);
++ n->y = std::min(n->y, lastY - marginY - n->height);
++ }
++ }
++ i++;
++ }
++
++ m_nodesModel->endResetModel();
++ updateArrowsPositions();
++ }
++
++ QStringList NodeView::codeSelectorModel() const
++ {
++ return m_codeSelectorModel;
++ }
++
++ int NodeView::codeSelectorIndex() const
++ {
++ return m_codeSelectorIndex;
++ }
++
++ int NodeView::getNodeIdWithName(const QString &name)
++ {
++ for (const auto &node : m_nodesModel->m_nodesList) {
++ if (node.name == name)
++ return node.nodeId;
++ }
++ return -1;
++ }
++
++ // Update the code selector (combobox) model
++ void NodeView::updateCodeSelectorModel()
++ {
++ QStringList codeSelectorModel;
++ for (const auto &node : m_nodesModel->m_nodesList) {
++ // Hide "Output" node as it currently doesn't contain code
++ if (node.type == NodesModel::DestinationNode)
++ continue;
++ codeSelectorModel << node.name;
++ }
++ if (codeSelectorModel != m_codeSelectorModel) {
++ m_codeSelectorModel = codeSelectorModel;
++ Q_EMIT codeSelectorModelChanged();
++ }
++ }
++
++ // Update the index of code selector (combobox) to match the currectly selected node
++ void NodeView::updateCodeSelectorIndex() {
++ int index = -1;
++ int i = 0;
++ for (const auto &node : m_nodesModel->m_nodesList) {
++ if (node.nodeId == m_selectedNodeId && node.type != NodesModel::DestinationNode) {
++ index = i;
++ break;
++ }
++ if (node.type != NodesModel::DestinationNode)
++ i++;
++ }
++ index = std::min(index, int(m_codeSelectorModel.size() - 1));
++ if (index != m_codeSelectorIndex) {
++ m_codeSelectorIndex = index;
++ Q_EMIT codeSelectorIndexChanged();
++ }
++ }
++
++ void NodeView::toggleNodeDisabled()
++ {
++ if (auto node = m_nodesModel->m_selectedNode) {
++ m_nodesModel->beginResetModel();
++ node->disabled = !node->disabled;
++ m_nodesModel->endResetModel();
++ }
++ updateActiveNodesList();
++ }
++
++
++ // This will return node id, which is not already in use
++ int NodeView::getUniqueNodeId()
++ {
++ if (!m_nodesModel)
++ return 0;
++
++ int id = 0;
++ // Get biggest id + 1
++ for (const auto &node : m_nodesModel->m_nodesList)
++ id = std::max(id, node.nodeId);
++ id++;
++
++ return id;
++ }
++
++ void NodeView::initializeNode(NodesModel::Node &node)
++ {
++ node.type = 2;
++ node.x = 20;
++ node.y = 60;
++ node.nodeId = getUniqueNodeId();
++ initializeNodeSize(node);
++ }
++
++ void NodeView::initializeNodeSize(NodesModel::Node &node)
++ {
++ if (node.type == NodesModel::CustomNode) {
++ node.width = 150;
++ node.height = 50;
++ } else {
++ node.width = 80;
++ node.height = 80;
++ }
++ }
++
++ // Returns unique node name, so name appended with a number.
++ // "Custom" -> "Custom2" -> "Custom3" etc.
++ QString NodeView::getUniqueNodeName(const QString &origName, int counter)
++ {
++ if (!m_nodesModel)
++ return origName;
++
++ // Initially try name as is, then first counter '2'.
++ QString counterString = (counter == 0) ? "" : QString::number(counter + 1);
++ QString name = origName + counterString;
++ counter++;
++ // Bail out just in case
++ if (counter > 99)
++ return name;
++ for (const auto &node : m_nodesModel->m_nodesList) {
++ if (node.name == name) {
++ name = getUniqueNodeName(origName, counter);
++ }
++ }
++ return name;
++ }