File indexing completed on 2024-05-12 05:37:07

0001 /*
0002     SPDX-FileCopyrightText: 2016, 2019 Kai Uwe Broulik <kde@privat.broulik.de>
0003 
0004     SPDX-License-Identifier: LGPL-2.1-or-later
0005 */
0006 
0007 #include "filemenu.h"
0008 
0009 #include <QApplication>
0010 #include <QClipboard>
0011 #include <QIcon>
0012 #include <QMenu>
0013 #include <QMimeData>
0014 #include <QQuickWindow>
0015 #include <QTimer>
0016 
0017 #include <KConfigGroup>
0018 #include <KFileItemActions>
0019 #include <KFileItemListProperties>
0020 #include <KLocalizedString>
0021 #include <KPropertiesDialog>
0022 #include <KProtocolManager>
0023 #include <KSharedConfig>
0024 #include <KStandardAction>
0025 #include <KUrlMimeData>
0026 
0027 #include <KIO/CopyJob> // for KIO::trash
0028 #include <KIO/DeleteJob>
0029 #include <KIO/FileUndoManager>
0030 #include <KIO/JobUiDelegate>
0031 #include <KIO/OpenFileManagerWindowJob>
0032 #include <KIO/WidgetsAskUserActionHandler>
0033 
0034 using namespace Qt::StringLiterals;
0035 
0036 FileMenu::FileMenu(QObject *parent)
0037     : QObject(parent)
0038 {
0039 }
0040 
0041 FileMenu::~FileMenu() = default;
0042 
0043 QUrl FileMenu::url() const
0044 {
0045     return m_url;
0046 }
0047 
0048 void FileMenu::setUrl(const QUrl &url)
0049 {
0050     if (m_url != url) {
0051         m_url = url;
0052         Q_EMIT urlChanged();
0053     }
0054 }
0055 
0056 QQuickItem *FileMenu::visualParent() const
0057 {
0058     return m_visualParent.data();
0059 }
0060 
0061 void FileMenu::setVisualParent(QQuickItem *visualParent)
0062 {
0063     if (m_visualParent.data() == visualParent) {
0064         return;
0065     }
0066 
0067     if (m_visualParent) {
0068         disconnect(m_visualParent.data(), nullptr, this, nullptr);
0069     }
0070     m_visualParent = visualParent;
0071     if (m_visualParent) {
0072         connect(m_visualParent.data(), &QObject::destroyed, this, &FileMenu::visualParentChanged);
0073     }
0074     Q_EMIT visualParentChanged();
0075 }
0076 
0077 bool FileMenu::visible() const
0078 {
0079     return m_visible;
0080 }
0081 
0082 void FileMenu::setVisible(bool visible)
0083 {
0084     if (m_visible == visible) {
0085         return;
0086     }
0087 
0088     if (visible) {
0089         open(0, 0);
0090     } else {
0091         // TODO warning or close?
0092     }
0093 }
0094 
0095 void FileMenu::open(int x, int y)
0096 {
0097     if (!m_visualParent || !m_visualParent->window()) {
0098         return;
0099     }
0100 
0101     if (!m_url.isValid()) {
0102         return;
0103     }
0104 
0105     KFileItem fileItem(m_url);
0106 
0107     QMenu *menu = new QMenu();
0108     menu->setAttribute(Qt::WA_DeleteOnClose, true);
0109     connect(menu, &QMenu::triggered, this, &FileMenu::actionTriggered);
0110 
0111     connect(menu, &QMenu::aboutToHide, this, [this] {
0112         m_visible = false;
0113         Q_EMIT visibleChanged();
0114     });
0115 
0116     if (KProtocolManager::supportsListing(m_url)) {
0117         QAction *openContainingFolderAction = menu->addAction(QIcon::fromTheme(QStringLiteral("folder-open")), i18n("Open Containing Folder"));
0118         connect(openContainingFolderAction, &QAction::triggered, [this] {
0119             KIO::highlightInFileManager({m_url});
0120         });
0121     }
0122 
0123     KFileItemActions *actions = new KFileItemActions(menu);
0124     KFileItemListProperties itemProperties(KFileItemList({fileItem}));
0125     actions->setItemListProperties(itemProperties);
0126     actions->setParentWidget(menu);
0127 
0128     actions->insertOpenWithActionsTo(nullptr, menu, QStringList());
0129 
0130     // KStandardAction? But then the Ctrl+C shortcut makes no sense in this context
0131     QAction *copyAction = menu->addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("&Copy"));
0132     connect(copyAction, &QAction::triggered, this, [fileItem] {
0133         // inspired by KDirModel::mimeData()
0134         QMimeData *data = new QMimeData(); // who cleans it up?
0135         KUrlMimeData::setUrls({fileItem.url()}, {fileItem.mostLocalUrl()}, data);
0136         QApplication::clipboard()->setMimeData(data);
0137     });
0138 
0139     QAction *copyPathAction = menu->addAction(QIcon::fromTheme(QStringLiteral("edit-copy-path")), i18nc("@action:incontextmenu", "Copy Location"));
0140     connect(copyPathAction, &QAction::triggered, this, [fileItem] {
0141         QString path = fileItem.localPath();
0142         if (path.isEmpty()) {
0143             path = fileItem.url().toDisplayString();
0144         }
0145         QApplication::clipboard()->setText(path);
0146     });
0147 
0148     menu->addSeparator();
0149 
0150     const bool canTrash = itemProperties.isLocal() && itemProperties.supportsMoving();
0151     if (canTrash) {
0152         auto moveToTrashLambda = [this] {
0153             auto handler = new KIO::WidgetsAskUserActionHandler(this);
0154             connect(handler, &KIO::WidgetsAskUserActionHandler::askUserDeleteResult, [handler](bool allow, const QList<QUrl> &urls) {
0155                 if (allow) {
0156                     auto job = KIO::trash(urls);
0157                     job->uiDelegate()->setAutoErrorHandlingEnabled(true);
0158                     KIO::FileUndoManager::self()->recordJob(KIO::FileUndoManager::Trash, urls, QUrl(QStringLiteral("trash:/")), job);
0159                 }
0160                 handler->deleteLater();
0161             });
0162             handler->askUserDelete({m_url}, KIO::AskUserActionInterface::Trash, KIO::AskUserActionInterface::DefaultConfirmation);
0163         };
0164         QAction *moveToTrashAction = KStandardAction::moveToTrash(this, moveToTrashLambda, menu);
0165         moveToTrashAction->setShortcut({}); // Can't focus notification to press Delete
0166         menu->addAction(moveToTrashAction);
0167     }
0168 
0169     KConfigGroup cg(KSharedConfig::openConfig(), u"KDE"_s);
0170     const bool showDeleteCommand = cg.readEntry("ShowDeleteCommand", false);
0171 
0172     if (itemProperties.supportsDeleting() && (!canTrash || showDeleteCommand)) {
0173         auto deleteLambda = [this] {
0174             auto handler = new KIO::WidgetsAskUserActionHandler(this);
0175             connect(handler, &KIO::WidgetsAskUserActionHandler::askUserDeleteResult, [handler](bool allow, const QList<QUrl> &urls) {
0176                 if (allow) {
0177                     auto job = KIO::del(urls);
0178                     job->uiDelegate()->setAutoErrorHandlingEnabled(true);
0179                 }
0180                 handler->deleteLater();
0181             });
0182             handler->askUserDelete({m_url}, KIO::AskUserActionInterface::Delete, KIO::AskUserActionInterface::DefaultConfirmation);
0183         };
0184         QAction *deleteAction = KStandardAction::deleteFile(this, deleteLambda, menu);
0185         deleteAction->setShortcut({});
0186         menu->addAction(deleteAction);
0187     }
0188 
0189     menu->addSeparator();
0190 
0191     actions->addActionsTo(menu);
0192 
0193     menu->addSeparator();
0194 
0195     QAction *propertiesAction = menu->addAction(QIcon::fromTheme(QStringLiteral("document-properties")), i18n("Properties"));
0196     connect(propertiesAction, &QAction::triggered, [fileItem] {
0197         KPropertiesDialog *dialog = new KPropertiesDialog(fileItem.url());
0198         dialog->setAttribute(Qt::WA_DeleteOnClose);
0199         dialog->show();
0200     });
0201 
0202     // this is a workaround where Qt will fail to realize a mouse has been released
0203     // this happens if a window which does not accept focus spawns a new window that takes focus and X grab
0204     // whilst the mouse is depressed
0205     // https://bugreports.qt.io/browse/QTBUG-59044
0206     // this causes the next click to go missing
0207 
0208     // by releasing manually we avoid that situation
0209     auto ungrabMouseHack = [this]() {
0210         if (m_visualParent && m_visualParent->window() && m_visualParent->window()->mouseGrabberItem()) {
0211             m_visualParent->window()->mouseGrabberItem()->ungrabMouse();
0212         }
0213     };
0214 
0215     QTimer::singleShot(0, m_visualParent, ungrabMouseHack);
0216     // end workaround
0217 
0218     QPoint pos;
0219     if (x == -1 && y == -1) { // align "bottom left of visualParent"
0220         menu->adjustSize();
0221 
0222         pos = m_visualParent->mapToGlobal(QPointF(0, m_visualParent->height())).toPoint();
0223 
0224         if (!qApp->isRightToLeft()) {
0225             pos.rx() += m_visualParent->width();
0226             pos.rx() -= menu->width();
0227         }
0228     } else {
0229         pos = m_visualParent->mapToGlobal(QPointF(x, y)).toPoint();
0230     }
0231 
0232     menu->setAttribute(Qt::WA_TranslucentBackground);
0233     menu->winId();
0234     menu->windowHandle()->setTransientParent(m_visualParent->window());
0235     menu->popup(pos);
0236 
0237     m_visible = true;
0238     Q_EMIT visibleChanged();
0239 }