File indexing completed on 2024-04-28 16:51:33
0001 /* 0002 SPDX-FileCopyrightText: 2019 Kai Uwe Broulik <kde@privat.broulik.de> 0003 0004 SPDX-License-Identifier: MIT 0005 */ 0006 0007 #include "purposeplugin.h" 0008 0009 #include <QClipboard> 0010 #include <QGuiApplication> 0011 #include <QJsonArray> 0012 #include <QJsonObject> 0013 0014 #include <KIO/MimeTypeFinderJob> 0015 0016 #include <Purpose/AlternativesModel> 0017 #include <PurposeWidgets/Menu> 0018 0019 PurposePlugin::PurposePlugin(QObject *parent) 0020 : AbstractBrowserPlugin(QStringLiteral("purpose"), 1, parent) 0021 { 0022 } 0023 0024 PurposePlugin::~PurposePlugin() 0025 { 0026 onUnload(); 0027 } 0028 0029 bool PurposePlugin::onUnload() 0030 { 0031 m_menu.reset(); 0032 return true; 0033 } 0034 0035 QJsonObject PurposePlugin::handleData(int serial, const QString &event, const QJsonObject &data) 0036 { 0037 if (event == QLatin1String("share")) { 0038 if (m_pendingReplySerial != -1 || (m_menu && m_menu->isVisible())) { 0039 return { 0040 {QStringLiteral("success"), false}, 0041 {QStringLiteral("errorCode"), QStringLiteral("BUSY")}, 0042 }; 0043 } 0044 0045 // store request serial for asynchronous reply 0046 m_pendingReplySerial = serial; 0047 0048 const QJsonObject shareData = data.value(QStringLiteral("data")).toObject(); 0049 0050 QString title = shareData.value(QStringLiteral("title")).toString(); 0051 const QString text = shareData.value(QStringLiteral("text")).toString(); 0052 const QString urlString = shareData.value(QStringLiteral("url")).toString(); 0053 0054 // Purpose ShareUrl plug-in type mandates a title. 0055 if (title.isEmpty()) { 0056 title = urlString; 0057 } 0058 0059 if (!m_menu) { 0060 m_menu.reset(new Purpose::Menu()); 0061 m_menu->model()->setPluginType(QStringLiteral("ShareUrl")); 0062 0063 connect(m_menu.data(), &QMenu::aboutToShow, this, [this] { 0064 m_menu->setProperty("actionInvoked", false); 0065 }); 0066 0067 connect(m_menu.data(), &QMenu::aboutToHide, this, [this] { 0068 // aboutToHide is emitted before an action is triggered and activeAction() is 0069 // the action currently hovered. This means we can't properly tell that the prompt 0070 // got canceled, when hovering an action and then hitting Escape to close the menu. 0071 // Hence delaying this and checking if an action got invoked :( 0072 0073 QMetaObject::invokeMethod( 0074 this, 0075 [this] { 0076 if (!m_menu->property("actionInvoked").toBool()) { 0077 sendPendingReply(false, 0078 { 0079 {QStringLiteral("errorCode"), QStringLiteral("CANCELED")}, 0080 }); 0081 } 0082 }, 0083 Qt::QueuedConnection); 0084 }); 0085 0086 connect(m_menu.data(), &QMenu::triggered, this, [this] { 0087 m_menu->setProperty("actionInvoked", true); 0088 }); 0089 0090 connect(m_menu.data(), &Purpose::Menu::finished, this, [this](const QJsonObject &output, int errorCode, const QString &errorMessage) { 0091 if (errorCode) { 0092 debug() << "Error:" << errorCode << errorMessage; 0093 0094 sendPendingReply(false, 0095 { 0096 {QStringLiteral("errorCode"), errorCode}, 0097 {QStringLiteral("errorMessage"), errorMessage}, 0098 }); 0099 return; 0100 } 0101 0102 const QString url = output.value(QStringLiteral("url")).toString(); 0103 if (!url.isEmpty()) { 0104 // Do this here rather than on the extension side to avoid having to request an additional permission after updating 0105 QGuiApplication::clipboard()->setText(url); 0106 } 0107 0108 debug() << "Finished:" << output; 0109 sendPendingReply(true, {{QStringLiteral("response"), output}}); 0110 }); 0111 } 0112 0113 QJsonObject shareJson; 0114 0115 if (!title.isEmpty()) { 0116 shareJson.insert(QStringLiteral("title"), title); 0117 } 0118 0119 QJsonArray urls; 0120 if (!urlString.isEmpty()) { 0121 urls.append(urlString); 0122 } 0123 // Sends even text as URL... 0124 if (!text.isEmpty()) { 0125 urls.append(text); 0126 } 0127 0128 if (!urls.isEmpty()) { 0129 shareJson.insert(QStringLiteral("urls"), urls); 0130 } 0131 0132 if (!text.isEmpty()) { 0133 showShareMenu(shareJson, QStringLiteral("text/plain")); 0134 return {}; 0135 } 0136 0137 if (!urls.isEmpty()) { 0138 auto *mimeJob = new KIO::MimeTypeFinderJob(QUrl(urlString)); 0139 mimeJob->setAuthenticationPromptEnabled(false); 0140 connect(mimeJob, &KIO::MimeTypeFinderJob::result, this, [this, mimeJob, shareJson] { 0141 showShareMenu(shareJson, mimeJob->mimeType()); 0142 }); 0143 mimeJob->start(); 0144 return {}; 0145 } 0146 0147 // navigator.share({title: "foo"}) is valid but makes no sense 0148 // and we also cannot share via Purpose without "urls" 0149 m_pendingReplySerial = -1; 0150 return { 0151 {QStringLiteral("success"), false}, 0152 {QStringLiteral("errorCode"), QStringLiteral("INVALID_ARGUMENT")}, 0153 }; 0154 } 0155 0156 return {}; 0157 } 0158 0159 void PurposePlugin::sendPendingReply(bool success, const QJsonObject &data) 0160 { 0161 QJsonObject reply = data; 0162 reply.insert(QStringLiteral("success"), success); 0163 0164 sendReply(m_pendingReplySerial, reply); 0165 m_pendingReplySerial = -1; 0166 } 0167 0168 void PurposePlugin::showShareMenu(const QJsonObject &data, const QString &mimeType) 0169 { 0170 QJsonObject shareData = data; 0171 0172 if (!mimeType.isEmpty() && mimeType != QLatin1String("application/octet-stream")) { 0173 shareData.insert(QStringLiteral("mimeType"), mimeType); 0174 } else { 0175 shareData.insert(QStringLiteral("mimeType"), QStringLiteral("*/*")); 0176 } 0177 0178 debug() << "Share mime type" << mimeType << "with data" << data; 0179 0180 m_menu->model()->setInputData(shareData); 0181 m_menu->reload(); 0182 0183 m_menu->popup(QCursor::pos()); 0184 }