File indexing completed on 2024-04-14 14:26:36

0001 /*
0002     SPDX-FileCopyrightText: 2015 Gregor Mi <codestruct@posteo.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.1-or-later
0005 */
0006 
0007 #ifndef KMORETOOLS_P_H
0008 #define KMORETOOLS_P_H
0009 
0010 #include "kmoretools.h"
0011 
0012 #include <QDebug>
0013 #include <QDir>
0014 #include <QJsonArray>
0015 #include <QJsonDocument>
0016 #include <QJsonObject>
0017 #include <QRegularExpression>
0018 #include <QUrl>
0019 
0020 #include <KIO/OpenUrlJob>
0021 #include <KLocalizedString>
0022 
0023 #define _ QStringLiteral
0024 
0025 /**
0026  * Makes sure that if the same inputId is given more than once
0027  * we will get unique IDs.
0028  *
0029  * See KMoreToolsTest::testMenuItemIdGen().
0030  */
0031 class KmtMenuItemIdGen
0032 {
0033 public:
0034     QString getId(const QString &inputId)
0035     {
0036         int postFix = desktopEntryNameUsageMap[inputId];
0037         desktopEntryNameUsageMap[inputId] = postFix + 1;
0038         return QStringLiteral("%1%2").arg(inputId).arg(postFix);
0039     }
0040 
0041     void reset()
0042     {
0043         desktopEntryNameUsageMap.clear();
0044     }
0045 
0046 private:
0047     QMap<QString, int> desktopEntryNameUsageMap;
0048 };
0049 
0050 /**
0051  * A serializeable menu item
0052  */
0053 class KmtMenuItemDto
0054 {
0055 public:
0056     QString id;
0057 
0058     /**
0059      * @note that is might contain an ampersand (&) which may be used for menu items.
0060      * Remove it with removeMenuAmpersand()
0061      */
0062     QString text;
0063 
0064     QIcon icon;
0065 
0066     KMoreTools::MenuSection menuSection;
0067 
0068     bool isInstalled = true;
0069 
0070     /**
0071      * only used if isInstalled == false
0072      */
0073     QUrl homepageUrl;
0074 
0075     QString appstreamId;
0076 
0077 public:
0078     void jsonRead(const QJsonObject &json)
0079     {
0080         id = json[_("id")].toString();
0081         menuSection = json[_("menuSection")].toString() == _("main") ? KMoreTools::MenuSection_Main : KMoreTools::MenuSection_More;
0082         isInstalled = json[_("isInstalled")].toBool();
0083     }
0084 
0085     void jsonWrite(QJsonObject &json) const
0086     {
0087         json[_("id")] = id;
0088         json[_("menuSection")] = menuSection == KMoreTools::MenuSection_Main ? _("main") : _("more");
0089         json[_("isInstalled")] = isInstalled;
0090     }
0091 
0092     bool operator==(const KmtMenuItemDto rhs) const
0093     {
0094         return this->id == rhs.id;
0095     }
0096 
0097     /**
0098      * todo: is there a QT method that can be used instead of this?
0099      */
0100     static QString removeMenuAmpersand(const QString &str)
0101     {
0102         QString newStr = str;
0103         newStr.replace(QRegularExpression(QStringLiteral("\\&([^&])")), QStringLiteral("\\1")); // &Hallo --> Hallo
0104         newStr.replace(_("&&"), _("&")); // &&Hallo --> &Hallo
0105         return newStr;
0106     }
0107 };
0108 
0109 /**
0110  * The serializeable menu structure.
0111  * Used for working with user interaction for persisted configuration.
0112  */
0113 class KmtMenuStructureDto
0114 {
0115 public:
0116     QList<KmtMenuItemDto> list;
0117 
0118 public: // should be private but we would like to unit test
0119     /**
0120      * NOT USED
0121      */
0122     QList<const KmtMenuItemDto *> itemsBySection(KMoreTools::MenuSection menuSection) const
0123     {
0124         QList<const KmtMenuItemDto *> r;
0125 
0126         for (const auto &item : std::as_const(list)) {
0127             if (item.menuSection == menuSection) {
0128                 r.append(&item);
0129             }
0130         }
0131 
0132         return r;
0133     }
0134 
0135     /**
0136      * don't store the returned pointer, but you can deref it which calls copy ctor
0137      */
0138     const KmtMenuItemDto *findInstalled(const QString &id) const
0139     {
0140         auto foundItem = std::find_if(list.begin(), list.end(), [id](const KmtMenuItemDto &item) {
0141             return item.id == id && item.isInstalled;
0142         });
0143         if (foundItem != list.end()) {
0144             // deref iterator which is a const MenuItemDto& from which we get the pointer
0145             // (todo: is this a good idea?)
0146             return &(*foundItem);
0147         }
0148 
0149         return nullptr;
0150     }
0151 
0152 public:
0153     QString serialize() const
0154     {
0155         QJsonObject jObj;
0156         jsonWrite(jObj);
0157         QJsonDocument doc(jObj);
0158         auto jByteArray = doc.toJson(QJsonDocument::Compact);
0159         return QString::fromUtf8(jByteArray);
0160     }
0161 
0162     void deserialize(const QString &text)
0163     {
0164         QJsonParseError parseError;
0165         QJsonDocument doc(QJsonDocument::fromJson(text.toUtf8(), &parseError));
0166         jsonRead(doc.object());
0167     }
0168 
0169     void jsonRead(const QJsonObject &json)
0170     {
0171         list.clear();
0172         auto jArr = json[_("menuitemlist")].toArray();
0173         for (int i = 0; i < jArr.size(); ++i) {
0174             auto jObj = jArr[i].toObject();
0175             KmtMenuItemDto item;
0176             item.jsonRead(jObj);
0177             list.append(item);
0178         }
0179     }
0180 
0181     void jsonWrite(QJsonObject &json) const
0182     {
0183         QJsonArray jArr;
0184         for (const auto &item : std::as_const(list)) {
0185             QJsonObject jObj;
0186             item.jsonWrite(jObj);
0187             jArr.append(jObj);
0188         }
0189         json[_("menuitemlist")] = jArr;
0190     }
0191 
0192     /**
0193      * @returns true if there are any not-installed items
0194      */
0195     std::vector<KmtMenuItemDto> notInstalledServices() const
0196     {
0197         std::vector<KmtMenuItemDto> target;
0198         std::copy_if(list.begin(), list.end(), std::back_inserter(target), [](const KmtMenuItemDto &item) {
0199             return !item.isInstalled;
0200         });
0201         return target;
0202     }
0203 
0204 public: // should be private but we would like to unit test
0205     /**
0206      * stable sorts:
0207      * 1. main items
0208      * 2. more items
0209      * 3. not installed items
0210      */
0211     void stableSortListBySection()
0212     {
0213         std::stable_sort(list.begin(), list.end(), [](const KmtMenuItemDto &i1, const KmtMenuItemDto &i2) {
0214             return (i1.isInstalled && i1.menuSection == KMoreTools::MenuSection_Main && i2.isInstalled && i2.menuSection == KMoreTools::MenuSection_More)
0215                 || (i1.isInstalled && !i2.isInstalled);
0216         });
0217     }
0218 
0219 public:
0220     /**
0221      * moves an item up or down respecting its category
0222      * @param direction: 1: down, -1: up
0223      */
0224     void moveWithinSection(const QString &id, int direction)
0225     {
0226         auto selItem = std::find_if(list.begin(), list.end(), [id](const KmtMenuItemDto &item) {
0227             return item.id == id;
0228         });
0229 
0230         if (selItem != list.end()) { // if found
0231             if (direction == 1) { // "down"
0232                 auto itemAfter = std::find_if(selItem + 1,
0233                                               list.end(), // find item where to insert after in the same category
0234                                               [selItem](const KmtMenuItemDto &item) {
0235                                                   return item.menuSection == selItem->menuSection;
0236                                               });
0237 
0238                 if (itemAfter != list.end()) {
0239                     int prevIndex = list.indexOf(*selItem);
0240                     list.insert(list.indexOf(*itemAfter) + 1, *selItem);
0241                     list.removeAt(prevIndex);
0242                 }
0243             } else if (direction == -1) { // "up"
0244                 // auto r_list = list;
0245                 // std::reverse(r_list.begin(), r_list.end()); // we need to search "up"
0246                 // auto itemBefore = std::find_if(selItem, list.begin(),// find item where to insert before in the same category
0247                 //                               [selItem](const MenuItemDto& item) { return item.menuSection == selItem->menuSection; });
0248 
0249                 // todo: can't std::find_if be used instead of this loop?
0250                 QList<KmtMenuItemDto>::iterator itemBefore = list.end();
0251                 auto it = selItem;
0252                 while (it != list.begin()) {
0253                     --it;
0254                     if (it->menuSection == selItem->menuSection) {
0255                         itemBefore = it;
0256                         break;
0257                     }
0258                 }
0259 
0260                 if (itemBefore != list.end()) {
0261                     int prevIndex = list.indexOf(*selItem);
0262                     list.insert(itemBefore, *selItem);
0263                     list.removeAt(prevIndex + 1);
0264                 }
0265             } else {
0266                 Q_ASSERT(false);
0267             }
0268         } else {
0269             qWarning() << "selItem != list.end() == false";
0270         }
0271 
0272         stableSortListBySection();
0273     }
0274 
0275     void moveToOtherSection(const QString &id)
0276     {
0277         auto selItem = std::find_if(list.begin(), list.end(), [id](const KmtMenuItemDto &item) -> bool {
0278             return item.id == id;
0279         });
0280 
0281         if (selItem != list.end()) { // if found
0282             if (selItem->menuSection == KMoreTools::MenuSection_Main) {
0283                 selItem->menuSection = KMoreTools::MenuSection_More;
0284             } else if (selItem->menuSection == KMoreTools::MenuSection_More) {
0285                 selItem->menuSection = KMoreTools::MenuSection_Main;
0286             } else {
0287                 Q_ASSERT(false);
0288             }
0289         }
0290 
0291         stableSortListBySection();
0292     }
0293 };
0294 
0295 /**
0296  * In menu structure consisting of main section items, more section items
0297  * and registered services which are not installed.
0298  * In contrast to KmtMenuStructureDto we are dealing here with
0299  * KMoreToolsMenuItem pointers instead of DTOs.
0300  */
0301 class KmtMenuStructure
0302 {
0303 public:
0304     QList<KMoreToolsMenuItem *> mainItems;
0305     QList<KMoreToolsMenuItem *> moreItems;
0306 
0307     /**
0308      * contains each not installed registered service once
0309      */
0310     QList<KMoreToolsService *> notInstalledServices;
0311 
0312 public:
0313     KmtMenuStructureDto toDto()
0314     {
0315         KmtMenuStructureDto result;
0316 
0317         for (auto item : std::as_const(mainItems)) {
0318             const auto a = item->action();
0319             KmtMenuItemDto dto;
0320             dto.id = item->id();
0321             dto.text = a->text(); // might be overridden, so we use directly from QAction
0322             dto.icon = a->icon();
0323             dto.isInstalled = true;
0324             dto.menuSection = KMoreTools::MenuSection_Main;
0325             result.list << dto;
0326         }
0327 
0328         for (auto item : std::as_const(moreItems)) {
0329             const auto a = item->action();
0330             KmtMenuItemDto dto;
0331             dto.id = item->id();
0332             dto.text = a->text(); // might be overridden, so we use directly from QAction
0333             dto.icon = a->icon();
0334             dto.isInstalled = true;
0335             dto.menuSection = KMoreTools::MenuSection_More;
0336             result.list << dto;
0337         }
0338 
0339         for (auto registeredService : std::as_const(notInstalledServices)) {
0340             KmtMenuItemDto dto;
0341             // dto.id = item->id(); // not used in this case
0342             dto.text = registeredService->formatString(_("$Name"));
0343             dto.icon = registeredService->icon();
0344             dto.isInstalled = false;
0345             // dto.menuSection = // not used in this case
0346             dto.homepageUrl = registeredService->homepageUrl();
0347             result.list << dto;
0348         }
0349 
0350         return result;
0351     }
0352 };
0353 
0354 /**
0355  * Helper class that deals with creating the menu where all the not-installed
0356  * services are listed.
0357  */
0358 class KmtNotInstalledUtil
0359 {
0360 public:
0361     /**
0362      * For one given application/service which is named @p title a QMenu is
0363      * created with the given @p icon and @p homepageUrl.
0364      * It will be used as submenu for the menu that displays the not-installed
0365      * services.
0366      */
0367     static QMenu *createSubmenuForNotInstalledApp(const QString &title, QWidget *parent, const QIcon &icon, const QUrl &homepageUrl, const QString &appstreamId)
0368     {
0369         QMenu *submenuForNotInstalled = new QMenu(title, parent);
0370         submenuForNotInstalled->setIcon(icon);
0371 
0372         if (homepageUrl.isValid()) {
0373             auto websiteAction = submenuForNotInstalled->addAction(i18nc("@action:inmenu", "Visit homepage"));
0374             websiteAction->setIcon(QIcon::fromTheme(QStringLiteral("internet-services")));
0375             auto url = homepageUrl;
0376             // todo/review: is it ok to have sender and receiver the same object?
0377             QObject::connect(websiteAction, &QAction::triggered, websiteAction, [url](bool) {
0378                 auto *job = new KIO::OpenUrlJob(url);
0379                 job->start();
0380             });
0381         }
0382 
0383         QUrl appstreamUrl = QUrl(QStringLiteral("appstream://") % appstreamId);
0384 
0385         if (!appstreamId.isEmpty()) {
0386             auto installAction = submenuForNotInstalled->addAction(i18nc("@action:inmenu", "Install"));
0387             installAction->setIcon(QIcon::fromTheme(QStringLiteral("download")));
0388             QObject::connect(installAction, &QAction::triggered, installAction, [appstreamUrl](bool) {
0389                 auto *job = new KIO::OpenUrlJob(appstreamUrl);
0390                 job->start();
0391             });
0392         }
0393 
0394         if (!homepageUrl.isValid() && appstreamId.isEmpty()) {
0395             submenuForNotInstalled->addAction(i18nc("@action:inmenu", "No further information available."))->setEnabled(false);
0396         }
0397 
0398         return submenuForNotInstalled;
0399     }
0400 };
0401 
0402 /**
0403  * Url handling utils
0404  */
0405 class KmtUrlUtil
0406 {
0407 public:
0408     /**
0409      * "file:///home/abc/hallo.txt" becomes "file:///home/abc"
0410      */
0411     static QUrl localFileAbsoluteDir(const QUrl &url)
0412     {
0413         if (!url.isLocalFile()) {
0414             qWarning() << "localFileAbsoluteDir: url must be local file";
0415         }
0416         QFileInfo fileInfo(url.toLocalFile());
0417         auto dir = QDir(fileInfo.absoluteDir()).absolutePath();
0418         return QUrl::fromLocalFile(dir);
0419     }
0420 };
0421 
0422 #endif