File indexing completed on 2024-12-29 05:06:00

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