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 }