File indexing completed on 2024-04-14 05:38:40

0001 /*
0002  * SPDX-FileCopyrightText: 2006 Peter Penz (peter.penz@gmx.at) and Cvetoslav Ludmiloff
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-or-later
0005  */
0006 
0007 #include "dolphincontextmenu.h"
0008 
0009 #include "dolphin_contextmenusettings.h"
0010 #include "dolphinmainwindow.h"
0011 #include "dolphinnewfilemenu.h"
0012 #include "dolphinplacesmodelsingleton.h"
0013 #include "dolphinremoveaction.h"
0014 #include "dolphinviewcontainer.h"
0015 #include "global.h"
0016 #include "trash/dolphintrash.h"
0017 #include "views/dolphinview.h"
0018 
0019 #include <KActionCollection>
0020 #include <KFileItemListProperties>
0021 #include <KHamburgerMenu>
0022 #include <KIO/EmptyTrashJob>
0023 #include <KIO/JobUiDelegate>
0024 #include <KIO/Paste>
0025 #include <KIO/RestoreJob>
0026 #include <KJobWidgets>
0027 #include <KLocalizedString>
0028 #include <KNewFileMenu>
0029 #include <KStandardAction>
0030 
0031 #include <QApplication>
0032 #include <QClipboard>
0033 #include <QKeyEvent>
0034 
0035 DolphinContextMenu::DolphinContextMenu(DolphinMainWindow *parent,
0036                                        const KFileItem &fileInfo,
0037                                        const KFileItemList &selectedItems,
0038                                        const QUrl &baseUrl,
0039                                        KFileItemActions *fileItemActions)
0040     : QMenu(parent)
0041     , m_mainWindow(parent)
0042     , m_fileInfo(fileInfo)
0043     , m_baseUrl(baseUrl)
0044     , m_baseFileItem(nullptr)
0045     , m_selectedItems(selectedItems)
0046     , m_selectedItemsProperties(nullptr)
0047     , m_context(NoContext)
0048     , m_copyToMenu(parent)
0049     , m_removeAction(nullptr)
0050     , m_fileItemActions(fileItemActions)
0051 {
0052     QApplication::instance()->installEventFilter(this);
0053 
0054     addAllActions();
0055 }
0056 
0057 DolphinContextMenu::~DolphinContextMenu()
0058 {
0059     delete m_baseFileItem;
0060     m_baseFileItem = nullptr;
0061     delete m_selectedItemsProperties;
0062     m_selectedItemsProperties = nullptr;
0063 }
0064 
0065 void DolphinContextMenu::addAllActions()
0066 {
0067     static_cast<KHamburgerMenu *>(m_mainWindow->actionCollection()->action(QStringLiteral("hamburger_menu")))->addToMenu(this);
0068 
0069     // get the context information
0070     const auto scheme = m_baseUrl.scheme();
0071     if (scheme == QLatin1String("trash")) {
0072         m_context |= TrashContext;
0073     } else if (scheme.contains(QLatin1String("search"))) {
0074         m_context |= SearchContext;
0075     } else if (scheme.contains(QLatin1String("timeline"))) {
0076         m_context |= TimelineContext;
0077     } else if (scheme == QStringLiteral("recentlyused")) {
0078         m_context |= RecentlyUsedContext;
0079     }
0080 
0081     if (!m_fileInfo.isNull() && !m_selectedItems.isEmpty()) {
0082         m_context |= ItemContext;
0083         // TODO: handle other use cases like devices + desktop files
0084     }
0085 
0086     // open the corresponding popup for the context
0087     if (m_context & TrashContext) {
0088         if (m_context & ItemContext) {
0089             addTrashItemContextMenu();
0090         } else {
0091             addTrashContextMenu();
0092         }
0093     } else if (m_context & ItemContext) {
0094         addItemContextMenu();
0095     } else {
0096         addViewportContextMenu();
0097     }
0098 }
0099 
0100 bool DolphinContextMenu::eventFilter(QObject *object, QEvent *event)
0101 {
0102     Q_UNUSED(object)
0103 
0104     if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) {
0105         QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
0106 
0107         if (m_removeAction && keyEvent->key() == Qt::Key_Shift) {
0108             if (event->type() == QEvent::KeyPress) {
0109                 m_removeAction->update(DolphinRemoveAction::ShiftState::Pressed);
0110             } else {
0111                 m_removeAction->update(DolphinRemoveAction::ShiftState::Released);
0112             }
0113         }
0114     }
0115 
0116     return false;
0117 }
0118 
0119 void DolphinContextMenu::addTrashContextMenu()
0120 {
0121     Q_ASSERT(m_context & TrashContext);
0122 
0123     QAction *emptyTrashAction = addAction(QIcon::fromTheme(QStringLiteral("trash-empty")), i18nc("@action:inmenu", "Empty Trash"), [this]() {
0124         Trash::empty(m_mainWindow);
0125     });
0126     emptyTrashAction->setEnabled(!Trash::isEmpty());
0127 
0128     QAction *propertiesAction = m_mainWindow->actionCollection()->action(QStringLiteral("properties"));
0129     addAction(propertiesAction);
0130 }
0131 
0132 void DolphinContextMenu::addTrashItemContextMenu()
0133 {
0134     Q_ASSERT(m_context & TrashContext);
0135     Q_ASSERT(m_context & ItemContext);
0136 
0137     addAction(QIcon::fromTheme("restoration"), i18nc("@action:inmenu", "Restore"), [this]() {
0138         QList<QUrl> selectedUrls;
0139         selectedUrls.reserve(m_selectedItems.count());
0140         for (const KFileItem &item : std::as_const(m_selectedItems)) {
0141             selectedUrls.append(item.url());
0142         }
0143 
0144         KIO::RestoreJob *job = KIO::restoreFromTrash(selectedUrls);
0145         KJobWidgets::setWindow(job, m_mainWindow);
0146         job->uiDelegate()->setAutoErrorHandlingEnabled(true);
0147     });
0148 
0149     QAction *deleteAction = m_mainWindow->actionCollection()->action(KStandardAction::name(KStandardAction::DeleteFile));
0150     addAction(deleteAction);
0151 
0152     QAction *propertiesAction = m_mainWindow->actionCollection()->action(QStringLiteral("properties"));
0153     addAction(propertiesAction);
0154 }
0155 
0156 void DolphinContextMenu::addDirectoryItemContextMenu()
0157 {
0158     // insert 'Open in new window' and 'Open in new tab' entries
0159     const KFileItemListProperties &selectedItemsProps = selectedItemsProperties();
0160     if (ContextMenuSettings::showOpenInNewTab()) {
0161         addAction(m_mainWindow->actionCollection()->action(QStringLiteral("open_in_new_tab")));
0162     }
0163     if (ContextMenuSettings::showOpenInNewWindow()) {
0164         addAction(m_mainWindow->actionCollection()->action(QStringLiteral("open_in_new_window")));
0165     }
0166 
0167     if (ContextMenuSettings::showOpenInSplitView()) {
0168         addAction(m_mainWindow->actionCollection()->action(QStringLiteral("open_in_split_view")));
0169     }
0170 
0171     // Insert 'Open With' entries
0172     addOpenWithActions();
0173 
0174     // set up 'Create New' menu
0175     DolphinNewFileMenu *newFileMenu = new DolphinNewFileMenu(m_mainWindow->actionCollection()->action(QStringLiteral("create_dir")), m_mainWindow);
0176     newFileMenu->checkUpToDate();
0177     newFileMenu->setWorkingDirectory(m_fileInfo.url());
0178     newFileMenu->setEnabled(selectedItemsProps.supportsWriting());
0179     connect(newFileMenu, &DolphinNewFileMenu::fileCreated, newFileMenu, &DolphinNewFileMenu::deleteLater);
0180     connect(newFileMenu, &DolphinNewFileMenu::directoryCreated, newFileMenu, &DolphinNewFileMenu::deleteLater);
0181 
0182     QMenu *menu = newFileMenu->menu();
0183     menu->setTitle(i18nc("@title:menu Create new folder, file, link, etc.", "Create New"));
0184     menu->setIcon(QIcon::fromTheme(QStringLiteral("list-add")));
0185     addMenu(menu);
0186 
0187     addSeparator();
0188 }
0189 
0190 void DolphinContextMenu::addOpenParentFolderActions()
0191 {
0192     addAction(QIcon::fromTheme(QStringLiteral("document-open-folder")), i18nc("@action:inmenu", "Open Path"), [this]() {
0193         const QUrl url = m_fileInfo.targetUrl();
0194         const QUrl parentUrl = KIO::upUrl(url);
0195         m_mainWindow->changeUrl(parentUrl);
0196         m_mainWindow->activeViewContainer()->view()->markUrlsAsSelected({url});
0197         m_mainWindow->activeViewContainer()->view()->markUrlAsCurrent(url);
0198     });
0199 
0200     addAction(QIcon::fromTheme(QStringLiteral("tab-new")), i18nc("@action:inmenu", "Open Path in New Tab"), [this]() {
0201         m_mainWindow->openNewTab(KIO::upUrl(m_fileInfo.targetUrl()));
0202     });
0203 
0204     addAction(QIcon::fromTheme(QStringLiteral("window-new")), i18nc("@action:inmenu", "Open Path in New Window"), [this]() {
0205         Dolphin::openNewWindow({m_fileInfo.targetUrl()}, m_mainWindow, Dolphin::OpenNewWindowFlag::Select);
0206     });
0207 }
0208 
0209 void DolphinContextMenu::addItemContextMenu()
0210 {
0211     Q_ASSERT(!m_fileInfo.isNull());
0212 
0213     const KFileItemListProperties &selectedItemsProps = selectedItemsProperties();
0214 
0215     m_fileItemActions->setItemListProperties(selectedItemsProps);
0216 
0217     if (m_selectedItems.count() == 1) {
0218         // single files
0219         if (m_fileInfo.isDir()) {
0220             addDirectoryItemContextMenu();
0221         } else if (m_context & TimelineContext || m_context & SearchContext || m_context & RecentlyUsedContext) {
0222             addOpenWithActions();
0223 
0224             addOpenParentFolderActions();
0225 
0226             addSeparator();
0227         } else {
0228             // Insert 'Open With" entries
0229             addOpenWithActions();
0230         }
0231         if (m_fileInfo.isLink()) {
0232             addAction(m_mainWindow->actionCollection()->action(QStringLiteral("show_target")));
0233             addSeparator();
0234         }
0235     } else {
0236         // multiple files
0237         bool selectionHasOnlyDirs = true;
0238         for (const auto &item : std::as_const(m_selectedItems)) {
0239             const QUrl &url = DolphinView::openItemAsFolderUrl(item);
0240             if (url.isEmpty()) {
0241                 selectionHasOnlyDirs = false;
0242                 break;
0243             }
0244         }
0245 
0246         if (selectionHasOnlyDirs && ContextMenuSettings::showOpenInNewTab()) {
0247             // insert 'Open in new tab' entry
0248             addAction(m_mainWindow->actionCollection()->action(QStringLiteral("open_in_new_tabs")));
0249         }
0250         // Insert 'Open With" entries
0251         addOpenWithActions();
0252     }
0253 
0254     insertDefaultItemActions(selectedItemsProps);
0255 
0256     addAdditionalActions(selectedItemsProps);
0257 
0258     // insert 'Copy To' and 'Move To' sub menus
0259     if (ContextMenuSettings::showCopyMoveMenu()) {
0260         m_copyToMenu.setUrls(m_selectedItems.urlList());
0261         m_copyToMenu.setReadOnly(!selectedItemsProps.supportsWriting());
0262         m_copyToMenu.setAutoErrorHandlingEnabled(true);
0263         m_copyToMenu.addActionsTo(this);
0264     }
0265 
0266     if (m_mainWindow->isSplitViewEnabledInCurrentTab()) {
0267         if (ContextMenuSettings::showCopyToOtherSplitView()) {
0268             addAction(m_mainWindow->actionCollection()->action(QStringLiteral("copy_to_inactive_split_view")));
0269         }
0270 
0271         if (ContextMenuSettings::showMoveToOtherSplitView()) {
0272             addAction(m_mainWindow->actionCollection()->action(QStringLiteral("move_to_inactive_split_view")));
0273         }
0274     }
0275 
0276     // insert 'Properties...' entry
0277     addSeparator();
0278     QAction *propertiesAction = m_mainWindow->actionCollection()->action(QStringLiteral("properties"));
0279     addAction(propertiesAction);
0280 }
0281 
0282 void DolphinContextMenu::addViewportContextMenu()
0283 {
0284     const KFileItemListProperties baseUrlProperties(KFileItemList() << baseFileItem());
0285     m_fileItemActions->setItemListProperties(baseUrlProperties);
0286 
0287     // Set up and insert 'Create New' menu
0288     KNewFileMenu *newFileMenu = m_mainWindow->newFileMenu();
0289     newFileMenu->checkUpToDate();
0290     newFileMenu->setWorkingDirectory(m_baseUrl);
0291     addMenu(newFileMenu->menu());
0292 
0293     // Show "open with" menu items even if the dir is empty, because there are legitimate
0294     // use cases for this, such as opening an empty dir in Kate or VSCode or something
0295     addOpenWithActions();
0296 
0297     QAction *pasteAction = createPasteAction();
0298     if (pasteAction) {
0299         addAction(pasteAction);
0300     }
0301 
0302     // Insert 'Add to Places' entry if it's not already in the places panel
0303     if (ContextMenuSettings::showAddToPlaces() && !placeExists(m_mainWindow->activeViewContainer()->url())) {
0304         addAction(m_mainWindow->actionCollection()->action(QStringLiteral("add_to_places")));
0305     }
0306     addSeparator();
0307 
0308     // Insert 'Sort By' and 'View Mode'
0309     if (ContextMenuSettings::showSortBy()) {
0310         addAction(m_mainWindow->actionCollection()->action(QStringLiteral("sort")));
0311     }
0312     if (ContextMenuSettings::showViewMode()) {
0313         addAction(m_mainWindow->actionCollection()->action(QStringLiteral("view_mode")));
0314     }
0315     if (ContextMenuSettings::showSortBy() || ContextMenuSettings::showViewMode()) {
0316         addSeparator();
0317     }
0318 
0319     addAdditionalActions(baseUrlProperties);
0320 
0321     addSeparator();
0322 
0323     QAction *propertiesAction = m_mainWindow->actionCollection()->action(QStringLiteral("properties"));
0324     addAction(propertiesAction);
0325 }
0326 
0327 void DolphinContextMenu::insertDefaultItemActions(const KFileItemListProperties &properties)
0328 {
0329     const KActionCollection *collection = m_mainWindow->actionCollection();
0330 
0331     // Insert 'Cut', 'Copy', 'Copy Location' and 'Paste'
0332     addAction(collection->action(KStandardAction::name(KStandardAction::Cut)));
0333     addAction(collection->action(KStandardAction::name(KStandardAction::Copy)));
0334     if (ContextMenuSettings::showCopyLocation()) {
0335         QAction *copyPathAction = collection->action(QString("copy_location"));
0336         copyPathAction->setEnabled(m_selectedItems.size() == 1);
0337         addAction(copyPathAction);
0338     }
0339     QAction *pasteAction = createPasteAction();
0340     if (pasteAction) {
0341         addAction(pasteAction);
0342     }
0343 
0344     // Insert 'Duplicate Here'
0345     if (ContextMenuSettings::showDuplicateHere()) {
0346         addAction(m_mainWindow->actionCollection()->action(QStringLiteral("duplicate")));
0347     }
0348 
0349     // Insert 'Rename'
0350     addAction(collection->action(KStandardAction::name(KStandardAction::RenameFile)));
0351 
0352     // Insert 'Add to Places' entry if appropriate
0353     if (ContextMenuSettings::showAddToPlaces() && m_selectedItems.count() == 1 && m_fileInfo.isDir() && !placeExists(m_fileInfo.url())) {
0354         addAction(m_mainWindow->actionCollection()->action(QStringLiteral("add_to_places")));
0355     }
0356 
0357     addSeparator();
0358 
0359     // Insert 'Move to Trash' and/or 'Delete'
0360     const bool showDeleteAction = (KSharedConfig::openConfig()->group(QStringLiteral("KDE")).readEntry("ShowDeleteCommand", false) || !properties.isLocal());
0361     const bool showMoveToTrashAction = (properties.isLocal() && properties.supportsMoving());
0362 
0363     if (showDeleteAction && showMoveToTrashAction) {
0364         delete m_removeAction;
0365         m_removeAction = nullptr;
0366         addAction(m_mainWindow->actionCollection()->action(KStandardAction::name(KStandardAction::MoveToTrash)));
0367         addAction(m_mainWindow->actionCollection()->action(KStandardAction::name(KStandardAction::DeleteFile)));
0368     } else if (showDeleteAction && !showMoveToTrashAction) {
0369         addAction(m_mainWindow->actionCollection()->action(KStandardAction::name(KStandardAction::DeleteFile)));
0370     } else {
0371         if (!m_removeAction) {
0372             m_removeAction = new DolphinRemoveAction(this, m_mainWindow->actionCollection());
0373         }
0374         addAction(m_removeAction);
0375         m_removeAction->update();
0376     }
0377 }
0378 
0379 bool DolphinContextMenu::placeExists(const QUrl &url) const
0380 {
0381     const KFilePlacesModel *placesModel = DolphinPlacesModelSingleton::instance().placesModel();
0382 
0383     const auto &matchedPlaces = placesModel->match(placesModel->index(0, 0), KFilePlacesModel::UrlRole, url, 1, Qt::MatchExactly);
0384 
0385     return !matchedPlaces.isEmpty();
0386 }
0387 
0388 QAction *DolphinContextMenu::createPasteAction()
0389 {
0390     QAction *action = nullptr;
0391     KFileItem destItem;
0392     if (!m_fileInfo.isNull() && m_selectedItems.count() <= 1) {
0393         destItem = m_fileInfo;
0394     } else {
0395         destItem = baseFileItem();
0396     }
0397 
0398     if (!destItem.isNull() && destItem.isDir()) {
0399         const QMimeData *mimeData = QApplication::clipboard()->mimeData();
0400         bool canPaste;
0401         const QString text = KIO::pasteActionText(mimeData, &canPaste, destItem);
0402         if (canPaste) {
0403             if (destItem == m_fileInfo) {
0404                 // if paste destination is a selected folder
0405                 action = new QAction(QIcon::fromTheme(QStringLiteral("edit-paste")), text, this);
0406                 connect(action, &QAction::triggered, m_mainWindow, &DolphinMainWindow::pasteIntoFolder);
0407             } else {
0408                 action = m_mainWindow->actionCollection()->action(KStandardAction::name(KStandardAction::Paste));
0409             }
0410         }
0411     }
0412 
0413     return action;
0414 }
0415 
0416 KFileItemListProperties &DolphinContextMenu::selectedItemsProperties() const
0417 {
0418     if (!m_selectedItemsProperties) {
0419         m_selectedItemsProperties = new KFileItemListProperties(m_selectedItems);
0420     }
0421     return *m_selectedItemsProperties;
0422 }
0423 
0424 KFileItem DolphinContextMenu::baseFileItem()
0425 {
0426     if (!m_baseFileItem) {
0427         const DolphinView *view = m_mainWindow->activeViewContainer()->view();
0428         KFileItem baseItem = view->rootItem();
0429         if (baseItem.isNull() || baseItem.url() != m_baseUrl) {
0430             m_baseFileItem = new KFileItem(m_baseUrl);
0431         } else {
0432             m_baseFileItem = new KFileItem(baseItem);
0433         }
0434     }
0435     return *m_baseFileItem;
0436 }
0437 
0438 void DolphinContextMenu::addOpenWithActions()
0439 {
0440     // insert 'Open With...' action or sub menu
0441     m_fileItemActions->insertOpenWithActionsTo(nullptr, this, QStringList{qApp->desktopFileName()});
0442 }
0443 
0444 void DolphinContextMenu::addAdditionalActions(const KFileItemListProperties &props)
0445 {
0446     addSeparator();
0447 
0448     QList<QAction *> additionalActions;
0449     if (props.isLocal() && ContextMenuSettings::showOpenTerminal()) {
0450         additionalActions << m_mainWindow->actionCollection()->action(QStringLiteral("open_terminal_here"));
0451     }
0452     m_fileItemActions->addActionsTo(this, KFileItemActions::MenuActionSource::All, additionalActions);
0453 
0454     const DolphinView *view = m_mainWindow->activeViewContainer()->view();
0455     const QList<QAction *> versionControlActions = view->versionControlActions(m_selectedItems);
0456     if (!versionControlActions.isEmpty()) {
0457         addActions(versionControlActions);
0458         addSeparator();
0459     }
0460 }
0461 
0462 #include "moc_dolphincontextmenu.cpp"