1 /* This file is part of FSView.
2     SPDX-FileCopyrightText: 2002, 2003 Josef Weidendorfer <Josef.Weidendorfer@gmx.de>
3 
4     Some file management code taken from the Dolphin file manager:
5     SPDX-FileCopyrightText: 2006-2009 Peter Penz <peter.penz19@mail.com>
6 
7     SPDX-License-Identifier: GPL-2.0-only
8 */
9 
10 /*
11  * The KPart embedding the FSView widget
12  */
13 
14 #include "fsview_part.h"
15 
16 #include <QClipboard>
17 #include <QTimer>
18 #include <QStyle>
19 
20 #include <kfileitem.h>
21 #include <kpluginfactory.h>
22 #if KPARTS_VERSION >= QT_VERSION_CHECK(5, 77, 0)
23 #include <KPluginMetaData>
24 #else
25 #include <kaboutdata.h>
26 #endif
27 
28 #include <kprotocolmanager.h>
29 #include <kio/copyjob.h>
30 #include <kio/deletejob.h>
31 #include <kio/paste.h>
32 #include <kmessagebox.h>
33 #include <kactionmenu.h>
34 #include <kactioncollection.h>
35 #include <kpropertiesdialog.h>
36 #include <KMimeTypeEditor>
37 #include <kio/jobuidelegate.h>
38 #include <KIO/FileUndoManager>
39 #include <KJobWidgets>
40 #include <kconfig.h>
41 #include <kconfiggroup.h>
42 #include <ksharedconfig.h>
43 #include <KLocalizedString>
44 #include <KIO/ApplicationLauncherJob>
45 
46 #include <QApplication>
47 #include <QMimeData>
48 
49 #include "fsviewdebug.h"
50 
51 K_PLUGIN_CLASS_WITH_JSON(FSViewPart, "fsview_part.json")
52 
53 // FSJob, for progress
54 
FSJob(FSView * v)55 FSJob::FSJob(FSView *v)
56     : KIO::Job()
57 {
58     _view = v;
59     connect(v, &FSView::progress, this, &FSJob::progressSlot);
60 }
61 
kill(bool)62 void FSJob::kill(bool /*quietly*/)
63 {
64     _view->stop();
65 
66     Job::kill();
67 }
68 
progressSlot(int percent,int dirs,const QString & cDir)69 void FSJob::progressSlot(int percent, int dirs, const QString &cDir)
70 {
71     if (percent < 100) {
72         emitPercent(percent, 100);
73         slotInfoMessage(this, i18np("Read 1 folder, in %2",
74                                     "Read %1 folders, in %2",
75                                     dirs, cDir), QString());
76     } else {
77         slotInfoMessage(this, i18np("1 folder", "%1 folders", dirs), QString());
78     }
79 }
80 
81 // FSViewPart
82 
FSViewPart(QWidget * parentWidget,QObject * parent,const KPluginMetaData & metaData,const QList<QVariant> &)83 FSViewPart::FSViewPart(QWidget *parentWidget,
84                        QObject *parent,
85 #if KPARTS_VERSION >= QT_VERSION_CHECK(5, 77, 0)
86                        const KPluginMetaData& metaData,
87 #endif
88                        const QList<QVariant> & /* args */)
89     : KParts::ReadOnlyPart(parent)
90 {
91 #if KPARTS_VERSION >= QT_VERSION_CHECK(5, 77, 0)
92     setMetaData(metaData);
93 #else
94     KAboutData aboutData(QStringLiteral("fsview"), i18n("FSView"), QStringLiteral("0.1"),
95                          i18n("Filesystem Viewer"),
96                          KAboutLicense::GPL,
97                          i18n("(c) 2002, Josef Weidendorfer"));
98     setComponentData(aboutData);
99 #endif
100 
101     _view = new FSView(new Inode(), parentWidget);
102     _view->setWhatsThis(i18n("<p>This is the FSView plugin, a graphical "
103                              "browsing mode showing filesystem utilization "
104                              "by using a tree map visualization.</p>"
105                              "<p>Note that in this mode, automatic updating "
106                              "when filesystem changes are made "
107                              "is intentionally <b>not</b> done.</p>"
108                              "<p>For details on usage and options available, "
109                              "see the online help under "
110                              "menu 'Help/FSView Manual'.</p>"));
111 
112     _view->show();
113     setWidget(_view);
114 
115     _ext = new FSViewBrowserExtension(this);
116     _job = nullptr;
117 
118     _areaMenu = new KActionMenu(i18n("Stop at Area"),
119                                 actionCollection());
120     actionCollection()->addAction(QStringLiteral("treemap_areadir"), _areaMenu);
121     _depthMenu = new KActionMenu(i18n("Stop at Depth"),
122                                  actionCollection());
123     actionCollection()->addAction(QStringLiteral("treemap_depthdir"), _depthMenu);
124     _visMenu = new KActionMenu(i18n("Visualization"),
125                                actionCollection());
126     actionCollection()->addAction(QStringLiteral("treemap_visdir"), _visMenu);
127 
128     _colorMenu = new KActionMenu(i18n("Color Mode"),
129                                  actionCollection());
130     actionCollection()->addAction(QStringLiteral("treemap_colordir"), _colorMenu);
131 
132     QAction *action;
133     action = actionCollection()->addAction(QStringLiteral("help_fsview"));
134     action->setText(i18n("&FSView Manual"));
135     action->setIcon(QIcon::fromTheme(QStringLiteral("fsview")));
136     action->setToolTip(i18n("Show FSView manual"));
137     action->setWhatsThis(i18n("Opens the help browser with the "
138                               "FSView documentation"));
139     connect(action, &QAction::triggered, this, &FSViewPart::showHelp);
140 
141     connect(_visMenu->menu(), &QMenu::aboutToShow,this, &FSViewPart::slotShowVisMenu);
142     connect(_areaMenu->menu(), &QMenu::aboutToShow, this, &FSViewPart::slotShowAreaMenu);
143     connect(_depthMenu->menu(), &QMenu::aboutToShow, this, &FSViewPart::slotShowDepthMenu);
144     connect(_colorMenu->menu(), &QMenu::aboutToShow, this, &FSViewPart::slotShowColorMenu);
145 
146     // Both of these click signals are connected.  Whether a single or
147     // double click activates an item is checked against the current
148     // style setting when the click happens.
149     connect(_view, &FSView::clicked, _ext, &FSViewBrowserExtension::itemSingleClicked);
150     connect(_view, &FSView::doubleClicked, _ext, &FSViewBrowserExtension::itemDoubleClicked);
151 
152     connect(_view, &TreeMapWidget::returnPressed, _ext, &FSViewBrowserExtension::selected);
153     connect(_view, QOverload<>::of(&TreeMapWidget::selectionChanged), this, &FSViewPart::updateActions);
154     connect(_view, &TreeMapWidget::contextMenuRequested, this, &FSViewPart::contextMenu);
155 
156     connect(_view, &FSView::started, this, &FSViewPart::startedSlot);
157     connect(_view, &FSView::completed, this, &FSViewPart::completedSlot);
158 
159     // Create common file management actions - this is necessary in KDE4
160     // as these common actions are no longer automatically part of KParts.
161     // Much of this is taken from Dolphin.
162     // FIXME: Renaming didn't even seem to work in KDE3! Implement (non-inline) renaming
163     // functionality.
164     //QAction* renameAction = m_actionCollection->addAction("rename");
165     //rename->setText(i18nc("@action:inmenu Edit", "Rename..."));
166     //rename->setShortcut(Qt::Key_F2);
167 
168     QAction *moveToTrashAction = actionCollection()->addAction(QStringLiteral("move_to_trash"));
169     moveToTrashAction->setText(i18nc("@action:inmenu File", "Move to Trash"));
170     moveToTrashAction->setIcon(QIcon::fromTheme(QStringLiteral("user-trash")));
171     actionCollection()->setDefaultShortcut(moveToTrashAction, QKeySequence(QKeySequence::Delete));
172     connect(moveToTrashAction, &QAction::triggered, _ext, &FSViewBrowserExtension::trash);
173 
174     QAction *deleteAction = actionCollection()->addAction(QStringLiteral("delete"));
175     deleteAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete")));
176     deleteAction->setText(i18nc("@action:inmenu File", "Delete"));
177     actionCollection()->setDefaultShortcut(deleteAction, QKeySequence(Qt::SHIFT | Qt::Key_Delete));
178     connect(deleteAction, &QAction::triggered, _ext, &FSViewBrowserExtension::del);
179 
180     QAction *editMimeTypeAction = actionCollection()->addAction(QStringLiteral("editMimeType"));
181     editMimeTypeAction->setText(i18nc("@action:inmenu Edit", "&Edit File Type..."));
182     connect(editMimeTypeAction, &QAction::triggered, _ext, &FSViewBrowserExtension::editMimeType);
183 
184     QAction *propertiesAction = actionCollection()->addAction(QStringLiteral("properties"));
185     propertiesAction->setText(i18nc("@action:inmenu File", "Properties"));
186     propertiesAction->setIcon(QIcon::fromTheme(QStringLiteral("document-properties")));
187     propertiesAction->setShortcut(Qt::ALT | Qt::Key_Return);
188     connect(propertiesAction, &QAction::triggered, this, &FSViewPart::slotProperties);
189 
190     QTimer::singleShot(1, this, SLOT(showInfo()));
191 
192     updateActions();
193 
194     setXMLFile(QStringLiteral("fsview_part.rc"));
195 }
196 
~FSViewPart()197 FSViewPart::~FSViewPart()
198 {
199     qCDebug(FSVIEWLOG);
200 
201     delete _job;
202     _view->saveFSOptions();
203 }
204 
componentName() const205 QString FSViewPart::componentName() const
206 {
207     // also the part ui.rc file is in the program folder
208     // TODO: change the component name to "fsviewpart" by removing this method and
209     // adapting the folder where the file is placed.
210     // Needs a way to also move any potential custom user ui.rc files
211     // from fsview/fsview_part.rc to fsviewpart/fsview_part.rc
212     return QStringLiteral("fsview");
213 }
214 
showInfo()215 void FSViewPart::showInfo()
216 {
217     QString info;
218     info = i18n("FSView intentionally does not support automatic updates "
219                 "when changes are made to files or directories, "
220                 "currently visible in FSView, from the outside.\n"
221                 "For details, see the 'Help/FSView Manual'.");
222 
223     KMessageBox::information(_view, info, QString(), QStringLiteral("ShowFSViewInfo"));
224 }
225 
showHelp()226 void FSViewPart::showHelp()
227 {
228     const KService::Ptr helpCenter = KService::serviceByDesktopName(QStringLiteral("org.kde.help"));
229     auto job = new KIO::ApplicationLauncherJob(helpCenter);
230     job->setUrls({QUrl(QStringLiteral("help:/konqueror/index.html#fsview"))});
231     job->start();
232 }
233 
startedSlot()234 void FSViewPart::startedSlot()
235 {
236     _job = new FSJob(_view);
237     _job->setUiDelegate(new KIO::JobUiDelegate());
238     emit started(_job);
239 }
240 
completedSlot(int dirs)241 void FSViewPart::completedSlot(int dirs)
242 {
243     if (_job) {
244         _job->progressSlot(100, dirs, QString());
245         delete _job;
246         _job = nullptr;
247     }
248 
249     KConfigGroup cconfig = _view->config()->group("MetricCache");
250     _view->saveMetric(&cconfig);
251 
252     emit completed();
253 }
254 
slotShowVisMenu()255 void FSViewPart::slotShowVisMenu()
256 {
257     _visMenu->menu()->clear();
258     _view->addVisualizationItems(_visMenu->menu(), 1301);
259 }
260 
slotShowAreaMenu()261 void FSViewPart::slotShowAreaMenu()
262 {
263     _areaMenu->menu()->clear();
264     _view->addAreaStopItems(_areaMenu->menu(), 1001, nullptr);
265 }
266 
slotShowDepthMenu()267 void FSViewPart::slotShowDepthMenu()
268 {
269     _depthMenu->menu()->clear();
270     _view->addDepthStopItems(_depthMenu->menu(), 1501, nullptr);
271 }
272 
slotShowColorMenu()273 void FSViewPart::slotShowColorMenu()
274 {
275     _colorMenu->menu()->clear();
276     _view->addColorItems(_colorMenu->menu(), 1401);
277 }
278 
openFile()279 bool FSViewPart::openFile() // never called since openUrl is reimplemented
280 {
281     qCDebug(FSVIEWLOG) << localFilePath();
282     _view->setPath(localFilePath());
283 
284     return true;
285 }
286 
openUrl(const QUrl & url)287 bool FSViewPart::openUrl(const QUrl &url)
288 {
289     qCDebug(FSVIEWLOG) << url.path();
290 
291     if (!url.isValid()) {
292         return false;
293     }
294     if (!url.isLocalFile()) {
295         return false;
296     }
297 
298     setUrl(url);
299     emit setWindowCaption(this->url().toDisplayString(QUrl::PreferLocalFile));
300 
301     _view->setPath(this->url().path());
302 
303     return true;
304 }
305 
closeUrl()306 bool FSViewPart::closeUrl()
307 {
308     qCDebug(FSVIEWLOG);
309 
310     _view->stop();
311 
312     return true;
313 }
314 
setNonStandardActionEnabled(const char * actionName,bool enabled)315 void FSViewPart::setNonStandardActionEnabled(const char *actionName, bool enabled)
316 {
317     QAction *action = actionCollection()->action(actionName);
318     action->setEnabled(enabled);
319 }
320 
updateActions()321 void FSViewPart::updateActions()
322 {
323     int canDel = 0, canCopy = 0, canMove = 0;
324 
325     const auto selectedItems = _view->selection();
326     for (TreeMapItem *item : selectedItems) {
327         Inode *inode = static_cast<Inode *>(item);
328         const QUrl u = QUrl::fromLocalFile(inode->path());
329         canCopy++;
330         if (KProtocolManager::supportsDeleting(u)) {
331             canDel++;
332         }
333         if (KProtocolManager::supportsMoving(u)) {
334             canMove++;
335         }
336     }
337 
338     // Standard KBrowserExtension actions.
339     emit _ext->enableAction("copy", canCopy > 0);
340     emit _ext->enableAction("cut", canMove > 0);
341     // Custom actions.
342     //setNonStandardActionEnabled("rename", canMove > 0 ); // FIXME
343     setNonStandardActionEnabled("move_to_trash", (canDel > 0 && canMove > 0));
344     setNonStandardActionEnabled("delete", canDel > 0);
345     setNonStandardActionEnabled("editMimeType", _view->selection().count() == 1);
346     setNonStandardActionEnabled("properties", _view->selection().count() == 1);
347 
348     const KFileItemList items = selectedFileItems();
349     emit _ext->selectionInfo(items);
350 
351     if (canCopy > 0) {
352         stateChanged(QStringLiteral("has_selection"));
353     } else {
354         stateChanged(QStringLiteral("has_no_selection"));
355     }
356 
357     qCDebug(FSVIEWLOG) << "deletable" << canDel;
358 }
359 
selectedFileItems() const360 KFileItemList FSViewPart::selectedFileItems() const
361 {
362     const auto selectedItems = _view->selection();
363     KFileItemList items;
364     items.reserve(selectedItems.count());
365     for (TreeMapItem *item : selectedItems) {
366         Inode *inode = static_cast<Inode *>(item);
367         const QUrl u = QUrl::fromLocalFile(inode->path());
368         const QString mimetype = inode->mimeType().name();
369         const QFileInfo &info = inode->fileInfo();
370         mode_t mode =
371             info.isFile() ? S_IFREG :
372             info.isDir() ? S_IFDIR :
373             info.isSymLink() ? S_IFLNK : (mode_t) - 1;
374         items.append(KFileItem(u, mimetype, mode));
375      }
376      return items;
377 }
378 
contextMenu(TreeMapItem *,const QPoint & p)379 void FSViewPart::contextMenu(TreeMapItem * /*item*/, const QPoint &p)
380 {
381     int canDel = 0, canCopy = 0, canMove = 0;
382 
383     const auto selectedItems = _view->selection();
384     for (TreeMapItem *item : selectedItems) {
385         Inode *inode = static_cast<Inode *>(item);
386         const QUrl u = QUrl::fromLocalFile(inode->path());
387 
388         canCopy++;
389         if (KProtocolManager::supportsDeleting(u)) {
390             canDel++;
391         }
392         if (KProtocolManager::supportsMoving(u)) {
393             canMove++;
394         }
395     }
396 
397     QList<QAction *> editActions;
398     KParts::BrowserExtension::ActionGroupMap actionGroups;
399     KParts::BrowserExtension::PopupFlags flags = KParts::BrowserExtension::ShowUrlOperations |
400             KParts::BrowserExtension::ShowProperties;
401 
402     bool addTrash = (canMove > 0);
403     bool addDel = false;
404     if (canDel == 0) {
405         flags |= KParts::BrowserExtension::NoDeletion;
406     } else {
407         if (!url().isLocalFile()) {
408             addDel = true;
409         } else if (QApplication::keyboardModifiers() & Qt::ShiftModifier) {
410             addTrash = false;
411             addDel = true;
412         } else {
413             KSharedConfig::Ptr globalConfig = KSharedConfig::openConfig(QStringLiteral("kdeglobals"), KConfig::IncludeGlobals);
414             KConfigGroup configGroup(globalConfig, "KDE");
415             addDel = configGroup.readEntry("ShowDeleteCommand", false);
416         }
417     }
418 
419     if (addTrash) {
420         editActions.append(actionCollection()->action(QStringLiteral("move_to_trash")));
421     }
422     if (addDel) {
423         editActions.append(actionCollection()->action(QStringLiteral("delete")));
424     }
425 
426 // FIXME: rename is currently unavailable. Requires popup renaming.
427 //     if (canMove)
428 //       editActions.append(actionCollection()->action("rename"));
429     actionGroups.insert(QStringLiteral("editactions"), editActions);
430 
431     const KFileItemList items = selectedFileItems();
432     if (items.count() > 0)
433         emit _ext->popupMenu(_view->mapToGlobal(p), items,
434                              KParts::OpenUrlArguments(),
435                              KParts::BrowserArguments(),
436                              flags,
437                              actionGroups);
438 }
439 
slotProperties()440 void FSViewPart::slotProperties()
441 {
442     QList<QUrl> urlList;
443     if (view()) {
444         urlList = view()->selectedUrls();
445     }
446 
447     if (!urlList.isEmpty()) {
448         KPropertiesDialog::showDialog(urlList.first(), view());
449     }
450 }
451 
452 // FSViewBrowserExtension
453 
FSViewBrowserExtension(FSViewPart * viewPart)454 FSViewBrowserExtension::FSViewBrowserExtension(FSViewPart *viewPart)
455     : KParts::BrowserExtension(viewPart)
456 {
457     _view = viewPart->view();
458 }
459 
~FSViewBrowserExtension()460 FSViewBrowserExtension::~FSViewBrowserExtension()
461 {}
462 
del()463 void FSViewBrowserExtension::del()
464 {
465     const QList<QUrl> urls = _view->selectedUrls();
466     KIO::JobUiDelegate uiDelegate;
467     uiDelegate.setWindow(_view);
468     if (uiDelegate.askDeleteConfirmation(urls,
469                                          KIO::JobUiDelegate::Delete, KIO::JobUiDelegate::DefaultConfirmation)) {
470         KIO::Job *job = KIO::del(urls);
471         KJobWidgets::setWindow(job, _view);
472         job->uiDelegate()->setAutoErrorHandlingEnabled(true);
473         connect(job, &KJob::result, this, &FSViewBrowserExtension::refresh);
474     }
475 }
476 
trash()477 void FSViewBrowserExtension::trash()
478 {
479     bool deleteNotTrash = ((QGuiApplication::keyboardModifiers() & Qt::ShiftModifier) != 0);
480     if (deleteNotTrash) {
481         del();
482     } else {
483         KIO::JobUiDelegate uiDelegate;
484         uiDelegate.setWindow(_view);
485         const QList<QUrl> urls = _view->selectedUrls();
486         if (uiDelegate.askDeleteConfirmation(urls,
487                                              KIO::JobUiDelegate::Trash, KIO::JobUiDelegate::DefaultConfirmation)) {
488             KIO::Job *job = KIO::trash(urls);
489             KIO::FileUndoManager::self()->recordJob(KIO::FileUndoManager::Trash, urls, QUrl("trash:/"), job);
490             KJobWidgets::setWindow(job, _view);
491             job->uiDelegate()->setAutoErrorHandlingEnabled(true);
492             connect(job, &KJob::result, this, &FSViewBrowserExtension::refresh);
493         }
494     }
495 }
496 
copySelection(bool move)497 void FSViewBrowserExtension::copySelection(bool move)
498 {
499     QMimeData *data = new QMimeData;
500     data->setUrls(_view->selectedUrls());
501     KIO::setClipboardDataCut(data, move);
502     QApplication::clipboard()->setMimeData(data);
503 }
504 
editMimeType()505 void FSViewBrowserExtension::editMimeType()
506 {
507     Inode *i = (Inode *) _view->selection().first();
508     if (i) {
509         KMimeTypeEditor::editMimeType(i->mimeType().name(), _view);
510     }
511 }
512 
513 // refresh treemap at end of KIO jobs
refresh()514 void FSViewBrowserExtension::refresh()
515 {
516     // only need to refresh common ancestor for all selected items
517     TreeMapItem *commonParent = _view->selection().commonParent();
518     if (!commonParent) {
519         return;
520     }
521 
522     /* if commonParent is a file, update parent directory */
523     if (!((Inode *)commonParent)->isDir()) {
524         commonParent = commonParent->parent();
525         if (!commonParent) {
526             return;
527         }
528     }
529 
530     qCDebug(FSVIEWLOG) << "refreshing"
531                        << ((Inode *)commonParent)->path();
532 
533     _view->requestUpdate((Inode *)commonParent);
534 }
535 
itemSingleClicked(TreeMapItem * i)536 void FSViewBrowserExtension::itemSingleClicked(TreeMapItem *i)
537 {
538     if (_view->style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick)) {
539         selected(i);
540     }
541 }
542 
543 
itemDoubleClicked(TreeMapItem * i)544 void FSViewBrowserExtension::itemDoubleClicked(TreeMapItem *i)
545 {
546     if (!_view->style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick)) {
547         selected(i);
548     }
549 }
550 
selected(TreeMapItem * i)551 void FSViewBrowserExtension::selected(TreeMapItem *i)
552 {
553     if (!i) {
554         return;
555     }
556 
557     QUrl url = QUrl::fromLocalFile(((Inode *)i)->path());
558     emit openUrlRequest(url);
559 }
560 
561 #include "fsview_part.moc"
562