Solved Drag and drop glitch
-
Hi everyone!
Summary:
I have a weird glitch when I drag a standard item: the item rectangle is not right behind the cursor. For some reason, it seems that the start point of drag is computed from the last selection and not the new one. Besides, this glitch happen only when dragging an item whereas the widget didn't have the focus. Then everything works fine.
I had to perform the drag and drop manually, which means I have most likely missed something, leading to that strange behavior.Graphical context:
I have two tree views: on the right one categories are listed, on the left one instances of categories are created. So when I drag and drop a category in the left, a new instance of that category is created. Moreover, instances can be reorganized via drag and drop within the left tree view.Visual illustrations
Click to see the animationRelevant code
- CategoryModel.cxx
[...] QMimeData* CategoryModel::mimeData(QModelIndexList const& p_indexes) const { QMimeData* mime = new QMimeData; auto const& firstIndex = p_indexes.first(); if (firstIndex.isValid()) { // Attach the model loader type so that the right model loader // can be created when dropping the category QString loaderType = data(firstIndex, eLoaderFactoryKeyRole).toString(); mime->setData("loader-item", loaderType.toLocal8Bit()); } return mime; } void CategoryModel::FillModelWithCategories() { clear(); auto xmlLoader = new QStandardItem("XML Loader"); xmlLoader->setData("XML Loader", eLoaderFactoryKeyRole); appendRow(xmlLoader); auto sqlLoader = new QStandardItem("SQL Loader"); sqlLoader->setData("SQL Loader", eLoaderFactoryKeyRole); appendRow(sqlLoader); auto csvLoader = new QStandardItem("CSV Loader"); csvLoader->setData("CSV Loader", eLoaderFactoryKeyRole); appendRow(csvLoader); }
- CategoryTreeView.cxx
[...] CategoryTreeView::CategoryTreeView(QWidget *parent): QTreeView(parent) { setSelectionMode(QAbstractItemView::SingleSelection); setDragEnabled(true); setDropIndicatorShown(true); setDragDropMode(QAbstractItemView::DragOnly); header()->hide(); setEditTriggers(QAbstractItemView::NoEditTriggers); }
- ModelLoaderModel.cxx
[...] Qt::DropActions ModelLoaderModel::supportedDragActions() const { return Qt::MoveAction; } Qt::DropActions ModelLoaderModel::supportedDropActions() const { return Qt::MoveAction | Qt::CopyAction; } Qt::ItemFlags ModelLoaderModel::flags(QModelIndex const& p_index) const { Qt::ItemFlags indexFlags = QAbstractItemModel::flags(p_index); // We can drag loader to move them, but not their children if (p_index.parent().isValid()) { indexFlags = indexFlags & ~Qt::ItemIsDragEnabled; } else { indexFlags = indexFlags | Qt::ItemIsDragEnabled; } if (p_index.column() == eValue) { indexFlags = indexFlags | Qt::ItemIsEditable; } return indexFlags; } bool ModelLoaderModel::canDropMimeData(QMimeData const* p_data, Qt::DropAction p_action, int p_row, int p_column, QModelIndex const& p_parent) const { Q_UNUSED(p_action); Q_UNUSED(p_row); Q_UNUSED(p_column); Q_UNUSED(p_parent); if (p_data->hasFormat("loader-item")) { return true; } bool dataAcceptable = false; for (auto const& format: p_data->formats()) { dataAcceptable = dataAcceptable || mimeTypes().contains(format); } return dataAcceptable; } bool ModelLoaderModel::setData(QModelIndex const& p_index, QVariant const& p_value, int p_role) { bool isSuccessful = QStandardItemModel::setData(p_index, p_value, p_role); // If p_value is the same as the current value, emit the dataChanged() signal anyway if (p_value == p_index.data()) { using RoleVector = QVector<int>; RoleVector roles; roles.push_back(p_role); Q_EMIT(dataChanged(p_index, p_index, roles)); } return isSuccessful; }
- ModelLoaderTreeView.cxx
[...] ModelLoaderTreeView::ModelLoaderTreeView(QWidget *parent) : QTreeView(parent) { // Enable drag and drop setSelectionMode(QAbstractItemView::SingleSelection); setDragEnabled(true); viewport()->setAcceptDrops(true); setDropIndicatorShown(true); setDragDropMode(QAbstractItemView::DragDrop); } void ModelLoaderTreeView::dragEnterEvent(QDragEnterEvent* p_event) { if (p_event->mimeData()->hasFormat("simulation-manager/loader-item") == false) { m_oldModelLoaderIndex = selectionModel()->currentIndex(); } QTreeView::dragEnterEvent(p_event); } void ModelLoaderTreeView::dragMoveEvent(QDragMoveEvent* p_event) { p_event->accept(); } void ModelLoaderTreeView::dropEvent(QDropEvent* p_event) { // Get mime data QMimeData const* mimeData = p_event->mimeData(); QStringList formats = mimeData->formats(); // Get index to drop according to drop postion auto indexDropped = indexAt(p_event->pos()); if (indexDropped.isValid() == false && m_oldModelLoaderIndex.isValid()) { indexDropped = model()->index(model()->rowCount()-1, 0); } else if (indexDropped.parent().isValid()) { auto parentIndex = indexDropped.parent(); if (parentIndex.row() < model()->rowCount() - 1) { if (m_oldModelLoaderIndex.isValid() == false || (m_oldModelLoaderIndex.isValid() && m_oldModelLoaderIndex.row() > parentIndex.row())) { parentIndex = parentIndex.sibling(parentIndex.row()+1, parentIndex.column()); } } indexDropped = parentIndex; } // Drop data, whether it commes from category, or internal tree if (formats.contains("loader-item")) { Q_EMIT(CreateModelLoaderRequested(indexDropped.row(), QString(mimeData->data("loader-item")))); return; } else { Q_EMIT(MoveModelLoaderResquested(indexDropped, m_oldModelLoaderIndex)); m_oldModelLoaderIndex = QModelIndex(); return; } }
- MainWidget.cxx
[...] MainWidget::MainWidget(QWidget* parent): QWidget(parent), m_modelLoaderModel(new ModelLoaderModel), m_modelLoaderCategoryModel(new CategoryModel) { Init(); } void MainWidget::Init() { // Set model and view categories, but do not fill categories // for some categories are not ready yet m_modelLoaderCategoryModel = new CategoryModel; m_modelLoaderCategoryModel->FillModelWithCategories(); m_categoryTreeView = new CategoryTreeView; m_categoryTreeView->setModel(m_modelLoaderCategoryModel); // Name the columns QStringList headers; headers << tr("Name") << tr("Value"); m_modelLoaderModel->setHorizontalHeaderLabels(headers); m_modelLoaderModel->setObjectName("modelLoaderModel"); // Associate model and view m_modelLoaderTreeView = new ModelLoaderTreeView; m_modelLoaderTreeView->setModel(m_modelLoaderModel); m_modelLoaderTreeView->header()->resizeSection(0, 400); auto mainLayout = new QHBoxLayout; mainLayout->addWidget(m_modelLoaderTreeView); mainLayout->addWidget(m_categoryTreeView); mainLayout->setStretch(0, 2); mainLayout->setStretch(1, 1); setLayout(mainLayout); // Connections connect(m_modelLoaderTreeView, SIGNAL(MoveModelLoaderResquested(QModelIndex, QModelIndex)), this, SLOT(OnInternalMoveModelLoader(QModelIndex, QModelIndex))); connect(m_modelLoaderTreeView, &ModelLoaderTreeView::CreateModelLoaderRequested, this, &MainWidget::OnCreateModelLoader); } void MainWidget::AddModelLoader(ModelLoaderInterface* p_modelLoader, int p_row) { // Ensure p_parent is correct when p_modelLoader is at top level QStandardItem* parentItem = m_modelLoaderModel->invisibleRootItem(); if (p_modelLoader != nullptr) { // Create model loader item and attach the model loader QStandardItem* modelItem = new QStandardItem(p_modelLoader->GetType()); // Set the value of Qt::UserRole to the model loader QVariant userValue = QVariant::fromValue(reinterpret_cast<quintptr>(p_modelLoader)); modelItem->setData(userValue, Qt::UserRole); // Add its properties AddProperties(modelItem, p_modelLoader); // Compute right row int insertedRow = p_row; if (p_row == -1) { insertedRow = parentItem->rowCount(); } // Insert model loader in Qt model m_modelLoaderModel->insertRow(insertedRow, modelItem); // Expand item in tree view auto insertedIndex = m_modelLoaderModel->index(insertedRow, eName); m_modelLoaderTreeView->expand(insertedIndex); // Select new model loader m_modelLoaderTreeView->selectionModel()->setCurrentIndex( insertedIndex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); } } void MainWidget::AddProperties(QStandardItem* p_parent, ModelLoaderInterface* p_modelLoader) { auto propertiesValuesMap = p_modelLoader->GetPropertiesValuesMap(); int row = p_parent->rowCount(); for (auto property: propertiesValuesMap.keys()) { QString propertyName = property; QStandardItem* propertyNameItem = new QStandardItem(propertyName); propertyNameItem->setEditable(false); p_parent->setChild(row, eName, propertyNameItem); QString propertyValue = propertiesValuesMap[property]; QStandardItem* propertyValueItem = new QStandardItem(propertyValue); propertyValueItem->setEditable(true); // Set the value of Qt::UserRole to the property name QVariant userValue; userValue.setValue(propertyName); propertyValueItem->setData(userValue, Qt::UserRole); p_parent->setChild(row, eValue, propertyValueItem); ++row; } } void MainWidget::OnInternalMoveModelLoader(QModelIndex const& p_newIndex, QModelIndex const& p_oldIndex) { bool isExpanded = m_modelLoaderTreeView->isExpanded(p_oldIndex.sibling(p_oldIndex.row(), ModelLoaderModel::eName)); m_modelLoaderModel->insertRow(p_newIndex.row(), m_modelLoaderModel->takeRow(p_oldIndex.row())); if (isExpanded) { m_modelLoaderTreeView->expand(p_newIndex.sibling(p_newIndex.row(), ModelLoaderModel::eName)); } m_modelLoaderTreeView->selectionModel()->setCurrentIndex( p_newIndex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); } void MainWidget::OnCreateModelLoader(int p_row, QString const& p_modelName) { ModelLoaderInterface* modelLoader = nullptr; if (p_modelName == "XML Loader") { modelLoader = new ModelLoaderXML; } else if (p_modelName == "SQL Loader") { modelLoader = new ModelLoaderSQL; } else if (p_modelName == "CSV Loader") { modelLoader = new ModelLoaderCSV; } else { return; } AddModelLoader(modelLoader, p_row); }
If you need the whole project to test it (a few Ko), let me know, I'll upload it on my GitHub.
-
Hi
I made a list where I dragged an image of the widget.
I had to adjust the holding point to make it look right using
setHotSpotI wonder if u can adjust same way ?
// make pixmap of widget
QDrag* drag = new QDrag(this);
drag->setMimeData(mimeData);
drag->setHotSpot(QPoint(pixmap.width() / 2, pixmap.height() / 2));
drag->setPixmap(pixmap); -
Oh, I didn't explore the QDrag object yet! Thanks for your suggestion, I'll give it a try!
-
OK so I tried something:
- Redefine mousePressEvent to record drag position
void ModelLoaderTreeView::mousePressEvent(QMouseEvent* p_event) { if (p_event->button() == Qt::LeftButton) { m_startPos = p_event->pos(); } QTreeView::mousePressEvent(p_event); }
- Redefine mouseMoveEvent to catch the moment just before dragMoveEvent takes over from mouseMoveEvent to perform a custom drag
void ModelLoaderTreeView::mouseMoveEvent(QMouseEvent* p_event) { if (p_event->buttons() & Qt::LeftButton) { int distance = (p_event->pos() - m_startPos).manhattanLength(); if (distance >= QApplication::startDragDistance()) { performDrag(); } } QTreeView::mouseMoveEvent(p_event); }
- Create a custom method named performDrag to handle the QDrag object
void ModelLoaderTreeView::performDrag() { auto modelLoaderModel = dynamic_cast<ModelLoaderModel*>(model()); auto item = modelLoaderModel->itemFromIndex(currentIndex()); if (item) { auto mimeData = new QMimeData; // deleted by Qt after the drag exec mimeData->setText(item->text()); auto drag = new QDrag(this); //deleted by Qt after exec drag->setMimeData(mimeData); auto pix = QPixmap("/path/to/picture.jpg"); drag->setPixmap(pix); drag->setHotSpot(QPoint(pix.width() / 2, pix.height() / 2)); if (drag->exec(Qt::MoveAction) == Qt::MoveAction) { delete item; } } }
This works fine, especially the hotSpot setter.
Unfortunately, the performDrag is executed ONLY when the tree view has the focus; meaning only when I don't need to do anything. I want to take care of the drag when the user drags an item whereas the tree view has NOT the focus. Maybe I should try to catch event from the parent widget that contains the two tree views? What do you guys think?Subsidiary issue: the performDrag creates a QMimeData which does not have the right data, leading to a forbidden drop, but I'll figure out how to allow the drop later, this is not the main issue here.
-
Hi,
Out of curiosity (and maybe not related to the bug at hand) why aren't you handling the drop in the second model rather that in the view ?
-
@SGaist well, I could handle drop both from view and model, and it seemed less difficult from view, so why not? But thanks a lot, I did handle the drop from the model and there is no glitch anymore.
But the issue now is that when the model drop the loader, the view automatically collapse it, and the item selected is always at the same row, i.e., the item moved is no longer selected.
What I want is:- select the item dropped;
- keep the previous state (expanded or collapsed) for the item dropped.
When I worked on the view, it was easy to know apply selection and state on moved item.
I have to somehow find a signal emitted when the model is done moving and/or view is updated so I can force the item to be selected with the previous expanded/collapsed state. Any idea? -
You could emit a signal from your model that has a QModelIndex and an enum as parameters to signal that e.g.
itemDropped(const QModelIndex& index, StatusFlag flag);
andStatusFlag
being eitherCollapsed
orExpanded
and connect a slot to it that would refresh the view. -
Thanks for your suggestion @SGaist . Unfortunately, I have already tried it but it didn't work: the signal being emitted before the QStandardItemModel::dropMimeData call, the view tries to work on the previous item.
Q_EMIT(expandItemDropped(index(row, column))); return QStandardItemModel::dropMimeData(data, action, realRow, column, parent);
By the way, I can't know from the model if an item is collapsed or expanded, for this is a view matter. Therefore I also emit a signal from the tree view as soon as the item begins to move in order to store that information.
So I'm a bit stuck here. I thought I could delay the signal emission either with a QTimer - which seems to be a VERY BAD idea - or with a QApplication::processEvents - though I've never understood how to use it correctly.
-
What about something like:
bool dropped = QStandardItemModel::dropMimeData(data, action, realRow, column, parent); if (dropped) { Q_EMIT(expandItemDropped(index(realRow, column))); } return dropped;
Or follow the pattern:
WARNING: pseudoCodeemit dropStarted(index); // connect a signal here that store whatever state you need, the parameter might be optional. // do what you need; emit dropEnded(); // act based on the state you stored before return dropped;
-
@SGaist I feel so stupid not having thought to split the method call and the return. Of course this is what I need to do. I guess it's because of the habit to call the parent method at the end, since I obviously split usually when I need to get the value before the return. Thanks a lot!
-
OK, the thing is, when a drop is done, there is a moment while the moved element appears both at the old position AND at the new position in the model. In other words, if the model has n elements, then, there are n+1 elements just after drop is done. Then the old element has to be removed somehow: I can tell since the view is OK and I also checked it through a QAction I triggered to print the model content in console. What I want is to perform actions on view as soon as the model is done dropping mime data, which is not the case obviously right after the QStandardItemModel::dropMimeData() call.
Is there a signal emitted when everything is done? Do I have to emit a custom signal from my reimplemented model? What am I missing?
-
Wouldn't that rather be: blocking update until you've done your processing ?
-
@SGaist maybe, but I'm not sure to understand what you mean by "update". Is it the QTreeView::update method? If so, I don't know how to block it.
Edit: what about reimplementing QAbstractItemModel::moveRows method?
-
I was thinking about the updatesEnabled property.
-
@SGaist Oh, I didn't know that, thanks for the tip!
Actually, I solved my problem. I studied the source code of QAbstractView::dropEvent and found that the method called QAbstractItemModel::dropMimeData. Mine didn't, so I just called the parent dropEvent. Instead of handling drag and drop from the model, I reused my old code, handling it from the view and simply call the parent dropEvent when creating the loader from a category. Doing so, the model dropMimeData was actually called and there was no more glitch.
Thanks for all your inputs, they were really helpful!