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

0001 /*
0002     SPDX-FileCopyrightText: 2003 Shie Erlich <erlich@users.sourceforge.net>
0003     SPDX-FileCopyrightText: 2003 Rafi Yanai <yanai@users.sourceforge.net>
0004     SPDX-FileCopyrightText: 2004-2022 Krusader Krew <https://krusader.org>
0005 
0006     SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008 
0009 #include "panelcontextmenu.h"
0010 
0011 // QtGui
0012 #include <QPixmap>
0013 
0014 #include <KCoreAddons/KPluginMetaData>
0015 #include <KCoreAddons/KProcess>
0016 #include <KI18n/KLocalizedString>
0017 #include <KIOCore/KFileItem>
0018 #include <KIOCore/KFileItemListProperties>
0019 #include <KIOWidgets/KAbstractFileItemActionPlugin>
0020 #include <KIOWidgets/KRun>
0021 #include <KService/KApplicationTrader>
0022 #include <KWidgetsAddons/KMessageBox>
0023 #include <KXmlGui/KActionCollection>
0024 #include <kio_version.h>
0025 #include <kservice_version.h>
0026 
0027 #include "../Archive/krarchandler.h"
0028 #include "../FileSystem/fileitem.h"
0029 #include "../FileSystem/filesystem.h"
0030 #include "../FileSystem/krtrashhandler.h"
0031 #include "../MountMan/kmountman.h"
0032 #include "../UserAction/useractionpopupmenu.h"
0033 #include "../defaults.h"
0034 #include "../icon.h"
0035 #include "../krservices.h"
0036 #include "../krslots.h"
0037 #include "../krusader.h"
0038 #include "../krusaderview.h"
0039 #include "../panelmanager.h"
0040 #include "PanelView/krview.h"
0041 #include "PanelView/krviewitem.h"
0042 #include "krpreviewpopup.h"
0043 #include "listpanel.h"
0044 #include "listpanelactions.h"
0045 #include "panelfunc.h"
0046 
0047 PanelContextMenu *PanelContextMenu::run(const QPoint &pos, KrPanel *panel)
0048 {
0049     auto menu = new PanelContextMenu(panel);
0050     QAction *res = menu->exec(pos);
0051     int result = res && res->data().canConvert<int>() ? res->data().toInt() : -1;
0052     menu->performAction(result);
0053     return menu;
0054 }
0055 
0056 /**
0057  * Copied from dolphin/src/dolphincontextmenu.cpp and modified to add only compress and extract submenus.
0058  */
0059 void PanelContextMenu::addCompressAndExtractPluginActions()
0060 {
0061     KFileItemListProperties props(_items);
0062 
0063     QVector<KPluginMetaData> jsonPlugins = KPluginLoader::findPlugins("kf5/kfileitemaction", [=](const KPluginMetaData &metaData) {
0064         return metaData.pluginId() == "compressfileitemaction" || metaData.pluginId() == "extractfileitemaction";
0065     });
0066 
0067     foreach (const KPluginMetaData &jsonMetadata, jsonPlugins) {
0068         auto *abstractPlugin = KPluginLoader(jsonMetadata.fileName()).factory()->create<KAbstractFileItemActionPlugin>();
0069         if (abstractPlugin) {
0070             abstractPlugin->setParent(this);
0071             addActions(abstractPlugin->actions(props, this));
0072         }
0073     }
0074 }
0075 
0076 PanelContextMenu::PanelContextMenu(KrPanel *krPanel, QWidget *parent)
0077     : QMenu(parent)
0078     , panel(krPanel)
0079 {
0080     // selected file names
0081     const QStringList fileNames = panel->gui->getSelectedNames();
0082 
0083     // file items
0084     QList<FileItem *> files;
0085     for (const QString &fileName : fileNames) {
0086         files.append(panel->func->files()->getFileItem(fileName));
0087     }
0088 
0089     // KFileItems
0090     bool allFilesAreDirs = true;
0091     for (FileItem *file : files) {
0092         _items.append(KFileItem(file->getUrl(), file->getMime(), file->getMode()));
0093         allFilesAreDirs &= file->isDir();
0094     }
0095 
0096     if (files.empty()) {
0097         addCreateNewMenu();
0098         addSeparator();
0099         addEmptyMenuEntries();
0100         return;
0101     }
0102 
0103     const bool multipleSelections = files.size() > 1;
0104 
0105     QSet<QString> protocols;
0106     for (FileItem *file : files) {
0107         protocols.insert(file->getUrl().scheme());
0108     }
0109     const bool inTrash = protocols.contains("trash");
0110     const bool trashOnly = inTrash && protocols.count() == 1;
0111 
0112     FileItem *file = files.first();
0113 
0114     // ------------ the OPEN/BROWSE option - open preferred service
0115 
0116     // open/run - if not only multiple dirs are selected
0117     if (!(multipleSelections && allFilesAreDirs)) {
0118         QAction *openAction = new QAction(this);
0119         openAction->setData(QVariant(OPEN_ID));
0120         if (multipleSelections) {
0121             openAction->setText(i18n("Open/Run Files"));
0122         } else {
0123             openAction->setText(file->isExecutable() && !file->isDir() ? i18n("Run") : i18n("Open"));
0124             const KrViewItemList viewItems = panel->view->getSelectedKrViewItems();
0125             openAction->setIcon(viewItems.first()->icon());
0126         }
0127         addAction(openAction);
0128     }
0129 
0130     // open in a new tab (if only folder(s) are selected)
0131     if (allFilesAreDirs) {
0132         QAction *openTab = addAction(multipleSelections ? i18n("Open in New Tabs") : i18n("Open in New Tab"));
0133         openTab->setData(QVariant(OPEN_TAB_ID));
0134         openTab->setIcon(Icon("tab-new"));
0135     }
0136 
0137     // browse archive - if one file is selected and the file can be browsed as archive...
0138     if (!multipleSelections
0139         && !panel->func->browsableArchivePath(file->getName()).isEmpty()
0140         // ...but user disabled archive browsing...
0141         && (!KConfigGroup(krConfig, "Archives").readEntry("ArchivesAsDirectories", _ArchivesAsDirectories)
0142             // ...or the file is not a standard archive (e.g. odt, docx, etc.)...
0143             || !KrArcHandler::arcSupported(file->getMime()))) {
0144         // ...it will not be browsed as a directory by default, but add an option for it
0145         QAction *browseAct = addAction(i18n("Browse Archive"));
0146         browseAct->setData(QVariant(BROWSE_ID));
0147         browseAct->setIcon(Icon("archive-insert-directory"));
0148     }
0149 
0150     // ------------- Preview - local filesystem only ?
0151     if (panel->func->files()->isLocal()) {
0152         // create the preview popup
0153         KrPreviewPopup preview;
0154         preview.setUrls(panel->func->files()->getUrls(fileNames));
0155         QAction *previewAction = addMenu(&preview);
0156         previewAction->setData(QVariant(PREVIEW_ID));
0157         previewAction->setText(i18n("Preview"));
0158         previewAction->setIcon(Icon("document-print-preview"));
0159     }
0160 
0161     // -------------- Open with: try to find-out which apps can open the file
0162     QSet<QString> uniqueMimeTypes;
0163     for (FileItem *file : files)
0164         uniqueMimeTypes.insert(file->getMime());
0165     const QStringList mimeTypes = uniqueMimeTypes.values();
0166 
0167 #if KSERVICE_VERSION >= QT_VERSION_CHECK(5, 83, 0)
0168     offers = mimeTypes.count() == 1 ? KApplicationTrader::queryByMimeType(mimeTypes.first()) : KFileItemActions::associatedApplications(mimeTypes);
0169 #elif KSERVICE_VERSION >= QT_VERSION_CHECK(5, 68, 0)
0170     offers = mimeTypes.count() == 1 ? KApplicationTrader::queryByMimeType(mimeTypes.first()) : KFileItemActions::associatedApplications(mimeTypes, QString());
0171 #else
0172     offers = mimeTypes.count() == 1 ? KMimeTypeTrader::self()->query(mimeTypes.first()) : KFileItemActions::associatedApplications(mimeTypes, QString());
0173 #endif
0174 
0175     if (!offers.isEmpty()) {
0176         auto *openWithMenu = new QMenu(this);
0177         for (int i = 0; i < offers.count(); ++i) {
0178             QExplicitlySharedDataPointer<KService> service = offers[i];
0179             if (service->isValid() && service->isApplication()) {
0180                 openWithMenu->addAction(Icon(service->icon()), service->name())->setData(QVariant(SERVICE_LIST_ID + i));
0181             }
0182         }
0183         openWithMenu->addSeparator();
0184         if (!multipleSelections && file->isDir()) {
0185             openWithMenu->addAction(Icon("utilities-terminal"), i18n("Terminal"))->setData(QVariant(OPEN_TERM_ID));
0186         }
0187         openWithMenu->addAction(i18n("Other..."))->setData(QVariant(CHOOSE_ID));
0188         QAction *openWithAction = addMenu(openWithMenu);
0189         openWithAction->setText(i18n("Open With"));
0190         openWithAction->setIcon(Icon("document-open"));
0191     }
0192 
0193     addSeparator();
0194 
0195     // --------------- user actions
0196     QAction *userAction = new UserActionPopupMenu(file->getUrl(), this);
0197     userAction->setText(i18n("User Actions"));
0198     addAction(userAction);
0199 
0200     // --------------- compress/extract actions
0201     // workaround for Bug 372999: application freezes very long time if many files are selected
0202     if (_items.length() < 1000)
0203         // add compress and extract plugins (if available)
0204         addCompressAndExtractPluginActions();
0205 
0206     // --------------- KDE file item actions
0207     // NOTE: design and usability problem here. Services disabled in kservicemenurc settings won't
0208     // be added to the menu. But Krusader does not provide a way do change these settings (only
0209     // Dolphin does).
0210     auto *fileItemActions = new KFileItemActions(this);
0211     fileItemActions->setItemListProperties(KFileItemListProperties(_items));
0212     fileItemActions->setParentWidget(MAIN_VIEW);
0213 #if KIO_VERSION >= QT_VERSION_CHECK(5, 79, 0)
0214     fileItemActions->addActionsTo(this);
0215 #else
0216     fileItemActions->addServiceActionsTo(this);
0217 #endif
0218 
0219     addSeparator();
0220 
0221     // ------------- 'create new' submenu
0222     addCreateNewMenu();
0223     addSeparator();
0224 
0225     // ---------- COPY
0226     addAction(panel->gui->actions()->actCopyF5);
0227     // ------- MOVE
0228     addAction(panel->gui->actions()->actMoveF6);
0229     // ------- RENAME - only one file
0230     if (!multipleSelections && !inTrash) {
0231         addAction(panel->gui->actions()->actRenameF2);
0232     }
0233 
0234     // -------- MOVE TO TRASH
0235     if (KConfigGroup(krConfig, "General").readEntry("Move To Trash", _MoveToTrash) && panel->func->files()->canMoveToTrash(fileNames)) {
0236         addAction(Icon("user-trash"), i18n("Move to Trash"))->setData(QVariant(TRASH_ID));
0237     }
0238     // -------- DELETE
0239     addAction(Icon("edit-delete"), i18n("Delete"))->setData(QVariant(DELETE_ID));
0240     // -------- SHRED - only one file
0241     /*      if ( panel->func->files() ->getType() == filesystem:fileSystemM_NORMAL &&
0242                 !fileitem->isDir() && !multipleSelections )
0243              addAction( i18n( "Shred" ) )->setData( QVariant( SHRED_ID ) );*/
0244 
0245     // ---------- link handling
0246     // create new shortcut or redirect links - only on local directories:
0247     if (panel->func->files()->isLocal()) {
0248         addSeparator();
0249         auto *linkMenu = new QMenu(this);
0250         linkMenu->addAction(i18n("New Symlink..."))->setData(QVariant(NEW_SYMLINK_ID));
0251         linkMenu->addAction(i18n("New Hardlink..."))->setData(QVariant(NEW_LINK_ID));
0252         if (file->isSymLink()) {
0253             linkMenu->addAction(i18n("Redirect Link..."))->setData(QVariant(REDIRECT_LINK_ID));
0254         }
0255         QAction *linkAction = addMenu(linkMenu);
0256         linkAction->setText(i18n("Link Handling"));
0257         linkAction->setIcon(Icon("insert-link"));
0258     }
0259     addSeparator();
0260 
0261     // ---------- calculate space
0262     if (panel->func->files()->isLocal() && (file->isDir() || multipleSelections))
0263         addAction(panel->gui->actions()->actCalculate);
0264 
0265     // ---------- mount/umount/eject
0266     if (panel->func->files()->isLocal() && file->isDir() && !multipleSelections) {
0267         const QString selectedDirectoryPath = file->getUrl().path();
0268         if (krMtMan.getStatus(selectedDirectoryPath) == KMountMan::MOUNTED)
0269             addAction(i18n("Unmount"))->setData(QVariant(UNMOUNT_ID));
0270         else if (krMtMan.getStatus(selectedDirectoryPath) == KMountMan::NOT_MOUNTED)
0271             addAction(i18n("Mount"))->setData(QVariant(MOUNT_ID));
0272         if (krMtMan.ejectable(selectedDirectoryPath))
0273             addAction(i18n("Eject"))->setData(QVariant(EJECT_ID));
0274     }
0275 
0276     // --------- send by mail
0277     if (KrServices::supportedTools().contains("MAIL") && !file->isDir()) {
0278         addAction(Icon("mail-send"), i18n("Send by Email"))->setData(QVariant(SEND_BY_EMAIL_ID));
0279     }
0280 
0281     // --------- empty trash
0282     if (trashOnly) {
0283         addAction(i18n("Restore"))->setData(QVariant(RESTORE_TRASHED_FILE_ID));
0284         addAction(i18n("Empty Trash"))->setData(QVariant(EMPTY_TRASH_ID));
0285     }
0286 
0287 #ifdef SYNCHRONIZER_ENABLED
0288     // --------- synchronize
0289     if (panel->view->numSelected()) {
0290         addAction(i18n("Synchronize Selected Files..."))->setData(QVariant(SYNC_SELECTED_ID));
0291     }
0292 #endif
0293 
0294     // --------- copy/paste
0295     addSeparator();
0296     addAction(panel->gui->actions()->actCut);
0297     addAction(panel->gui->actions()->actCopy);
0298     addAction(panel->gui->actions()->actPaste);
0299     addSeparator();
0300 
0301     // --------- properties
0302     addAction(panel->gui->actions()->actProperties);
0303 }
0304 
0305 void PanelContextMenu::addEmptyMenuEntries()
0306 {
0307     addAction(panel->gui->actions()->actPaste);
0308 }
0309 
0310 void PanelContextMenu::addCreateNewMenu()
0311 {
0312     auto *createNewMenu = new QMenu(this);
0313     createNewMenu->addAction(panel->gui->actions()->actNewFolderF7);
0314     createNewMenu->addAction(panel->gui->actions()->actNewFileShiftF4);
0315 
0316     QAction *newMenuAction = addMenu(createNewMenu);
0317     newMenuAction->setText(i18n("Create New"));
0318     newMenuAction->setIcon(Icon("document-new"));
0319 }
0320 
0321 void PanelContextMenu::performAction(int id)
0322 {
0323     const QUrl singleURL = _items.isEmpty() ? QUrl() : _items.first().url();
0324 
0325     switch (id) {
0326     case -1: // the user clicked outside of the menu
0327         return;
0328     case OPEN_TAB_ID:
0329         for (const KFileItem &fileItem : _items) {
0330             panel->manager()->duplicateTab(fileItem.url(), panel);
0331         }
0332         break;
0333     case OPEN_ID:
0334         for (const KFileItem &fileItem : _items) {
0335             // do not open dirs if multiple files are selected
0336             if (_items.size() == 1 || !fileItem.isDir()) {
0337                 panel->func->execute(fileItem.name());
0338             }
0339         }
0340         break;
0341     case BROWSE_ID:
0342         panel->func->goInside(singleURL.fileName());
0343         break;
0344     case TRASH_ID:
0345         panel->func->deleteFiles(true);
0346         break;
0347     case DELETE_ID:
0348         panel->func->deleteFiles(false);
0349         break;
0350     case EJECT_ID:
0351         krMtMan.eject(singleURL.adjusted(QUrl::StripTrailingSlash).path());
0352         break;
0353     case CHOOSE_ID: // open-with dialog
0354         panel->func->displayOpenWithDialog(_items.urlList());
0355         break;
0356     case MOUNT_ID:
0357         krMtMan.mount(singleURL.adjusted(QUrl::StripTrailingSlash).path());
0358         break;
0359     case NEW_LINK_ID:
0360         panel->func->krlink(false);
0361         break;
0362     case NEW_SYMLINK_ID:
0363         panel->func->krlink(true);
0364         break;
0365     case REDIRECT_LINK_ID:
0366         panel->func->redirectLink();
0367         break;
0368     case EMPTY_TRASH_ID:
0369         KrTrashHandler::emptyTrash();
0370         break;
0371     case RESTORE_TRASHED_FILE_ID:
0372         KrTrashHandler::restoreTrashedFiles(_items.urlList());
0373         break;
0374     case UNMOUNT_ID:
0375         krMtMan.unmount(singleURL.adjusted(QUrl::StripTrailingSlash).path());
0376         break;
0377     case SEND_BY_EMAIL_ID: {
0378         SLOTS->sendFileByEmail(_items.urlList());
0379         break;
0380     }
0381 #ifdef SYNCHRONIZER_ENABLED
0382     case SYNC_SELECTED_ID: {
0383         QStringList selectedNames;
0384         for (const KFileItem &item : _items) {
0385             selectedNames.append(item.name());
0386         }
0387         const KrViewItemList otherItems = panel->otherPanel()->view->getSelectedKrViewItems();
0388         for (KrViewItem *otherItem : otherItems) {
0389             const QString &name = otherItem->name();
0390             if (!selectedNames.contains(name)) {
0391                 selectedNames.append(name);
0392             }
0393         }
0394         SLOTS->slotSynchronizeDirs(selectedNames);
0395     } break;
0396 #endif
0397     case OPEN_TERM_ID:
0398         SLOTS->runTerminal(singleURL.path());
0399         break;
0400     }
0401 
0402     // check if something from the open-with-offered-services was selected
0403     if (id >= SERVICE_LIST_ID) {
0404         const QStringList names = panel->gui->getSelectedNames();
0405         panel->func->runService(*(offers[id - SERVICE_LIST_ID]), panel->func->files()->getUrls(names));
0406     }
0407 }