#include "filemanager.h" #include "ui_filemanager.h" #include "ui_fitskeyword.h" #include #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) { setTabsClosable(true); setExpanding(false); for(auto &t : _tabs) { QDir dir(t); int i = addTab(tabName(t)); setTabToolTip(i, t); } connect(this, &QTabBar::currentChanged, [this](int index){ QString path = _tabs.at(index); emit pathChanged(path); }); connect(this, &QTabBar::tabCloseRequested, [this](int index){ if(_tabs.size() >= 2) { _tabs.remove(index); removeTab(index); } }); connect(this, &QTabBar::currentChanged, [this](int index){ emit tabChanged(_tabs[index]); }); } QHBoxLayout *PathTabBar::createLayout() { QHBoxLayout *hlayout = new QHBoxLayout(); hlayout->addWidget(this); hlayout->addStretch(2); QPushButton *addButton = new QPushButton("+"); connect(addButton, &QPushButton::clicked, [this](){ QString path = _tabs[currentIndex()]; _tabs.append(path); int i = addTab(tabName(path)); setTabToolTip(i, path); }); hlayout->addWidget(addButton); return hlayout; } const QStringList &PathTabBar::tabPaths() const { return _tabs; } QString PathTabBar::currentTabPath() const { int index = std::clamp(currentIndex(), 0, (int)_tabs.size()); return _tabs[index]; } void PathTabBar::pathChanged(const QString &path) { QDir dir(path); int index = currentIndex(); setTabText(index, tabName(path)); setTabToolTip(index, path); _tabs[index] = path; } QString PathTabBar::tabName(const QString &path) { QDir dir(path); if(dir.dirName().isEmpty()) return path; else return dir.dirName(); } FITSSelection::FITSSelection(const QStringList &keywords, QWidget *parent) : QDialog(parent) ,ui(new Ui::FITSKeyword) { ui->setupUi(this); connect(ui->addButton, &QPushButton::clicked, [this](){ auto item = ui->keywordList->findItems(ui->keyword->text(), Qt::MatchFixedString | Qt::MatchCaseSensitive); if(item.size())return; ui->keywordList->addItem(ui->keyword->text()); }); connect(ui->removeButton, &QPushButton::clicked, [this](){ auto items = ui->keywordList->selectedItems(); for(auto item : items) delete item; }); ui->keywordList->addItems(keywords); } FITSSelection::~FITSSelection() { delete ui; } QStringList FITSSelection::FITSKeywords() const { QStringList keywords; for(int i = 0; i < ui->keywordList->count(); i++) keywords.append(ui->keywordList->item(i)->text()); return keywords; } FileManager::FileManager(const QSet &openFilter, QWidget *parent) : QMainWindow(parent) ,ui(new Ui::FileManager) { ui->setupUi(this); QStringList standardLocations = QStandardPaths::standardLocations(QStandardPaths::PicturesLocation); QString picturesPath; if(standardLocations.size()) picturesPath = standardLocations.first(); QSettings settings; QStringList leftTabs = settings.value("filemanager/leftTabPaths", picturesPath).toStringList(); QStringList rightTabs = settings.value("filemanager/rightTabPaths", picturesPath).toStringList(); if(leftTabs.empty())leftTabs.append(picturesPath); if(rightTabs.empty())rightTabs.append(picturesPath); ui->leftTab->setOpenFilter(openFilter); ui->rightTab->setOpenFilter(openFilter); _rightTabBar = new PathTabBar(rightTabs); ui->rightLayout->insertLayout(0, _rightTabBar->createLayout()); connect(_rightTabBar, &PathTabBar::tabChanged, ui->rightTab, &DirView::setDir); _leftTabBar = new PathTabBar(leftTabs); ui->leftLayout->insertLayout(0, _leftTabBar->createLayout()); connect(_leftTabBar, &PathTabBar::tabChanged, ui->leftTab, &DirView::setDir); connect(ui->leftTab, &DirView::dirChanged, ui->leftPath, &QLineEdit::setText); connect(ui->leftTab, &DirView::dirChanged, _leftTabBar, &PathTabBar::pathChanged); connect(ui->rightTab, &DirView::dirChanged, ui->rightPath, &QLineEdit::setText); 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); ui->leftTab->setDir(_leftTabBar->currentTabPath()); ui->leftTab->setFITSKeywords(settings.value("filemanager/leftFitsKeywords", QStringList("OBJECT")).toStringList()); ui->leftTab->header()->restoreState(settings.value("filemanager/leftTabHeader").toByteArray()); ui->rightTab->setDir(_rightTabBar->currentTabPath()); ui->rightTab->setFITSKeywords(settings.value("filemanager/rightFitsKeywords", QStringList("OBJECT")).toStringList()); ui->rightTab->header()->restoreState(settings.value("filemanager/rightTabHeader").toByteArray()); ui->actionLoad_FITS_keywordsLeft->setChecked(settings.value("filemanager/leftLoadFitsKeywords", true).toBool()); ui->actionLoad_FITS_keywordsRight->setChecked(settings.value("filemanager/rightLoadFitsKeywords", true).toBool()); restoreGeometry(settings.value("filemanager/geometry").toByteArray()); setAttribute(Qt::WA_DeleteOnClose); 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); QFileInfoList drives = QDir::drives(); for(auto &drive : drives) { QString path = drive.absoluteFilePath(); 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() { QSettings settings; settings.setValue("filemanager/leftFitsKeywords", ui->leftTab->FITSKeywords()); settings.setValue("filemanager/leftTabPaths", _leftTabBar->tabPaths()); settings.setValue("filemanager/leftTabHeader", ui->leftTab->header()->saveState()); settings.setValue("filemanager/rightFitsKeywords", ui->rightTab->FITSKeywords()); settings.setValue("filemanager/rightTabPaths", _rightTabBar->tabPaths()); settings.setValue("filemanager/rightTabHeader", ui->rightTab->header()->saveState()); settings.setValue("filemanager/leftLoadFitsKeywords", ui->actionLoad_FITS_keywordsLeft->isChecked()); 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() { QStringList columns; if(sender() == ui->actionSelect_columnsLeft) columns = ui->leftTab->FITSKeywords(); if(sender() == ui->actionSelect_columnsRight) columns = ui->rightTab->FITSKeywords(); FITSSelection selection(columns, this); int ret = selection.exec(); if(ret == QDialog::Accepted) { if(sender() == ui->actionSelect_columnsLeft) ui->leftTab->setFITSKeywords(selection.FITSKeywords()); if(sender() == ui->actionSelect_columnsRight) ui->rightTab->setFITSKeywords(selection.FITSKeywords()); } } void FileManager::copySelectedFilesPaths() { if(sender() == ui->actionCopySelectedFilesPathsLeft) ui->leftTab->copySelectedFilesPathsToClipboard(); if(sender() == ui->actionCopySelectedFilesPathsRight) ui->rightTab->copySelectedFilesPathsToClipboard(); } void FileManager::pathEdited() { if(sender() == ui->leftPath) { QDir dir(ui->leftPath->text()); if(dir.exists()) ui->leftTab->setDir(dir.absolutePath()); } if(sender() == ui->rightPath) { QDir dir(ui->rightPath->text()); if(dir.exists()) ui->rightTab->setDir(dir.absolutePath()); } } 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; static QCache cache; if(!init) { cache.setMaxCost(10000); init = false; } return &cache; } DirFileSystemModel::DirFileSystemModel(QWidget *parentWidget) : QFileSystemModel(parentWidget) ,_parentWidget(parentWidget) { _cache = getCacheInstance(); setFilter(QDir::AllEntries | QDir::NoDot); _fitsKeywords = {"OBJECT"}; } void DirFileSystemModel::setDir(const QString &path) { _dir = index(path); } QString DirFileSystemModel::dir() const { return fileInfo(_dir).canonicalFilePath(); } void DirFileSystemModel::setFITSKeywords(const QStringList &keywords) { beginResetModel(); _fitsKeywords = keywords; endResetModel(); } const QStringList &DirFileSystemModel::FITSKeywords() const { return _fitsKeywords; } Qt::ItemFlags DirFileSystemModel::flags(const QModelIndex &index) const { Qt::ItemFlags ret = QFileSystemModel::flags(index) & ~Qt::ItemIsEditable; if(index.row() == 0)ret &= ~Qt::ItemIsDragEnabled; return ret; } int DirFileSystemModel::columnCount(const QModelIndex &parent) const { return QFileSystemModel::columnCount(parent) + _fitsKeywords.size(); } QVariant DirFileSystemModel::data(const QModelIndex &index, int role) const { if(index.column() >= QFileSystemModel::columnCount() && role == Qt::DisplayRole) { QFileInfo info = fileInfo(index); QString path = info.canonicalFilePath(); QString suffix = info.suffix(); ImageInfoData *infoData = nullptr; if(_cache->contains(path)) { infoData = _cache->object(path); } else { if(_loadFitsKeywords) { infoData = new ImageInfoData; if(isFITS(suffix)) readFITSHeader(path, *infoData); else if(isXISF(suffix)) readXISFHeader(path, *infoData); _cache->insert(path, infoData); } } if(infoData) { int column = index.column() - QFileSystemModel::columnCount(); if(column < _fitsKeywords.size()) { const QString &key = _fitsKeywords.at(column); for(auto &record : infoData->fitsHeader) if(record.key == key) return record.value; } } return ""; } return QFileSystemModel::data(index, role); } QVariant DirFileSystemModel::headerData(int section, Qt::Orientation orientation, int role) const { if(orientation == Qt::Horizontal && role == Qt::DisplayRole && section >= QFileSystemModel::columnCount()) return _fitsKeywords.at(section - QFileSystemModel::columnCount()); return QFileSystemModel::headerData(section, orientation, role); } bool DirFileSystemModel::hasChildren(const QModelIndex &parent) const { if(parent.parent() == _dir)return false; 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; } DirView::DirView(QWidget *parent) : QTreeView(parent) { _dirFileSystemModel = new DirFileSystemModel(this); _dirFileSystemModel->setRootPath(QDir::drives().first().path()); _dirFileSystemModel->setReadOnly(false); setDragEnabled(true); setAcceptDrops(true); connect(_dirFileSystemModel, &DirFileSystemModel::filesAction, this, &DirView::filesAction); setModel(_dirFileSystemModel); setSelectionMode(QAbstractItemView::ExtendedSelection); connect(this, &QTreeView::doubleClicked, [this](const QModelIndex &index){ QFileInfo info = _dirFileSystemModel->fileInfo(index); if(_dirFileSystemModel->isDir(index)) { setDir(info.canonicalFilePath()); } else if(info.isFile()) { if(_openFilter.contains(info.suffix().toLower())) emit openFile(info.absoluteFilePath()); else QDesktopServices::openUrl(QUrl::fromLocalFile(info.absoluteFilePath())); } }); header()->setContextMenuPolicy(Qt::CustomContextMenu); connect(header(), &QHeaderView::customContextMenuRequested, this, &DirView::headerContextMenu); } void DirView::setDir(const QString &path) { QString oldPath = _dirFileSystemModel->dir(); #ifdef Q_OS_WINDOWS const int ROOT_LEN = 3; #else const int ROOT_LEN = 1; #endif if(oldPath.left(ROOT_LEN) != path.left(ROOT_LEN)) _dirFileSystemModel->setRootPath(path.left(ROOT_LEN)); QString newPath = path; if(!QFileInfo::exists(path)) { QDir dir(path); do { dir.setPath(QDir::cleanPath(dir.filePath(".."))); }while(!dir.exists() && !dir.isRoot()); newPath = dir.path(); } _dirFileSystemModel->setDir(newPath); setRootIndex(_dirFileSystemModel->index(newPath, 0)); clearSelection(); if(oldPath != newPath)emit dirChanged(newPath); } QString DirView::dir() const { return _dirFileSystemModel->dir(); } void DirView::setOpenFilter(const QSet &openFilter) { _openFilter = openFilter; } void DirView::setFITSKeywords(const QStringList &keywords) { QString d = dir(); _dirFileSystemModel->setFITSKeywords(keywords); setDir(d); } const QStringList &DirView::FITSKeywords() const { return _dirFileSystemModel->FITSKeywords(); } void DirView::headerContextMenu(const QPoint &pos) { QHeaderView *head = header(); QMenu menu; int count = head->count(); for(int i = 0; i < count; i++) { QAction *a = menu.addAction(head->model()->headerData(i, Qt::Horizontal).toString()); a->setCheckable(true); a->setChecked(!head->isSectionHidden(i)); a->setData(i); } QAction *a = menu.exec(mapToGlobal(pos)); if(a) { if(a->isChecked())head->showSection(a->data().toInt()); else head->hideSection(a->data().toInt()); } } 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); }