Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

Reordering rows of QTableView with drag and drop



  • I have a QTableView on my own implementation of QAbstractItemModel.

    The project is a simple drag and drop implementation with some options for testing on-the-fly, available on GitHub:

    Screen Shot 2021-05-15 at 3.19.51 PM.png

    I would like to be able to quickly sort/re-order the rows of the table with drag and drop.

    I followed the instructions in Qt's documentation on "Using Drag and Drop with View Items", more specifically "Using model/view classes".

    With that, I can select an entire row (let's call it D) and re-position it by dragging it into the narrow space between two other rows.

    That is sort of what I wanted, but it is not very agile or natural. When I drop D on another row R, I would like D to "push" R down and insert itself in R's original place. This way, I don't have to carefully "aim" in between rows.

    However, dropping D on R overwrites R, even if the QTableView's property dragDropOverwriteMode is false.

    What I want seems to be a very common way to reorder rows in applications in general, so I believe Qt must have a simple way of providing that. What would that be?

    A couple of more details:

    • I used both QAbstractItemView::InternalMove (which seems ideally named for what I want) and QAbstractItemView::DragDrop for the QTableView's dragDropMode property, but as far as I can tell they behave exactly the same.

    • I have also set the model's supportedDropActions to return Qt::MoveAction only.


  • Lifetime Qt Champion

    Hi,

    AFAIK, what you are looking for is QAbstractItemView::InternalMove.


  • Moderators

    @Rodrigo-B said in Reordering rows of QTableView with drag and drop:

    What I want seems to be a very common way to reorder rows in applications in general, so I believe Qt must have a simple way of providing that. What would that be?

    Yes, you would believe that, but unfortunately no. You have to hack-away in the event handlers to make this work. I'll see if I can dig you up some code ... (may take a day or two)



  • @SGaist, as mentioned in the post, I tried InternalMove but it didn't do anything different from DragDrop.

    Also, with all due respect, it looks to me that InternalMove doesn't do what you think it does. I've been trying to get drag and drop to work for a long time now, and searching through these archives I see you have said a few times that the solution was to use InternalMove, but it never was the solution. As far as I can tell, InternalMove doesn't differ much from DragDrop. Maybe it blocks drops from external views but when it's about drops from the same view everything seems to work exactly the same.



  • @kshegunov I was afraid that was going to be the answer. Yes, if you could send me some examples that would be great, as I have been trying to get this to work for a long time now.


  • Moderators

    I think this should get you started:

    MyTableView::MyTableView(QWidget *parent)
        :  QTableView(parent), m_dropRow(0)
    {
       setSelectionMode(QAbstractItemView::SingleSelection);
       setSelectionBehavior(QAbstractItemView::SelectRows);
       setDragEnabled(true);
       setAcceptDrops(true);
       setDragDropMode(QAbstractItemView::DragDrop);
       setDefaultDropAction(Qt::MoveAction);
       setDragDropOverwriteMode(false);
       setDropIndicatorShown(true);
    }
    
    int MyTableView::selectedRow() const
    {
       QItemSelectionModel *selection = selectionModel();
       return selection->hasSelection() ? selection->selectedRows().front().row() : -1;
    }
    
    void MyTableView::reset()
    {
       QTableView::reset();
    
        QObject::connect(model(), &QAbstractTableModel::rowsInserted, this, [this](const QModelIndex &parent, int first, int last) {
            m_dropRow = first;
        });
    }
    
    void MyTableView::dropEvent(QDropEvent *e)
    {
       if (e->source() != this || e->dropAction() != Qt::MoveAction)
          return;
       int dragRow = selectedRow();
    
       QTableView::dropEvent(e);  // m_dropRow is set by inserted row
    
       if (m_dropRow > dragRow)
          --m_dropRow;
    
       QMetaObject::invokeMethod(this, std::bind(&MyTableView::selectRow, this, m_dropRow), Qt::QueuedConnection);  // Postpones selection
    }
    


  • @kshegunov

    Thank you!

    I tried using the code but got stuck on the QMetaObject::invokeMethod call, which gives me the following error message:

    error: no matching function for call to 'invokeMethod'
    

    Any idea of what may be causing it?

    Here's the full compilation unit and .pro file in case they are useful:

    #include "mainwindow.h"
    #include "ui_mainwindow.h"
    
    #include <QItemSelectionModel>
    #include <QMetaObject>
    #include <QTableView>
    #include <QDropEvent>
    
    
    class MyTableView: QTableView {
        
        Q_OBJECT
        
        int m_dropRow;
        
        MyTableView(QWidget *parent)
            :  QTableView(parent), m_dropRow(0)
        {
           setSelectionMode(QAbstractItemView::SingleSelection);
           setSelectionBehavior(QAbstractItemView::SelectRows);
           setDragEnabled(true);
           setAcceptDrops(true);
           setDragDropMode(QAbstractItemView::DragDrop);
           setDefaultDropAction(Qt::MoveAction);
           setDragDropOverwriteMode(false);
           setDropIndicatorShown(true);
        }
        
        int selectedRow() const
        {
           QItemSelectionModel *selection = selectionModel();
           return selection->hasSelection() ? selection->selectedRows().front().row() : -1;
        }
        
        void reset()
        {
           QTableView::reset();
        
            QObject::connect(model(), &QAbstractTableModel::rowsInserted, this, [this](const QModelIndex &parent, int first, int last) {
                m_dropRow = first;
            });
        }
        
        void dropEvent(QDropEvent *e)
        {
           if (e->source() != this || e->dropAction() != Qt::MoveAction)
              return;
           int dragRow = selectedRow();
        
           QTableView::dropEvent(e);  // m_dropRow is set by inserted row
        
           if (m_dropRow > dragRow)
              --m_dropRow;
           
           QMetaObject::invokeMethod(this, 
                                     std::bind(&MyTableView::selectRow, this, m_dropRow), 
                                     Qt::QueuedConnection);  // Postpones selection
        }
        
        
    };
    
    MainWindow::MainWindow(QWidget *parent)
        : QMainWindow(parent)
        , ui(new Ui::MainWindow)
    {
        ui->setupUi(this);
    }
    
    MainWindow::~MainWindow()
    {
        delete ui;
    }
    
    QT       += core gui
    
    greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
    
    CONFIG += c++11
    
    # You can make your code fail to compile if it uses deprecated APIs.
    # In order to do so, uncomment the following line.
    #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0
    
    SOURCES += \
        main.cpp \
        mainwindow.cpp
    
    HEADERS += \
        mainwindow.h
    
    FORMS += \
        mainwindow.ui
    
    # Default rules for deployment.
    qnx: target.path = /tmp/$${TARGET}/bin
    else: unix:!android: target.path = /opt/$${TARGET}/bin
    !isEmpty(target.path): INSTALLS += target
    

  • Moderators

    Wait, what Qt version is this?



  • @kshegunov I believe 5.15.2 because my Qt Creator project's Run Settings show an environment variable QTDIR set to /Users/xxxxxx/Qt/5.15.2/clang_64.

    I also have an Anaconda Qt package 5.9 installed because I was using PyQt, but given that I am running the project directly from within the Qt Creator project with the 5.15.2 setting, I believe that's the version being used.

    PS: just checked that the header files being included are definitely the ones in the 5.15 .2 install.


  • Moderators

    There's something fishy, because these are excerpts from code that is built and runs against Qt 5.12.x. Could you please check the kit you have in your creator, the compiler version and also if substituting with this makes it compile:

    Q_INVOKABLE int selectedRow() const
    // ...
    

    and

    QMetaObject::invokeMethod(this, "selectRow", Q_ARG(int, m_dropRow), Qt::QueuedConnection);
    


  • @kshegunov Thank you again.

    I made the changes you suggest in the code but unfortunately I still get the same error message.

    I've placed the project files for download if that is helpful.

    I've installed Qt Creator 4.15.2 just two months ago. I'm using the Clang 5.15.2 Clang 64-bit kit.

    As for compiler:

    $ /usr/bin/clang --version
    Apple clang version 12.0.0 (clang-1200.0.32.29)
    Target: x86_64-apple-darwin19.6.0
    Thread model: posix
    InstalledDir: /Library/Developer/CommandLineTools/usr/bin

    Hopefully that sheds light on it. Thank you.


  • Moderators

    I don't use MacOS, but I'll build it on linux later this evening just to make sure.



  • @kshegunov friendly reminder about this, if you can. Thanks!


  • Moderators

    I'm sorry! I completely forgot.
    I just downloaded it. The std::bind version works out of the box when you fix the inheritance error (look down for details).

    Otherwise there's an argument order error for the runtime resolution method:

    QMetaObject::invokeMethod(this, "selectRow", Qt::QueuedConnection, Q_ARG(int, m_dropRow));
    

    The inheritance error, though, affects both invocations:

    class MyTableView: QTableView
    

    inherits privately, it should be:

    class MyTableView: public QTableView
    


  • @kshegunov said in Reordering rows of QTableView with drag and drop:

    QMetaObject::invokeMethod(this, "selectRow", Qt::QueuedConnection, Q_ARG(int, m_dropRow));

    Thank you, that fixed my compilation errors! I will experiment with it tomorrow to try to solve my original problem and let you know.



  • Hi @kshegunov ,

    Now I had time to experiment with your suggestion but I am not seeing how it may solve the problem.

    I've updated with GitHub repository with it if anyone wishes to run it.

    There are several things I do not understand:

    1. The main one is that I don't see the stated desired effect: I wanted the dropping a row into another to insert itself rather than overwrite, but it still overwrites.
    2. Another thing that puzzles me is that, when I drag a row in between two rows, it used to insert itself there (as is the standard behavior), but now it also overwrites. That is to say, it seems to have gone in the opposite direction of what was desired.
    3. It seems to me your code has the purpose of selecting the row after drag and drop, but I don't see that behavior. There is no selection after the operation.

    Would you please shed some light on these questions? Thanks again.


  • Moderators

    @Rodrigo-B said in Reordering rows of QTableView with drag and drop:

    Now I had time to experiment with your suggestion but I am not seeing how it may solve the problem.

    That's because I'd forgotten a method. I'm sorry, it happens sometimes when you snip pieces from existing code without thinking too much. I've created a pull request for you, so you could check it out.



  • @kshegunov Thank you, I really appreciate that you went over the code and provided a pull request! However, it still doesn't seem to work. When I drop the first row (Lion) on third row (Mouse), I would expect the Lion row to be inserted right below Mouse, and Gazelle to move up and be the first row. Instead, Lion overwrites Gazelle in the second row and remains in the first row. As far as I can tell, everything is being overwritten rather than moved.

    In any case, I am starting to see the idea here... modifying variables in dropMimeData so data goes where we want. So if you don't have the time to look into this I will probably be able to mess around and find a solution. Thanks!


  • Moderators

    @Rodrigo-B said in Reordering rows of QTableView with drag and drop:

    Instead, Lion overwrites Gazelle in the second row and remains in the first row. As far as I can tell, everything is being overwritten rather than moved.

    That's odd, because it works on linux as described.
    Video: https://drive.google.com/file/d/14vvAHgGdyRoJkKqgm_tbT8uRwKt_3JhL/view?usp=sharing



  • @kshegunov Wow, that's mindblogging! Here's my video showing that my local code is sync'ed with the repository containing your change, and behaving completely differently:

    https://drive.google.com/file/d/11m9d5xOGGhJN-WM-OhRjHAhGK1_t7vMR/view?usp=sharing

    Not quite sure how to proceed now other than submitting as a bug...


  • Lifetime Qt Champion

    Hi
    Just as a note. Win 10. Qt5.15.2
    seems to work as expected:
    alt text


  • Moderators

    @Rodrigo-B said in Reordering rows of QTableView with drag and drop:

    Wow, that's mindblogging!

    I imagine there's something different on MacOS in the way the drag&drop is handled. But as I said I haven't and I don't own a mac and I've never tested that code on it, so I truly have no idea what it may be, sorry.

    Not quite sure how to proceed now other than submitting as a bug...

    Yes, you're welcome to do that, although if I were you I wouldn't hold my breath.

    Note:
    Not sure if it's relevant, but I noticed how you start the application (from the run button). Make sure you've made a full rebuild before testing, you may have stale code.



  • @kshegunov said in Reordering rows of QTableView with drag and drop:

    Note:
    Not sure if it's relevant, but I noticed how you start the application (from the run button). Make sure you've made a full rebuild before testing, you may have stale code.

    Unfortunately things don't change after a Clean and Rebuild (nice observation though).

    Alright, I will keep messing with it to see if I find out more.

    Thanks @mrjj for running it on Windows, and thanks @kshegunov very much for your help!



  • @kshegunov

    Update/good news: when I remove your overriding implementation of MyTableView::dropEvent, things work fine (although we lose the selection persistence after the drop, as was the goal of that piece of code).

    Actually, if I keep the method but replace the delayed row selection by selectRow(m_dropRow) (as shown below), I get the anomalous behavior. If I remove this line, things work properly but without selecting the moved row afterwards.

    It seems the delayed row selection does not work in the Mac (makes sense since probably different OSs deal with events differently). Then the row selection happens before the dropMimeData and changes the targeted row, causing the bizarre behavior we observed.

    void dropEvent(QDropEvent *e)
        {
           if (e->source() != this || e->dropAction() != Qt::MoveAction)
              return;
    
           int dragRow = selectedRow();
    
           QTableView::dropEvent(e);  // m_dropRow is set by inserted row
    
           if (m_dropRow > dragRow)
              --m_dropRow;
    
           selectRow(m_dropRow); // non-delayed selection has the same effect, so QueuedConnection seems not to work on the Mac.
    
    //       QMetaObject::invokeMethod(this,
    //                                 std::bind(&MyTableView::selectRow, this, m_dropRow),
    //                                 Qt::QueuedConnection);  // Postpones selection
        }
    


  • Followup question for the community: it seems changes to drag and drop often happen in dropMimeData, as @kshegunov suggested above.

    However, that seems like a less than ideal solution because if violates the Model-View paradigm. The behavior of drag and drop seems to be more related to the view than to the model. For example, I might want to use the same model in two different views but wish to see the behavior described in only one of those views.

    Would it be possible to obtain the same behavior but overriding view methods only?



  • Apologies if this is a repeat of known info... the thread seemed to have delved off topic for a bit so maybe I missed it.

    @Rodrigo-B said in Reordering rows of QTableView with drag and drop:

    However, dropping D on R overwrites R, even if the QTableView's property dragDropOverwriteMode is false.

    I always thought this behavior was strange as well.

    I work around the issue by returning false from my model's dropMimeData() method in response to Qt::MoveAction from any of the built-in "QItemView" classes. Even if the move succeeded. I've already done the moving inside dropMimeData() and the model has already updated, so the View will reflect it regardless of what we return there. Returning false makes the item views cancel any further action, like removing any rows(*). It's not ideal since the actual QDrag never gets properly accepted, but I also haven't seen that it matters.

    If I want to DnD from my own custom views (or I've re-implemented QAbstractItemView::startDrag()) then I can pass some meta data to my model's dropMimeData() which will trigger the correct true/false result of the drop (eg. if I'm only drag/dropping rows, I can pass column = -2 and the model knows to return the actual result instead of always false).

    * More specifically, in QAbstractItemView::startDrag()[1] where it waits for the drag.exec() == Qt::MoveAction, it will then not run the internal d->clearOrRemove() method, which is what does the actual removals. Another way to hack it may be to change the accepted drop method by re-implementing the (simpler) QAbstractItemView::dropEvent[2].

    HTH,
    -Max

    [1] https://code.woboq.org/qt5/qtbase/src/widgets/itemviews/qabstractitemview.cpp.html#_ZN17QAbstractItemView9startDragE6QFlagsIN2Qt10DropActionEE
    [2] https://code.woboq.org/qt5/qtbase/src/widgets/itemviews/qabstractitemview.cpp.html#_ZN17QAbstractItemView9dropEventEP10QDropEvent



  • @Rodrigo-B said in Reordering rows of QTableView with drag and drop:

    Followup question for the community: it seems changes to drag and drop often happen in dropMimeData, as @kshegunov suggested above.

    However, that seems like a less than ideal solution because if violates the Model-View paradigm. The behavior of drag and drop seems to be more related to the view than to the model. For example, I might want to use the same model in two different views but wish to see the behavior described in only one of those views.

    Can you post a more concrete example? I think everything your model needs to know to perform the appropriate drop action is passed to dropMimeData(). I'm having trouble thinking of a situation where the "view" would know better what to do with the data than the model would. One could dis/allow certain moves in the view, but there's no way the view can do a move/update if the underlying model can't handle it.

    The default QAbstract*Model::dropMimeData() methods are convenient, but relatively simplistic. Re-implementing provides a lot more control.

    Cheers,
    -Max


  • Moderators

    @Max-Paperno said in Reordering rows of QTableView with drag and drop:

    Can you post a more concrete example? I think everything your model needs to know to perform the appropriate drop action is passed to dropMimeData().

    I believe @Rodrigo-B's point is that the model is not supposed to perform any appropriate drop actions at all.

    I'm having trouble thinking of a situation where the "view" would know better what to do with the data than the model would.

    Why? Say you have an object of type X and a widget representing that object for the user to edit. Would you implement the drag-drop handling in the class X or in the widget class? I'd rather do it in the UI, at least to me it looks more in line with what d&d does. If my X class is also possible to be used without UI, then I'd have no drag-drop at all, so it does seem "wrong" to force it to deal with it ...



  • Right, certainly the UI is the "first stop" with DnD, and it can and does control, to a large extent, how the user can "physically" interact with the data. But ultimately it's up to the model how to react to those results (move, copy, overwrite, edit, etc). If, for example, you don't want the UI to allow the user to replace (move-overwrite) an item by dropping another onto it, then that move can be prohibited in the UI (or like in the OP, replaced with a move-only action). But the model has to do the actual data manipulation, otherwise that breaks the model/view separation. If the model "can't" do what the UI is telling it (for whatever reason, including data integrity), then it shouldn't be forced to. Especially considering that the model should be the one to encode and decode the MIME data since it would be a) most "familiar" with it and b) is a convenient central place to keep that functionality (and not just for views).

    Also just to point out that the "drop" part in dropMimeData() (et. al.) makes it sound limited to DnD. Actually the same method can be used to paste clipboard data, for example, or any other kind of import/export action where MIME-encapsulated data makes sense.

    Of course one is also free to re-implement the respective DnD methods in the views (or write custom views) and do all the data manipulation of the model from there (using the respective insert/remove/move methods). Including en/decoding the data. But the default behavior is to let the model handle that part.

    I might even go out on a limb and say that the main reason for this topic is that the UI is doing something unexpected by trying to be "smart" about what to do with the data after it has been dropped. From my POV it should not try to manipulate the data after the model has done its thing... ever (keep selection maybe, but that has nothing to do with the model). The UI should be telling the model exactly what to do... eg. if dropping a row onto another row is meant to re-arrange the items (not overwrite the recipient), then the view needs to send the appropriate combination of action, row number, and parent. Then the model knows, oh, this row should be moved before this other one, not replaced. The view really does have ultimate control over what to tell the model it wants to do.

    Cheers,
    -Max


  • Moderators

    @Max-Paperno said in Reordering rows of QTableView with drag and drop:

    Right, certainly the UI is the "first stop" with DnD, and it can and does control, to a large extent, how the user can "physically" interact with the data. But ultimately it's up to the model how to react to those results (move, copy, overwrite, edit, etc).

    Yet it doesn't. It reacts directly to the raw representation, not to the move, not to the copy. The model is the one unpacking and interpreting the mime-data. Which is the gist of my whole argument.

    If, for example, you don't want the UI to allow the user to replace (move-overwrite) an item by dropping another onto it, then that move can be prohibited in the UI (or like in the OP, replaced with a move-only action). But the model has to do the actual data manipulation, otherwise that breaks the model/view separation.

    The model should expose an API to all clients to that API to manipulate the data, which it does. However, it also does interprets what's in the mime-encapsulated data. To bring the argument to an absurdity, why not force the model to interpret a TCP protocol? This simply violates the separation of concerns, and I consider it an API bug. Not a significant one, mind you, but still ...

    If the model "can't" do what the UI is telling it (for whatever reason, including data integrity), then it shouldn't be forced to.

    Yes, but the UI doesn't tell the model "move this row", it tells it, "hey I have this thing, please do what it says inside".

    Especially considering that the model should be the one to encode and decode the MIME data since it would be a) most "familiar" with it and b) is a convenient central place to keep that functionality (and not just for views).

    Also just to point out that the "drop" part in dropMimeData() (et. al.) makes it sound limited to DnD. Actually the same method can be used to paste clipboard data, for example, or any other kind of import/export action where MIME-encapsulated data makes sense.

    Not in my opinion. The moment you start pushing unsanitized user-supplied data over opaque datatypes to a model, that's the moment you break any three-tier validation you may've hoped to achieve. If the data comes through a network channel, you'd not push it directly to the model, be it mime-compatible or not, would you? Neither should you do it with an UI.

    Of course one is also free to re-implement the respective DnD methods in the views (or write custom views) and do all the data manipulation of the model from there (using the respective insert/remove/move methods). Including en/decoding the data. But the default behavior is to let the model handle that part.

    Which is what it shouldn't, is the point. It's not its place to interpret what comes from whatever place it happens to come.

    I might even go out on a limb and say that the main reason for this topic is that the UI is doing something unexpected by trying to be "smart" about what to do with the data after it has been dropped. From my POV it should not try to manipulate the data after the model has done its thing... ever (keep selection maybe, but that has nothing to do with the model).

    And it doesn't. In the proposed fix it's actually necessary to override the model's function to prevent it from overwriting the row. The only thing the view tries to achieve here is to keep the selection, it doesn't modify the drop event at all.

    The UI should be telling the model exactly what to do... eg. if dropping a row onto another row is meant to re-arrange the items (not overwrite the recipient), then the view needs to send the appropriate combination of action, row number, and parent. Then the model knows, oh, this row should be moved before this other one, not replaced. The view really does have ultimate control over what to tell the model it wants to do.

    Heh, isn't that my argument? The default implementation instead of that passes on the mime-data, instead of saying, "hey, I want you to put me a new row there".



  • @kshegunov said in Reordering rows of QTableView with drag and drop:

    Yes, but the UI doesn't tell the model "move this row", it tells it, "hey I have this thing, please do what it says inside".

    Ah yes, you're right. And maybe where the API is lacking. For example, what is the significance of of a MoveAction if one doesn't know where the data is being moved from? In fact the default ItemModels implementations pretty much ignore the action attribute in dropMimeData() except to validate that's it's not a Link or no-op. And then the UI Views try to compensate for that. I haven't used the default behaviors in so long, I forget how limited they are.

    Instead I like to encode meta data about the source data (which is being dragged/copied) in mimeData(), typically as a "header" of another MIME type, or sometimes the data itself has enough details. Then the "canDrop" and "drop" methods know exactly "move this row," like you said. Not some ambiguous "here's some data, plop it in." In fact if data is only going to be moved/copied within the same model (no external destinations), one doesn't even need to encode the data itself, just where it currently lives in the model (its Q[Persistent]ModelIndex, essentially, or some other unique record ID which is already common in structured data, like a database), which is also much more efficient and easier compared to the default routines. OTOH if the data may move between model instances, then again the destination model can know that the source is external, in which case a "move" operation becomes an insert/replace operation.

    Personally I think none of this should be handled by the UI at all. Whether the model itself should handle it, or some other helper/proxy class is an implementation detail (personal taste, needs, etc). That's really the main point I was questioning in my original reply to Rodrigo. I don't want to have to create a custom UI for each data model's preferred behavior, after all, I'd rather centralize that functionality, just like the data model is centralized.

    The one place this breaks down somewhat is when moving data out of one model and into another. The source model needs to be notified that the data can be safely removed. So the UI, or some other mechanism, does need to do that. But this is pretty basic, eg. if the remote drop (not onto itself) event was accepted as a move, then tell the local model it's OK to delete that data. What is harder is to coordinate cut/paste operations, since there's nothing like a QDrag to monitor.

    To clarify, I'm talking about manipulating well-structured data sets. If one wants to, say, drop some selected text onto a field and have that text become the new value, that's really a whole different thing (and simpler, I think). Or arbitrarily copy data between models, including adding columns if necessary, which is what using all the built-in defaults basically allows.

    The moment you start pushing unsanitized user-supplied data over opaque datatypes to a model, that's the moment you break any three-tier validation you may've hoped to achieve. If the data comes through a network channel, you'd not push it directly to the model, be it mime-compatible or not, would you?

    I think this is veering even further off into OT territory... :-) From a data manipulation perspective, cut/copy/paste is just like DnD but with a clipboard object. If one is concerned about validating the MIME data, then that is a factor regardless of how the data is originally obtained (to varying degrees, perhaps, eg. local clipboard vs. Internet stream :-). I certainly don't want any UI component to do that, but again where/how/if this happens is an implementation detail up to the author(s). The validation needs of any particular data/project may vary from just checking the MIME type to running it through a virus checker... who knows.

    Best,
    -Max


  • Moderators

    @Max-Paperno said in Reordering rows of QTableView with drag and drop:

    The one place this breaks down somewhat is when moving data out of one model and into another.

    Or when the consumer of the model wants to handle input differently, be it a View (i.e. UI element) or something else.

    OTOH if the data may move between model instances, then again the destination model can know that the source is external, in which case a "move" operation becomes an insert/replace operation.

    Data moves between the consumers of the data storage (in this case that'd be the view), not between the actual storages (which is the model in this particular case) is my main point. You wouldn't just expect copying a MySQL data file into a PgSQL engine to simply work, you have that boundary in between them through which you channel all inputs and outputs.

    Ah yes, you're right. And maybe where the API is lacking.

    Yes, I think I am, but this whole discussion is really irrelevant, simply because that stuff ain't gonna change any time soon. So whatever the "verdict" is, it means nothing in the grand scheme of things.



  • Well, maybe it's semantics, but to me data moves between storage containers, based on how it is manipulated via the UI, and preferably with integrity checks in the actual storage mechanism before it is written there. Not "between consumers." That's how actual structured DBs are designed, anyway. The "UI" I would edit or run queries in to transfer data between DBs knows nothing about the actual data, nor should it. And just because I'm telling it to "channel" some data from some other DB (using my queries), doesn't mean the destination should just blindly accept that data (eg. even in well-formed data there may be possible relational constraints and other minutiae).

    I've been designing DBs and various UIs to interact with them for 25 years, so I think I have a pretty good handle on it by now, or at least what works for me. IMHO, the UI should be as oblivious to the data as possible while still providing a good/appropriate UX. In fact I try to drive the UI with the data (or rather meta-data) as much as possible. Like the QtDesigner property editor, for example (some interesting code there if one were ever inclined to check it out). Because let's face it, who wants to re-write the same basic CRUD UI over and over. This is what the Qt views try to provide, an abstract UI which can be somewhat controlled via meta data from the data/model via an agreed-upon API. And they're relatively easy to customize further, as needed.

    Anyway I would also like to point out that the Qt Item model classes do not necessarily need to store any data ("data" is not even in the name ;-). In fact one could argue that they shouldn't, and if one needs persistent data, then they obviously just can't. They're already interfaces to the data, not the actual data. The most obvious examples of this are the QSQL*Table models, or the QFileSystemModel. Also proxy models, which are clearly just further interface abstraction, not actual data.

    Cheers,
    -Max



  • @Rodrigo-B said in Reordering rows of QTableView with drag and drop:

    Actually, if I keep the method but replace the delayed row selection by selectRow(m_dropRow) (as shown below), I get the anomalous behavior. If I remove this line, things work properly but without selecting the moved row afterwards.
    It seems the delayed row selection does not work in the Mac (makes sense since probably different OSs deal with events differently). Then the row selection happens before the dropMimeData and changes the targeted row, causing the bizarre behavior we observed.

    Firstly, sorry about hijacking your thread.

    Second, I think you're right and the queued event is delivered to the view object "too soon" on the Mac. This sort of invocation doesn't come with any promise of linearity. Instead you could try replacing the invokeMethod() part in MyTableView with a single-shot timer (tested and works here, Win10 MSVC17 Qt 5.14.2):

    // mytableview.h
    
    #include <QTimer>
    ...
    void dropEvent...
    {
      ....
      QTimer::singleShot(1, this, [this]() {
        selectRow(m_dropRow);
      });
    }
    

    The 1 is milliseconds and should be enough, but 10ms won't be noticeable to a user either.

    The whole thing seems like an awkward workaround, but does seem to work. If I think of anything better, I'll let you know :-)

    HTH,
    -Max

    PS. what was with the ands in tablemodel.cpp if() clauses? Does that actually build on something w/out errors? Just curious!


  • Moderators

    @Max-Paperno said in Reordering rows of QTableView with drag and drop:

    Well, maybe it's semantics, but to me data moves between storage containers, based on how it is manipulated via the UI, and preferably with integrity checks in the actual storage mechanism before it is written there. Not "between consumers." That's how actual structured DBs are designed, anyway. The "UI" I would edit or run queries in to transfer data between DBs knows nothing about the actual data, nor should it. And just because I'm telling it to "channel" some data from some other DB (using my queries), doesn't mean the destination should just blindly accept that data (eg. even in well-formed data there may be possible relational constraints and other minutiae).

    This isn't what I meant. The database doesn't spill its data raw, does it? And even if does, nobody's expecting filling in that raw data into another database engine to work. What I meant was, that software's designed as layers over layers, and as data goes trough it gets transformed to prepare it for the next layer.

    Firstly, sorry about hijacking your thread.

    He specifically asked for this discussion, but if desired I could always fork it.

    PS. what was with the ands in tablemodel.cpp if() clauses? Does that actually build on something w/out errors? Just curious!

    I disapprove of it, but it's valid c++, every compliant compiler should build it.



  • @Max-Paperno said in Reordering rows of QTableView with drag and drop:

    The whole thing seems like an awkward workaround, but does seem to work. If I think of anything better, I'll let you know :-)

    @Rodrigo-B I think the version below is better. No funky timers and such. And it can preserve multiple selections (if dropping multiple rows). The comments are longer than the code, so hopefully there's enough detail in there, but let me know if anything is unclear. I cut this down to the bare minimum, everything else acts like a default QTableView and can be controlled externally.

    #ifndef MYTABLEVIEW_H
    #define MYTABLEVIEW_H
    
    #include <QTableView>
    #include <QDropEvent>
    
    class MyTableView: public QTableView
    {
        Q_OBJECT
    
        QItemSelection m_droppedSelection;
        bool m_dropAccepted = false;
    
      public:
        using QTableView::QTableView;
    
      protected:
        void startDrag(Qt::DropActions supportedActions) override
        {
          // Clear the accepted flag, this will be set to true in dropEvent() if the drop was accepted.
          m_dropAccepted = false;
    
          // Note that startDrag() will, essentially, end up calling dropEvent() if/when the drag is dropped.
          // dropEvent() may or may not accept the drop, which is why we use the m_dropAccepted flag above.
          // Actually dropEvent() is called by the event processing system, but since all the GUI
          // stuff runs in one thread anyway, this is essentially the same thing.
          QTableView::startDrag(supportedActions);
    
          // We've now returned from possibly executing dropEvent().
          // If the drop was accepted, re-select the dropped rows, which were obtained during dropEvent().
          if (m_dropAccepted)
            selectionModel()->select(m_droppedSelection, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
        }
    
        void dropEvent(QDropEvent *e) override
        {
          // Clear any stored selections.
          m_droppedSelection.clear();
    
          // This will determine what the "new" selection will be after a drop is completed.
          // Connecting to rowsInserted works because when data is dropped on the model it will be
          // inserted as new rows.. At least that is the default behavior of QTableModel. 
          // We also keep the connection handle to disconnect it later.
          const QMetaObject::Connection monitor =
          connect(model(), &QAbstractItemModel::rowsInserted, this, [this](const QModelIndex&, int first, int last) {
            m_droppedSelection = QItemSelection(model()->index(first, 0), model()->index(last, 0));
          });
    
          // Now do the default stuff.  Here is where the event may or may not be accepted,
          // and the new data insert would happen if it was.
          QTableView::dropEvent(e);
    
          // Let startDrag() know we accepted the drop and there's something to re-select.
          if (e->isAccepted() && e->dropAction() == Qt::MoveAction && !m_droppedSelection.isEmpty())
            m_dropAccepted = true;
    
          // We probably don't want m_droppedSelection populating every time rows are added, and
          // definitely don't want multiple connections.
          disconnect(monitor);
        }
    
    };
    #endif // MYTABLEVIEW_H
    


  • @kshegunov said in Reordering rows of QTableView with drag and drop:

    PS. what was with the ands in tablemodel.cpp if() clauses? Does that actually build on something w/out errors? Just curious!

    I disapprove of it, but it's valid c++, every compliant compiler should build it.

    That's insane, I have literally never noticed this used or mentioned anywhere in over 20 years of C[++]! Or maybe I did and blocked it out :-) And after some rudimentary searching through my usual C++ references, I can't find this mentioned anywhere now either. Ironically the only place I did find a reference to it is in the MSVC docs. Where I also see that MSVC requires a special switch to enable this usage. Good on them (for once!). I might have never noticed it this time if it wasn't for that.

    Thanks for the education!
    -Max



  • @Max-Paperno
    We debated this a while ago in some thread here. There is at minimum and, or and not, I think there are some others. This is standard C++, not just MSVC, but I forget which C++ it came into.

    Actually I think we found it was quite an old C++, not recent, so it's been around a long time. It turned out (can't find the reference[*]) this was not so much to do with making it "readable" (if that's your view), it is actually because some terminals/languages(?) have trouble with the symbol characters in || etc. so they allowed words instead...!

    Recently I saw code posted here by someone in another thread translating to C++ and using not, probably because s/he thought that was normal for C++. I shuddered ;-)

    [*] OK, it's to do with "digraphs" & "trigraphs": see https://en.wikipedia.org/wiki/Digraphs_and_trigraphs, https://dev.to/delta456/modern-c-and-or-and-not-as-boolean-operators-2jgf, and others.



  • For funsies, and to support my PoV that the item model is the proper interface to the data ;-), here's an option to toggle the drop behavior between move and overwrite using only the item model. It's @kshegunov 's solution but with a toggle switch to control the behavior. Items can still be dropped between lines to move them, in either mode. Most importantly, the QTableView::dragDropOverwriteMode remains false regardless (more on this below, after the code break).

    In tablemodel.h I added:

      public:
        using QAbstractTableModel::QAbstractTableModel;  // for proper constructor
        bool m_dropOverwriteEnabled = false;  // could be private
        void setDropOverwriteEnabled(bool enable = true) { m_dropOverwriteEnabled = enable; }  // a "slot"
    

    In tablemodel.cpp, dropMimeData() becomes:

    bool TableModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int /*column*/, const QModelIndex &parent)
    {
      QModelIndex newParent;
      // If dropping onto another item and overwrite mode is disabled,
      // move the dropped data to the next row down (as if it was dropped
      // between rows).
      if (!m_dropOverwriteEnabled && parent.isValid()) 
        row = qMin(parent.row() + 1, rowCount());
      else
        newParent = parent;
    
      // ensure drops on empty space in the view go into the last row
      if (!newParent.isValid() && row == -1)
        row = rowCount();
    
      return QAbstractTableModel::dropMimeData(data, action, row, 0, newParent);
    }
    

    Then connect some toggle trigger like your checkbox to the setDropOverwriteEnabled() "slot."
    Here's a simplified MainWindow widget which brings all the above together (including the previous MyTableView):

    #ifndef MAINWINDOW_H
    #define MAINWINDOW_H
    
    #include "tablemodel.h"
    #include "mytableview.h"
    #include <QWidget>
    #include <QCheckBox>
    #include <QTableView>
    #include <QVBoxLayout>
    
    class MainWindow : public QWidget
    {
      Q_OBJECT
      public:
        MainWindow(QWidget *parent = nullptr)
          : QWidget(parent)
        {
          setWindowTitle("Drag and Drop app");
    
          MyTableView *tableView = new MyTableView(this);
          tableView->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);
          tableView->setSelectionMode(QAbstractItemView::ExtendedSelection);
          tableView->setSelectionBehavior(QAbstractItemView::SelectRows);
          tableView->setDragDropMode(QAbstractItemView::InternalMove);
          tableView->setDragEnabled(true);
          tableView->setAcceptDrops(true);
          tableView->setDropIndicatorShown(true);
          tableView->setDragDropOverwriteMode(false);
    
          TableModel *model = new TableModel(this);
          tableView->setModel(model);
    
          QCheckBox *dropOverwriteModeCheckbox = new QCheckBox("Drop overwrite mode", this);
          connect(dropOverwriteModeCheckbox, &QCheckBox::toggled, model, &TableModel::setDropOverwriteEnabled);
    
          QVBoxLayout *layout = new QVBoxLayout(this);
          layout->addWidget(tableView);
          layout->addWidget(dropOverwriteModeCheckbox);
        }
    };
    #endif // MAINWINDOW_H
    

    Now regarding QTableView::dragDropOverwriteMode, when set to true the view just does strange things. For one thing one can no longer drop between rows for some reason. And dropping on top of another row acts differently depending on if one is moving an item down vs. up. Moving up sorta works but creates a new blank record at the bottom. Moving down doesn't work at all. Honestly I have no idea what this is supposed to do, nor do I remember ever actually having it enabled on a view with DnD. Even the docs seem sorta confused on the subject:

    If its value is true, the selected data will overwrite the existing item data when dropped, while moving the data will clear the item. If its value is false, the selected data will be inserted as a new item when the data is dropped. When the data is moved, the item is removed as well.

    Sounds pretty vague to me. And nothing about not being able to drop between items (I think?). Maybe there's some combination of flags and other model or view settings which are needed for this to actually work in some sensible fashion.

    Anyway, HTH,
    -Max


  • Moderators

    @Max-Paperno said in Reordering rows of QTableView with drag and drop:

    For funsies, and to support my PoV that the item model is the proper interface to the data ;-), here's an option to toggle the drop behavior between move and overwrite using only the item model. It's @kshegunov 's solution but with a toggle switch to control the behavior. Items can still be dropped between lines to move them, in either mode. Most importantly, the QTableView::dragDropOverwriteMode remains false regardless (more on this below, after the code break).

    Thanks for the code. However it doesn't address @Rodrigo-B's question (which we both missed in fact):

    @Rodrigo-B said in Reordering rows of QTableView with drag and drop:

    Would it be possible to obtain the same behavior but overriding view methods only?

    So, I'd say yes, but you have to completely reimplement the dropEvent handler in the view and not delegate to the model.


Log in to reply