From 9cca1836778cce099908a8c81e94aa9db4c3976c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Du=C5=A1an=20Poizl?= Date: Sun, 14 Sep 2025 20:31:45 +0200 Subject: [PATCH] Working copy/move operation --- src/filemanager.cpp | 321 +++++++++++++++++++++++++++++++++++++++++++- src/filemanager.h | 56 +++++++- src/filemanager.ui | 54 +++++++- 3 files changed, 416 insertions(+), 15 deletions(-) diff --git a/src/filemanager.cpp b/src/filemanager.cpp index b258d9e..2f6a30c 100644 --- a/src/filemanager.cpp +++ b/src/filemanager.cpp @@ -5,8 +5,213 @@ #include #include #include +#include +#include +#include #include "loadimage.h" +class FileTimes +{ +public: + explicit FileTimes(const QString &path) + { + QFile file(path); +#ifndef Q_OS_WIN + birthTime = file.fileTime(QFileDevice::FileBirthTime); +#endif + modificationTime = file.fileTime(QFileDevice::FileModificationTime); + accessTime = file.fileTime(QFileDevice::FileAccessTime); + } + void apply(const QString &path) + { + QFile file(path); + if(file.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::ExistingOnly)) + { +#ifndef Q_OS_WIN // Only windows allow changing birth time + file.setFileTime(birthTime, QFileDevice::FileBirthTime); +#endif + file.setFileTime(accessTime, QFileDevice::FileAccessTime); + file.setFileTime(modificationTime, QFileDevice::FileModificationTime); + } + } +private: + QDateTime birthTime; + QDateTime modificationTime; + QDateTime accessTime; +}; + +FileTransfer::FileTransfer(FileManager *fm) : + _fm(fm) +{ +} + +FileTransfer::~FileTransfer() +{ + _run = false; +} + +void FileTransfer::copy(const QStringList &src, const QString &dst) +{ + _run = true; + perform(src, dst, true); + emit finished(); +} + +void FileTransfer::move(const QStringList &src, const QString &dst) +{ + _run = true; + perform(src, dst, false); + emit finished(); +} + +void FileTransfer::cancel() +{ + _run = false; +} + +void FileTransfer::perform(const QStringList &src, const QString &dst, bool copy) +{ + QDir dstDir(dst); + if(!dstDir.exists()) + { + emit error(tr("Error"), tr("Destination directory %1 doesn't exists").arg(dstDir.absolutePath())); + return; + } + + QList actions; + QStringList dirs; + + emit progress(0); + + for(const QString &i : src) + { + QFileInfo srcInfo(i); + if(srcInfo.absolutePath() == dst || dst.startsWith(srcInfo.absoluteFilePath())) + return; + + if(srcInfo.isDir()) + { + QDir srcDir(i); + //qDebug() << "dir" << srcInfo.absoluteFilePath() << srcInfo.fileName(); + if(!copy && !dstDir.exists(srcInfo.fileName())) + { + if(QFile::rename(srcInfo.absoluteFilePath(), dstDir.absoluteFilePath(srcInfo.fileName()))) + continue; + } + actions.append({srcInfo.absoluteFilePath(), srcInfo.fileName(), true}); + if(!copy)dirs.prepend(srcInfo.absoluteFilePath()); + QDirIterator it(i, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks); + while(it.hasNext()) + { + QFileInfo info = it.nextFileInfo(); + if(info.fileName() == "." || info.fileName() == "..") + continue; + + QString relativePath = srcDir.dirName() + "/" + srcDir.relativeFilePath(info.absoluteFilePath()); + if(info.isDir()) + { + actions.append({"", relativePath, true}); + if(!copy)dirs.prepend(info.absoluteFilePath()); + //qDebug() << "dir" << info.absoluteFilePath() << relativePath; + } + else + { + actions.append({info.absoluteFilePath(), dstDir.absoluteFilePath(relativePath), false}); + //qDebug() << "file" << info.absoluteFilePath() << dstDir.absoluteFilePath(relativePath); + } + } + } + else + { + actions.append({srcInfo.absoluteFilePath(), dstDir.absoluteFilePath(srcInfo.fileName())}); + //qDebug() << "file" << srcInfo.absoluteFilePath() << dstDir.absoluteFilePath(srcInfo.fileName()); + } + } + + bool overwriteAll = false; + bool skipAll = false; + int total = actions.size(); + int i = 0; + for(auto &a : actions) + { + if(!_run) + return; + + if(a.dir) + { + dstDir.mkpath(a.dst); + } + else + { + QFileInfo dstInfo(a.dst); + if(dstInfo.exists()) + { + if(overwriteAll) + { + QFile::remove(dstInfo.absoluteFilePath()); + } + else if(skipAll) + { + emit progress(i++ * 100 / total); + continue; + } + else + { + QMessageBox::StandardButton ret; + QMetaObject::invokeMethod(_fm, "overwrite", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QMessageBox::StandardButton, ret), Q_ARG(QString, dstInfo.fileName())); + switch(ret) + { + case QMessageBox::YesToAll: + overwriteAll = true;//break; is intentionally missing + case QMessageBox::Yes: + QFile::remove(dstInfo.absoluteFilePath()); + break; + case QMessageBox::NoToAll: + skipAll = true;//break; is intentionally missing + case QMessageBox::No: + emit progress(i++ * 100 / total); + continue; + break; + case QMessageBox::Cancel: + return; + default: + break; + } + } + } + FileTimes t(a.src); + if(copy) + { + if(!QFile::copy(a.src, a.dst)) + { + emit error(tr("Copy failed"), tr("Failed to copy file %1 to %2").arg(a.src).arg(a.dst)); + return; + } + } + else + { + if(!QFile::rename(a.src, a.dst)) + { + emit error(tr("Move failed"), tr("Failed to move file %1 to %2").arg(a.src).arg(a.dst)); + return; + } + } + t.apply(a.dst); + } + emit progress(i++ * 100 / total); + } + + if(!copy) + { + for(const QString &d : dirs) + { + QDir dir(d); + if(dir.isEmpty()) + dir.removeRecursively(); + } + } +} + PathTabBar::PathTabBar(const QStringList &tabs) : _tabs(tabs) { @@ -147,6 +352,8 @@ FileManager::FileManager(const QSet &openFilter, QWidget *parent) : QMa connect(ui->rightTab, &DirView::dirChanged, _rightTabBar, &PathTabBar::pathChanged); connect(ui->leftTab, &DirView::openFile, this, &FileManager::openFile); connect(ui->rightTab, &DirView::openFile, this, &FileManager::openFile); + connect(ui->leftTab, &DirView::filesAction, this, &FileManager::copyMoveFiles, Qt::QueuedConnection); + connect(ui->rightTab, &DirView::filesAction, this, &FileManager::copyMoveFiles, Qt::QueuedConnection); connect(ui->actionLoad_FITS_keywordsLeft, &QAction::toggled, ui->leftTab, &DirView::loadFitsKeywords); connect(ui->actionLoad_FITS_keywordsRight, &QAction::toggled, ui->rightTab, &DirView::loadFitsKeywords); @@ -165,6 +372,8 @@ FileManager::FileManager(const QSet &openFilter, QWidget *parent) : QMa connect(ui->actionSelect_columnsLeft, &QAction::triggered, this, &FileManager::selectFITSKeywords); connect(ui->actionSelect_columnsRight, &QAction::triggered, this, &FileManager::selectFITSKeywords); + connect(ui->actionCopySelectedFilesPathsLeft, &QAction::triggered, this, &FileManager::copySelectedFilesPaths); + connect(ui->actionCopySelectedFilesPathsRight, &QAction::triggered, this, &FileManager::copySelectedFilesPaths); connect(ui->leftPath, &QLineEdit::returnPressed, this, &FileManager::pathEdited); connect(ui->rightPath, &QLineEdit::returnPressed, this, &FileManager::pathEdited); @@ -175,6 +384,25 @@ FileManager::FileManager(const QSet &openFilter, QWidget *parent) : QMa ui->menuLeft_Tab->addAction(drive.absoluteFilePath(), [path, this](){ ui->leftTab->setDir(path); }); ui->menuRight_Tab->addAction(drive.absoluteFilePath(), [path, this](){ ui->rightTab->setDir(path); }); } + + ui->progressBar->hide(); + ui->cancelButton->hide(); + + _thread = new QThread(this); + _thread->start(); + _fileTransfer = new FileTransfer(this); + _fileTransfer->moveToThread(_thread); + connect(_fileTransfer, &FileTransfer::progress, ui->progressBar, &QProgressBar::setValue); + connect(_fileTransfer, &FileTransfer::error, this, &FileManager::errorMessage); + connect(_fileTransfer, &FileTransfer::finished, [this](){ + ui->leftTab->setDragEnabled(true); + ui->rightTab->setDragEnabled(true); + ui->progressBar->hide(); + ui->cancelButton->hide(); + }); + connect(this, &FileManager::copy, _fileTransfer, &FileTransfer::copy); + connect(this, &FileManager::move, _fileTransfer, &FileTransfer::move); + connect(ui->cancelButton, &QPushButton::clicked, [this](){ _fileTransfer->cancel(); }); } FileManager::~FileManager() @@ -190,6 +418,12 @@ FileManager::~FileManager() settings.setValue("filemanager/rightLoadFitsKeywords", ui->actionLoad_FITS_keywordsRight->isChecked()); settings.setValue("filemanager/geometry", saveGeometry()); delete ui; + + _fileTransfer->cancel(); + _thread->quit(); + _thread->wait(); + + delete _fileTransfer; } void FileManager::selectFITSKeywords() @@ -213,6 +447,14 @@ void FileManager::selectFITSKeywords() } } +void FileManager::copySelectedFilesPaths() +{ + if(sender() == ui->actionCopySelectedFilesPathsLeft) + ui->leftTab->copySelectedFilesPathsToClipboard(); + if(sender() == ui->actionCopySelectedFilesPathsRight) + ui->rightTab->copySelectedFilesPathsToClipboard(); +} + void FileManager::pathEdited() { if(sender() == ui->leftPath) @@ -229,6 +471,40 @@ void FileManager::pathEdited() } } +void FileManager::copyMoveFiles(Qt::DropAction action, const QStringList &src, const QString &dst) +{ + ui->leftTab->setDragEnabled(false); + ui->rightTab->setDragEnabled(false); + ui->progressBar->show(); + ui->cancelButton->show(); + + switch(action) + { + case Qt::CopyAction: + emit copy(src, dst); + break; + case Qt::MoveAction: + emit move(src, dst); + break; + case Qt::LinkAction: + default: + break; + } +} + +QMessageBox::StandardButton FileManager::overwrite(const QString &dst) +{ + QMessageBox::StandardButton button = QMessageBox::question(this, tr("Overwrite file?"), tr("Destination file %1 already exists. Overwrite?").arg(dst), + QMessageBox::Yes | QMessageBox::YesToAll | QMessageBox::No | QMessageBox::NoToAll | QMessageBox::Cancel); + + return button; +} + +void FileManager::errorMessage(const QString &title, const QString &text) +{ + QMessageBox::critical(this, title, text); +} + QCache* DirFileSystemModel::getCacheInstance() { static bool init = true; @@ -241,7 +517,8 @@ QCache* DirFileSystemModel::getCacheInstance() return &cache; } -DirFileSystemModel::DirFileSystemModel(QObject *parent) : QFileSystemModel(parent) +DirFileSystemModel::DirFileSystemModel(QWidget *parentWidget) : QFileSystemModel(parentWidget) + ,_parentWidget(parentWidget) { _cache = getCacheInstance(); setFilter(QDir::AllEntries | QDir::NoDot); @@ -272,7 +549,9 @@ const QStringList &DirFileSystemModel::FITSKeywords() const Qt::ItemFlags DirFileSystemModel::flags(const QModelIndex &index) const { - return QFileSystemModel::flags(index) & ~Qt::ItemIsEditable; + Qt::ItemFlags ret = QFileSystemModel::flags(index) & ~Qt::ItemIsEditable; + if(index.row() == 0)ret &= ~Qt::ItemIsDragEnabled; + return ret; } int DirFileSystemModel::columnCount(const QModelIndex &parent) const @@ -335,6 +614,21 @@ bool DirFileSystemModel::hasChildren(const QModelIndex &parent) const return QFileSystemModel::hasChildren(parent); } +bool DirFileSystemModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) +{ + Q_UNUSED(row); + Q_UNUSED(column); + if(data->hasUrls()) + { + QStringList srcPaths; + for(auto &url : data->urls()) + srcPaths.append(url.toLocalFile()); + emit filesAction(action, srcPaths, filePath(parent)); + } + + return false; +} + void DirFileSystemModel::loadFitsKeywords(bool enable) { _loadFitsKeywords = enable; @@ -348,6 +642,8 @@ DirView::DirView(QWidget *parent) : QTreeView(parent) setDragEnabled(true); setAcceptDrops(true); + connect(_dirFileSystemModel, &DirFileSystemModel::filesAction, this, &DirView::filesAction); + setModel(_dirFileSystemModel); setSelectionMode(QAbstractItemView::ExtendedSelection); @@ -435,3 +731,24 @@ void DirView::loadFitsKeywords(bool enable) { _dirFileSystemModel->loadFitsKeywords(enable); } + +void DirView::copySelectedFilesPathsToClipboard() const +{ + QList urls; + QString text; + auto selected = selectionModel()->selectedRows(); + for(auto &item : selected) + { + if(item.column() == 0) + { + QString path = _dirFileSystemModel->filePath(item); + text.append(path); text.append('\n'); + urls.append(QUrl::fromLocalFile(path)); + } + } + QClipboard *clipboard = QApplication::clipboard(); + QMimeData *mimeData = new QMimeData(); + mimeData->setUrls(urls); + mimeData->setText(text); + clipboard->setMimeData(mimeData); +} diff --git a/src/filemanager.h b/src/filemanager.h index 3563215..ac49843 100644 --- a/src/filemanager.h +++ b/src/filemanager.h @@ -8,6 +8,7 @@ #include #include #include +#include #include "imageinfodata.h" namespace Ui { @@ -15,6 +16,34 @@ class FileManager; class FITSKeyword; } +class FileManager; + +class FileTransfer: public QObject +{ + Q_OBJECT +public: + explicit FileTransfer(FileManager *fm); + ~FileTransfer(); +public slots: + void copy(const QStringList &src, const QString &dst); + void move(const QStringList &src, const QString &dst); + void cancel(); +signals: + void progress(int percent); + void finished(); + void error(const QString &title, const QString &text); +private: + void perform(const QStringList &src, const QString &dst, bool copy); + struct Action + { + QString src; + QString dst; + bool dir = false; + }; + FileManager *_fm; + bool _run = true; +}; + class PathTabBar : public QTabBar { Q_OBJECT @@ -51,25 +80,28 @@ public: ~FileManager(); public slots: void selectFITSKeywords(); + void copySelectedFilesPaths(); void pathEdited(); + void copyMoveFiles(Qt::DropAction action, const QStringList &src, const QString &dst); + QMessageBox::StandardButton overwrite(const QString &dst); + void errorMessage(const QString &title, const QString &text); signals: void openFile(const QString &path); + void copy(const QStringList &src, const QString &dst); + void move(const QStringList &src, const QString &dst); private: Ui::FileManager *ui; PathTabBar *_leftTabBar; PathTabBar *_rightTabBar; + QThread *_thread; + FileTransfer *_fileTransfer; }; class DirFileSystemModel : public QFileSystemModel { Q_OBJECT - mutable QCache *_cache = nullptr; - static QCache* getCacheInstance(); - QModelIndex _dir; - QStringList _fitsKeywords; - bool _loadFitsKeywords = true; public: - explicit DirFileSystemModel(QObject *parent = nullptr); + explicit DirFileSystemModel(QWidget *parentWidget); void setDir(const QString &path); QString dir() const; void setFITSKeywords(const QStringList &keywords); @@ -79,8 +111,18 @@ public: QVariant data(const QModelIndex &index, int role) const override; QVariant headerData(int section, Qt::Orientation orientation, int role) const override; bool hasChildren(const QModelIndex &parent) const override; + bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override; public slots: void loadFitsKeywords(bool enable); +signals: + void filesAction(Qt::DropAction action, const QStringList &src, const QString &dst); +private: + mutable QCache *_cache = nullptr; + static QCache* getCacheInstance(); + QModelIndex _dir; + QStringList _fitsKeywords; + bool _loadFitsKeywords = true; + QWidget *_parentWidget = nullptr; }; class DirView : public QTreeView @@ -98,9 +140,11 @@ public: public slots: void headerContextMenu(const QPoint &pos); void loadFitsKeywords(bool enable); + void copySelectedFilesPathsToClipboard() const; signals: void dirChanged(const QString &path); void openFile(const QString &path); + void filesAction(Qt::DropAction action, const QStringList &src, const QString &dst); }; #endif // FILEMANAGER_H diff --git a/src/filemanager.ui b/src/filemanager.ui index 486ef7c..8ca86d6 100644 --- a/src/filemanager.ui +++ b/src/filemanager.ui @@ -14,24 +14,52 @@ File Manager - + - + - + + + + + + + + - + + + + + + + + - + - + + + + 1 + 0 + + + + 0 + + - + + + Cancel + + @@ -52,6 +80,7 @@ + @@ -60,6 +89,7 @@ + @@ -97,6 +127,16 @@ Select columns + + + Copy selected files paths + + + + + Copy selected files paths + +