Skip to content
  • Categories
  • Recent
  • Tags
  • Popular
  • Users
  • Groups
  • Search
  • Get Qt Extensions
  • Unsolved
Collapse
Brand Logo
  1. Home
  2. Qt Development
  3. General and Desktop
  4. Drag and drop glitch
Forum Updated to NodeBB v4.3 + New Features

Drag and drop glitch

Scheduled Pinned Locked Moved Solved General and Desktop
15 Posts 3 Posters 4.4k Views 3 Watching
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
This topic has been deleted. Only users with topic management privileges can see it.
  • ValentinMicheletV Offline
    ValentinMicheletV Offline
    ValentinMichelet
    wrote on last edited by ValentinMichelet
    #1

    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 animation

    Relevant 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.

    1 Reply Last reply
    0
    • mrjjM Offline
      mrjjM Offline
      mrjj
      Lifetime Qt Champion
      wrote on last edited by
      #2

      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
      setHotSpot

      I 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);

      1 Reply Last reply
      2
      • ValentinMicheletV Offline
        ValentinMicheletV Offline
        ValentinMichelet
        wrote on last edited by
        #3

        Oh, I didn't explore the QDrag object yet! Thanks for your suggestion, I'll give it a try!

        1 Reply Last reply
        0
        • ValentinMicheletV Offline
          ValentinMicheletV Offline
          ValentinMichelet
          wrote on last edited by
          #4

          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.

          1 Reply Last reply
          0
          • SGaistS Offline
            SGaistS Offline
            SGaist
            Lifetime Qt Champion
            wrote on last edited by
            #5

            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 ?

            Interested in AI ? www.idiap.ch
            Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

            ValentinMicheletV 1 Reply Last reply
            1
            • SGaistS SGaist

              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 ?

              ValentinMicheletV Offline
              ValentinMicheletV Offline
              ValentinMichelet
              wrote on last edited by
              #6

              @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?

              1 Reply Last reply
              0
              • SGaistS Offline
                SGaistS Offline
                SGaist
                Lifetime Qt Champion
                wrote on last edited by
                #7

                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); and StatusFlag being either Collapsed or Expanded and connect a slot to it that would refresh the view.

                Interested in AI ? www.idiap.ch
                Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

                ValentinMicheletV 1 Reply Last reply
                0
                • SGaistS SGaist

                  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); and StatusFlag being either Collapsed or Expanded and connect a slot to it that would refresh the view.

                  ValentinMicheletV Offline
                  ValentinMicheletV Offline
                  ValentinMichelet
                  wrote on last edited by
                  #8

                  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.

                  1 Reply Last reply
                  0
                  • SGaistS Offline
                    SGaistS Offline
                    SGaist
                    Lifetime Qt Champion
                    wrote on last edited by
                    #9

                    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: pseudoCode

                    emit 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;
                    

                    Interested in AI ? www.idiap.ch
                    Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

                    ValentinMicheletV 1 Reply Last reply
                    0
                    • SGaistS SGaist

                      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: pseudoCode

                      emit 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;
                      
                      ValentinMicheletV Offline
                      ValentinMicheletV Offline
                      ValentinMichelet
                      wrote on last edited by ValentinMichelet
                      #10

                      @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!

                      1 Reply Last reply
                      0
                      • ValentinMicheletV Offline
                        ValentinMicheletV Offline
                        ValentinMichelet
                        wrote on last edited by
                        #11

                        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?

                        1 Reply Last reply
                        0
                        • SGaistS Offline
                          SGaistS Offline
                          SGaist
                          Lifetime Qt Champion
                          wrote on last edited by
                          #12

                          Wouldn't that rather be: blocking update until you've done your processing ?

                          Interested in AI ? www.idiap.ch
                          Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

                          ValentinMicheletV 1 Reply Last reply
                          0
                          • SGaistS SGaist

                            Wouldn't that rather be: blocking update until you've done your processing ?

                            ValentinMicheletV Offline
                            ValentinMicheletV Offline
                            ValentinMichelet
                            wrote on last edited by ValentinMichelet
                            #13

                            @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?

                            1 Reply Last reply
                            0
                            • SGaistS Offline
                              SGaistS Offline
                              SGaist
                              Lifetime Qt Champion
                              wrote on last edited by
                              #14

                              I was thinking about the updatesEnabled property.

                              Interested in AI ? www.idiap.ch
                              Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

                              ValentinMicheletV 1 Reply Last reply
                              0
                              • SGaistS SGaist

                                I was thinking about the updatesEnabled property.

                                ValentinMicheletV Offline
                                ValentinMicheletV Offline
                                ValentinMichelet
                                wrote on last edited by
                                #15

                                @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!

                                1 Reply Last reply
                                0

                                • Login

                                • Login or register to search.
                                • First post
                                  Last post
                                0
                                • Categories
                                • Recent
                                • Tags
                                • Popular
                                • Users
                                • Groups
                                • Search
                                • Get Qt Extensions
                                • Unsolved