File indexing completed on 2024-04-28 04:58:11

0001 /* This file is part of the KDE project
0002     SPDX-FileCopyrightText: 1998-2008 David Faure <faure@kde.org>
0003     SPDX-FileCopyrightText: 2001 Holger Freyther <freyther@yahoo.com>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "konq_popupmenu.h"
0009 
0010 #include <KActionCollection>
0011 
0012 #include <kfileitemlistproperties.h>
0013 #include "kfileitemactions.h"
0014 #include "kabstractfileitemactionplugin.h"
0015 #include "kpropertiesdialog.h"
0016 
0017 #include <klocalizedstring.h>
0018 #include <kbookmarkmanager.h>
0019 #include <kbookmarkdialog.h>
0020 #include <KIO/OpenUrlJob>
0021 #include <KIO/JobUiDelegate>
0022 #include <kprotocolmanager.h>
0023 #include <knewfilemenu.h>
0024 #include <kconfiggroup.h>
0025 #include <KSharedConfig>
0026 #include <kdesktopfile.h>
0027 #include <kauthorized.h>
0028 #include <kacceleratormanager.h>
0029 #include <KIO/EmptyTrashJob>
0030 #include <KIO/JobUiDelegate>
0031 #include <KIO/RestoreJob>
0032 #include <KIO/CommandLauncherJob>
0033 #include <kio_version.h>
0034 #include <KIO/OpenUrlJob>
0035 #include <KTerminalLauncherJob>
0036 #include <KFileCopyToMenu>
0037 #include <KJobWidgets>
0038 #include <KJobUiDelegate>
0039 #include <KMimeTypeEditor>
0040 #include <KPluginMetaData>
0041 #include <KIO/JobUiDelegateFactory>
0042 
0043 #include <QIcon>
0044 #include <QFileInfo>
0045 #include <QMimeDatabase>
0046 
0047 /*
0048  Test cases:
0049   iconview file: background
0050   iconview file: file (with and without servicemenus)
0051   iconview file: directory
0052   iconview remote protocol (e.g. ftp: or fish:)
0053   iconview trash:/
0054   sidebar directory tree
0055   sidebar Devices / Hard Disc
0056   khtml background
0057   khtml link
0058   khtml image (www.kde.org RMB on K logo)
0059   khtmlimage (same as above, then choose View image, then RMB)
0060   selected text in khtml
0061   embedded katepart
0062   folder on the desktop
0063   trash link on the desktop
0064   trashed file or directory
0065   application .desktop file
0066  Then the same after uninstalling kdeaddons/konq-plugins (arkplugin in particular)
0067 */
0068 
0069 class KonqPopupMenuPrivate
0070 {
0071 public:
0072     KonqPopupMenuPrivate(KonqPopupMenu *qq, KActionCollection &actions, QWidget *parentWidget)
0073         : q(qq),
0074           m_parentWidget(parentWidget),
0075           m_popupFlags(KonqPopupMenu::DefaultPopupItems),
0076           m_pMenuNew(nullptr),
0077           m_copyToMenu(parentWidget),
0078           m_bookmarkManager(nullptr),
0079           m_actions(actions)
0080     {
0081     }
0082 
0083     ~KonqPopupMenuPrivate()
0084     {
0085         qDeleteAll(m_ownActions);
0086     }
0087 
0088     void addNamedAction(const char *name);
0089     void addGroup(KonqPopupMenu::ActionGroup group);
0090     void populate();
0091     void aboutToShow();
0092 
0093     void slotPopupNewDir();
0094     void slotPopupNewView();
0095     void slotPopupEmptyTrashBin();
0096     void slotConfigTrashBin();
0097     void slotPopupRestoreTrashedItems();
0098     void slotPopupAddToBookmark();
0099     void slotPopupMimeType();
0100     void slotPopupProperties();
0101     void slotShowOriginalFile();
0102 
0103     KonqPopupMenu *q;
0104     QWidget *m_parentWidget;
0105     QString m_urlTitle;
0106     KonqPopupMenu::Flags m_popupFlags;
0107     KNewFileMenu *m_pMenuNew;
0108     QUrl m_sViewURL;
0109     KFileItemListProperties m_popupItemProperties;
0110     KFileItemActions m_menuActions;
0111     KFileCopyToMenu m_copyToMenu;
0112     KBookmarkManager *m_bookmarkManager;
0113     KActionCollection &m_actions;
0114     QList<QAction *> m_ownActions;
0115     KonqPopupMenu::ActionGroupMap m_actionGroups;
0116 };
0117 
0118 //////////////////
0119 
0120 KonqPopupMenu::KonqPopupMenu(const KFileItemList &items,
0121                              const QUrl &viewURL,
0122                              KActionCollection &actions,
0123                              Flags popupFlags,
0124                              QWidget *parentWidget)
0125     : QMenu(parentWidget),
0126       d(new KonqPopupMenuPrivate(this, actions, parentWidget))
0127 {
0128     d->m_sViewURL = viewURL;
0129     d->m_popupItemProperties.setItems(items);
0130     d->m_menuActions.setParentWidget(parentWidget);
0131     d->m_popupFlags = popupFlags;
0132 
0133     connect(this, &QMenu::aboutToShow, this, [this]() { d->aboutToShow(); });
0134 }
0135 
0136 void KonqPopupMenuPrivate::addNamedAction(const char *name)
0137 {
0138     QAction *act = m_actions.action(QString::fromLatin1(name));
0139     if (act) {
0140         q->addAction(act);
0141     }
0142 }
0143 
0144 void KonqPopupMenuPrivate::aboutToShow()
0145 {
0146     populate();
0147     KAcceleratorManager::manage(q);
0148 }
0149 
0150 void KonqPopupMenuPrivate::populate()
0151 {
0152     Q_ASSERT(m_popupItemProperties.items().count() >= 1);
0153 
0154     bool bTrashIncluded = false;
0155 
0156     const KFileItemList lstItems = m_popupItemProperties.items();
0157     QList<QUrl> lstUrls;
0158     lstUrls.reserve(lstItems.count());
0159     for (const KFileItem &item : lstItems) {
0160         const QUrl url = item.url();
0161         lstUrls.append(url);
0162         if (!bTrashIncluded && ((url.scheme() == QLatin1String("trash") && url.path().length() <= 1))) {
0163             bTrashIncluded = true;
0164         }
0165     }
0166 
0167     const bool isDirectory = m_popupItemProperties.isDirectory();
0168     const bool sReading = m_popupItemProperties.supportsReading();
0169     bool sDeleting = (m_popupFlags & KonqPopupMenu::NoDeletion) == 0
0170                      && m_popupItemProperties.supportsDeleting();
0171     const bool sWriting = m_popupItemProperties.supportsWriting();
0172     const bool sMoving = sDeleting && m_popupItemProperties.supportsMoving();
0173 
0174     QUrl url = m_sViewURL.adjusted(QUrl::NormalizePathSegments);
0175 
0176     bool isTrashLink     = false;
0177     bool isCurrentTrash = false;
0178     bool currentDir     = false;
0179     bool isSymLink = false;
0180     bool isSymLinkInSameDir = false; // true for "ln -s foo bar", false for links to foo/sub or /foo
0181 
0182     //check if url is current directory
0183     if (lstItems.count() == 1) {
0184         const KFileItem firstPopupItem = lstItems.constFirst();
0185         if (firstPopupItem.isLink()) {
0186             isSymLink = true;
0187             isSymLinkInSameDir = !firstPopupItem.linkDest().contains(QLatin1Char('/'));
0188         }
0189         const QUrl firstPopupURL(firstPopupItem.url().adjusted(QUrl::NormalizePathSegments));
0190         currentDir = (firstPopupURL.matches(url, QUrl::StripTrailingSlash));
0191         if (firstPopupItem.isDesktopFile()) {
0192             KDesktopFile desktopFile(firstPopupItem.localPath());
0193             const KConfigGroup cfg = desktopFile.desktopGroup();
0194             isTrashLink = cfg.readEntry("Type") == QLatin1String("Link")
0195                           && cfg.readEntry("URL") == QLatin1String("trash:/");
0196         }
0197 
0198         if (isTrashLink) {
0199             sDeleting = false;
0200         }
0201 
0202         // isCurrentTrash: popup on trash:/ itself, or on the trash.desktop link
0203         isCurrentTrash = (firstPopupURL.scheme() == QLatin1String("trash") && firstPopupURL.path().length() <= 1)
0204                          || isTrashLink;
0205     }
0206 
0207     const bool isIntoTrash = (url.scheme() == QLatin1String("trash")) && !isCurrentTrash; // trashed file, not trash:/ itself
0208     const bool bIsLink  = (m_popupFlags & KonqPopupMenu::IsLink);
0209 
0210     addGroup(KonqPopupMenu::TopActions); // used e.g. for ShowMenuBar. includes a separator at the end
0211 
0212     QAction *act;
0213     QAction *actNewWindow = nullptr;
0214 
0215     // Either 'newview' is in the actions we're given (probably in the tabhandling group)
0216     // or we need to insert it ourselves (e.g. for the desktop).
0217     // In the first case, actNewWindow must remain 0.
0218     if (((m_popupFlags & KonqPopupMenu::ShowNewWindow) != 0) && sReading) {
0219         const QString openStr = i18n("&Open");
0220         actNewWindow = new QAction(m_parentWidget /*for status tips*/);
0221         m_ownActions.append(actNewWindow);
0222         actNewWindow->setIcon(QIcon::fromTheme(QStringLiteral("window-new")));
0223         actNewWindow->setText(openStr);
0224         QObject::connect(actNewWindow, &QAction::triggered, [this]() {
0225             slotPopupNewView();
0226         });
0227     }
0228 
0229     if (isDirectory && sWriting && !isCurrentTrash) { // A dir, and we can create things into it
0230         const bool mkdirRequested = m_popupFlags & KonqPopupMenu::ShowCreateDirectory;
0231         if ((currentDir || mkdirRequested) && m_pMenuNew) { // Current dir -> add the "new" menu
0232             // As requested by KNewFileMenu :
0233             m_pMenuNew->checkUpToDate();
0234             m_pMenuNew->setWorkingDirectory(url);
0235 
0236             q->addAction(m_pMenuNew);
0237             q->addSeparator();
0238         } else if (mkdirRequested) {
0239             QAction *actNewDir = new QAction(m_parentWidget);
0240             m_ownActions.append(actNewDir);
0241             actNewDir->setIcon(QIcon::fromTheme(QStringLiteral("folder-new")));
0242             actNewDir->setText(i18n("Create &Folder..."));
0243             QObject::connect(actNewDir, &QAction::triggered, [this]() {
0244                 slotPopupNewDir();
0245             });
0246             q->addAction(actNewDir);
0247             q->addSeparator();
0248         }
0249     } else if (isIntoTrash) {
0250         // Trashed item, offer restoring
0251         act = new QAction(m_parentWidget /*for status tips*/);
0252         m_ownActions.append(act);
0253         act->setText(i18n("&Restore"));
0254         //PORT QT5 act->setHelpText(i18n("Restores this file or directory, back to the location where it was deleted from initially"));
0255         QObject::connect(act, &QAction::triggered, [this]() {
0256             slotPopupRestoreTrashedItems();
0257         });
0258         q->addAction(act);
0259     }
0260 
0261     if (!currentDir && isSymLink && !isSymLinkInSameDir) {
0262         // #65151: offer to open the target's parent dir
0263         act = new QAction(m_parentWidget);
0264         m_ownActions.append(act);
0265         act->setText(isDirectory ? i18n("Show Original Directory") : i18n("Show Original File"));
0266         //PORT TO QT5 act->setHelpText(i18n("Opens a new file manager window showing the target of this link, in its parent directory."));
0267         QObject::connect(act, &QAction::triggered, [this]() {
0268             slotShowOriginalFile();
0269         });
0270         q->addAction(act);
0271     }
0272 
0273     // "open in new window" is either provided by us, or by the tabhandling group
0274     if (actNewWindow) {
0275         q->addAction(actNewWindow);
0276         q->addSeparator();
0277     }
0278     addGroup(KonqPopupMenu::TabHandlingActions);   // includes a separator at the end
0279 
0280     if (m_popupFlags & KonqPopupMenu::ShowUrlOperations) {
0281         if (!currentDir && sReading) {
0282             if (sDeleting) {
0283                 addNamedAction("cut");
0284             }
0285             addNamedAction("copy");
0286         }
0287 
0288         if (isDirectory && sWriting) {
0289             if (currentDir) {
0290                 addNamedAction("paste");
0291             } else {
0292                 addNamedAction("pasteto");
0293             }
0294         }
0295     }
0296     if (isCurrentTrash) {
0297         act = new QAction(m_parentWidget);
0298         m_ownActions.append(act);
0299         act->setIcon(QIcon::fromTheme(QStringLiteral("trash-empty")));
0300         act->setText(i18n("&Empty Trash Bin"));
0301         KConfig trashConfig(QStringLiteral("trashrc"), KConfig::SimpleConfig);
0302         act->setEnabled(!trashConfig.group("Status").readEntry("Empty", true));
0303         QObject::connect(act, &QAction::triggered, [this]() {
0304             slotPopupEmptyTrashBin();
0305         });
0306         q->addAction(act);
0307     }
0308     if (isCurrentTrash) {
0309         act = new QAction(m_parentWidget);
0310         m_ownActions.append(act);
0311         act->setIcon(QIcon::fromTheme(QStringLiteral("trash-empty")));
0312         act->setText(i18n("&Configure Trash Bin"));
0313         QObject::connect(act, &QAction::triggered, [this]() {
0314             slotConfigTrashBin();
0315         });
0316         q->addAction(act);
0317     }
0318 
0319     // This is used by KHTML, see khtml_popupmenu.rc (copy, selectAll, searchProvider etc.)
0320     // and by DolphinPart (rename, trash, delete)
0321     addGroup(KonqPopupMenu::EditActions);
0322 
0323     if (m_popupFlags & KonqPopupMenu::ShowTextSelectionItems) {
0324         // OK, we have to stop here.
0325 
0326         // Anything else that is provided by the part
0327         addGroup(KonqPopupMenu::CustomActions);
0328         return;
0329     }
0330 
0331     if (!isCurrentTrash && !isIntoTrash && (m_popupFlags & KonqPopupMenu::ShowBookmark)) {
0332         QString caption;
0333         if (currentDir) {
0334             const bool httpPage = m_sViewURL.scheme().startsWith(QLatin1String("http"), Qt::CaseInsensitive);
0335             if (httpPage) {
0336                 caption = i18n("&Bookmark This Page");
0337             } else {
0338                 caption = i18n("&Bookmark This Location");
0339             }
0340         } else if (isDirectory) {
0341             caption = i18n("&Bookmark This Folder");
0342         } else if (bIsLink) {
0343             caption = i18n("&Bookmark This Link");
0344         } else {
0345             caption = i18n("&Bookmark This File");
0346         }
0347 
0348         act = new QAction(m_parentWidget);
0349         m_ownActions.append(act);
0350         act->setObjectName(QStringLiteral("bookmark_add"));   // for unittest
0351         act->setIcon(QIcon::fromTheme(QStringLiteral("bookmark-new")));
0352         act->setText(caption);
0353         QObject::connect(act, &QAction::triggered, [this]() {
0354             slotPopupAddToBookmark();
0355         });
0356         if (lstItems.count() > 1) {
0357             act->setEnabled(false);
0358         }
0359         if (KAuthorized::authorizeAction(QStringLiteral("bookmarks"))) {
0360             q->addAction(act);
0361         }
0362         if (bIsLink) {
0363             addGroup(KonqPopupMenu::LinkActions);    // see khtml
0364         }
0365     }
0366 
0367     // "Open With" actions
0368 
0369     m_menuActions.setItemListProperties(m_popupItemProperties);
0370 
0371     if (sReading) {
0372         const QStringList excludedEntries = {
0373             QStringLiteral("kfmclient"),
0374             QStringLiteral("kfmclient_dir"),
0375             QStringLiteral("kfmclient_html")
0376         };
0377         m_menuActions.insertOpenWithActionsTo(nullptr, q, excludedEntries);
0378 
0379         QList<QAction *> previewActions = m_actionGroups.value(KonqPopupMenu::PreviewActions);
0380         if (!previewActions.isEmpty()) {
0381             if (previewActions.count() == 1) {
0382                 q->addAction(previewActions.first());
0383             } else {
0384                 QMenu *subMenu = new QMenu(i18n("Preview In"), q);
0385                 subMenu->menuAction()->setObjectName(QStringLiteral("preview_submenu"));   // for the unittest
0386                 q->addMenu(subMenu);
0387                 subMenu->addActions(previewActions);
0388             }
0389         }
0390     }
0391 
0392     // Second block, builtin + user
0393     QList<QAction *> additionalActions;
0394     if (isDirectory && m_popupItemProperties.items().count() == 1) {
0395         QAction *openTerminalHere = new QAction(QIcon::fromTheme("utilities-terminal"), i18n("Open Terminal Here"), m_parentWidget);
0396         openTerminalHere->setObjectName("open-terminal-here");
0397         QObject::connect(openTerminalHere, &QAction::triggered, q, [this]() {
0398                 const QString localPath = m_popupItemProperties.urlList().constFirst().toLocalFile();
0399                 // 5.84 because the header wasn't usable in 5.83
0400               auto *job = new KTerminalLauncherJob(QString{});
0401               job->setWorkingDirectory(localPath);
0402               job->start();
0403         });
0404         additionalActions << openTerminalHere;
0405     }
0406     m_menuActions.addActionsTo(q, KFileItemActions::MenuActionSource::Services, additionalActions);
0407 
0408     q->addSeparator();
0409 
0410     // Use the Dolphin setting for showing the "Copy To" and "Move To" actions
0411     KSharedConfig::Ptr dolphin = KSharedConfig::openConfig(QStringLiteral("dolphinrc"));
0412 
0413     // CopyTo/MoveTo menus
0414     if (m_popupFlags & KonqPopupMenu::ShowUrlOperations &&
0415             KConfigGroup(dolphin, "General").readEntry("ShowCopyMoveMenu", false)) {
0416 
0417         m_copyToMenu.setUrls(lstUrls);
0418         m_copyToMenu.setReadOnly(sMoving == false);
0419         m_copyToMenu.addActionsTo(q);
0420         q->addSeparator();
0421     }
0422 
0423     if (!isCurrentTrash && !isIntoTrash && sReading && (m_popupFlags & KonqPopupMenu::NoPlugins) == 0) {
0424         m_menuActions.addActionsTo(q, KFileItemActions::MenuActionSource::Plugins);
0425     }
0426 
0427     if ((m_popupFlags & KonqPopupMenu::ShowProperties) && KPropertiesDialog::canDisplay(lstItems)) {
0428         act = new QAction(m_parentWidget);
0429         m_ownActions.append(act);
0430         act->setObjectName(QStringLiteral("properties"));   // for unittest
0431         act->setText(i18n("&Properties"));
0432         QObject::connect(act, &QAction::triggered, [this]() {
0433             slotPopupProperties();
0434         });
0435         q->addAction(act);
0436     }
0437 
0438     while (!q->actions().isEmpty() &&
0439             q->actions().last()->isSeparator()) {
0440         delete q->actions().last();
0441     }
0442 
0443     // Anything else that is provided by the part
0444     addGroup(KonqPopupMenu::CustomActions);
0445 
0446     QObject::connect(&m_menuActions, &KFileItemActions::openWithDialogAboutToBeShown, q, &KonqPopupMenu::openWithDialogAboutToBeShown);
0447 }
0448 
0449 KonqPopupMenu::~KonqPopupMenu()
0450 {
0451     delete d;
0452 }
0453 
0454 void KonqPopupMenu::setNewFileMenu(KNewFileMenu *newMenu)
0455 {
0456     d->m_pMenuNew = newMenu;
0457 }
0458 
0459 void KonqPopupMenu::setBookmarkManager(KBookmarkManager *manager)
0460 {
0461     d->m_bookmarkManager = manager;
0462 }
0463 
0464 void KonqPopupMenu::setActionGroups(const KonqPopupMenu::ActionGroupMap &actionGroups)
0465 {
0466     d->m_actionGroups = actionGroups;
0467 }
0468 
0469 void KonqPopupMenu::setURLTitle(const QString &urlTitle)
0470 {
0471     d->m_urlTitle = urlTitle;
0472 }
0473 
0474 void KonqPopupMenuPrivate::slotPopupNewView()
0475 {
0476     for (const QUrl &url: m_popupItemProperties.urlList()) {
0477         KIO::OpenUrlJob *job = new KIO::OpenUrlJob(url);
0478         job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, m_parentWidget));
0479         job->start();
0480     }
0481 }
0482 
0483 void KonqPopupMenuPrivate::slotPopupNewDir()
0484 {
0485     m_pMenuNew->createDirectory();
0486 }
0487 
0488 void KonqPopupMenuPrivate::slotPopupEmptyTrashBin()
0489 {
0490     KJobUiDelegate* baseUiDelegate = KIO::createDefaultJobUiDelegate(KJobUiDelegate::Flags{}, m_parentWidget);
0491     KIO::JobUiDelegate* uiDelegate = qobject_cast<KIO::JobUiDelegate*>(baseUiDelegate);
0492     uiDelegate->setWindow(m_parentWidget);
0493     if (uiDelegate && uiDelegate->askDeleteConfirmation(QList<QUrl>(), KIO::JobUiDelegate::EmptyTrash, KIO::JobUiDelegate::DefaultConfirmation)) {
0494         KIO::Job *job = KIO::emptyTrash();
0495         KJobWidgets::setWindow(job, m_parentWidget);
0496         job->uiDelegate()->setAutoErrorHandlingEnabled(true); // or connect to the result signal
0497     }
0498 }
0499 
0500 void KonqPopupMenuPrivate::slotConfigTrashBin()
0501 {
0502     KIO::CommandLauncherJob *job = new KIO::CommandLauncherJob(QStringLiteral("kcmshell5 kcmtrash"));
0503     job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, m_parentWidget));
0504     job->start();
0505 }
0506 
0507 void KonqPopupMenuPrivate::slotPopupRestoreTrashedItems()
0508 {
0509     KIO::RestoreJob *job = KIO::restoreFromTrash(m_popupItemProperties.urlList());
0510     KJobWidgets::setWindow(job, m_parentWidget);
0511     job->uiDelegate()->setAutoErrorHandlingEnabled(true);
0512 }
0513 
0514 void KonqPopupMenuPrivate::slotPopupAddToBookmark()
0515 {
0516     KBookmarkGroup root;
0517     if (m_popupItemProperties.urlList().count() == 1) {
0518         const QUrl url = m_popupItemProperties.urlList().first();
0519         const QString title = m_urlTitle.isEmpty() ? url.toDisplayString() : m_urlTitle;
0520         KBookmarkDialog dlg(m_bookmarkManager, m_parentWidget);
0521         dlg.addBookmark(title, url, QString());
0522     } else {
0523         root = m_bookmarkManager->root();
0524         for(const QUrl &url: m_popupItemProperties.urlList()) {
0525             root.addBookmark(url.toDisplayString(), url, QString());
0526         }
0527         m_bookmarkManager->emitChanged(root);
0528     }
0529 }
0530 
0531 void KonqPopupMenuPrivate::slotPopupMimeType()
0532 {
0533     KMimeTypeEditor::editMimeType(m_popupItemProperties.mimeType(), m_parentWidget);
0534 }
0535 
0536 void KonqPopupMenuPrivate::slotPopupProperties()
0537 {
0538     KPropertiesDialog::showDialog(m_popupItemProperties.items(), m_parentWidget, false);
0539 }
0540 
0541 void KonqPopupMenuPrivate::addGroup(KonqPopupMenu::ActionGroup group)
0542 {
0543     q->addActions(m_actionGroups.value(group));
0544 }
0545 
0546 void KonqPopupMenuPrivate::slotShowOriginalFile()
0547 {
0548     const KFileItem item = m_popupItemProperties.items().first();
0549     QUrl destUrl = QUrl::fromLocalFile(item.linkDest());
0550 
0551     if (!destUrl.isValid()) {
0552         return;
0553     }
0554 
0555     // Now destUrl points to the target file, let's go up to parent dir
0556     destUrl = destUrl.adjusted(QUrl::RemoveFilename);
0557 
0558     KIO::OpenUrlJob *job = new KIO::OpenUrlJob(destUrl, QStringLiteral("inode/directory"));
0559     job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, m_parentWidget));
0560     job->start();
0561 }