diff --git a/model/foldermodel.cpp b/model/foldermodel.cpp index 8508bec..d052262 100644 --- a/model/foldermodel.cpp +++ b/model/foldermodel.cpp @@ -75,6 +75,17 @@ #include #include +static bool isDropBetweenSharedViews(const QList &urls, const QUrl &folderUrl) +{ + for (const auto &url : urls) { + if (folderUrl.adjusted(QUrl::StripTrailingSlash) != url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash)) { + return false; + } + } + return true; +} + + FolderModel::FolderModel(QObject *parent) : QSortFilterProxyModel(parent) , m_dirWatch(nullptr) @@ -89,6 +100,7 @@ FolderModel::FolderModel(QObject *parent) , m_selectedItemSize("") , m_actionCollection(this) , m_dragInProgress(false) + , m_dropTargetPositionsCleanup(new QTimer(this)) , m_viewAdapter(nullptr) , m_mimeAppManager(MimeAppManager::self()) , m_sizeJob(nullptr) @@ -102,11 +114,65 @@ FolderModel::FolderModel(QObject *parent) m_dirLister->setShowingDotFiles(m_showHiddenFiles); // connect(dirLister, &DirLister::error, this, &FolderModel::notification); + connect(m_dirLister, &KCoreDirLister::started, this, std::bind(&FolderModel::setStatus, this, Status::Listing)); + + void (KCoreDirLister::*myCompletedSignal)() = &KCoreDirLister::completed; + QObject::connect(m_dirLister, myCompletedSignal, this, [this] { + setStatus(Status::Ready); + emit listingCompleted(); + }); + + void (KCoreDirLister::*myCanceledSignal)() = &KCoreDirLister::canceled; + QObject::connect(m_dirLister, myCanceledSignal, this, [this] { + setStatus(Status::Canceled); + emit listingCanceled(); + }); + m_dirModel = new KDirModel(this); m_dirModel->setDirLister(m_dirLister); m_dirModel->setDropsAllowed(KDirModel::DropOnDirectory | KDirModel::DropOnLocalExecutable); m_dirModel->moveToThread(qApp->thread()); + // If we have dropped items queued for moving, go unsorted now. + connect(this, &QAbstractItemModel::rowsAboutToBeInserted, this, [this]() { + if (!m_dropTargetPositions.isEmpty()) { + setSortMode(-1); + } + }); + + // Position dropped items at the desired target position. + connect(this, &QAbstractItemModel::rowsInserted, this, [this](const QModelIndex &parent, int first, int last) { + for (int i = first; i <= last; ++i) { + const auto idx = index(i, 0, parent); + const auto url = itemForIndex(idx).url(); + auto it = m_dropTargetPositions.find(url.fileName()); + if (it != m_dropTargetPositions.end()) { + const auto pos = it.value(); + m_dropTargetPositions.erase(it); + Q_EMIT move(pos.x(), pos.y(), {url}); + } + } + }); + + /* + * Dropped files may not actually show up as new files, e.g. when we overwrite + * an existing file. Or files that fail to be listed by the dirLister, or... + * To ensure we don't grow the map indefinitely, clean it up periodically. + * The cleanup timer is (re)started whenever we modify the map. We use a quite + * high interval of 10s. This should ensure, that we don't accidentally wipe + * the mapping when we actually still want to use it. Since the time between + * adding an entry in the map and it showing up in the model should be + * small, this should rarely, if ever happen. + */ + m_dropTargetPositionsCleanup->setInterval(10000); + m_dropTargetPositionsCleanup->setSingleShot(true); + connect(m_dropTargetPositionsCleanup, &QTimer::timeout, this, [this]() { + if (!m_dropTargetPositions.isEmpty()) { + qDebug() << "clearing drop target positions after timeout:" << m_dropTargetPositions; + m_dropTargetPositions.clear(); + } + }); + m_selectionModel = new QItemSelectionModel(this, this); connect(m_selectionModel, &QItemSelectionModel::selectionChanged, this, &FolderModel::selectionChanged); @@ -1038,24 +1104,38 @@ void FolderModel::drop(QQuickItem *target, QObject *dropEvent, int row) dropTargetUrl = item.mostLocalUrl(); } + auto dropTargetFolderUrl = dropTargetUrl; + if (dropTargetFolderUrl.fileName() == QLatin1Char('.')) { + // the target URL for desktop:/ is e.g. 'file://home/user/Desktop/.' + dropTargetFolderUrl = dropTargetFolderUrl.adjusted(QUrl::RemoveFilename); + } + + const int x = dropEvent->property("x").toInt(); + const int y = dropEvent->property("y").toInt(); + const QPoint dropPos = {x, y}; + + if (m_dragInProgress && row == -1) { + if (mimeData->urls().isEmpty()) + return; + + setSortMode(-1); + + for (const auto &url : mimeData->urls()) { + m_dropTargetPositions.insert(url.fileName(), dropPos); + } + + emit move(x, y, mimeData->urls()); + + return; + } + + if (idx.isValid() && !(flags(idx) & Qt::ItemIsDropEnabled)) { return; } - // 处理url - QList sourceUrls; - for (const QUrl &url : mimeData->urls()) { - QFileInfo info(url.toLocalFile()); - QUrl newUrl = QUrl::fromLocalFile(info.dir().path()); - - // 相同的目录下不加入 - if (newUrl != dropTargetUrl) { - sourceUrls.append(url); - } - } - - if (!sourceUrls.isEmpty()) { - KIO::Job *job = KIO::move(sourceUrls, dropTargetUrl, KIO::HideProgressInfo); + if (!isDropBetweenSharedViews(mimeData->urls(), dropTargetFolderUrl)) { + KIO::Job *job = KIO::move(mimeData->urls(), dropTargetUrl, KIO::HideProgressInfo); job->start(); } } diff --git a/model/foldermodel.h b/model/foldermodel.h index d6595a1..8b0f4f2 100644 --- a/model/foldermodel.h +++ b/model/foldermodel.h @@ -48,6 +48,7 @@ class FolderModel : public QSortFilterProxyModel, public QQmlParserStatus Q_INTERFACES(QQmlParserStatus) Q_PROPERTY(QString url READ url WRITE setUrl NOTIFY urlChanged) Q_PROPERTY(QUrl resolvedUrl READ resolvedUrl NOTIFY resolvedUrlChanged) + Q_PROPERTY(Status status READ status NOTIFY statusChanged) Q_PROPERTY(int sortMode READ sortMode WRITE setSortMode NOTIFY sortModeChanged) Q_PROPERTY(bool sortDirsFirst READ sortDirsFirst WRITE setSortDirsFirst NOTIFY sortDirsFirstChanged) Q_PROPERTY(bool dragging READ dragging NOTIFY draggingChanged) @@ -225,6 +226,8 @@ public: signals: void urlChanged(); + void listingCompleted() const; + void listingCanceled() const; void resolvedUrlChanged(); void statusChanged(); void sortModeChanged(); @@ -243,6 +246,7 @@ signals: void showHiddenFilesChanged(); void notification(const QString &message); + void move(int x, int y, QList urls); private slots: void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected); @@ -296,6 +300,9 @@ private: QPoint m_dragHotSpotScrollOffset; bool m_dragInProgress; + QHash m_dropTargetPositions; + QTimer *m_dropTargetPositionsCleanup; + QPointer m_viewAdapter; // Save path history diff --git a/qml/FolderPage.qml b/qml/FolderPage.qml index 5f3bc39..bf6c6b0 100644 --- a/qml/FolderPage.qml +++ b/qml/FolderPage.qml @@ -144,7 +144,8 @@ Item { text: qsTr("Empty folder") font.pointSize: 15 anchors.centerIn: parent - visible: false + visible: dirModel.status === FM.FolderModel.Ready + && _viewLoader.item.count === 0 } FM.FolderModel { @@ -260,6 +261,15 @@ Item { visible: dirModel.selectionCount >= 1 } + FishUI.BusyIndicator { + id: _busyIndicator + Layout.alignment: Qt.AlignLeft + Layout.fillHeight: true + width: height + running: visible + visible: dirModel.status === FM.FolderModel.Listing + } + Label { text: dirModel.selectedItemSize }