diff options
Diffstat (limited to 'src/Authoring/Qt3DStudio/Palettes/TimelineGraphicsView/RowMover.cpp')
-rw-r--r-- | src/Authoring/Qt3DStudio/Palettes/TimelineGraphicsView/RowMover.cpp | 451 |
1 files changed, 451 insertions, 0 deletions
diff --git a/src/Authoring/Qt3DStudio/Palettes/TimelineGraphicsView/RowMover.cpp b/src/Authoring/Qt3DStudio/Palettes/TimelineGraphicsView/RowMover.cpp new file mode 100644 index 00000000..3e96f2dc --- /dev/null +++ b/src/Authoring/Qt3DStudio/Palettes/TimelineGraphicsView/RowMover.cpp @@ -0,0 +1,451 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt 3D Studio. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "RowMover.h" +#include "RowTree.h" +#include "RowManager.h" +#include "TimelineGraphicsScene.h" +#include "TimelineConstants.h" +#include "StudioPreferences.h" + +#include <QtGui/qpainter.h> +#include <QtWidgets/qapplication.h> +#include <QtWidgets/qgraphicsitem.h> +#include <QtWidgets/qgraphicslinearlayout.h> + +RowMover::RowMover(TimelineGraphicsScene *scene) + : TimelineItem() + , m_scene(scene) +{ + setZValue(99); + setGeometry(0, 0, TimelineConstants::TREE_MAX_W, 10); + + m_autoExpandTimer.setSingleShot(true); + connect(&m_autoExpandTimer, &QTimer::timeout, [this]() { + if (m_rowAutoExpand) { + m_rowAutoExpand->updateExpandStatus(RowTree::ExpandState::Expanded, true); + // Update RowMover after the expansion. The +50 below is just a small margin to ensure + // correct row heights before updateTargetRowLater is called. + QTimer::singleShot(TimelineConstants::EXPAND_ANIMATION_DURATION + 50, [this]() { + if (updateTargetRowLater) + updateTargetRowLater(); + }); + } + }); +} + +void RowMover::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + Q_UNUSED(option) + Q_UNUSED(widget) + + static const QPolygon polygon({QPoint(0, 0), QPoint(0, 3), QPoint(7, 3), QPoint(7, 1), + QPoint(int(TimelineConstants::TREE_BOUND_W), 1), + QPoint(int(TimelineConstants::TREE_BOUND_W), 0)}); + painter->setPen(QPen(CStudioPreferences::timelineRowMoverColor(), 1)); + painter->setBrush(CStudioPreferences::timelineRowMoverColor()); + painter->drawConvexPolygon(polygon); +} + +RowTree *RowMover::insertionTarget() const +{ + return m_insertionTarget.data(); +} + +RowTree *RowMover::insertionParent() const +{ + return m_insertionParent; +} + +QVector<RowTree *> RowMover::sourceRows() const +{ + return m_sourceRows; +} + +void RowMover::removeSourceRow(RowTree *row) +{ + m_sourceRows.remove(m_sourceRows.indexOf(row)); +} + +bool RowMover::shouldDeleteAfterMove() const +{ + return m_deleteAfterMove; +} + +void RowMover::resetInsertionParent(RowTree *newParent) +{ + if (m_insertionParent) { + m_insertionParent->setDnDState(RowTree::DnDState::None, RowTree::DnDState::Parent); + m_insertionParent = nullptr; + } + + if (newParent) { + m_insertionParent = newParent; + m_insertionParent->setDnDState(RowTree::DnDState::Parent, RowTree::DnDState::None); + } else { + m_insertionTarget = nullptr; + } +} + +bool RowMover::isActive() const +{ + return m_active; +} + +void RowMover::start(const QVector<RowTree *> &rows) +{ + m_deleteAfterMove = false; + m_sourceRows.clear(); + if (!rows.isEmpty()) { + // Remove rows that have an ancestor included in the selection or ones that are of + // invalid type for moving + for (auto candidateRow : rows) { + bool omit = !candidateRow->draggable(); + if (!omit) { + for (auto checkRow : rows) { + if (candidateRow->isDecendentOf(checkRow)) + omit = true; + } + if (!omit) + m_sourceRows.append(candidateRow); + } + } + if (!m_sourceRows.isEmpty()) { + m_active = true; + for (auto row : qAsConst(m_sourceRows)) + row->setDnDState(RowTree::DnDState::Source, RowTree::DnDState::None, true); + qApp->setOverrideCursor(Qt::ClosedHandCursor); + } + } +} + +void RowMover::end(bool force) +{ + if (m_active || force) { + m_active = false; + for (auto row : qAsConst(m_sourceRows)) + row->setDnDState(RowTree::DnDState::None, RowTree::DnDState::Any, true); + + m_sourceRows.clear(); + + if (!m_insertionTarget.isNull()) + m_insertionTarget->setDnDState(RowTree::DnDState::None); + + setVisible(false); + resetInsertionParent(); + updateTargetRowLater = {}; + qApp->changeOverrideCursor(Qt::ArrowCursor); + qApp->restoreOverrideCursor(); + + m_autoExpandTimer.stop(); + } +} + +void RowMover::updateState(int depth, double y) +{ + setPos(24 + depth * TimelineConstants::ROW_DEPTH_STEP, y); + setVisible(true); +} + +bool RowMover::isNextSiblingRow(RowTree *rowMain, RowTree *rowSibling) const +{ + // order matters, rowSibling is below rowMain + return rowMain->parentRow() == rowSibling->parentRow() + && rowSibling->index() - rowMain->index() == 1; +} + +bool RowMover::sourceRowsHasMaster() const +{ + for (auto sourceRow : qAsConst(m_sourceRows)) { + if (sourceRow->isMaster() && sourceRow->objectType() != OBJTYPE_LAYER) + return true; + } + + return false; +} + +bool RowMover::isSourceRowsDescendant(RowTree *row) const +{ + if (row) { + for (auto sourceRow : qAsConst(m_sourceRows)) { + if (row == sourceRow || row->isDecendentOf(sourceRow)) + return true; + } + } + + return false; +} + +// rowType parameter is used to highlight the target row when RowMover is not active, +// i.e. when dragging from project or basic objects palettes +void RowMover::updateTargetRow(const QPointF &scenePos, EStudioObjectType rowType, + Q3DStudio::DocumentEditorFileType::Enum fileType, + bool firstTry) +{ + // DnD a presentation / Qml stream from the project panel (to set it as a subpresentation) + if (rowType & (OBJTYPE_PRESENTATION | OBJTYPE_QML_STREAM | OBJTYPE_MATERIALDATA)) { + if (!m_insertionTarget.isNull()) + m_insertionTarget->setDnDState(RowTree::DnDState::None, RowTree::DnDState::SP_TARGET); + + RowTree *rowAtMouse = m_scene->rowManager()->getRowAtPos(scenePos); + if (rowAtMouse) { + // m_insertionTarget will go through CFileDropSource::ValidateTarget() which will + // filter out invalid drop rows + m_insertionTarget = rowAtMouse; + m_insertType = Q3DStudio::DocumentEditorInsertType::LastChild; + + if (rowType == OBJTYPE_MATERIALDATA) { + if (rowAtMouse->objectType() & OBJTYPE_IS_MATERIAL) + m_insertionTarget->setDnDState(RowTree::DnDState::SP_TARGET); + } else { + if (rowAtMouse->objectType() & (OBJTYPE_LAYER | OBJTYPE_IS_MATERIAL | OBJTYPE_IMAGE) + && !rowAtMouse->isDefaultMaterial()) { + m_insertionTarget->setDnDState(RowTree::DnDState::SP_TARGET); + } + } + m_rowAutoExpand = rowAtMouse; + m_autoExpandTimer.start(TimelineConstants::AUTO_EXPAND_TIME); + } else { + m_rowAutoExpand = nullptr; + m_autoExpandTimer.stop(); + } + return; + } else if (fileType == Q3DStudio::DocumentEditorFileType::Image) { // DnD an image + if (!m_insertionTarget.isNull()) + m_insertionTarget->setDnDState(RowTree::DnDState::None, RowTree::DnDState::SP_TARGET); + // if draggin in the middle of a layer, mat, or image row, set the image as a texture. + RowTree *rowAtMouse = m_scene->rowManager()->getRowAtPos(scenePos); + if (rowAtMouse) { + double y = rowAtMouse->mapFromScene(scenePos).y(); + if (y > TimelineConstants::ROW_H * .25 && y < TimelineConstants::ROW_H * .75) { + if (rowAtMouse->objectType() & (OBJTYPE_LAYER | OBJTYPE_IS_MATERIAL | OBJTYPE_IMAGE) + && !rowAtMouse->isDefaultMaterial()) { + m_rowAutoExpand = nullptr; + m_autoExpandTimer.stop(); + setVisible(false); + resetInsertionParent(); + + m_insertionTarget = rowAtMouse; + m_insertionTarget->setDnDState(RowTree::DnDState::SP_TARGET); + m_insertType = Q3DStudio::DocumentEditorInsertType::LastChild; + return; + } + } + } + } + + EStudioObjectType theRowType = rowType; + if (theRowType == OBJTYPE_UNKNOWN && m_sourceRows.size() == 1) + theRowType = m_sourceRows[0]->objectType(); + + // row will be inserted just below rowInsert1 and just above rowInsert2 (if it exists) + RowTree *rowInsert1 = m_scene->rowManager() + ->getRowAtPos(scenePos + QPointF(0, TimelineConstants::ROW_H * -.5)); + RowTree *rowInsert2 = m_scene->rowManager() + ->getRowAtPos(scenePos + QPointF(0, TimelineConstants::ROW_H * .5)); + + // If on top half of the top row, adjust half row down, as we can never insert anything + // above the top row + if (!rowInsert1 && rowInsert2 && rowInsert2->index() == 0) { + rowInsert1 = m_scene->rowManager()->getRowAtPos(scenePos); + rowInsert2 = m_scene->rowManager() + ->getRowAtPos(scenePos + QPointF(0, TimelineConstants::ROW_H)); + } + + bool valid = rowInsert1 != nullptr; + + if (valid) { + // if dragging over a property or a parent of a property, move to the first row + // after the property + if (rowInsert1->isProperty()) { + rowInsert1 = rowInsert1->parentRow()->childProps().last(); + rowInsert2 = m_scene->rowManager() + ->getRowAtPos(QPointF(0, rowInsert1->y() + TimelineConstants::ROW_H)); + } else if (rowInsert1->hasPropertyChildren() && rowInsert1->expanded()) { + rowInsert1 = rowInsert1->childProps().last(); + rowInsert2 = m_scene->rowManager() + ->getRowAtPos(QPointF(0, rowInsert1->y() + TimelineConstants::ROW_H)); + } + + // calc insertion depth + bool inAComponent = static_cast<RowTree *>(m_scene->layoutTree()->itemAt(1))->isComponent(); + int depth; + int depthMin = 2; + if (rowInsert2) + depthMin = rowInsert2->depth(); + else if (theRowType != OBJTYPE_LAYER && !inAComponent) + depthMin = 3; + + int depthMax = rowInsert1->depth(); + bool srcHasMaster = sourceRowsHasMaster(); + if (!rowInsert1->locked() && rowInsert1->isContainer() && !m_sourceRows.contains(rowInsert1) + // prevent insertion a master row under a non-master unless under a component root + && (!(srcHasMaster && !rowInsert1->isMaster()) || rowInsert1->isComponentRoot())) { + depthMax++; // Container: allow insertion as a child + } else { + RowTree *row = rowInsert1->parentRow(); + if (rowInsert1->isPropertyOrMaterial() && !rowInsert1->parentRow()->isContainer()) { + depthMax--; // non-container with properties and/or a material + if (row) + row = row->parentRow(); + } + if (srcHasMaster) { + while (row && !row->isMaster() && !row->isComponent()) { + depthMax--; + row = row->parentRow(); + } + } + } + + if (theRowType == OBJTYPE_LAYER) { + depth = 2; // layers can only be moved on depth 2 + } else if (theRowType == OBJTYPE_EFFECT) { + depth = 3; // effects can only be moved on depth 3 (layer direct child) + } else { + static const int LEFT_MARGIN = 20; + depth = (int(scenePos.x()) - LEFT_MARGIN) / TimelineConstants::ROW_DEPTH_STEP; + depth = qBound(depthMin, depth, depthMax); + } + // calc insertion parent + RowTree *insertParent = rowInsert1; + // If we are dragging objects to a component, + // let insertParent be the component itself, not + // the parent for the component and set the source rows + // to be deleted soon (their duplicates will be inserted + // in the component). Do this only if m_active is true + // i.e. user is dragging a row within timeline (not from object/project panel) + // AND drop depth is larger than for the component (user is dropping items _in_ + // the component, not at the same depth as the component itself) + if (insertParent->isComponent() && !insertParent->isComponentRoot() + && depth > insertParent->depth() && m_active) { + m_deleteAfterMove = true; + } else { + m_deleteAfterMove = false; + for (int i = rowInsert1->depth(); i >= depth; --i) + insertParent = insertParent->parentRow(); + } + + resetInsertionParent(insertParent); + + if (depth < depthMin || depth > depthMax + || (theRowType != OBJTYPE_UNKNOWN + && !CStudioObjectTypes::AcceptableParent(theRowType, + m_insertionParent->objectType())) + || m_insertionParent->locked()) { + valid = false; + } + + for (auto sourceRow : qAsConst(m_sourceRows)) { + if (m_insertionParent == sourceRow + || m_insertionParent->isDecendentOf(sourceRow) + || !CStudioObjectTypes::AcceptableParent(sourceRow->objectType(), + m_insertionParent->objectType())) { + // prevent insertion under itself, or under unacceptable parent + valid = false; + break; + } + } + + // calc insertion target and type + if (rowInsert1 == m_insertionParent) { + if (m_insertionParent->expanded() && !m_insertionParent->childRows().empty()) { + m_insertionTarget = m_insertionParent->childRows().at(0); + m_insertType = Q3DStudio::DocumentEditorInsertType::PreviousSibling; + } else { + m_insertionTarget = m_insertionParent; + m_insertType = Q3DStudio::DocumentEditorInsertType::LastChild; + } + } else if (rowInsert1->isProperty() && depth == rowInsert1->depth()) { + if (m_insertionParent->childRows().isEmpty()) { + m_insertionTarget = m_insertionParent; + m_insertType = Q3DStudio::DocumentEditorInsertType::LastChild; + } else { + m_insertionTarget = m_insertionParent->childRows().at(0); + m_insertType = Q3DStudio::DocumentEditorInsertType::PreviousSibling; + } + } else { + m_insertionTarget = rowInsert1; + m_insertType = Q3DStudio::DocumentEditorInsertType::NextSibling; + if (depth < rowInsert1->depth()) { + for (int i = depth; i < rowInsert1->depth(); ++i) + m_insertionTarget = m_insertionTarget->parentRow(); + } + } + // Don't allow single move right next to moving row at same depth + if (m_sourceRows.size() == 1 + && (m_insertionTarget == m_sourceRows[0] + || ((m_insertType == Q3DStudio::DocumentEditorInsertType::NextSibling + && isNextSiblingRow(m_insertionTarget, m_sourceRows[0])) + || (m_insertType == Q3DStudio::DocumentEditorInsertType::PreviousSibling + && isNextSiblingRow(m_sourceRows[0], m_insertionTarget))))) { + valid = false; + } + if (valid) { + updateState(depth, rowInsert1->y() + rowInsert1->size().height()); + + // auto expand + if (!rowInsert1->locked() && !rowInsert1->expanded() && rowInsert1->isContainer() + && !rowInsert1->empty() && !isSourceRowsDescendant(rowInsert1) + && depth == rowInsert1->depth() + 1) { + updateTargetRowLater = std::bind(&RowMover::updateTargetRow, this, + scenePos, rowType, fileType, true); + m_rowAutoExpand = rowInsert1; + m_autoExpandTimer.start(TimelineConstants::AUTO_EXPAND_TIME); + } else { + m_rowAutoExpand = nullptr; + m_autoExpandTimer.stop(); + } + } else if (firstTry && rowInsert2 + && m_scene->rowManager()->getRowAtPos(scenePos) == rowInsert2) { + // If the current drop location turns out to be invalid and we are on the upper half of + // a row, we try to resolve target row as if we were half row below. This allows drags + // to be accepted over the entire row if inserting above the row is not valid. + updateTargetRow(scenePos + QPointF(0, TimelineConstants::ROW_H * .5), + rowType, fileType, false); + valid = !m_insertionTarget.isNull(); + } + } + + if (!valid) { + m_rowAutoExpand = nullptr; + m_autoExpandTimer.stop(); + setVisible(false); + resetInsertionParent(); + } +} + +int RowMover::type() const +{ + // Enable the use of qgraphicsitem_cast with this item. + return TypeRowMover; +} + +Q3DStudio::DocumentEditorInsertType::Enum RowMover::insertionType() const +{ + return m_insertType; +} |