1 /*
2     SPDX-FileCopyrightText: 2015 Gregor Mi <codestruct@posteo.org>
3 
4     SPDX-License-Identifier: LGPL-2.1-or-later
5 */
6 
7 #ifndef KMORETOOLS_P_H
8 #define KMORETOOLS_P_H
9 
10 #include "kmoretools.h"
11 
12 #include <QDebug>
13 #include <QDesktopServices>
14 #include <QDir>
15 #include <QJsonArray>
16 #include <QJsonDocument>
17 #include <QRegularExpression>
18 #include <QTextCodec>
19 #include <QUrl>
20 
21 #include <KLocalizedString>
22 
23 #define _ QStringLiteral
24 
25 /**
26  * Makes sure that if the same inputId is given more than once
27  * we will get unique IDs.
28  *
29  * See KMoreToolsTest::testMenuItemIdGen().
30  */
31 class KmtMenuItemIdGen
32 {
33 public:
getId(const QString & inputId)34     QString getId(const QString &inputId)
35     {
36         int postFix = desktopEntryNameUsageMap[inputId];
37         desktopEntryNameUsageMap[inputId] = postFix + 1;
38         return QStringLiteral("%1%2").arg(inputId).arg(postFix);
39     }
40 
reset()41     void reset()
42     {
43         desktopEntryNameUsageMap.clear();
44     }
45 
46 private:
47     QMap<QString, int> desktopEntryNameUsageMap;
48 };
49 
50 /**
51  * A serializeable menu item
52  */
53 class KmtMenuItemDto
54 {
55 public:
56     QString id;
57 
58     /**
59      * @note that is might contain an ampersand (&) which may be used for menu items.
60      * Remove it with removeMenuAmpersand()
61      */
62     QString text;
63 
64     QIcon icon;
65 
66     KMoreTools::MenuSection menuSection;
67 
68     bool isInstalled = true;
69 
70     /**
71      * only used if isInstalled == false
72      */
73     QUrl homepageUrl;
74 
75     QString appstreamId;
76 
77 public:
jsonRead(const QJsonObject & json)78     void jsonRead(const QJsonObject &json)
79     {
80         id = json[_("id")].toString();
81         menuSection = json[_("menuSection")].toString() == _("main") ? KMoreTools::MenuSection_Main : KMoreTools::MenuSection_More;
82         isInstalled = json[_("isInstalled")].toBool();
83     }
84 
jsonWrite(QJsonObject & json)85     void jsonWrite(QJsonObject &json) const
86     {
87         json[_("id")] = id;
88         json[_("menuSection")] = menuSection == KMoreTools::MenuSection_Main ? _("main") : _("more");
89         json[_("isInstalled")] = isInstalled;
90     }
91 
92     bool operator==(const KmtMenuItemDto rhs) const
93     {
94         return this->id == rhs.id;
95     }
96 
97     /**
98      * todo: is there a QT method that can be used instead of this?
99      */
removeMenuAmpersand(const QString & str)100     static QString removeMenuAmpersand(const QString &str)
101     {
102         QString newStr = str;
103         newStr.replace(QRegularExpression(QStringLiteral("\\&([^&])")), QStringLiteral("\\1")); // &Hallo --> Hallo
104         newStr.replace(_("&&"), _("&")); // &&Hallo --> &Hallo
105         return newStr;
106     }
107 };
108 
109 /**
110  * The serializeable menu structure.
111  * Used for working with user interaction for persisted configuration.
112  */
113 class KmtMenuStructureDto
114 {
115 public:
116     QList<KmtMenuItemDto> list;
117 
118 public: // should be private but we would like to unit test
119     /**
120      * NOT USED
121      */
itemsBySection(KMoreTools::MenuSection menuSection)122     QList<const KmtMenuItemDto *> itemsBySection(KMoreTools::MenuSection menuSection) const
123     {
124         QList<const KmtMenuItemDto *> r;
125 
126         for (const auto &item : std::as_const(list)) {
127             if (item.menuSection == menuSection) {
128                 r.append(&item);
129             }
130         }
131 
132         return r;
133     }
134 
135     /**
136      * don't store the returned pointer, but you can deref it which calls copy ctor
137      */
findInstalled(const QString & id)138     const KmtMenuItemDto *findInstalled(const QString &id) const
139     {
140         auto foundItem = std::find_if(list.begin(), list.end(), [id](const KmtMenuItemDto &item) {
141             return item.id == id && item.isInstalled;
142         });
143         if (foundItem != list.end()) {
144             // deref iterator which is a const MenuItemDto& from which we get the pointer
145             // (todo: is this a good idea?)
146             return &(*foundItem);
147         }
148 
149         return nullptr;
150     }
151 
152 public:
serialize()153     QString serialize() const
154     {
155         QJsonObject jObj;
156         jsonWrite(jObj);
157         QJsonDocument doc(jObj);
158         auto jByteArray = doc.toJson(QJsonDocument::Compact);
159         // http://stackoverflow.com/questions/14131127/qbytearray-to-qstring
160         // QJsonDocument uses UTF-8 => we use 106=UTF-8
161         // return QTextCodec::codecForMib(106)->toUnicode(jByteArray);
162         return QString::fromUtf8(jByteArray); // accidentally the ctor of QString takes an UTF-8 byte array
163     }
164 
deserialize(const QString & text)165     void deserialize(const QString &text)
166     {
167         QJsonParseError parseError;
168         QJsonDocument doc(QJsonDocument::fromJson(text.toUtf8(), &parseError));
169         jsonRead(doc.object());
170     }
171 
jsonRead(const QJsonObject & json)172     void jsonRead(const QJsonObject &json)
173     {
174         list.clear();
175         auto jArr = json[_("menuitemlist")].toArray();
176         for (int i = 0; i < jArr.size(); ++i) {
177             auto jObj = jArr[i].toObject();
178             KmtMenuItemDto item;
179             item.jsonRead(jObj);
180             list.append(item);
181         }
182     }
183 
jsonWrite(QJsonObject & json)184     void jsonWrite(QJsonObject &json) const
185     {
186         QJsonArray jArr;
187         for (const auto &item : std::as_const(list)) {
188             QJsonObject jObj;
189             item.jsonWrite(jObj);
190             jArr.append(jObj);
191         }
192         json[_("menuitemlist")] = jArr;
193     }
194 
195     /**
196      * @returns true if there are any not-installed items
197      */
notInstalledServices()198     std::vector<KmtMenuItemDto> notInstalledServices() const
199     {
200         std::vector<KmtMenuItemDto> target;
201         std::copy_if(list.begin(), list.end(), std::back_inserter(target), [](const KmtMenuItemDto &item) {
202             return !item.isInstalled;
203         });
204         return target;
205     }
206 
207 public: // should be private but we would like to unit test
208     /**
209      * stable sorts:
210      * 1. main items
211      * 2. more items
212      * 3. not installed items
213      */
stableSortListBySection()214     void stableSortListBySection()
215     {
216         std::stable_sort(list.begin(), list.end(), [](const KmtMenuItemDto &i1, const KmtMenuItemDto &i2) {
217             return (i1.isInstalled && i1.menuSection == KMoreTools::MenuSection_Main && i2.isInstalled && i2.menuSection == KMoreTools::MenuSection_More)
218                 || (i1.isInstalled && !i2.isInstalled);
219         });
220     }
221 
222 public:
223     /**
224      * moves an item up or down respecting its category
225      * @param direction: 1: down, -1: up
226      */
moveWithinSection(const QString & id,int direction)227     void moveWithinSection(const QString &id, int direction)
228     {
229         auto selItem = std::find_if(list.begin(), list.end(), [id](const KmtMenuItemDto &item) {
230             return item.id == id;
231         });
232 
233         if (selItem != list.end()) { // if found
234             if (direction == 1) { // "down"
235                 auto itemAfter = std::find_if(selItem + 1,
236                                               list.end(), // find item where to insert after in the same category
237                                               [selItem](const KmtMenuItemDto &item) {
238                                                   return item.menuSection == selItem->menuSection;
239                                               });
240 
241                 if (itemAfter != list.end()) {
242                     int prevIndex = list.indexOf(*selItem);
243                     list.insert(list.indexOf(*itemAfter) + 1, *selItem);
244                     list.removeAt(prevIndex);
245                 }
246             } else if (direction == -1) { // "up"
247                 // auto r_list = list;
248                 // std::reverse(r_list.begin(), r_list.end()); // we need to search "up"
249                 // auto itemBefore = std::find_if(selItem, list.begin(),// find item where to insert before in the same category
250                 //                               [selItem](const MenuItemDto& item) { return item.menuSection == selItem->menuSection; });
251 
252                 // todo: can't std::find_if be used instead of this loop?
253                 QList<KmtMenuItemDto>::iterator itemBefore = list.end();
254                 auto it = selItem;
255                 while (it != list.begin()) {
256                     --it;
257                     if (it->menuSection == selItem->menuSection) {
258                         itemBefore = it;
259                         break;
260                     }
261                 }
262 
263                 if (itemBefore != list.end()) {
264                     int prevIndex = list.indexOf(*selItem);
265                     list.insert(itemBefore, *selItem);
266                     list.removeAt(prevIndex + 1);
267                 }
268             } else {
269                 Q_ASSERT(false);
270             }
271         } else {
272             qWarning() << "selItem != list.end() == false";
273         }
274 
275         stableSortListBySection();
276     }
277 
moveToOtherSection(const QString & id)278     void moveToOtherSection(const QString &id)
279     {
280         auto selItem = std::find_if(list.begin(), list.end(), [id](const KmtMenuItemDto &item) -> bool {
281             return item.id == id;
282         });
283 
284         if (selItem != list.end()) { // if found
285             if (selItem->menuSection == KMoreTools::MenuSection_Main) {
286                 selItem->menuSection = KMoreTools::MenuSection_More;
287             } else if (selItem->menuSection == KMoreTools::MenuSection_More) {
288                 selItem->menuSection = KMoreTools::MenuSection_Main;
289             } else {
290                 Q_ASSERT(false);
291             }
292         }
293 
294         stableSortListBySection();
295     }
296 };
297 
298 /**
299  * In menu structure consisting of main section items, more section items
300  * and registered services which are not installed.
301  * In contrast to KmtMenuStructureDto we are dealing here with
302  * KMoreToolsMenuItem pointers instead of DTOs.
303  */
304 class KmtMenuStructure
305 {
306 public:
307     QList<KMoreToolsMenuItem *> mainItems;
308     QList<KMoreToolsMenuItem *> moreItems;
309 
310     /**
311      * contains each not installed registered service once
312      */
313     QList<KMoreToolsService *> notInstalledServices;
314 
315 public:
toDto()316     KmtMenuStructureDto toDto()
317     {
318         KmtMenuStructureDto result;
319 
320         for (auto item : std::as_const(mainItems)) {
321             const auto a = item->action();
322             KmtMenuItemDto dto;
323             dto.id = item->id();
324             dto.text = a->text(); // might be overridden, so we use directly from QAction
325             dto.icon = a->icon();
326             dto.isInstalled = true;
327             dto.menuSection = KMoreTools::MenuSection_Main;
328             result.list << dto;
329         }
330 
331         for (auto item : std::as_const(moreItems)) {
332             const auto a = item->action();
333             KmtMenuItemDto dto;
334             dto.id = item->id();
335             dto.text = a->text(); // might be overridden, so we use directly from QAction
336             dto.icon = a->icon();
337             dto.isInstalled = true;
338             dto.menuSection = KMoreTools::MenuSection_More;
339             result.list << dto;
340         }
341 
342         for (auto registeredService : std::as_const(notInstalledServices)) {
343             KmtMenuItemDto dto;
344             // dto.id = item->id(); // not used in this case
345             dto.text = registeredService->formatString(_("$Name"));
346             dto.icon = registeredService->icon();
347             dto.isInstalled = false;
348             // dto.menuSection = // not used in this case
349             dto.homepageUrl = registeredService->homepageUrl();
350             result.list << dto;
351         }
352 
353         return result;
354     }
355 };
356 
357 /**
358  * Helper class that deals with creating the menu where all the not-installed
359  * services are listed.
360  */
361 class KmtNotInstalledUtil
362 {
363 public:
364     /**
365      * For one given application/service which is named @p title a QMenu is
366      * created with the given @p icon and @p homepageUrl.
367      * It will be used as submenu for the menu that displays the not-installed
368      * services.
369      */
createSubmenuForNotInstalledApp(const QString & title,QWidget * parent,const QIcon & icon,const QUrl & homepageUrl,const QString & appstreamId)370     static QMenu *createSubmenuForNotInstalledApp(const QString &title, QWidget *parent, const QIcon &icon, const QUrl &homepageUrl, const QString &appstreamId)
371     {
372         QMenu *submenuForNotInstalled = new QMenu(title, parent);
373         submenuForNotInstalled->setIcon(icon);
374 
375         if (homepageUrl.isValid()) {
376             auto websiteAction = submenuForNotInstalled->addAction(i18nc("@action:inmenu", "Visit homepage"));
377             websiteAction->setIcon(QIcon::fromTheme(QStringLiteral("internet-services")));
378             auto url = homepageUrl;
379             // todo/review: is it ok to have sender and receiver the same object?
380             QObject::connect(websiteAction, &QAction::triggered, websiteAction, [url](bool) {
381                 QDesktopServices::openUrl(url);
382             });
383         }
384 
385         QUrl appstreamUrl = QUrl(QStringLiteral("appstream://") % appstreamId);
386 
387         if (!appstreamId.isEmpty()) {
388             auto installAction = submenuForNotInstalled->addAction(i18nc("@action:inmenu", "Install"));
389             installAction->setIcon(QIcon::fromTheme(QStringLiteral("download")));
390             QObject::connect(installAction, &QAction::triggered, installAction, [appstreamUrl](bool) {
391                 QDesktopServices::openUrl(appstreamUrl);
392             });
393         }
394 
395         if (!homepageUrl.isValid() && appstreamId.isEmpty()) {
396             submenuForNotInstalled->addAction(i18nc("@action:inmenu", "No further information available."))->setEnabled(false);
397         }
398 
399         return submenuForNotInstalled;
400     }
401 };
402 
403 /**
404  * Url handling utils
405  */
406 class KmtUrlUtil
407 {
408 public:
409     /**
410      * "file:///home/abc/hallo.txt" becomes "file:///home/abc"
411      */
localFileAbsoluteDir(const QUrl & url)412     static QUrl localFileAbsoluteDir(const QUrl &url)
413     {
414         if (!url.isLocalFile()) {
415             qWarning() << "localFileAbsoluteDir: url must be local file";
416         }
417         QFileInfo fileInfo(url.toLocalFile());
418         auto dir = QDir(fileInfo.absoluteDir()).absolutePath();
419         return QUrl::fromLocalFile(dir);
420     }
421 };
422 
423 #endif
424