From e708eb3d8324ff2812f4dcfaa71ca2e492dbb90c Mon Sep 17 00:00:00 2001 From: Kai Koehne <kai.koehne@nokia.com> Date: Mon, 19 Jul 2010 14:28:53 +0200 Subject: [PATCH] Qml Outline: Support rearrangement of the items via drag&drop --- src/plugins/qmljseditor/qmljsoutline.cpp | 5 + src/plugins/qmljseditor/qmloutlinemodel.cpp | 299 +++++++++++++++++++- src/plugins/qmljseditor/qmloutlinemodel.h | 28 +- 3 files changed, 316 insertions(+), 16 deletions(-) diff --git a/src/plugins/qmljseditor/qmljsoutline.cpp b/src/plugins/qmljseditor/qmljsoutline.cpp index 3eced196845..1794ee89a21 100644 --- a/src/plugins/qmljseditor/qmljsoutline.cpp +++ b/src/plugins/qmljseditor/qmljsoutline.cpp @@ -25,6 +25,11 @@ QmlJSOutlineTreeView::QmlJSOutlineTreeView(QWidget *parent) : // see also CppOutlineTreeView setFocusPolicy(Qt::NoFocus); setExpandsOnDoubleClick(false); + + setDragEnabled(true); + viewport()->setAcceptDrops(true); + setDropIndicatorShown(true); + setDragDropMode(InternalMove); } QmlJSOutlineFilterModel::QmlJSOutlineFilterModel(QObject *parent) : diff --git a/src/plugins/qmljseditor/qmloutlinemodel.cpp b/src/plugins/qmljseditor/qmloutlinemodel.cpp index 2dd378787b2..c0d4afd53ba 100644 --- a/src/plugins/qmljseditor/qmloutlinemodel.cpp +++ b/src/plugins/qmljseditor/qmloutlinemodel.cpp @@ -1,8 +1,12 @@ #include "qmloutlinemodel.h" #include "qmljseditor.h" +#include "qmljsrefactoringchanges.h" + #include <qmljs/parser/qmljsastvisitor_p.h> #include <qmljs/qmljsinterpreter.h> #include <qmljs/qmljslookupcontext.h> +#include <qmljs/qmljsmodelmanagerinterface.h> +#include <qmljs/qmljsrewriter.h> #include <coreplugin/icore.h> #include <QtCore/QDebug> @@ -20,15 +24,20 @@ namespace QmlJSEditor { namespace Internal { QmlOutlineItem::QmlOutlineItem(QmlOutlineModel *model) : - m_outlineModel(model) + m_outlineModel(model), + m_node(0), + m_idNode(0) { + Qt::ItemFlags defaultFlags = flags(); + setFlags(Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags); + setEditable(false); } QVariant QmlOutlineItem::data(int role) const { if (role == Qt::ToolTipRole) { - AST::SourceLocation location = data(QmlOutlineModel::SourceLocationRole).value<AST::SourceLocation>(); - AST::UiQualifiedId *uiQualifiedId = data(QmlOutlineModel::IdPointerRole).value<AST::UiQualifiedId*>(); + AST::SourceLocation location = sourceLocation(); + AST::UiQualifiedId *uiQualifiedId = m_idNode; if (!uiQualifiedId) return QVariant(); @@ -45,9 +54,46 @@ QVariant QmlOutlineItem::data(int role) const return QStandardItem::data(role); } +int QmlOutlineItem::type() const +{ + return UserType; +} + +QmlJS::AST::SourceLocation QmlOutlineItem::sourceLocation() const +{ + return data(QmlOutlineModel::SourceLocationRole).value<QmlJS::AST::SourceLocation>(); +} + +void QmlOutlineItem::setSourceLocation(const QmlJS::AST::SourceLocation &location) +{ + setData(QVariant::fromValue(location), QmlOutlineModel::SourceLocationRole); +} + +QmlJS::AST::Node *QmlOutlineItem::node() const +{ + return m_node; +} + +void QmlOutlineItem::setNode(QmlJS::AST::Node *node) +{ + m_node = node; +} + +QmlJS::AST::UiQualifiedId *QmlOutlineItem::idNode() const +{ + return m_idNode; +} + +void QmlOutlineItem::setIdNode(QmlJS::AST::UiQualifiedId *idNode) +{ + m_idNode = idNode; +} + QmlOutlineItem &QmlOutlineItem::copyValues(const QmlOutlineItem &other) { *this = other; + m_node = other.m_node; + m_idNode = other.m_idNode; emitDataChanged(); return *this; } @@ -72,6 +118,42 @@ QString QmlOutlineItem::prettyPrint(const QmlJS::Interpreter::Value *value, QmlJ return typeId; } +/** + Returns mapping of every UiObjectMember object to it's direct UiObjectMember parent object. + */ +class ObjectMemberParentVisitor : public AST::Visitor +{ +public: + QHash<AST::UiObjectMember*,AST::UiObjectMember*> operator()(Document::Ptr doc) { + parent.clear(); + if (doc && doc->ast()) + doc->ast()->accept(this); + return parent; + } + +private: + QHash<AST::UiObjectMember*,AST::UiObjectMember*> parent; + QList<AST::UiObjectMember *> stack; + + bool preVisit(AST::Node *node) + { + if (AST::UiObjectMember *objMember = node->uiObjectMemberCast()) + stack.append(objMember); + return true; + } + + void postVisit(AST::Node *node) + { + if (AST::UiObjectMember *objMember = node->uiObjectMemberCast()) { + stack.removeLast(); + if (!stack.isEmpty()) { + parent.insert(objMember, stack.last()); + } + } + } +}; + + class QmlOutlineModelSync : protected AST::Visitor { public: @@ -157,6 +239,78 @@ QmlOutlineModel::QmlOutlineModel(QObject *parent) : m_icons = Icons::instance(); const QString resourcePath = Core::ICore::instance()->resourcePath(); QmlJS::Icons::instance()->setIconFilesPath(resourcePath + "/qmlicons"); + + // TODO: Maybe add a Copy Action? + setSupportedDragActions(Qt::MoveAction); + setItemPrototype(new QmlOutlineItem(this)); +} + +QStringList QmlOutlineModel::mimeTypes() const +{ + QStringList types; + types << QLatin1String("application/x-qtcreator-qmloutlinemodel"); + return types; +} + + +QMimeData *QmlOutlineModel::mimeData(const QModelIndexList &indexes) const +{ + if (indexes.count() <= 0) + return 0; + QStringList types = mimeTypes(); + QMimeData *data = new QMimeData(); + QString format = types.at(0); + QByteArray encoded; + QDataStream stream(&encoded, QIODevice::WriteOnly); + stream << indexes.size(); + + // We store pointers to the QmlOutlineItems dropped. + // This works because we're only supporting drag&drop inside the model + for (int i = 0; i < indexes.size(); ++i) { + QmlOutlineItem *item = static_cast<QmlOutlineItem*>(itemFromIndex(indexes.at(i))); + stream << (quint64)item; + } + data->setData(format, encoded); + return data; +} + +bool QmlOutlineModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int /*column*/, const QModelIndex &parent) +{ + if (debug) + qDebug() << __FUNCTION__ << row << parent; + + // check if the action is supported + if (!data || !(action == Qt::CopyAction || action == Qt::MoveAction)) + return false; + + // We cannot reparent outside of the root item + if (!parent.isValid()) + return false; + + // check if the format is supported + QStringList types = mimeTypes(); + if (types.isEmpty()) + return false; + QString format = types.at(0); + if (!data->hasFormat(format)) + return false; + + // decode and insert + QByteArray encoded = data->data(format); + QDataStream stream(&encoded, QIODevice::ReadOnly); + int indexSize; + stream >> indexSize; + QList<QmlOutlineItem*> itemsToMove; + for (int i = 0; i < indexSize; ++i) { + quint64 itemPtr; + stream >> itemPtr; + itemsToMove << reinterpret_cast<QmlOutlineItem*>(itemPtr); + } + + QmlOutlineItem *targetItem = static_cast<QmlOutlineItem*>(itemFromIndex(parent)); + reparentNodes(targetItem, row, itemsToMove); + // Prevent view from calling insertRow(), removeRow() on it's own + return false; } QmlJS::Document::Ptr QmlOutlineModel::document() const @@ -204,9 +358,9 @@ QModelIndex QmlOutlineModel::enterObjectDefinition(AST::UiObjectDefinition *objD } prototype.setIcon(m_typeToIcon.value(typeName)); prototype.setData(ElementType, ItemTypeRole); - prototype.setData(QVariant::fromValue(getLocation(objDef)), SourceLocationRole); - prototype.setData(QVariant::fromValue(static_cast<AST::Node*>(objDef)), NodePointerRole); - prototype.setData(QVariant::fromValue(static_cast<AST::UiQualifiedId*>(objDef->qualifiedTypeNameId)), IdPointerRole); + prototype.setSourceLocation(getLocation(objDef)); + prototype.setNode(objDef); + prototype.setIdNode(objDef->qualifiedTypeNameId); return enterNode(prototype); } @@ -223,9 +377,9 @@ QModelIndex QmlOutlineModel::enterScriptBinding(AST::UiScriptBinding *scriptBind prototype.setText(asString(scriptBinding->qualifiedId)); prototype.setIcon(m_icons->scriptBindingIcon()); prototype.setData(PropertyType, ItemTypeRole); - prototype.setData(QVariant::fromValue(getLocation(scriptBinding)), SourceLocationRole); - prototype.setData(QVariant::fromValue(static_cast<AST::Node*>(scriptBinding)), NodePointerRole); - prototype.setData(QVariant::fromValue(static_cast<AST::UiQualifiedId*>(scriptBinding->qualifiedId)), IdPointerRole); + prototype.setSourceLocation(getLocation(scriptBinding)); + prototype.setNode(scriptBinding); + prototype.setIdNode(scriptBinding->qualifiedId); return enterNode(prototype); } @@ -242,8 +396,8 @@ QModelIndex QmlOutlineModel::enterPublicMember(AST::UiPublicMember *publicMember prototype.setText(publicMember->name->asString()); prototype.setIcon(m_icons->publicMemberIcon()); prototype.setData(PropertyType, ItemTypeRole); - prototype.setData(QVariant::fromValue(getLocation(publicMember)), SourceLocationRole); - prototype.setData(QVariant::fromValue(static_cast<AST::Node*>(publicMember)), NodePointerRole); + prototype.setSourceLocation(getLocation(publicMember)); + prototype.setNode(publicMember); return enterNode(prototype); } @@ -256,7 +410,8 @@ void QmlOutlineModel::leavePublicMember() QmlJS::AST::Node *QmlOutlineModel::nodeForIndex(const QModelIndex &index) { if (index.isValid()) { - return index.data(NodePointerRole).value<QmlJS::AST::Node*>(); + QmlOutlineItem *item = static_cast<QmlOutlineItem*>(itemFromIndex(index)); + return item->node(); } return 0; } @@ -332,6 +487,126 @@ void QmlOutlineModel::leaveNode() m_treePos.last()++; } +void QmlOutlineModel::reparentNodes(QmlOutlineItem *targetItem, int row, QList<QmlOutlineItem*> itemsToMove) +{ + Utils::ChangeSet changeSet; + + AST::UiObjectMember *targetObjectMember = dynamic_cast<AST::UiObjectMember*>(targetItem->node()); + if (!targetObjectMember) + return; + + QList<Utils::ChangeSet::Range> changedRanges; + + for (int i = 0; i < itemsToMove.size(); ++i) { + QmlOutlineItem *outlineItem = itemsToMove.at(i); + AST::UiObjectMember *sourceObjectMember = dynamic_cast<AST::UiObjectMember*>(outlineItem->node()); + if (!sourceObjectMember) + return; + + bool insertionOrderSpecified = true; + AST::UiObjectMember *memberToInsertAfter = 0; + { + if (row == -1) { + insertionOrderSpecified = false; + } else if (row > 0) { + QmlOutlineItem *outlineItem = static_cast<QmlOutlineItem*>(targetItem->child(row - 1)); + memberToInsertAfter = dynamic_cast<QmlJS::AST::UiObjectMember*>(outlineItem->node()); + } + } + + Utils::ChangeSet::Range range; + if (sourceObjectMember) + moveObjectMember(sourceObjectMember, targetObjectMember, insertionOrderSpecified, + memberToInsertAfter, &changeSet, &range); + changedRanges << range; + } + + QmlJSRefactoringChanges refactoring(QmlJS::ModelManagerInterface::instance(), m_semanticInfo.snapshot); + refactoring.changeFile(m_semanticInfo.document->fileName(), changeSet); + foreach (const Utils::ChangeSet::Range &range, changedRanges) { + refactoring.reindent(m_semanticInfo.document->fileName(), range); + } + refactoring.apply(); +} + +void QmlOutlineModel::moveObjectMember(AST::UiObjectMember *toMove, + AST::UiObjectMember *newParent, + bool insertionOrderSpecified, + AST::UiObjectMember *insertAfter, + Utils::ChangeSet *changeSet, + Utils::ChangeSet::Range *addedRange) +{ + Q_ASSERT(toMove); + Q_ASSERT(newParent); + Q_ASSERT(changeSet); + + QHash<QmlJS::AST::UiObjectMember*, QmlJS::AST::UiObjectMember*> parentMembers; + { + ObjectMemberParentVisitor visitor; + parentMembers = visitor(m_semanticInfo.document); + } + + AST::UiObjectMember *oldParent = parentMembers.value(toMove); + Q_ASSERT(oldParent); + + // make sure that target parent is actually a direct ancestor of target sibling + if (insertAfter) + newParent = parentMembers.value(insertAfter); + + const QString documentText = m_semanticInfo.document->source(); + + if (AST::UiObjectDefinition *objDefinition = dynamic_cast<AST::UiObjectDefinition*>(newParent)) { + // target is an element + + Rewriter rewriter(documentText, changeSet, QStringList()); + rewriter.removeObjectMember(toMove, oldParent); + + AST::UiObjectMemberList *listInsertAfter = 0; + if (insertionOrderSpecified) { + if (insertAfter) { + listInsertAfter = objDefinition->initializer->members; + while (listInsertAfter && (listInsertAfter->member != insertAfter)) + listInsertAfter = listInsertAfter->next; + } + } + + if (AST::UiScriptBinding *moveScriptBinding = dynamic_cast<AST::UiScriptBinding*>(toMove)) { + const QString propertyName = asString(moveScriptBinding->qualifiedId); + QString propertyValue; + { + const int offset = moveScriptBinding->statement->firstSourceLocation().begin(); + const int length = moveScriptBinding->statement->lastSourceLocation().end() - offset; + propertyValue = documentText.mid(offset, length); + } + Rewriter::BindingType bindingType = Rewriter::ScriptBinding; + + if (insertionOrderSpecified) { + *addedRange = rewriter.addBinding(objDefinition->initializer, propertyName, propertyValue, bindingType, listInsertAfter); + } else { + *addedRange = rewriter.addBinding(objDefinition->initializer, propertyName, propertyValue, bindingType); + } + } else { + QString strToMove; + { + const int offset = toMove->firstSourceLocation().begin(); + const int length = toMove->lastSourceLocation().end() - offset; + strToMove = documentText.mid(offset, length); + } + + if (insertionOrderSpecified) { + *addedRange = rewriter.addObject(objDefinition->initializer, strToMove, listInsertAfter); + } else { + *addedRange = rewriter.addObject(objDefinition->initializer, strToMove); + } + } + } else { + // target is a property + } + +// addedRange->start = newParent->lastSourceLocation().offset - 1; +// addedRange->end = addedRange->start + strToMove.length(); +} + QStandardItem *QmlOutlineModel::parentItem() { QStandardItem *parent = m_currentItem->parent(); diff --git a/src/plugins/qmljseditor/qmloutlinemodel.h b/src/plugins/qmljseditor/qmloutlinemodel.h index 8c853483f90..6fd0c4bbf76 100644 --- a/src/plugins/qmljseditor/qmloutlinemodel.h +++ b/src/plugins/qmljseditor/qmloutlinemodel.h @@ -2,6 +2,7 @@ #define QMLOUTLINEMODEL_H #include "qmljseditor.h" +#include <utils/changeset.h> #include <qmljs/qmljsdocument.h> #include <qmljs/qmljsicons.h> #include <qmljs/qmljslookupcontext.h> @@ -27,6 +28,16 @@ public: //QStandardItem QVariant data(int role = Qt::UserRole + 1) const; + int type() const; + + QmlJS::AST::SourceLocation sourceLocation() const; + void setSourceLocation(const QmlJS::AST::SourceLocation &location); + + QmlJS::AST::Node *node() const; + void setNode(QmlJS::AST::Node *node); + + QmlJS::AST::UiQualifiedId *idNode() const; + void setIdNode(QmlJS::AST::UiQualifiedId *idNode); QmlOutlineItem ©Values(const QmlOutlineItem &other); // so that we can assign all values at onc @@ -34,8 +45,11 @@ private: QString prettyPrint(const QmlJS::Interpreter::Value *value, QmlJS::Interpreter::Context *context) const; QmlOutlineModel *m_outlineModel; + QmlJS::AST::Node *m_node; + QmlJS::AST::UiQualifiedId *m_idNode; }; + class QmlOutlineModel : public QStandardItemModel { Q_OBJECT @@ -44,8 +58,6 @@ public: enum CustomRoles { SourceLocationRole = Qt::UserRole + 1, ItemTypeRole, - NodePointerRole, - IdPointerRole }; enum ItemTypes { @@ -55,6 +67,11 @@ public: QmlOutlineModel(QObject *parent = 0); + // QStandardItemModel + QStringList mimeTypes() const; + QMimeData *mimeData(const QModelIndexList &indexes) const; + bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent); + QmlJS::Document::Ptr document() const; void update(const SemanticInfo &semanticInfo); QmlJS::AST::Node *nodeForIndex(const QModelIndex &index); @@ -76,6 +93,11 @@ private: QModelIndex enterNode(const QmlOutlineItem &prototype); void leaveNode(); + void reparentNodes(QmlOutlineItem *targetItem, int targetRow, QList<QmlOutlineItem*> itemsToMove); + void moveObjectMember(QmlJS::AST::UiObjectMember *toMove, QmlJS::AST::UiObjectMember *newParent, + bool insertionOrderSpecified, QmlJS::AST::UiObjectMember *insertAfter, + Utils::ChangeSet *changeSet, Utils::ChangeSet::Range *addedRange); + QStandardItem *parentItem(); static QString asString(QmlJS::AST::UiQualifiedId *id); @@ -100,7 +122,5 @@ private: } // namespace QmlJSEditor Q_DECLARE_METATYPE(QmlJS::AST::SourceLocation); -Q_DECLARE_METATYPE(QmlJS::AST::Node*); -Q_DECLARE_METATYPE(QmlJS::AST::UiQualifiedId*); #endif // QMLOUTLINEMODEL_H -- GitLab