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 }