/*************************************************************************** * Copyright (C) 2006 David Faure * * Copyright (C) 2008 Fredrik Höglund * * Copyright (C) 2008 Rafael Fernández López * * Copyright (C) 2011 Marco Martin * * Copyright (C) 2014 by Eike Hein * * Copyright (C) 2021 Reven Martin * * Copyright (C) 2021 Reion Wong * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * ***************************************************************************/ #include "foldermodel.h" #include "dirlister.h" #include "../dialogs/filepropertiesdialog.h" #include "../dialogs/createfolderdialog.h" #include "../dialogs/openwithdialog.h" #include "../helper/datehelper.h" #include "../helper/filelauncher.h" #include "../cio/cfilesizejob.h" // Qt #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Qt Quick #include #include // KIO #include #include #include #include #include #include #include #include #include #include #include #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) , m_sortMode(0) , m_sortDesc(false) , m_sortDirsFirst(true) , m_showHiddenFiles(false) , m_filterMode(NoFilter) , m_filterPatternMatchAll(true) , m_complete(false) , m_isDesktop(false) , m_selectedItemSize("") , m_actionCollection(this) , m_dragInProgress(false) , m_dropTargetPositionsCleanup(new QTimer(this)) , m_viewAdapter(nullptr) , m_mimeAppManager(MimeAppManager::self()) , m_sizeJob(nullptr) { QSettings settings("cutefishos", qApp->applicationName()); m_showHiddenFiles = settings.value("showHiddenFiles", false).toBool(); m_dirLister = new DirLister(this); m_dirLister->setDelayedMimeTypes(true); m_dirLister->setAutoErrorHandlingEnabled(false, nullptr); 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); setSourceModel(m_dirModel); setSortLocaleAware(true); setFilterCaseSensitivity(Qt::CaseInsensitive); setDynamicSortFilter(true); sort(m_sortMode, m_sortDesc ? Qt::DescendingOrder : Qt::AscendingOrder); createActions(); connect(this, SIGNAL(rowsInserted(QModelIndex, int, int)), SIGNAL(countChanged())); connect(this, SIGNAL(rowsRemoved(QModelIndex, int, int)), SIGNAL(countChanged())); connect(this, SIGNAL(modelReset()), SIGNAL(countChanged())); } FolderModel::~FolderModel() { } void FolderModel::classBegin() { } void FolderModel::componentComplete() { m_complete = true; invalidate(); } QHash FolderModel::roleNames() const { return staticRoleNames(); } QHash FolderModel::staticRoleNames() { QHash roleNames; roleNames[Qt::DisplayRole] = "display"; roleNames[Qt::DecorationRole] = "decoration"; roleNames[BlankRole] = "blank"; roleNames[SelectedRole] = "selected"; roleNames[IsDirRole] = "isDir"; roleNames[IsHiddenRole] = "isHidden"; roleNames[IsLinkRole] = "isLink"; roleNames[UrlRole] = "url"; roleNames[DisplayNameRole] = "displayName"; roleNames[FileNameRole] = "fileName"; roleNames[FileSizeRole] = "fileSize"; roleNames[IconNameRole] = "iconName"; roleNames[ThumbnailRole] = "thumbnail"; roleNames[ModifiedRole] = "modified"; roleNames[IsDesktopFileRole] = "desktopFile"; return roleNames; } QVariant FolderModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); KFileItem item = itemForIndex(index); switch (role) { case BlankRole: return m_dragIndexes.contains(index); case SelectedRole: return m_selectionModel->isSelected(index); case UrlRole: return item.url(); case DisplayNameRole: { if (item.isDesktopFile()) { KDesktopFile dfile(item.localPath()); if (!dfile.readName().isEmpty()) return dfile.readName(); } return item.url().fileName(); } case FileNameRole: { return item.url().fileName(); } case IsDesktopFileRole: { return item.isDesktopFile(); } case IsDirRole: { return item.isDir(); } case IsHiddenRole: { return item.isHidden(); } case IsLinkRole: { return item.isLink(); } case FileSizeRole: { if (item.isDir()) { QDir dir(item.url().toLocalFile()); dir.setFilter(QDir::Dirs | QDir::AllEntries | QDir::NoDotAndDotDot); uint count = dir.count(); return count == 1 ? tr("%1 item").arg(count) : tr("%1 items").arg(count); } return KIO::convertSize(item.size()); } case IconNameRole: return item.iconName(); case ThumbnailRole: { if (item.isLocalFile()) { // Svg Image if (item.mimetype() == "image/svg" || item.mimetype() == "image/svg+xml") { return item.url(); } // Support if (isSupportThumbnails(item.mimetype())) { return "image://thumbnailer/" + item.url().toString(); } } return QVariant(); } case ModifiedRole: { return DateHelper::friendlyTime(item.time(KFileItem::ModificationTime)); } default: break; } return QSortFilterProxyModel::data(index, role); } int FolderModel::indexForKeyboardSearch(const QString &text, int startFromIndex) const { startFromIndex = qMax(0, startFromIndex); for (int i = startFromIndex; i < rowCount(); ++i) { if (fileItem(i).text().startsWith(text, Qt::CaseInsensitive)) { return i; } } for (int i = 0; i < startFromIndex; ++i) { if (fileItem(i).text().startsWith(text, Qt::CaseInsensitive)) { return i; } } return -1; } KFileItem FolderModel::itemForIndex(const QModelIndex &index) const { return m_dirModel->itemForIndex(mapToSource(index)); } KFileItem FolderModel::fileItem(int index) const { if (index >= 0 && index < count()) { return itemForIndex(FolderModel::index(index, 0)); } return KFileItem(); } QList FolderModel::selectedUrls() const { const auto indexes = m_selectionModel->selectedIndexes(); QList urls; urls.reserve(indexes.count()); for (const QModelIndex &index : indexes) { urls.append(itemForIndex(index).url()); } return urls; } QString FolderModel::url() const { return m_url; } void FolderModel::setUrl(const QString &url) { if (url.isEmpty()) return; bool isTrash = url.startsWith("trash:/"); QUrl resolvedNewUrl = resolve(url); QFileInfo info(resolvedNewUrl.toLocalFile()); if (!QFile::exists(resolvedNewUrl.toLocalFile()) && !isTrash) { emit notification(tr("The file or folder %1 does not exist.").arg(url)); return; } // TODO: selected ? if (info.isFile() && !isTrash) { resolvedNewUrl = QUrl::fromLocalFile(info.dir().path()); } // Refresh this directory. if (url == m_url) { m_dirModel->dirLister()->updateDirectory(resolvedNewUrl); return; } m_pathHistory.append(resolvedNewUrl); beginResetModel(); m_url = resolvedNewUrl.toString(QUrl::PreferLocalFile); m_dirModel->dirLister()->openUrl(resolvedNewUrl); clearDragImages(); m_dragIndexes.clear(); endResetModel(); emit urlChanged(); emit resolvedUrlChanged(); // if (m_dirWatch) { // delete m_dirWatch; // m_dirWatch = nullptr; // } // if (resolvedNewUrl.isValid()) { // m_dirWatch = new KDirWatch(this); // m_dirWatch->addFile(resolvedNewUrl.toLocalFile() + QLatin1String("/.directory")); // } } QUrl FolderModel::resolvedUrl() const { return m_dirModel->dirLister()->url(); } QUrl FolderModel::resolve(const QString &url) { QUrl resolvedUrl; if (url.startsWith(QLatin1Char('~'))) { resolvedUrl = QUrl::fromLocalFile(QDir::homePath()); } else { resolvedUrl = QUrl::fromUserInput(url); } return resolvedUrl; } FolderModel::Status FolderModel::status() const { return m_status; } void FolderModel::setStatus(FolderModel::Status status) { if (m_status != status) { m_status = status; emit statusChanged(); } } int FolderModel::sortMode() const { return m_sortMode; } void FolderModel::setSortMode(int mode) { if (m_sortMode != mode) { m_sortMode = mode; if (mode == -1 /* Unsorted */) { setDynamicSortFilter(false); } else { invalidateIfComplete(); sort(m_sortMode, m_sortDesc ? Qt::DescendingOrder : Qt::AscendingOrder); setDynamicSortFilter(true); } emit sortModeChanged(); } } bool FolderModel::sortDirsFirst() const { return m_sortDirsFirst; } void FolderModel::setSortDirsFirst(bool enable) { if (m_sortDirsFirst != enable) { m_sortDirsFirst = enable; if (m_sortMode != -1 /* Unsorted */) { invalidateIfComplete(); sort(m_sortMode, m_sortDesc ? Qt::DescendingOrder : Qt::AscendingOrder); } emit sortDirsFirstChanged(); } } int FolderModel::filterMode() const { return m_filterMode; } void FolderModel::setFilterMode(int filterMode) { if (m_filterMode != (FilterMode)filterMode) { m_filterMode = (FilterMode)filterMode; invalidateFilterIfComplete(); emit filterModeChanged(); } } QStringList FolderModel::filterMimeTypes() const { return m_mimeSet.values(); } void FolderModel::setFilterMimeTypes(const QStringList &mimeList) { #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) const QSet &set = QSet::fromList(mimeList); #else const QSet set(mimeList.constBegin(), mimeList.constEnd()); #endif if (m_mimeSet != set) { m_mimeSet = set; invalidateFilterIfComplete(); emit filterMimeTypesChanged(); } } QString FolderModel::filterPattern() const { return m_filterPattern; } void FolderModel::setFilterPattern(const QString &pattern) { if (m_filterPattern == pattern) { return; } m_filterPattern = pattern; m_filterPatternMatchAll = (pattern == QLatin1String("*")); const QStringList patterns = pattern.split(QLatin1Char(' ')); m_regExps.clear(); m_regExps.reserve(patterns.count()); foreach (const QString &pattern, patterns) { QRegExp rx(pattern); rx.setPatternSyntax(QRegExp::Wildcard); rx.setCaseSensitivity(Qt::CaseInsensitive); m_regExps.append(rx); } invalidateFilterIfComplete(); emit filterPatternChanged(); } QObject *FolderModel::viewAdapter() const { return m_viewAdapter; } void FolderModel::setViewAdapter(QObject *adapter) { if (m_viewAdapter != adapter) { ItemViewAdapter *viewAdapter = dynamic_cast(adapter); m_viewAdapter = viewAdapter; emit viewAdapterChanged(); } } bool FolderModel::dragging() const { return m_dragInProgress; } bool FolderModel::isDir(const QModelIndex &index, const KDirModel *dirModel) const { KFileItem item = dirModel->itemForIndex(index); if (item.isDir()) { return true; } return false; } bool FolderModel::lessThan(const QModelIndex &left, const QModelIndex &right) const { const KDirModel *dirModel = static_cast(sourceModel()); if (m_sortDirsFirst || left.column() == KDirModel::Size) { bool leftIsDir = isDir(left, dirModel); bool rightIsDir = isDir(right, dirModel); if (leftIsDir && !rightIsDir) { return (sortOrder() == Qt::AscendingOrder); } if (!leftIsDir && rightIsDir) { return (sortOrder() == Qt::DescendingOrder); } } const KFileItem leftItem = dirModel->data(left, KDirModel::FileItemRole).value(); const KFileItem rightItem = dirModel->data(right, KDirModel::FileItemRole).value(); const int column = left.column(); int result = 0; switch (column) { case KDirModel::Size: { if (isDir(left, dirModel) && isDir(right, dirModel)) { const int leftChildCount = dirModel->data(left, KDirModel::ChildCountRole).toInt(); const int rightChildCount = dirModel->data(right, KDirModel::ChildCountRole).toInt(); if (leftChildCount < rightChildCount) result = -1; else if (leftChildCount > rightChildCount) result = +1; } else { const KIO::filesize_t leftSize = leftItem.size(); const KIO::filesize_t rightSize = rightItem.size(); if (leftSize < rightSize) result = -1; else if (leftSize > rightSize) result = +1; } break; } case KDirModel::ModifiedTime: { const long long leftTime = leftItem.entry().numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1); const long long rightTime = rightItem.entry().numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1); if (leftTime < rightTime) result = -1; else if (leftTime > rightTime) result = +1; break; } case KDirModel::Type: result = QString::compare(dirModel->data(left, Qt::DisplayRole).toString(), dirModel->data(right, Qt::DisplayRole).toString()); break; default: break; } if (result != 0) return result < 0; QCollator collator; result = collator.compare(leftItem.text(), rightItem.text()); if (result != 0) return result < 0; result = collator.compare(leftItem.name(), rightItem.name()); if (result != 0) return result < 0; return QString::compare(leftItem.url().url(), rightItem.url().url(), Qt::CaseSensitive); } Qt::DropActions FolderModel::supportedDragActions() const { return Qt::CopyAction | Qt::MoveAction | Qt::LinkAction; } Qt::DropActions FolderModel::supportedDropActions() const { return Qt::CopyAction | Qt::MoveAction | Qt::LinkAction; } KFileItem FolderModel::rootItem() const { return m_dirModel->dirLister()->rootItem(); } int FolderModel::count() const { return rowCount(); } int FolderModel::selectionCount() const { return m_selectionModel->selectedIndexes().size(); } QString FolderModel::homePath() const { return QStandardPaths::writableLocation(QStandardPaths::HomeLocation); } QString FolderModel::desktopPath() const { return QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); } QAction *FolderModel::action(const QString &name) const { return m_actionCollection.action(name); } void FolderModel::up() { const QUrl &url = KIO::upUrl(resolvedUrl()); if (url.isValid()) { setUrl(url.toString()); } } void FolderModel::goBack() { QUrl url = m_pathHistory.previousPath(); if (url.isEmpty()) url = resolvedUrl(); setUrl(url.toString()); } void FolderModel::goForward() { QUrl url = m_pathHistory.posteriorPath(); if (url.isEmpty()) url = resolvedUrl(); setUrl(url.toString()); } void FolderModel::refresh() { m_dirModel->dirLister()->updateDirectory(m_dirModel->dirLister()->url()); } void FolderModel::undo() { if (KIO::FileUndoManager::self()->undoAvailable()) { KIO::FileUndoManager::self()->undo(); } } bool FolderModel::supportSetAsWallpaper(const QString &mimeType) { if (mimeType == "image/jpeg" || mimeType == "image/png") return true; return false; } int FolderModel::fileExtensionBoundary(int row) { const QModelIndex idx = index(row, 0); const QString &name = data(idx, Qt::DisplayRole).toString(); int boundary = name.length(); if (data(idx, IsDirRole).toBool()) { return boundary; } QMimeDatabase db; const QString &ext = db.suffixForFileName(name); if (ext.isEmpty()) { boundary = name.lastIndexOf(QLatin1Char('.')); if (boundary < 1) { boundary = name.length(); } } else { boundary -= ext.length() + 1; } return boundary; } bool FolderModel::hasSelection() const { return m_selectionModel->hasSelection(); } bool FolderModel::isSelected(int row) const { if (row < 0) return false; return m_selectionModel->isSelected(index(row, 0)); } bool FolderModel::isBlank(int row) const { if (row < 0) { return true; } return data(index(row, 0), BlankRole).toBool(); } void FolderModel::setSelected(int row) { if (row < 0) return; m_selectionModel->select(index(row, 0), QItemSelectionModel::Select); } void FolderModel::selectAll() { setRangeSelected(0, rowCount() - 1); } void FolderModel::toggleSelected(int row) { if (row < 0) return; m_selectionModel->select(index(row, 0), QItemSelectionModel::Toggle); } void FolderModel::setRangeSelected(int anchor, int to) { if (anchor < 0 || to < 0) { return; } QItemSelection selection(index(anchor, 0), index(to, 0)); m_selectionModel->select(selection, QItemSelectionModel::ClearAndSelect); } void FolderModel::updateSelection(const QVariantList &rows, bool toggle) { QItemSelection newSelection; int iRow = -1; foreach (const QVariant &row, rows) { iRow = row.toInt(); if (iRow < 0) { return; } const QModelIndex &idx = index(iRow, 0); newSelection.select(idx, idx); } if (toggle) { QItemSelection pinnedSelection = m_pinnedSelection; pinnedSelection.merge(newSelection, QItemSelectionModel::Toggle); m_selectionModel->select(pinnedSelection, QItemSelectionModel::ClearAndSelect); } else { m_selectionModel->select(newSelection, QItemSelectionModel::ClearAndSelect); } } void FolderModel::clearSelection() { if (m_selectionModel->hasSelection()) m_selectionModel->clear(); } void FolderModel::pinSelection() { m_pinnedSelection = m_selectionModel->selection(); } void FolderModel::unpinSelection() { m_pinnedSelection = QItemSelection(); } void FolderModel::newFolder() { CreateFolderDialog *dlg = new CreateFolderDialog; dlg->setPath(rootItem().url().toString()); dlg->show(); } void FolderModel::rename(int row, const QString &name) { if (row < 0) return; QModelIndex idx = index(row, 0); m_dirModel->setData(mapToSource(idx), name, Qt::EditRole); } void FolderModel::copy() { if (!m_selectionModel->hasSelection()) return; if (QAction *action = m_actionCollection.action("copy")) if (!action->isEnabled()) return; QMimeData *mimeData = QSortFilterProxyModel::mimeData(m_selectionModel->selectedIndexes()); QApplication::clipboard()->setMimeData(mimeData); } void FolderModel::paste() { const QMimeData *mimeData = QApplication::clipboard()->mimeData(); bool enable = false; // Update paste action if (QAction *paste = m_actionCollection.action(QStringLiteral("paste"))) { QList urls = KUrlMimeData::urlsFromMimeData(mimeData); if (!urls.isEmpty()) { if (!rootItem().isNull()) { enable = rootItem().isWritable(); } } paste->setEnabled(enable); } if (enable) { KIO::paste(mimeData, m_dirModel->dirLister()->url()); } } void FolderModel::cut() { if (!m_selectionModel->hasSelection()) return; if (QAction *action = m_actionCollection.action("cut")) if (!action->isEnabled()) return; QMimeData *mimeData = QSortFilterProxyModel::mimeData(m_selectionModel->selectedIndexes()); KIO::setClipboardDataCut(mimeData, true); QApplication::clipboard()->setMimeData(mimeData); } void FolderModel::openSelected() { if (!m_selectionModel->hasSelection()) return; const QList urls = selectedUrls(); if (!m_isDesktop) { if (urls.size() == 1 && KFileItem(urls.first()).isDir()) { setUrl(urls.first().toLocalFile()); return; } } for (const QUrl &url : urls) { KFileItem item(url); QString mimeType = item.mimetype(); // Desktop file. if (mimeType == "application/x-desktop") { FileLauncher::self()->launchApp(url.toLocalFile(), ""); continue; } // runnable if (mimeType == "application/x-executable" || mimeType == "application/x-sharedlib" || mimeType == "application/x-iso9660-appimage" || mimeType == "application/vnd.appimage") { QFileInfo fileInfo(url.toLocalFile()); if (!fileInfo.isExecutable()) { QFile file(url.toLocalFile()); file.setPermissions(file.permissions() | QFile::ExeOwner | QFile::ExeUser | QFile::ExeGroup | QFile::ExeOther); } FileLauncher::self()->launchExecutable(url.toLocalFile()); continue; } QString defaultAppDesktopFile = m_mimeAppManager->getDefaultAppByMimeType(item.currentMimeType()); // If no default application is found, // look for the first one of the frequently used applications. if (defaultAppDesktopFile.isEmpty()) { QStringList recommendApps = m_mimeAppManager->getRecommendedAppsByMimeType(item.currentMimeType()); if (recommendApps.count() > 0) { defaultAppDesktopFile = recommendApps.first(); } } if (!defaultAppDesktopFile.isEmpty()) { FileLauncher::self()->launchApp(defaultAppDesktopFile, url.toLocalFile()); continue; } QDesktopServices::openUrl(url); } } void FolderModel::showOpenWithDialog() { if (!m_selectionModel->hasSelection()) return; const QList urls = selectedUrls(); OpenWithDialog *dlg = new OpenWithDialog(urls.first()); dlg->show(); } void FolderModel::deleteSelected() { if (!m_selectionModel->hasSelection()) { return; } if (QAction *action = m_actionCollection.action(QStringLiteral("del"))) { if (!action->isEnabled()) { return; } } KIO::DeleteJob *job = KIO::del(selectedUrls()); job->start(); } void FolderModel::moveSelectedToTrash() { if (!m_selectionModel->hasSelection()) { return; } if (QAction *action = m_actionCollection.action(QStringLiteral("trash"))) { if (!action->isEnabled()) { return; } } const QList urls = selectedUrls(); KIO::JobUiDelegate uiDelegate; if (uiDelegate.askDeleteConfirmation(urls, KIO::JobUiDelegate::Trash, KIO::JobUiDelegate::DefaultConfirmation)) { KIO::Job *job = KIO::trash(urls); job->uiDelegate()->setAutoErrorHandlingEnabled(true); KIO::FileUndoManager::self()->recordJob(KIO::FileUndoManager::Trash, urls, QUrl(QStringLiteral("trash:/")), job); } } void FolderModel::emptyTrash() { QProcess::startDetached("cutefish-filemanager", QStringList() << "-e"); } void FolderModel::keyDeletePress() { if (!m_selectionModel->hasSelection()) return; resolvedUrl().scheme() == "trash" ? openDeleteDialog() : moveSelectedToTrash(); } void FolderModel::setDragHotSpotScrollOffset(int x, int y) { m_dragHotSpotScrollOffset.setX(x); m_dragHotSpotScrollOffset.setY(y); } void FolderModel::addItemDragImage(int row, int x, int y, int width, int height, const QVariant &image) { if (row < 0) return; delete m_dragImages.take(row); DragImage *dragImage = new DragImage(); dragImage->row = row; dragImage->rect = QRect(x, y, width, height); dragImage->image = image.value(); dragImage->blank = false; m_dragImages.insert(row, dragImage); } void FolderModel::clearDragImages() { qDeleteAll(m_dragImages); m_dragImages.clear(); } void FolderModel::dragSelected(int x, int y) { if (m_dragInProgress) return; m_dragInProgress = true; emit draggingChanged(); QMetaObject::invokeMethod(this, "dragSelectedInternal", Qt::QueuedConnection, Q_ARG(int, x), Q_ARG(int, y)); } void FolderModel::drop(QQuickItem *target, QObject *dropEvent, int row) { QMimeData *mimeData = qobject_cast(dropEvent->property("mimeData").value()); if (!mimeData) { return; } QModelIndex idx; KFileItem item; if (row > -1 && row < rowCount()) { idx = index(row, 0); item = itemForIndex(idx); } QUrl dropTargetUrl; // So we get to run mostLocalUrl() over the current URL. if (item.isNull()) { item = rootItem(); } if (item.isNull()) { dropTargetUrl = m_dirModel->dirLister()->url(); } else { 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; } if (!isDropBetweenSharedViews(mimeData->urls(), dropTargetFolderUrl)) { KIO::Job *job = KIO::move(mimeData->urls(), dropTargetUrl, KIO::HideProgressInfo); job->start(); } } void FolderModel::setWallpaperSelected() { if (!m_selectionModel) return; QUrl url = selectedUrls().first(); if (!url.isLocalFile()) return; QDBusInterface iface("com.cutefish.Settings", "/Theme", "com.cutefish.Theme", QDBusConnection::sessionBus(), nullptr); if (iface.isValid()) iface.call("setWallpaper", url.toLocalFile()); } void FolderModel::openContextMenu(QQuickItem *visualParent, Qt::KeyboardModifiers modifiers) { Q_UNUSED(modifiers); updateActions(); const QModelIndexList indexes = m_selectionModel->selectedIndexes(); QMenu *menu = new QMenu; // Open folder menu. if (indexes.isEmpty()) { QAction *selectAll = new QAction(tr("Select All"), this); connect(selectAll, &QAction::triggered, this, &FolderModel::selectAll); menu->addAction(m_actionCollection.action("newFolder")); menu->addSeparator(); menu->addAction(m_actionCollection.action("paste")); menu->addAction(selectAll); if (m_actionCollection.action("terminal")->isVisible()) { menu->addSeparator(); menu->addAction(m_actionCollection.action("terminal")); } if (m_isDesktop) { menu->addAction(m_actionCollection.action("changeBackground")); } menu->addSeparator(); menu->addAction(m_actionCollection.action("showHidden")); menu->addSeparator(); menu->addAction(m_actionCollection.action("emptyTrash")); menu->addAction(m_actionCollection.action("properties")); } else { // Open the items menu. // Trash items menu->addAction(m_actionCollection.action("restore")); menu->addAction(m_actionCollection.action("open")); menu->addAction(m_actionCollection.action("openInNewWindow")); menu->addAction(m_actionCollection.action("openWith")); menu->addSeparator(); menu->addAction(m_actionCollection.action("cut")); menu->addAction(m_actionCollection.action("copy")); menu->addAction(m_actionCollection.action("trash")); menu->addAction(m_actionCollection.action("del")); menu->addAction(m_actionCollection.action("rename")); if (m_actionCollection.action("terminal")->isVisible()) { menu->addSeparator(); menu->addAction(m_actionCollection.action("terminal")); } menu->addAction(m_actionCollection.action("wallpaper")); menu->addSeparator(); menu->addAction(m_actionCollection.action("properties")); } QPoint position; if (visualParent) { position = visualParent->mapToGlobal(QPointF(0, visualParent->height())).toPoint(); } else { position = QCursor::pos(); } menu->installEventFilter(this); menu->setAttribute(Qt::WA_TranslucentBackground); menu->winId(); menu->popup(position); connect(menu, &QMenu::aboutToHide, [menu]() { menu->deleteLater(); }); } void FolderModel::openPropertiesDialog() { const QModelIndexList indexes = m_selectionModel->selectedIndexes(); if (indexes.isEmpty()) { FilePropertiesDialog *dlg = new FilePropertiesDialog(QUrl::fromLocalFile(url())); dlg->show(); return; } KFileItemList items; items.reserve(indexes.count()); for (const QModelIndex &index : indexes) { KFileItem item = itemForIndex(index); if (!item.isNull()) { items.append(item); } } FilePropertiesDialog *dlg = new FilePropertiesDialog(items); dlg->show(); } void FolderModel::openInTerminal() { QString url; if (m_selectionModel->hasSelection()) { KFileItem item = itemForIndex(m_selectionModel->selectedIndexes().first()); if (item.isDir()) { url = item.url().toLocalFile(); } } else { url = rootItem().url().toLocalFile(); } m_mimeAppManager->launchTerminal(url); } void FolderModel::openChangeWallpaperDialog() { QProcess::startDetached("cutefish-settings", QStringList() << "-m" << "background"); } void FolderModel::openDeleteDialog() { QQuickView *view = new QQuickView; view->setModality(Qt::ApplicationModal); view->setFlags(Qt::Dialog); view->setTitle(tr("File Manager")); view->setResizeMode(QQuickView::SizeRootObjectToView); view->setSource(QUrl("qrc:/qml/Dialogs/DeleteDialog.qml")); view->rootContext()->setContextProperty("model", this); view->rootContext()->setContextProperty("view", view); connect(view, &QQuickView::visibleChanged, this, [=] { if (!view->isVisible()) { view->deleteLater(); } }); view->show(); } void FolderModel::openInNewWindow(const QString &url) { if (!url.isEmpty()) { QProcess::startDetached("cutefish-filemanager", QStringList() << url); return; } // url 为空则打开已选择的 items. if (!m_selectionModel->hasSelection()) return; for (const QModelIndex &index : m_selectionModel->selectedIndexes()) { KFileItem item = itemForIndex(index); if (item.isDir()) { QProcess::startDetached("cutefish-filemanager", QStringList() << item.url().toLocalFile()); } } } void FolderModel::updateSelectedItemsSize() { } void FolderModel::keyboardSearch(const QString &text) { if (rowCount() == 0) return; int index; int currentIndex = -1; if (m_selectionModel->hasSelection()) { currentIndex = m_selectionModel->selectedIndexes().first().row(); } index = indexForKeyboardSearch(text, (currentIndex + 1) % rowCount()); if (index < 0 || currentIndex == index) return; if (index >= 0) { clearSelection(); setSelected(index); emit scrollToItem(index); } } void FolderModel::clearPixmapCache() { QPixmapCache::clear(); } void FolderModel::restoreFromTrash() { if (!m_selectionModel->hasSelection()) return; if (QAction *action = m_actionCollection.action("restore")) if (!action->isVisible()) return; KIO::RestoreJob *job = KIO::restoreFromTrash(selectedUrls()); job->start(); } void FolderModel::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) { QModelIndexList indices = selected.indexes(); indices.append(deselected.indexes()); QVector roles; roles.append(SelectedRole); foreach (const QModelIndex &index, indices) { emit dataChanged(index, index, roles); } if (!m_selectionModel->hasSelection()) { clearDragImages(); } else { foreach (const QModelIndex &idx, deselected.indexes()) { delete m_dragImages.take(idx.row()); } } updateActions(); emit selectionCountChanged(); // The desktop does not need to calculate the selected file size. if (m_isDesktop) return; // Start calculating file size. if (m_sizeJob == nullptr) { m_sizeJob = new CFileSizeJob; connect(m_sizeJob, &CFileSizeJob::sizeChanged, this, [=] { m_selectedItemSize = KIO::convertSize(m_sizeJob->totalSize()); if (!m_selectionModel->hasSelection()) m_selectedItemSize = ""; emit selectedItemSizeChanged(); }); connect(m_sizeJob, &CFileSizeJob::result, this, [=] { m_selectedItemSize = KIO::convertSize(m_sizeJob->totalSize()); if (!m_selectionModel->hasSelection()) m_selectedItemSize = ""; emit selectedItemSizeChanged(); }); } m_sizeJob->stop(); if (!m_selectionModel->hasSelection()) { m_sizeJob->blockSignals(true); m_selectedItemSize = ""; emit selectedItemSizeChanged(); } else { bool fileExists = false; for (const QModelIndex &index : m_selectionModel->selectedIndexes()) { if (itemForIndex(index).isFile()) { fileExists = true; break; } } // Reion: The size label at the bottom needs to be updated // only if you select the include file. if (fileExists) { m_sizeJob->blockSignals(false); m_sizeJob->start(selectedUrls()); } } } void FolderModel::dragSelectedInternal(int x, int y) { if (!m_viewAdapter || !m_selectionModel->hasSelection()) { m_dragInProgress = false; emit draggingChanged(); return; } QQuickItem *item = qobject_cast(m_viewAdapter->adapterView()); QDrag *drag = new QDrag(item); addDragImage(drag, x, y); m_dragIndexes = m_selectionModel->selectedIndexes(); std::sort(m_dragIndexes.begin(), m_dragIndexes.end()); // TODO: Optimize to emit contiguous groups. emit dataChanged(m_dragIndexes.first(), m_dragIndexes.last(), QVector() << BlankRole); QModelIndexList sourceDragIndexes; sourceDragIndexes.reserve(m_dragIndexes.count()); foreach (const QModelIndex &index, m_dragIndexes) { sourceDragIndexes.append(mapToSource(index)); } drag->setMimeData(m_dirModel->mimeData(sourceDragIndexes)); // Due to spring-loading (aka auto-expand), the URL might change // while the drag is in-flight - in that case we don't want to // unnecessarily emit dataChanged() for (possibly invalid) indices // after it ends. const QUrl currentUrl(m_dirModel->dirLister()->url()); item->grabMouse(); drag->exec(supportedDragActions()); item->ungrabMouse(); m_dragInProgress = false; emit draggingChanged(); if (m_dirModel->dirLister()->url() == currentUrl) { const QModelIndex first(m_dragIndexes.first()); const QModelIndex last(m_dragIndexes.last()); m_dragIndexes.clear(); // TODO: Optimize to emit contiguous groups. emit dataChanged(first, last, QVector() << BlankRole); } } bool FolderModel::isSupportThumbnails(const QString &mimeType) const { const QStringList supportsMimetypes = {"image/bmp", "image/png", "image/gif", "image/jpeg", "image/web", /*"application/pdf", "application/rtf", "application/doc", "application/odf", "audio/mpeg", "video/mp4"*/}; if (supportsMimetypes.contains(mimeType)) return true; return false; } bool FolderModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { const KDirModel *dirModel = static_cast(sourceModel()); const KFileItem item = dirModel->itemForIndex(dirModel->index(sourceRow, KDirModel::Name, sourceParent)); if (m_filterMode == NoFilter) { return true; } if (m_filterMode == FilterShowMatches) { return (matchPattern(item) && matchMimeType(item)); } else { return !(matchPattern(item) && matchMimeType(item)); } } bool FolderModel::matchMimeType(const KFileItem &item) const { if (m_mimeSet.isEmpty()) { return false; } if (m_mimeSet.contains(QLatin1String("all/all")) || m_mimeSet.contains(QLatin1String("all/allfiles"))) { return true; } const QString mimeType = item.determineMimeType().name(); return m_mimeSet.contains(mimeType); } bool FolderModel::matchPattern(const KFileItem &item) const { if (m_filterPatternMatchAll) { return true; } const QString name = item.name(); QListIterator i(m_regExps); while (i.hasNext()) { if (i.next().exactMatch(name)) { return true; } } return false; } bool FolderModel::showHiddenFiles() const { return m_showHiddenFiles; } void FolderModel::setShowHiddenFiles(bool showHiddenFiles) { if (m_showHiddenFiles != showHiddenFiles) { m_showHiddenFiles = showHiddenFiles; m_dirLister->setShowingDotFiles(m_showHiddenFiles); m_dirLister->emitChanges(); QSettings settings("cutefishos", qApp->applicationName()); settings.setValue("showHiddenFiles", m_showHiddenFiles); emit showHiddenFilesChanged(); } } QString FolderModel::selectedItemSize() const { return m_selectedItemSize; } bool FolderModel::isDesktop() const { return m_isDesktop; } void FolderModel::setIsDesktop(bool isDesktop) { if (m_isDesktop == isDesktop) return; m_isDesktop = isDesktop; emit isDesktopChanged(); } void FolderModel::invalidateIfComplete() { if (!m_complete) return; invalidate(); } void FolderModel::invalidateFilterIfComplete() { if (!m_complete) return; invalidateFilter(); } void FolderModel::createActions() { QAction *open = new QAction(tr("Open"), this); connect(open, &QAction::triggered, this, &FolderModel::openSelected); QAction *openWith = new QAction(tr("Open with"), this); connect(openWith, &QAction::triggered, this, &FolderModel::showOpenWithDialog); QAction *cut = new QAction(tr("Cut"), this); connect(cut, &QAction::triggered, this, &FolderModel::cut); QAction *copy = new QAction(tr("Copy"), this); connect(copy, &QAction::triggered, this, &FolderModel::copy); QAction *paste = new QAction(tr("Paste"), this); connect(paste, &QAction::triggered, this, &FolderModel::paste); QAction *newFolder = new QAction(tr("New Folder"), this); connect(newFolder, &QAction::triggered, this, &FolderModel::newFolder); QAction *trash = new QAction(tr("Move To Trash"), this); connect(trash, &QAction::triggered, this, &FolderModel::moveSelectedToTrash); QAction *emptyTrash = new QAction(tr("Empty Trash"), this); connect(emptyTrash, &QAction::triggered, this, &FolderModel::emptyTrash); QAction *del = new QAction(tr("Delete"), this); connect(del, &QAction::triggered, this, &FolderModel::openDeleteDialog); QAction *rename = new QAction(tr("Rename"), this); connect(rename, &QAction::triggered, this, &FolderModel::requestRename); QAction *terminal = new QAction(tr("Open in Terminal"), this); connect(terminal, &QAction::triggered, this, &FolderModel::openInTerminal); QAction *wallpaper = new QAction(tr("Set as Wallpaper"), this); connect(wallpaper, &QAction::triggered, this, &FolderModel::setWallpaperSelected); QAction *properties = new QAction(tr("Properties"), this); QObject::connect(properties, &QAction::triggered, this, &FolderModel::openPropertiesDialog); QAction *changeBackground = new QAction(tr("Change background"), this); QObject::connect(changeBackground, &QAction::triggered, this, &FolderModel::openChangeWallpaperDialog); QAction *restore = new QAction(tr("Restore"), this); QObject::connect(restore, &QAction::triggered, this, &FolderModel::restoreFromTrash); QAction *showHidden = new QAction(tr("Show hidden files"), this); QObject::connect(showHidden, &QAction::triggered, this, [=] { setShowHiddenFiles(!m_showHiddenFiles); }); QAction *openInNewWindow = new QAction(tr("Open in new window"), this); QObject::connect(openInNewWindow, &QAction::triggered, this, [=] { this->openInNewWindow(); }); m_actionCollection.addAction(QStringLiteral("open"), open); m_actionCollection.addAction(QStringLiteral("openWith"), openWith); m_actionCollection.addAction(QStringLiteral("cut"), cut); m_actionCollection.addAction(QStringLiteral("copy"), copy); m_actionCollection.addAction(QStringLiteral("paste"), paste); m_actionCollection.addAction(QStringLiteral("newFolder"), newFolder); m_actionCollection.addAction(QStringLiteral("trash"), trash); m_actionCollection.addAction(QStringLiteral("emptyTrash"), emptyTrash); m_actionCollection.addAction(QStringLiteral("del"), del); m_actionCollection.addAction(QStringLiteral("rename"), rename); m_actionCollection.addAction(QStringLiteral("terminal"), terminal); m_actionCollection.addAction(QStringLiteral("wallpaper"), wallpaper); m_actionCollection.addAction(QStringLiteral("properties"), properties); m_actionCollection.addAction(QStringLiteral("changeBackground"), changeBackground); m_actionCollection.addAction(QStringLiteral("restore"), restore); m_actionCollection.addAction(QStringLiteral("showHidden"), showHidden); m_actionCollection.addAction(QStringLiteral("openInNewWindow"), openInNewWindow); } void FolderModel::updateActions() { const QModelIndexList indexes = m_selectionModel->selectedIndexes(); KFileItemList items; QList urls; bool hasRemoteFiles = false; bool isTrashLink = false; bool hasDir = false; const bool isTrash = (resolvedUrl().scheme() == QLatin1String("trash")); if (indexes.isEmpty()) { items << rootItem(); } else { items.reserve(indexes.count()); urls.reserve(indexes.count()); for (const QModelIndex &index : indexes) { KFileItem item = itemForIndex(index); if (!item.isNull()) { hasRemoteFiles |= item.localPath().isEmpty(); items.append(item); urls.append(item.url()); } if (item.isDir()) hasDir = true; } } KFileItemListProperties itemProperties(items); // Check if we're showing the menu for the trash link if (items.count() == 1 && items.at(0).isDesktopFile()) { KDesktopFile file(items.at(0).localPath()); if (file.hasLinkType() && file.readUrl() == QLatin1String("trash:/")) { isTrashLink = true; } } if (QAction *openAction = m_actionCollection.action(QStringLiteral("open"))) { openAction->setVisible(!isTrash); } if (QAction *copyAction = m_actionCollection.action(QStringLiteral("copy"))) { copyAction->setVisible(!isTrash); } if (QAction *cutAction = m_actionCollection.action(QStringLiteral("cut"))) { cutAction->setVisible(!isTrash); } if (QAction *restoreAction = m_actionCollection.action(QStringLiteral("restore"))) { restoreAction->setVisible(items.count() >= 1 && isTrash); } if (QAction *openWith = m_actionCollection.action(QStringLiteral("openWith"))) { openWith->setVisible(items.count() == 1 && !isTrash); } if (QAction *newFolder = m_actionCollection.action(QStringLiteral("newFolder"))) { newFolder->setVisible(!isTrash); newFolder->setEnabled(rootItem().isWritable()); } if (QAction *paste = m_actionCollection.action(QStringLiteral("paste"))) { bool enable = false; const QMimeData *mimeData = QApplication::clipboard()->mimeData(); QList urls = KUrlMimeData::urlsFromMimeData(mimeData); if (!urls.isEmpty()) { if (!rootItem().isNull()) { enable = rootItem().isWritable(); } } paste->setEnabled(enable); } if (QAction *rename = m_actionCollection.action(QStringLiteral("rename"))) { rename->setEnabled(itemProperties.supportsMoving()); rename->setVisible(!isTrash); } if (QAction *trash = m_actionCollection.action("trash")) { trash->setEnabled(itemProperties.supportsMoving()); trash->setVisible(!isTrash); } if (QAction *emptyTrash = m_actionCollection.action("emptyTrash")) { emptyTrash->setVisible(isTrash); } if (QAction *del = m_actionCollection.action(QStringLiteral("del"))) { del->setVisible(isTrash && itemProperties.supportsDeleting()); } if (QAction *terminal = m_actionCollection.action("terminal")) { terminal->setVisible(items.size() == 1 && items.first().isDir() && !isTrash); } if (QAction *terminal = m_actionCollection.action("wallpaper")) { terminal->setVisible(items.size() == 1 && supportSetAsWallpaper(items.first().mimetype())); } if (QAction *properties = m_actionCollection.action("properties")) { properties->setVisible(!isTrash); } if (QAction *showHidden = m_actionCollection.action("showHidden")) { showHidden->setCheckable(true); showHidden->setChecked(m_showHiddenFiles); } if (QAction *openInNewWindow = m_actionCollection.action("openInNewWindow")) { openInNewWindow->setVisible(hasDir); } } void FolderModel::addDragImage(QDrag *drag, int x, int y) { if (!drag || m_dragImages.isEmpty()) return; QRegion region; foreach (DragImage *image, m_dragImages) { image->blank = isBlank(image->row); image->rect.translate(-m_dragHotSpotScrollOffset.x(), -m_dragHotSpotScrollOffset.y()); if (!image->blank && !image->image.isNull()) { region = region.united(image->rect); } } QRect rect = region.boundingRect(); QPoint offset = rect.topLeft(); rect.translate(-offset.x(), -offset.y()); QImage dragImage(rect.size(), QImage::Format_RGBA8888); dragImage.fill(Qt::transparent); QPainter painter(&dragImage); QPoint pos; foreach (DragImage *image, m_dragImages) { if (!image->blank && !image->image.isNull()) { pos = image->rect.translated(-offset.x(), -offset.y()).topLeft(); image->cursorOffset.setX(pos.x() - (x - offset.x())); image->cursorOffset.setY(pos.y() - (y - offset.y())); painter.drawImage(pos, image->image); } // FIXME HACK: Operate on copy. image->rect.translate(m_dragHotSpotScrollOffset.x(), m_dragHotSpotScrollOffset.y()); } drag->setPixmap(QPixmap::fromImage(dragImage)); drag->setHotSpot(QPoint(x - offset.x(), y - offset.y())); }