Skip to content

Commit

Permalink
Tree Drag & Drop (#51)
Browse files Browse the repository at this point in the history
* Changed `TreeModel` constructor to accept `ResourceModelMap` so the tree model is capable of adding new resources to the map when doing a drag copy operation.
* Changed `MainWindow::CreateResource` to allocate a new `TreeNode` directly instead of through the root. I did this originally anticipating that `TreeModel::addNode` would be used by the drag and drop code, but ultimately it wasn't.
* Changed TreeModel::addNode to append the allocated child and undid the change in #47 we merged a few days ago.
* Created the helper `ResourceModelMap::CreateResourceName` which gives a unique name for a new resource of the given type. This was created by extracting the logic from `MainWindow::CreateResource` so that it could also be used by a drag copy operation. Again, I will reiterate from my previous comments on #42 that this can later be made more efficient by just caching the last number (maybe max id) used to create a new resource of that type.
* Changed the `dragDropMode` of the main window's tree view to `DragDrop` instead of `InternalMove` because we want to support copying the resources to the same target. Without this change the copy operation (done by holding CTRL during the drag) becomes just a move operation when the source and target are the same view.
* Changed `TreeModel::flags` to indicate `Qt::ItemIsDropEnabled` where appropriate (e.g, folders & the root) and `Qt::ItemIsDragEnabled` where appropriate (e.g, every valid/visible node).
* Changed `TreeModel::parent` to return an invalid model index, which means the root, when the parent node found in the `parents` map is null. This is actually an error condition and should hypothetically only occur if a logic mistake is made somewhere else, but I ran into it while doing this drag and drop and hence made the change.
* Added `TreeModel::supportedDropActions` to indicate that we support `MoveAction` and not just `CopyAction` (the default implementation of `QAbstractItemModel`).
* Added the private inline helper `TreeNode::treeNodeMime` so I didn't have to repeat the literal string multiple times, which decreases the potential for mistakes and typos and turns them into a compile-time error instead.
* Added `TreeModel::mimeTypes` to return the mime types supported by the tree model, for now it's just the "RadialGM/TreeNode" custom mime that is currently pointer based.
* Added `TreeModel::mimeData` to serialize tree nodes for drag and drop operations. 
    - It cannot do any IPC transferring of the data because it is purely pointer based for now.
    - I guard against drag operations originating outside the process by writing the application process id to the data stream and checking it later during the drop processing.
    - This can later be changed to just serialize the proto into bytes, but that requires some extra work I didn't want to do yet. For example, it will need to do some file transferring as well for certain resources like the sound file of a sound resource. I am not too sure how interested people would be in this proposed feature yet.
    - I sort the indices from lowest to highest row number so that I can make certain assumptions when processing a drop operation. For example, one of the things I like to assume is that other nodes removed from the same parent have effectively decreased the index of the current row we are removing. This also allows me to create new names for the nodes during a drag copy operation in a consecutive order.
* Added `TreeModel::dropMimeData` so the tree model can accept drop operations.
    - `ExtractSubrange` is used to remove the node from its parent's `child` repeated pointer field. I used this because it seemed to work but also the documentation says it releases ownership without destroying the item, which is what was intended.
    - `MoveAction` is the trickier one because it requires adjusting the insert and remove rows based on what rows we are dragging. This is even more complicated considering the fact that I chose to allow non-contiguous selections in the tree.
    - `CopyAction` is handled by doing a copy of the proto, not removing the previous nodes, and inserting the new nodes with a unique name based on their type. Later this will need to duplicate files on disk for certain resources, like the sound data file for a sound.
    - The insertion of the moved or copied nodes is done by appending it at the end of the new parent's `child` field. I then swap it into place since `RepeatedPtrField` has no built-in way of inserting items anywhere other than the end.
  • Loading branch information
RobertBColton authored Nov 17, 2018
1 parent cd8ff62 commit c099a98
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 23 deletions.
27 changes: 12 additions & 15 deletions MainWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ void MainWindow::openProject(std::unique_ptr<buffers::Project> openedProject) {
project = std::move(openedProject);

resourceMap.reset(new ResourceModelMap(project->mutable_game()->mutable_root(), nullptr));
treeModel.reset(new TreeModel(project->mutable_game()->mutable_root(), nullptr));
treeModel.reset(new TreeModel(project->mutable_game()->mutable_root(), resourceMap.get(), nullptr));

ui->treeView->setModel(treeModel.get());
treeModel->connect(treeModel.get(), &TreeModel::ResourceRenamed, resourceMap.get(),
Expand Down Expand Up @@ -342,29 +342,26 @@ void MainWindow::on_actionClearRecentMenu_triggered() { recentFiles->clear(); }

void MainWindow::CreateResource(TypeCase typeCase) {
auto *root = this->project->mutable_game()->mutable_root();
auto *child = root->add_child();
auto child = std::unique_ptr<TreeNode>(new TreeNode());
auto fieldNum = ResTypeFields[typeCase];
const Descriptor *desc = child->GetDescriptor();
const Reflection *refl = child->GetReflection();
const FieldDescriptor *field = desc->FindFieldByNumber(fieldNum);

// find a unique name for the new resource
const std::string pre = field->name();
std::string name;
int i = 0;
do {
name = pre + std::to_string(i++);
} while (resourceMap->GetResourceByName(typeCase, name) != nullptr);
child->set_name(name);

// allocate and set the child's resource field
refl->MutableMessage(child, field);
refl->MutableMessage(child.get(), field);

this->resourceMap->AddResource(child, resourceMap.get());
this->treeModel->addNode(child, root);
// find a unique name for the new resource
const QString name = resourceMap->CreateResourceName(child.get());
child->set_name(name.toStdString());

this->resourceMap->AddResource(child.get(), resourceMap.get());

// open the new resource for editing
openSubWindow(child);
openSubWindow(child.get());

// release ownership of the new child to its parent and the tree
this->treeModel->addNode(child.release(), root);
}

void MainWindow::on_actionCreate_Sprite_triggered() { CreateResource(TypeCase::kSprite); }
Expand Down
2 changes: 1 addition & 1 deletion MainWindow.ui
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@
<bool>true</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::InternalMove</enum>
<enum>QAbstractItemView::DragDrop</enum>
</property>
<property name="defaultDropAction">
<enum>Qt::MoveAction</enum>
Expand Down
18 changes: 18 additions & 0 deletions Models/ResourceModelMap.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "Models/ResourceModelMap.h"
#include "Editors/BaseEditor.h"
#include "MainWindow.h"

ResourceModelMap::ResourceModelMap(buffers::TreeNode* root, QObject* parent) : QObject(parent) {
Expand All @@ -21,6 +22,23 @@ void ResourceModelMap::AddResource(buffers::TreeNode* child, QObject* parent) {
_resources[child->type_case()][QString::fromStdString(child->name())] = new ProtoModel(child, parent);
}

QString ResourceModelMap::CreateResourceName(TreeNode* node) {
auto fieldNum = ResTypeFields[node->type_case()];
const Descriptor* desc = node->GetDescriptor();
const FieldDescriptor* field = desc->FindFieldByNumber(fieldNum);
return CreateResourceName(node->type_case(), QString::fromStdString(field->name()));
}

QString ResourceModelMap::CreateResourceName(int type, const QString& typeName) {
const QString pre = typeName;
QString name;
int i = 0;
do {
name = pre + QString::number(i++);
} while (GetResourceByName(type, name) != nullptr);
return name;
}

ProtoModel* ResourceModelMap::GetResourceByName(int type, const QString& name) {
if (_resources[type].contains(name))
return _resources[type][name];
Expand Down
2 changes: 2 additions & 0 deletions Models/ResourceModelMap.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ class ResourceModelMap : public QObject {
ProtoModel* GetResourceByName(int type, const QString& name);
ProtoModel* GetResourceByName(int type, const std::string& name);
void AddResource(buffers::TreeNode* node, QObject* parent);
QString CreateResourceName(TreeNode* node);
QString CreateResourceName(int type, const QString& typeName);

public slots:
void ResourceRenamed(TypeCase type, const QString& oldName, const QString& newName);
Expand Down
125 changes: 119 additions & 6 deletions Models/TreeModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
#include "Components/ArtManager.h"
#include "Models/ResourceModelMap.h"

#include <QCoreApplication>
#include <QMimeData>

IconMap TreeModel::iconMap;

TreeModel::TreeModel(buffers::TreeNode *root, QObject *parent) : QAbstractItemModel(parent), root(root) {
TreeModel::TreeModel(buffers::TreeNode *root, ResourceModelMap *resourceMap, QObject *parent)
: QAbstractItemModel(parent), root(root), resourceMap(resourceMap) {
iconMap = {{TypeCase::kFolder, ArtManager::GetIcon("group")},
{TypeCase::kSprite, ArtManager::GetIcon("sprite")},
{TypeCase::kSound, ArtManager::GetIcon("sound")},
Expand Down Expand Up @@ -83,9 +87,14 @@ QVariant TreeModel::data(const QModelIndex &index, int role) const {
}

Qt::ItemFlags TreeModel::flags(const QModelIndex &index) const {
if (!index.isValid()) return nullptr;

return QAbstractItemModel::flags(index) | Qt::ItemIsEditable;
Qt::ItemFlags flags = QAbstractItemModel::flags(index);

if (index.isValid()) {
auto *node = static_cast<TreeNode *>(index.internalPointer());
if (node->folder()) flags |= Qt::ItemIsDropEnabled;
return Qt::ItemIsDragEnabled | Qt::ItemIsEditable | flags;
} else
return Qt::ItemIsDropEnabled | flags;
}

QVariant TreeModel::headerData(int /*section*/, Qt::Orientation /*orientation*/, int role) const {
Expand Down Expand Up @@ -116,7 +125,7 @@ QModelIndex TreeModel::parent(const QModelIndex &index) const {
buffers::TreeNode *childItem = static_cast<buffers::TreeNode *>(index.internalPointer());
buffers::TreeNode *parentItem = parents[childItem];

if (parentItem == root) return QModelIndex();
if (parentItem == root || !parentItem) return QModelIndex();

return createIndex(parentItem->child_size(), 0, parentItem);
}
Expand All @@ -133,9 +142,113 @@ int TreeModel::rowCount(const QModelIndex &parent) const {
return parentItem->child_size();
}

Qt::DropActions TreeModel::supportedDropActions() const { return Qt::MoveAction | Qt::CopyAction; }

QStringList TreeModel::mimeTypes() const { return QStringList(treeNodeMime()); }

QMimeData *TreeModel::mimeData(const QModelIndexList &indexes) const {
QMimeData *mimeData = new QMimeData();
QByteArray data;

QDataStream stream(&data, QIODevice::WriteOnly);
QList<QModelIndex> nodes;

for (const QModelIndex &index : indexes) {
if (!index.isValid() || nodes.contains(index)) continue;
nodes << index;
}

// rows are moved starting with the lowest so we can create
// unique names in the order of insertion
std::sort(nodes.begin(), nodes.end(), std::less<QModelIndex>());

stream << QCoreApplication::applicationPid();
stream << nodes.count();
for (const QModelIndex &index : nodes) {
TreeNode *node = static_cast<TreeNode *>(index.internalPointer());
stream << reinterpret_cast<qlonglong>(node) << index.row();
}
mimeData->setData(treeNodeMime(), data);
return mimeData;
}

bool TreeModel::dropMimeData(const QMimeData *mimeData, Qt::DropAction action, int row, int /*column*/,
const QModelIndex &parent) {
if (action != Qt::MoveAction && action != Qt::CopyAction) return false;
// ensure the data is in the format we expect
if (!mimeData->hasFormat(treeNodeMime())) return false;
QByteArray data = mimeData->data(treeNodeMime());
QDataStream stream(&data, QIODevice::ReadOnly);

qint64 senderPid;
stream >> senderPid;
// ensure the data is coming from the same process since mime is pointer based
if (senderPid != QCoreApplication::applicationPid()) return false;

TreeNode *parentNode = static_cast<TreeNode *>(parent.internalPointer());
if (!parentNode) parentNode = root;
int count;
stream >> count;
if (count <= 0) return false;
if (row == -1) row = rowCount(parent);
QHash<TreeNode *, unsigned> removedCount;

for (int i = 0; i < count; ++i) {
qlonglong nodePtr;
stream >> nodePtr;
int itemRow;
stream >> itemRow;
TreeNode *node = reinterpret_cast<TreeNode *>(nodePtr);

if (action != Qt::CopyAction) {
auto *oldParent = parents[node];

// offset the row we are removing by the number of
// rows already removed from the same parent
if (parentNode != oldParent || row > itemRow) {
itemRow -= removedCount[oldParent]++;
}

// if moving the node within the same parent we need to adjust the row
// since its own removal will affect the row we reinsert it at
if (parentNode == oldParent && row > itemRow) --row;

auto index = this->createIndex(itemRow, 0, node);
beginRemoveRows(index.parent(), itemRow, itemRow);
auto oldRepeated = oldParent->mutable_child();
oldRepeated->ExtractSubrange(itemRow, 1, nullptr);
parents.remove(node);
endRemoveRows();
} else {
// duplicate the node
auto *dup = node->New();
dup->CopyFrom(*node);
node = dup;
// give the duplicate node a new name
const QString name = resourceMap->CreateResourceName(node);
node->set_name(name.toStdString());
// add the new node to the resource map
resourceMap->AddResource(node, resourceMap);
}

beginInsertRows(parent, row, row);
parentNode->mutable_child()->AddAllocated(node);
for (int j = parentNode->child_size() - 1; j > row; --j) {
parentNode->mutable_child()->SwapElements(j, j - 1);
}
parents[node] = parentNode;
endInsertRows();

++row;
}

return true;
}

void TreeModel::addNode(buffers::TreeNode *child, buffers::TreeNode *parent) {
auto rootIndex = QModelIndex();
emit beginInsertRows(rootIndex, parent->child_size() - 1, parent->child_size() - 1);
emit beginInsertRows(rootIndex, parent->child_size(), parent->child_size());
parent->mutable_child()->AddAllocated(child);
parents[child] = parent;
emit endInsertRows();
}
11 changes: 10 additions & 1 deletion Models/TreeModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#define TREEMODEL_H

#include "Components/ArtManager.h"
#include "Models/ResourceModelMap.h"
#include "codegen/treenode.pb.h"

#include <QAbstractItemModel>
Expand All @@ -18,7 +19,7 @@ class TreeModel : public QAbstractItemModel {
public:
static IconMap iconMap;

explicit TreeModel(buffers::TreeNode *root, QObject *parent);
explicit TreeModel(buffers::TreeNode *root, ResourceModelMap *resourceMap, QObject *parent);

bool setData(const QModelIndex &index, const QVariant &value, int role) override;
QVariant data(const QModelIndex &index, int role) const override;
Expand All @@ -29,16 +30,24 @@ class TreeModel : public QAbstractItemModel {
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;

Qt::DropActions supportedDropActions() const override;
QStringList mimeTypes() const override;
QMimeData *mimeData(const QModelIndexList &indexes) const override;
bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column,
const QModelIndex &parent) override;

void addNode(buffers::TreeNode *child, buffers::TreeNode *parent);

signals:
void ResourceRenamed(TypeCase type, const QString &oldName, const QString &newName);

private:
buffers::TreeNode *root;
ResourceModelMap *resourceMap;
QHash<buffers::TreeNode *, buffers::TreeNode *> parents;

void SetupParents(buffers::TreeNode *root);
inline QString treeNodeMime() const { return QStringLiteral("RadialGM/TreeNode"); }
};

#endif // TREEMODEL_H

0 comments on commit c099a98

Please sign in to comment.