1 /*
2     SPDX-FileCopyrightText: 2015 Gregor Mi <codestruct@posteo.org>
3 
4     SPDX-License-Identifier: LGPL-2.1-or-later
5 */
6 
7 #include "kmoretoolsmenufactory.h"
8 
9 #include "kmoretools_p.h"
10 #include "kmoretoolspresets_p.h"
11 #include "knewstuff_debug.h"
12 #include <QDebug>
13 #include <QStorageInfo>
14 
15 #include <KDialogJobUiDelegate>
16 #include <KIO/ApplicationLauncherJob>
17 #include <KLocalizedString>
18 #include <KNS3/KMoreTools>
19 #include <KNS3/KMoreToolsPresets>
20 
21 class KMoreToolsMenuFactoryPrivate
22 {
23 public:
24     // Note that this object must live long enough in case the user opens
25     // the "Configure..." dialog
26     KMoreTools *kmt = nullptr;
27 
28     QMenu *menu = nullptr;
29     QWidget *parentWidget = nullptr;
30 };
31 
32 class KMoreToolsLazyMenu : public QMenu
33 {
34 private Q_SLOTS:
onAboutToShow()35     void onAboutToShow()
36     {
37         // qDebug() << "onAboutToShow";
38         clear();
39         m_aboutToShowFunc(this);
40     }
41 
42 public:
KMoreToolsLazyMenu(QWidget * parent=nullptr)43     KMoreToolsLazyMenu(QWidget *parent = nullptr)
44         : QMenu(parent)
45     {
46         connect(this, &QMenu::aboutToShow, this, &KMoreToolsLazyMenu::onAboutToShow);
47     }
48 
setAboutToShowAction(std::function<void (QMenu *)> aboutToShowFunc)49     void setAboutToShowAction(std::function<void(QMenu *)> aboutToShowFunc)
50     {
51         m_aboutToShowFunc = aboutToShowFunc;
52     }
53 
54 private:
55     std::function<void(QMenu *)> m_aboutToShowFunc;
56 };
57 
KMoreToolsMenuFactory(const QString & uniqueId)58 KMoreToolsMenuFactory::KMoreToolsMenuFactory(const QString &uniqueId)
59     : d(new KMoreToolsMenuFactoryPrivate())
60 {
61     d->kmt = new KMoreTools(uniqueId);
62     Q_UNUSED(m_off)
63 }
64 
~KMoreToolsMenuFactory()65 KMoreToolsMenuFactory::~KMoreToolsMenuFactory()
66 {
67     if (d->menu && !d->menu->parent()) {
68         delete d->menu;
69     }
70 
71     delete d->kmt;
72 
73     delete d;
74 }
75 
runApplication(const KService::Ptr & service,const QList<QUrl> & urls)76 static void runApplication(const KService::Ptr &service, const QList<QUrl> &urls)
77 {
78     auto *job = new KIO::ApplicationLauncherJob(service);
79     job->setUrls(urls);
80     job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, nullptr));
81     job->start();
82 }
83 
84 // "file static" => no symbol will be exported
addItemFromKmtService(KMoreToolsMenuBuilder * menuBuilder,QMenu * menu,KMoreToolsService * kmtService,const QUrl & url,bool isMoreSection)85 static void addItemFromKmtService(KMoreToolsMenuBuilder *menuBuilder, QMenu *menu, KMoreToolsService *kmtService, const QUrl &url, bool isMoreSection)
86 {
87     auto menuItem = menuBuilder->addMenuItem(kmtService, isMoreSection ? KMoreTools::MenuSection_More : KMoreTools::MenuSection_Main);
88 
89     if (kmtService->isInstalled()) {
90         auto kService = kmtService->installedService();
91 
92         if (!kService) {
93             // if the corresponding desktop file is not installed
94             // then the isInstalled was true because of the Exec line check
95             // and we use the desktopfile provided by KMoreTools.
96             // Otherwise *kService would crash.
97             qCDebug(KNEWSTUFF) << "Desktop file not installed:" << kmtService->desktopEntryName() << "=> Use desktop file provided by KMoreTools";
98             kService = kmtService->kmtProvidedService();
99         }
100 
101         if (!url.isEmpty() && kmtService->maxUrlArgCount() > 0) {
102             menu->connect(menuItem->action(), &QAction::triggered, menu, [kService, url](bool) {
103                 runApplication(kService, {url});
104             });
105         } else {
106             menu->connect(menuItem->action(), &QAction::triggered, menu, [kService](bool) {
107                 runApplication(kService, {});
108             });
109         }
110     }
111 }
112 
113 // "file static" => no symbol will be exported
addItemsFromKmtServiceList(KMoreToolsMenuBuilder * menuBuilder,QMenu * menu,const QList<KMoreToolsService * > & kmtServiceList,const QUrl & url,bool isMoreSection,QString firstMoreSectionDesktopEntryName)114 static void addItemsFromKmtServiceList(KMoreToolsMenuBuilder *menuBuilder,
115                                        QMenu *menu,
116                                        const QList<KMoreToolsService *> &kmtServiceList,
117                                        const QUrl &url,
118                                        bool isMoreSection,
119                                        QString firstMoreSectionDesktopEntryName)
120 {
121     for (auto kmtService : kmtServiceList) {
122         // Check the pointer just in case a null pointer got in somewhere
123         if (!kmtService) {
124             continue;
125         }
126         if (kmtService->desktopEntryName() == firstMoreSectionDesktopEntryName) {
127             // once we reach the potential first "more section desktop entry name"
128             // all remaining services are added to the more section by default
129             isMoreSection = true;
130         }
131         addItemFromKmtService(menuBuilder, menu, kmtService, url, isMoreSection);
132     }
133 }
134 
135 /**
136  * "file static" => no symbol will be exported
137  * @param isMoreSection: true => all items will be added into the more section
138  * @param firstMoreSectionDesktopEntryName: only valid when @p isMoreSection is false:
139  *                                           see KMoreToolsPresets::registerServicesByGroupingNames
140  */
addItemsForGroupingNameWithSpecialHandling(KMoreToolsMenuBuilder * menuBuilder,QMenu * menu,QList<KMoreToolsService * > kmtServiceList,const QString & groupingName,const QUrl & url,bool isMoreSection,QString firstMoreSectionDesktopEntryName)141 static void addItemsForGroupingNameWithSpecialHandling(KMoreToolsMenuBuilder *menuBuilder,
142                                                        QMenu *menu,
143                                                        QList<KMoreToolsService *> kmtServiceList,
144                                                        const QString &groupingName,
145                                                        const QUrl &url,
146                                                        bool isMoreSection,
147                                                        QString firstMoreSectionDesktopEntryName)
148 {
149     //
150     // special handlings
151     //
152     if (groupingName == QLatin1String("disk-usage") && !url.isEmpty()) {
153         //
154         // "disk-usage" plus a given URL. If no url is given there is no need
155         // for special handling
156         //
157 
158         auto filelightAppIter = std::find_if(kmtServiceList.begin(), kmtServiceList.end(), [](KMoreToolsService *s) {
159             return s->desktopEntryName() == QLatin1String("org.kde.filelight");
160         });
161 
162         if (filelightAppIter != kmtServiceList.end()) {
163             auto filelightApp = *filelightAppIter;
164 
165             // because we later add all remaining items
166             kmtServiceList.removeOne(filelightApp);
167 
168             if (url.isLocalFile()) { // 2015-01-12: Filelight can handle FTP connections
169                 // but KIO/kioexec cannot (bug or feature?), so we
170                 // don't offer it in this case
171 
172                 const auto filelight1Item = menuBuilder->addMenuItem(filelightApp);
173 
174                 if (filelightApp->isInstalled()) {
175                     const auto filelightService = filelightApp->installedService();
176 
177                     filelight1Item->action()->setText(
178                         filelightApp->formatString(i18nc("@action:inmenu %1=\"$GenericName\"", "%1 - current folder", QStringLiteral("$GenericName"))));
179                     menu->connect(filelight1Item->action(), &QAction::triggered, menu, [filelightService, url](bool) {
180                         runApplication(filelightService, {url});
181                     });
182 
183                     const auto filelight2Item = menuBuilder->addMenuItem(filelightApp);
184                     filelight2Item->action()->setText(
185                         filelightApp->formatString(i18nc("@action:inmenu %1=\"$GenericName\"", "%1 - current device", QStringLiteral("$GenericName"))));
186                     menu->connect(filelight2Item->action(), &QAction::triggered, menu, [filelightService, url](bool) {
187                         const QStorageInfo info(url.toLocalFile());
188 
189                         if (info.isValid() && info.isReady()) {
190                             runApplication(filelightService, {QUrl::fromLocalFile(info.rootPath())});
191                         }
192                     });
193                 }
194             }
195 
196             auto filelight3Item = menuBuilder->addMenuItem(filelightApp, KMoreTools::MenuSection_More);
197             if (filelightApp->isInstalled()) {
198                 filelight3Item->action()->setText(
199                     filelightApp->formatString(i18nc("@action:inmenu %1=\"$GenericName\"", "%1 - all devices", QStringLiteral("$GenericName"))));
200                 const auto filelightService = filelightApp->installedService();
201                 menu->connect(filelight3Item->action(), &QAction::triggered, menu, [filelightService](bool) {
202                     runApplication(filelightService, {});
203                 });
204             }
205         } else {
206             qWarning() << "org.kde.filelight should be present in KMoreTools but it is not!";
207         }
208 
209     } else if (groupingName == QLatin1String("disk-partitions")) {
210         // better because the Partition editors all have the same GenericName
211         menuBuilder->setInitialItemTextTemplate(QStringLiteral("$GenericName ($Name)"));
212 
213         addItemsFromKmtServiceList(menuBuilder, menu, kmtServiceList, url, isMoreSection, firstMoreSectionDesktopEntryName);
214 
215         menuBuilder->setInitialItemTextTemplate(QStringLiteral("$GenericName")); // set back to default
216 
217         return; // skip processing remaining list (would result in duplicates)
218 
219     } else if (groupingName == QLatin1String("git-clients-and-actions")) {
220         // Here we change the default item text and make sure that the url
221         // argument is properly handled.
222         //
223 
224         menuBuilder->setInitialItemTextTemplate(QStringLiteral("$Name")); // just use the application name
225 
226         for (auto kmtService : std::as_const(kmtServiceList)) {
227             // Check the pointer just in case a null pointer got in somewhere
228             if (!kmtService) {
229                 continue;
230             }
231             QUrl argUrl = url;
232 
233             if (url.isLocalFile()) { // this can only be done for local files, remote urls probably won't work for git clients anyway
234                 // by default we need an URL pointing to a directory
235                 // (this impl currently leads to wrong behaviour if the root dir of a git repo is chosen because it always goes one level up)
236                 argUrl = KmtUrlUtil::localFileAbsoluteDir(url); // needs local file
237 
238                 if (kmtService->desktopEntryName() == _("git-cola-view-history.kmt-edition")) {
239                     // in this case we need the file because we would like to see its history
240                     argUrl = url;
241                 }
242             }
243 
244             addItemFromKmtService(menuBuilder, menu, kmtService, argUrl, isMoreSection);
245         }
246 
247         menuBuilder->setInitialItemTextTemplate(QStringLiteral("$GenericName")); // set back to default
248 
249         return; // skip processing remaining list (would result in duplicates)
250     }
251 
252     //
253     // default handling (or process remaining list)
254     //
255     menuBuilder->setInitialItemTextTemplate(QStringLiteral("$Name")); // just use the application name
256     addItemsFromKmtServiceList(menuBuilder, menu, kmtServiceList, url, isMoreSection, firstMoreSectionDesktopEntryName);
257     menuBuilder->setInitialItemTextTemplate(QStringLiteral("$GenericName")); // set back to default
258 }
259 
createMenuFromGroupingNames(const QStringList & groupingNames,const QUrl & url)260 QMenu *KMoreToolsMenuFactory::createMenuFromGroupingNames(const QStringList &groupingNames, const QUrl &url)
261 {
262     delete d->menu;
263 
264     auto menu = new KMoreToolsLazyMenu(d->parentWidget);
265     menu->setAboutToShowAction([this, groupingNames, url](QMenu *m) {
266         fillMenuFromGroupingNames(m, groupingNames, url);
267     });
268     d->menu = menu;
269 
270     return d->menu;
271 }
272 
fillMenuFromGroupingNames(QMenu * menu,const QStringList & groupingNames,const QUrl & url)273 void KMoreToolsMenuFactory::fillMenuFromGroupingNames(QMenu *menu, const QStringList &groupingNames, const QUrl &url)
274 {
275     const auto menuBuilder = d->kmt->menuBuilder();
276     menuBuilder->clear();
277 
278     bool isMoreSection = false;
279 
280     for (const auto &groupingName : groupingNames) {
281         if (groupingName == QLatin1String("more:")) {
282             isMoreSection = true;
283             continue;
284         }
285 
286         QString firstMoreSectionDesktopEntryName;
287         auto kmtServiceList = KMoreToolsPresetsPrivate::registerServicesByGroupingNames(&firstMoreSectionDesktopEntryName, d->kmt, {groupingName});
288 
289         addItemsForGroupingNameWithSpecialHandling(menuBuilder, menu, kmtServiceList, groupingName, url, isMoreSection, firstMoreSectionDesktopEntryName);
290     }
291 
292     menuBuilder->buildByAppendingToMenu(menu);
293 }
294 
setParentWidget(QWidget * widget)295 void KMoreToolsMenuFactory::setParentWidget(QWidget *widget)
296 {
297     d->parentWidget = widget;
298 }
299