File indexing completed on 2024-05-12 04:42:02

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