From 01d20f6263f5fd2f71890b201141ff2d0729705c Mon Sep 17 00:00:00 2001
From: Lukas Holecek <hluk@email.cz>
Date: Tue, 30 Mar 2021 18:38:36 +0200
Subject: [PATCH] itemsync: Synchronize own item positions

Fixes #1558
---
 plugins/itemsync/filewatcher.h           |  2 +-
 plugins/itemsync/itemsync.cpp            | 66 ++++++++++++++++++------
 plugins/itemsync/itemsync.h              | 15 +++---
 plugins/itemsync/tests/itemsynctests.cpp | 56 ++++++++++++++++++++
 plugins/itemsync/tests/itemsynctests.h   |  2 +
 5 files changed, 114 insertions(+), 27 deletions(-)

diff --git a/plugins/itemsync/filewatcher.h b/plugins/itemsync/filewatcher.h
index 2f09f3372..bc1f6c723 100644
--- a/plugins/itemsync/filewatcher.h
+++ b/plugins/itemsync/filewatcher.h
@@ -69,7 +69,7 @@ public:
     static Hash calculateHash(const QByteArray &bytes);
 
     FileWatcher(const QString &path, const QStringList &paths, QAbstractItemModel *model,
-                int maxItems, const QList<FileFormat> &formatSettings, QObject *parent);
+                int maxItems, const QList<FileFormat> &formatSettings, QObject *parent = nullptr);
 
     const QString &path() const { return m_path; }
 
diff --git a/plugins/itemsync/itemsync.cpp b/plugins/itemsync/itemsync.cpp
index 5fc4a91ae..1f9675385 100644
--- a/plugins/itemsync/itemsync.cpp
+++ b/plugins/itemsync/itemsync.cpp
@@ -419,22 +419,15 @@ bool ItemSync::eventFilter(QObject *, QEvent *event)
     return ItemWidget::filterMouseEvents(m_label, event);
 }
 
-ItemSyncSaver::ItemSyncSaver(const QString &tabPath)
-    : m_tabPath(tabPath)
-    , m_watcher(nullptr)
-{
-}
-
-ItemSyncSaver::ItemSyncSaver(
-        QAbstractItemModel *model,
-        const QString &tabPath,
-        const QString &path,
-        const QStringList &files,
-        int maxItems,
-        const QList<FileFormat> &formatSettings)
-    : m_tabPath(tabPath)
-    , m_watcher(new FileWatcher(path, files, model, maxItems, formatSettings, this))
+ItemSyncSaver::ItemSyncSaver(QAbstractItemModel *model, const QString &tabPath, FileWatcher *watcher)
+    : m_model(model)
+    , m_tabPath(tabPath)
+    , m_watcher(watcher)
 {
+    if (m_watcher)
+        m_watcher->setParent(this);
+    connect( model, &QAbstractItemModel::rowsMoved,
+             this, &ItemSyncSaver::onRowsMoved );
 }
 
 bool ItemSyncSaver::saveItems(const QString &tabName, const QAbstractItemModel &model, QIODevice *file)
@@ -538,6 +531,44 @@ void ItemSyncSaver::setFocus(bool focus)
         m_watcher->setUpdatesEnabled(focus);
 }
 
+void ItemSyncSaver::onRowsMoved(const QModelIndex &, int start, int end, const QModelIndex &, int destinationRow)
+{
+    if (!m_model)
+        return;
+
+    /* If own items were moved, change their base names in data to trigger
+     * updating/renaming file names so they are also moved in other app instances.
+     *
+     * The implementation works in common cases but will fail if:
+     * - Items are moved after the last own item.
+     * - Items move repeatedly to some not-top position.
+     */
+    const int count = end - start + 1;
+
+    const int baseRow = destinationRow < start
+        ? destinationRow + count
+        : destinationRow;
+    QString baseName;
+    if (destinationRow > 0) {
+        const QModelIndex baseIndex = m_model->index(baseRow, 0);
+        baseName = FileWatcher::getBaseName(baseIndex);
+
+        if ( !isOwnFile(baseName) )
+            return;
+
+        if ( !baseName.isEmpty() && !baseName.contains(QLatin1Char('-')) )
+            baseName.append(QLatin1String("-0000"));
+    }
+
+    for (int row = baseRow - 1; row >= baseRow - count; --row) {
+        const auto index = m_model->index(row, 0);
+        if ( isOwnItem(index) ) {
+            const QVariantMap data = {{mimeBaseName, baseName}};
+            m_model->setData(index, data, contentType::updateData);
+        }
+    }
+}
+
 QString ItemSyncScriptable::getMimeBaseName() const
 {
     return mimeBaseName;
@@ -780,7 +811,7 @@ ItemSaverPtr ItemSyncLoader::loadItems(const QString &tabName, QAbstractItemMode
     const auto tabPath = m_tabPaths.value(tabName);
     const auto path = files.isEmpty() ? tabPath : QFileInfo(files.first()).absolutePath();
     if ( path.isEmpty() )
-        return std::make_shared<ItemSyncSaver>(tabPath);
+        return std::make_shared<ItemSyncSaver>(model, tabPath, nullptr);
 
     QDir dir(path);
     if ( !dir.mkpath(".") ) {
@@ -788,5 +819,6 @@ ItemSaverPtr ItemSyncLoader::loadItems(const QString &tabName, QAbstractItemMode
         return nullptr;
     }
 
-    return std::make_shared<ItemSyncSaver>(model, tabPath, dir.path(), files, maxItems, m_formatSettings);
+    auto *watcher = new FileWatcher(path, files, model, maxItems, m_formatSettings);
+    return std::make_shared<ItemSyncSaver>(model, tabPath, watcher);
 }
diff --git a/plugins/itemsync/itemsync.h b/plugins/itemsync/itemsync.h
index bf932327d..fbbfcdb32 100644
--- a/plugins/itemsync/itemsync.h
+++ b/plugins/itemsync/itemsync.h
@@ -60,15 +60,7 @@ class ItemSyncSaver final : public QObject, public ItemSaverInterface
     Q_OBJECT
 
 public:
-    explicit ItemSyncSaver(const QString &tabPath);
-
-    ItemSyncSaver(
-            QAbstractItemModel *model,
-            const QString &tabPath,
-            const QString &path,
-            const QStringList &files,
-            int maxItems,
-            const QList<FileFormat> &formatSettings);
+    ItemSyncSaver(QAbstractItemModel *model, const QString &tabPath, FileWatcher *watcher);
 
     bool saveItems(const QString &tabName, const QAbstractItemModel &model, QIODevice *file) override;
 
@@ -80,7 +72,12 @@ public:
 
     void setFocus(bool focus) override;
 
+    void setFileWatcher(FileWatcher *watcher);
+
 private:
+    void onRowsMoved(const QModelIndex &, int start, int end, const QModelIndex &, int destinationRow);
+
+    QPointer<QAbstractItemModel> m_model;
     QString m_tabPath;
     FileWatcher *m_watcher;
 };
diff --git a/plugins/itemsync/tests/itemsynctests.cpp b/plugins/itemsync/tests/itemsynctests.cpp
index 46ffd9010..dbfbc8ca2 100644
--- a/plugins/itemsync/tests/itemsynctests.cpp
+++ b/plugins/itemsync/tests/itemsynctests.cpp
@@ -692,3 +692,59 @@ void ItemSyncTests::addItemsWhenFullOmitDeletingNotOwned()
     FilePtr f1(dir1.file(testFileName1));
     QVERIFY(f1->exists());
 }
+
+void ItemSyncTests::moveOwnItemsSortsBaseNames()
+{
+    TestDir dir1(1);
+    const QString tab1 = testTab(1);
+    RUN(Args() << "show" << tab1, "");
+
+    const Args args = Args() << "separator" << "," << "tab" << tab1;
+
+    const QString testScript = R"(
+        var baseNames = [];
+        for (var i = 0; i < 4; ++i) {
+            baseNames.push(str(read(plugins.itemsync.mimeBaseName, i)));
+        }
+        for (var i = 0; i < 3; ++i) {
+            var j = i + 1;
+            if (baseNames[i] <= baseNames[j]) {
+                print("Failed test: baseNames["+i+"] > baseNames["+j+"]\\n");
+                print("  Where baseNames["+i+"] = '" + baseNames[i] + "'\\n");
+                print("        baseNames["+j+"] = '" + baseNames[j] + "'\\n");
+            }
+        }
+    )";
+
+    RUN(args << "add" << "A" << "B" << "C" << "D", "");
+    RUN(args << "read(0,1,2,3)", "D,C,B,A");
+    RUN(args << testScript, "");
+
+    RUN(args << "keys" << "END" << "CTRL+UP", "");
+    RUN(args << "read(0,1,2,3)", "D,C,A,B");
+    RUN(args << testScript, "");
+
+    RUN(args << "keys" << "DOWN" << "CTRL+UP", "");
+    RUN(args << "read(0,1,2,3)", "D,C,B,A");
+    RUN(args << testScript, "");
+
+    RUN(args << "keys" << "DOWN" << "SHIFT+UP" << "CTRL+UP", "");
+    RUN(args << "read(0,1,2,3)", "D,B,A,C");
+    RUN(args << testScript, "");
+
+    RUN(args << "keys" << "CTRL+UP", "");
+    RUN(args << "read(0,1,2,3)", "B,A,D,C");
+    RUN(args << testScript, "");
+
+    RUN(args << "keys" << "CTRL+DOWN", "");
+    RUN(args << "read(0,1,2,3)", "D,B,A,C");
+    RUN(args << testScript, "");
+
+    RUN(args << "keys" << "END" << "CTRL+HOME", "");
+    RUN(args << "read(0,1,2,3)", "C,D,B,A");
+    RUN(args << testScript, "");
+
+    RUN(args << "keys" << "END" << "UP" << "CTRL+HOME", "");
+    RUN(args << "read(0,1,2,3)", "B,C,D,A");
+    RUN(args << testScript, "");
+}
diff --git a/plugins/itemsync/tests/itemsynctests.h b/plugins/itemsync/tests/itemsynctests.h
index 7c00a3543..ca001cdae 100644
--- a/plugins/itemsync/tests/itemsynctests.h
+++ b/plugins/itemsync/tests/itemsynctests.h
@@ -65,6 +65,8 @@ private slots:
 
     void addItemsWhenFullOmitDeletingNotOwned();
 
+    void moveOwnItemsSortsBaseNames();
+
 private:
     TestInterfacePtr m_test;
 };
-- 
GitLab