File indexing completed on 2024-04-28 17:06:20

0001 /*
0002     SPDX-FileCopyrightText: 2010 Jan Lepper <dehtris@yahoo.de>
0003     SPDX-FileCopyrightText: 2010-2022 Krusader Krew <https://krusader.org>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 #include "krfiletreeview.h"
0008 
0009 #include "panelfunc.h"
0010 
0011 #include "../FileSystem/filesystemprovider.h"
0012 #include "../compat.h"
0013 #include "../defaults.h"
0014 #include "../icon.h"
0015 #include "../krglobal.h"
0016 
0017 #include <QAction>
0018 #include <QApplication>
0019 #include <QCursor>
0020 #include <QDir>
0021 #include <QDropEvent>
0022 #include <QHeaderView>
0023 #include <QMenu>
0024 #include <QMimeData>
0025 #include <QProxyStyle>
0026 
0027 #include <KFileItemListProperties>
0028 #include <KJobWidgets>
0029 #include <KUrlMimeData>
0030 
0031 #include <KI18n/KLocalizedString>
0032 #include <KIO/DropJob>
0033 #include <KIO/Paste>
0034 #include <KIO/PasteJob>
0035 #include <KIOCore/KFileItem>
0036 #include <KIOWidgets/KDirLister>
0037 #include <KIOWidgets/KFileItemDelegate>
0038 #include <KIOWidgets/KPropertiesDialog>
0039 
0040 class KrDirModel : public KDirModel
0041 {
0042 public:
0043     KrDirModel(QWidget *parent)
0044         : KDirModel(parent)
0045     {
0046     }
0047 
0048 protected:
0049     Qt::ItemFlags flags(const QModelIndex &index) const override
0050     {
0051         Qt::ItemFlags itflags = KDirModel::flags(index);
0052         if (index.column() != KDirModel::Name)
0053             itflags &= ~Qt::ItemIsDropEnabled;
0054         return itflags;
0055     }
0056 };
0057 
0058 class TreeStyle : public QProxyStyle
0059 {
0060 public:
0061     explicit TreeStyle(QStyle *style)
0062         : QProxyStyle(style)
0063     {
0064     }
0065 
0066     int styleHint(StyleHint hint, const QStyleOption *option, const QWidget *widget, QStyleHintReturn *returnData) const override
0067     {
0068         if (hint == QStyle::SH_ItemView_ActivateItemOnSingleClick) {
0069             return true;
0070         }
0071 
0072         return QProxyStyle::styleHint(hint, option, widget, returnData);
0073     }
0074 };
0075 
0076 KrFileTreeView::KrFileTreeView(QWidget *parent)
0077     : QTreeView(parent)
0078     , mStartTreeFromCurrent(false)
0079     , mStartTreeFromPlace(true)
0080 {
0081     mSourceModel = new KrDirModel(this);
0082     mSourceModel->dirLister()->setDirOnlyMode(true);
0083 
0084     mProxyModel = new KDirSortFilterProxyModel(this);
0085     mProxyModel->setSourceModel(mSourceModel);
0086     setModel(mProxyModel);
0087 
0088     mFilePlacesModel = new KFilePlacesModel(this);
0089 
0090     setItemDelegate(new KFileItemDelegate(this));
0091     setUniformRowHeights(true);
0092 
0093     // drag&drop
0094     setAcceptDrops(true);
0095     setDragEnabled(true);
0096     setDropIndicatorShown(true);
0097     mSourceModel->setDropsAllowed(KDirModel::DropOnDirectory);
0098 
0099     setStyle(new TreeStyle(style()));
0100     connect(this, &KrFileTreeView::activated, this, &KrFileTreeView::slotActivated);
0101 
0102     connect(mSourceModel, &KDirModel::expand, this, &KrFileTreeView::slotExpanded);
0103 
0104     QFontMetrics fontMetrics(viewport()->font());
0105     header()->resizeSection(KDirModel::Name, fontMetrics.horizontalAdvance("WWWWWWWWWWWWWWW"));
0106 
0107     header()->setContextMenuPolicy(Qt::CustomContextMenu);
0108     connect(header(), &QHeaderView::customContextMenuRequested, this, &KrFileTreeView::showHeaderContextMenu);
0109 
0110     setBriefMode(true);
0111 
0112     setContextMenuPolicy(Qt::CustomContextMenu);
0113     connect(this, &KrFileTreeView::customContextMenuRequested, this, &KrFileTreeView::slotCustomContextMenuRequested);
0114 
0115     setTree(mStartTreeFromCurrent, mStartTreeFromPlace);
0116 }
0117 
0118 void KrFileTreeView::setCurrentUrl(const QUrl &url)
0119 {
0120     mCurrentUrl = url;
0121     if (mStartTreeFromCurrent) {
0122         setTreeRoot(url);
0123     } else {
0124         if (mStartTreeFromPlace) {
0125             const QModelIndex index = mFilePlacesModel->closestItem(url); // magic here
0126             const QUrl rootBase = index.isValid() ? mFilePlacesModel->url(index) : QUrl::fromLocalFile(QDir::root().path());
0127             setTreeRoot(rootBase);
0128         }
0129         if (isVisible(url)) {
0130             // avoid unwanted scrolling by KDirModel::expandToUrl()
0131             setCurrentIndex(mProxyModel->mapFromSource(mSourceModel->indexForUrl(url)));
0132         } else {
0133             mSourceModel->expandToUrl(url);
0134         }
0135     }
0136 }
0137 
0138 QUrl KrFileTreeView::urlForProxyIndex(const QModelIndex &index) const
0139 {
0140     const KFileItem item = mSourceModel->itemForIndex(mProxyModel->mapToSource(index));
0141 
0142     return !item.isNull() ? item.url() : QUrl();
0143 }
0144 
0145 void KrFileTreeView::slotActivated(const QModelIndex &index)
0146 {
0147     const QUrl url = urlForProxyIndex(index);
0148     if (url.isValid()) {
0149         emit urlActivated(url);
0150     }
0151 }
0152 
0153 void KrFileTreeView::dropEvent(QDropEvent *event)
0154 {
0155     QUrl destination = urlForProxyIndex(indexAt(event->pos()));
0156     if (destination.isEmpty()) {
0157         return;
0158     }
0159 
0160     FileSystemProvider::instance().startDropFiles(event, destination);
0161 }
0162 
0163 void KrFileTreeView::slotExpanded(const QModelIndex &baseIndex)
0164 {
0165     const QModelIndex index = mProxyModel->mapFromSource(baseIndex);
0166 
0167     expand(index); // expand view now after model was expanded
0168     selectionModel()->clearSelection();
0169     selectionModel()->setCurrentIndex(index, QItemSelectionModel::SelectCurrent);
0170 
0171     scrollTo(index);
0172 }
0173 
0174 void KrFileTreeView::showHeaderContextMenu()
0175 {
0176     QMenu popup(this);
0177     popup.setToolTipsVisible(true);
0178 
0179     QAction *detailAction = popup.addAction(i18n("Show Details"));
0180     detailAction->setCheckable(true);
0181     detailAction->setChecked(!briefMode());
0182     detailAction->setToolTip(i18n("Show columns with details"));
0183     QAction *showHiddenAction = popup.addAction(i18n("Show Hidden Folders"));
0184     showHiddenAction->setCheckable(true);
0185     showHiddenAction->setChecked(mSourceModel->dirLister()->showingDotFiles());
0186     showHiddenAction->setToolTip(i18n("Show folders starting with a dot"));
0187 
0188     popup.addSeparator();
0189     auto *rootActionGroup = new QActionGroup(this);
0190 
0191     QAction *startFromRootAction = popup.addAction(i18n("Start From Root"));
0192     startFromRootAction->setCheckable(true);
0193     startFromRootAction->setChecked(!mStartTreeFromCurrent && !mStartTreeFromPlace);
0194     startFromRootAction->setToolTip(i18n("Set root of the tree to root of filesystem"));
0195     startFromRootAction->setActionGroup(rootActionGroup);
0196 
0197     QAction *startFromCurrentAction = popup.addAction(i18n("Start From Current"));
0198     startFromCurrentAction->setCheckable(true);
0199     startFromCurrentAction->setChecked(mStartTreeFromCurrent);
0200     startFromCurrentAction->setToolTip(i18n("Set root of the tree to the current folder"));
0201     startFromCurrentAction->setActionGroup(rootActionGroup);
0202 
0203     QAction *startFromPlaceAction = popup.addAction(i18n("Start From Place"));
0204     startFromPlaceAction->setCheckable(true);
0205     startFromPlaceAction->setChecked(mStartTreeFromPlace);
0206     startFromPlaceAction->setToolTip(i18n("Set root of the tree to closest folder listed in 'Places'"));
0207     startFromPlaceAction->setActionGroup(rootActionGroup);
0208 
0209     QAction *triggeredAction = popup.exec(QCursor::pos());
0210     if (triggeredAction == detailAction) {
0211         setBriefMode(!detailAction->isChecked());
0212     } else if (triggeredAction == showHiddenAction) {
0213         KDirLister *dirLister = mSourceModel->dirLister();
0214         dirLister->setShowingDotFiles(showHiddenAction->isChecked());
0215         dirLister->emitChanges();
0216     } else if (triggeredAction && triggeredAction->actionGroup() == rootActionGroup) {
0217         setTree(startFromCurrentAction->isChecked(), startFromPlaceAction->isChecked());
0218     }
0219 }
0220 
0221 void KrFileTreeView::slotCustomContextMenuRequested(const QPoint &point)
0222 {
0223     const QModelIndex index = indexAt(point);
0224     if (!index.isValid())
0225         return;
0226 
0227     const KFileItem fileItem = mSourceModel->itemForIndex(mProxyModel->mapToSource(index));
0228     const KFileItemListProperties capabilities(KFileItemList() << fileItem);
0229 
0230     auto *popup = new QMenu(this);
0231 
0232     // TODO nice to have: "open with"
0233 
0234     // cut/copy/paste
0235     QAction *cutAction = new QAction(Icon(QStringLiteral("edit-cut")), i18nc("@action:inmenu", "Cut"), this);
0236     cutAction->setEnabled(capabilities.supportsMoving());
0237     connect(cutAction, &QAction::triggered, this, [=]() {
0238         copyToClipBoard(fileItem, true);
0239     });
0240     popup->addAction(cutAction);
0241 
0242     QAction *copyAction = new QAction(Icon(QStringLiteral("edit-copy")), i18nc("@action:inmenu", "Copy"), this);
0243     connect(copyAction, &QAction::triggered, this, [=]() {
0244         copyToClipBoard(fileItem, false);
0245     });
0246     popup->addAction(copyAction);
0247 
0248     const QMimeData *mimeData = QApplication::clipboard()->mimeData();
0249     bool canPaste;
0250     const QString text = KIO::pasteActionText(mimeData, &canPaste, fileItem);
0251     QAction *pasteAction = new QAction(Icon(QStringLiteral("edit-paste")), text, this);
0252     connect(pasteAction, &QAction::triggered, this, [=]() {
0253         KIO::PasteJob *job = KIO::paste(QApplication::clipboard()->mimeData(), fileItem.url());
0254         KJobWidgets::setWindow(job, this);
0255     });
0256     pasteAction->setEnabled(canPaste);
0257     popup->addAction(pasteAction);
0258 
0259     popup->addSeparator();
0260 
0261     // TODO nice to have: rename
0262 
0263     // trash
0264     if (KConfigGroup(krConfig, "General").readEntry("Move To Trash", _MoveToTrash)) {
0265         QAction *moveToTrashAction = new QAction(Icon(QStringLiteral("user-trash")), i18nc("@action:inmenu", "Move to Trash"), this);
0266         const bool enableMoveToTrash = capabilities.isLocal() && capabilities.supportsMoving();
0267         moveToTrashAction->setEnabled(enableMoveToTrash);
0268         connect(moveToTrashAction, &QAction::triggered, this, [=]() {
0269             deleteFile(fileItem, true);
0270         });
0271         popup->addAction(moveToTrashAction);
0272     }
0273 
0274     // delete
0275     QAction *deleteAction = new QAction(Icon(QStringLiteral("edit-delete")), i18nc("@action:inmenu", "Delete"), this);
0276     deleteAction->setEnabled(capabilities.supportsDeleting());
0277     connect(deleteAction, &QAction::triggered, this, [=]() {
0278         deleteFile(fileItem, false);
0279     });
0280     popup->addAction(deleteAction);
0281 
0282     popup->addSeparator();
0283 
0284     // properties
0285     if (!fileItem.isNull()) {
0286         QAction *propertiesAction = new QAction(i18nc("@action:inmenu", "Properties"), this);
0287         propertiesAction->setIcon(Icon(QStringLiteral("document-properties")));
0288         connect(propertiesAction, &QAction::triggered, this, [=]() {
0289             KPropertiesDialog *dialog = new KPropertiesDialog(fileItem.url(), this);
0290             dialog->setAttribute(Qt::WA_DeleteOnClose);
0291             dialog->show();
0292         });
0293         popup->addAction(propertiesAction);
0294     }
0295 
0296     QPointer<QMenu> popupPtr = popup;
0297     popup->exec(QCursor::pos());
0298     if (popupPtr.data()) {
0299         popupPtr.data()->deleteLater();
0300     }
0301 }
0302 
0303 void KrFileTreeView::copyToClipBoard(const KFileItem &fileItem, bool cut) const
0304 {
0305     auto *mimeData = new QMimeData();
0306 
0307     QList<QUrl> kdeUrls;
0308     kdeUrls.append(fileItem.url());
0309     QList<QUrl> mostLocalUrls;
0310     bool dummy;
0311     mostLocalUrls.append(fileItem.mostLocalUrl(&dummy));
0312 
0313     KIO::setClipboardDataCut(mimeData, cut);
0314     KUrlMimeData::setUrls(kdeUrls, mostLocalUrls, mimeData);
0315 
0316     QApplication::clipboard()->setMimeData(mimeData);
0317 }
0318 
0319 void KrFileTreeView::deleteFile(const KFileItem &fileItem, bool moveToTrash) const
0320 {
0321     const QList<QUrl> confirmedFiles = ListPanelFunc::confirmDeletion(QList<QUrl>() << fileItem.url(), moveToTrash, false, true);
0322     if (confirmedFiles.isEmpty())
0323         return;
0324 
0325     FileSystemProvider::instance().startDeleteFiles(confirmedFiles, moveToTrash);
0326 }
0327 
0328 bool KrFileTreeView::briefMode() const
0329 {
0330     return isColumnHidden(mProxyModel->columnCount() - 1); // find out by last column
0331 }
0332 
0333 void KrFileTreeView::setBriefMode(bool brief)
0334 {
0335     for (int i = 1; i < mProxyModel->columnCount(); i++) { // show only first column
0336         setColumnHidden(i, brief);
0337     }
0338 }
0339 
0340 void KrFileTreeView::setTree(bool startFromCurrent, bool startFromPlace)
0341 {
0342     mStartTreeFromCurrent = startFromCurrent;
0343     mStartTreeFromPlace = startFromPlace;
0344 
0345     if (!mStartTreeFromCurrent && !mStartTreeFromPlace) {
0346         setTreeRoot(QUrl::fromLocalFile(QDir::root().path()));
0347     }
0348     setCurrentUrl(mCurrentUrl); // refresh
0349 }
0350 
0351 void KrFileTreeView::setTreeRoot(const QUrl &rootBase)
0352 {
0353     if (rootBase == mCurrentTreeBase) // avoid collapsing the subdirs in tree
0354         return;
0355 
0356     mCurrentTreeBase = rootBase;
0357     mSourceModel->dirLister()->openUrl(mCurrentTreeBase);
0358 }
0359 
0360 void KrFileTreeView::saveSettings(KConfigGroup cfg) const
0361 {
0362     KConfigGroup group = KConfigGroup(&cfg, "TreeView");
0363     group.writeEntry("BriefMode", briefMode());
0364     group.writeEntry("ShowHiddenFolders", mSourceModel->dirLister()->showingDotFiles());
0365     group.writeEntry("StartFromCurrent", mStartTreeFromCurrent);
0366     group.writeEntry("StartFromPlace", mStartTreeFromPlace);
0367 }
0368 
0369 void KrFileTreeView::restoreSettings(const KConfigGroup &cfg)
0370 {
0371     const KConfigGroup group = KConfigGroup(&cfg, "TreeView");
0372     setBriefMode(group.readEntry("BriefMode", true));
0373     mSourceModel->dirLister()->setShowingDotFiles(group.readEntry("ShowHiddenFolders", false));
0374     setTree(group.readEntry("StartFromCurrent", false), group.readEntry("StartFromPlace", false));
0375 }
0376 
0377 bool KrFileTreeView::isVisible(const QUrl &url)
0378 {
0379     QModelIndex index = indexAt(rect().topLeft());
0380     while (index.isValid()) {
0381         if (url == urlForProxyIndex(index)) {
0382             return true;
0383         }
0384         index = indexBelow(index);
0385     }
0386     return false;
0387 }