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 #include "kmoretools.h"
0008 
0009 #include "kmoretools_debug.h"
0010 #include "kmoretools_p.h"
0011 #include "kmoretoolsconfigdialog_p.h"
0012 
0013 #include <QApplication>
0014 #include <QDebug>
0015 #include <QStandardPaths>
0016 
0017 #include <KConfig>
0018 #include <KConfigGroup>
0019 #include <KLocalizedString>
0020 #include <optional>
0021 
0022 class KMoreToolsPrivate
0023 {
0024 public:
0025     QString uniqueId;
0026 
0027     // allocated via new, don't forget to delete
0028     QList<KMoreToolsService *> serviceList;
0029 
0030     QMap<QString, KMoreToolsMenuBuilder *> menuBuilderMap;
0031 
0032 public:
0033     KMoreToolsPrivate(const QString &uniqueId)
0034         : uniqueId(uniqueId)
0035     {
0036     }
0037 
0038     ~KMoreToolsPrivate()
0039     {
0040         qDeleteAll(menuBuilderMap);
0041         qDeleteAll(serviceList);
0042     }
0043 
0044     /**
0045      * @return uniqueId if kmtDesktopfileSubdir is empty
0046      * else kmtDesktopfileSubdir
0047      */
0048     QString kmtDesktopfileSubdirOrUniqueId(const QString &kmtDesktopfileSubdir)
0049     {
0050         if (kmtDesktopfileSubdir.isEmpty()) {
0051             return uniqueId;
0052         }
0053 
0054         return kmtDesktopfileSubdir;
0055     }
0056 
0057     /**
0058      * Finds a file in the '/usr/share'/kf5/kmoretools/'uniqueId'/ directory.
0059      * '/usr/share' = "~/.local/share", "/usr/local/share", "/usr/share" (see QStandardPaths::GenericDataLocation)
0060      * 'uniqueId' = @see uniqueId()
0061      *
0062      * @param can be a filename with or without relative path. But no absolute path.
0063      * @returns the first occurrence if there are more than one found
0064      */
0065     QString findFileInKmtDesktopfilesDir(const QString &filename)
0066     {
0067         return findFileInKmtDesktopfilesDir(uniqueId, filename);
0068     }
0069 
0070     static QString findFileInKmtDesktopfilesDir(const QString &kmtDesktopfileSubdir, const QString &filename)
0071     {
0072         const QString kmtDesktopfilesFilename = QLatin1String("kf6/kmoretools/") + kmtDesktopfileSubdir + QLatin1Char('/') + filename;
0073         const QString foundKmtFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, kmtDesktopfilesFilename);
0074 
0075         return foundKmtFile;
0076     }
0077 };
0078 
0079 KMoreTools::KMoreTools(const QString &uniqueId)
0080     : d(new KMoreToolsPrivate(uniqueId))
0081 {
0082 }
0083 
0084 KMoreTools::~KMoreTools() = default;
0085 
0086 KMoreToolsService *KMoreTools::registerServiceByDesktopEntryName(const QString &desktopEntryName,
0087                                                                  const QString &kmtDesktopfileSubdir,
0088                                                                  KMoreTools::ServiceLocatingMode serviceLocatingMode)
0089 {
0090     const QString foundKmtDesktopfilePath =
0091         d->findFileInKmtDesktopfilesDir(d->kmtDesktopfileSubdirOrUniqueId(kmtDesktopfileSubdir), desktopEntryName + QLatin1String(".desktop"));
0092     const bool isKmtDesktopfileProvided = !foundKmtDesktopfilePath.isEmpty();
0093 
0094     KService::Ptr kmtDesktopfile;
0095 
0096     if (isKmtDesktopfileProvided) {
0097         kmtDesktopfile = KService::Ptr(new KService(foundKmtDesktopfilePath));
0098         if (!kmtDesktopfile->isValid()) {
0099             qCCritical(KMORETOOLS) << "KMoreTools::registerServiceByDesktopEntryName: the kmt-desktopfile " << desktopEntryName
0100                                    << " is provided but no Exec line is specified. The desktop file is probably faulty. Please fix. Return nullptr.";
0101             return nullptr;
0102         }
0103     } else {
0104         qCWarning(KMORETOOLS) << "KMoreTools::registerServiceByDesktopEntryName: desktopEntryName " << desktopEntryName
0105                               << " (kmtDesktopfileSubdir=" << kmtDesktopfileSubdir
0106                               << ") not provided (or at the wrong place) in the installed kmt-desktopfiles directory. If the service is also not installed on "
0107                                  "the system the user won't get nice translated app name and description.";
0108         qCDebug(KMORETOOLS) << "`-- More info at findFileInKmtDesktopfilesDir, QStandardPaths::standardLocations = "
0109                             << QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); // /usr/share etc.
0110     }
0111 
0112     bool isInstalled = false;
0113     KService::Ptr installedService;
0114     if (serviceLocatingMode == KMoreTools::ServiceLocatingMode_Default) { // == default behaviour: search for installed services
0115         installedService = KService::serviceByDesktopName(desktopEntryName);
0116         isInstalled = installedService != nullptr;
0117     } else if (serviceLocatingMode == KMoreTools::ServiceLocatingMode_ByProvidedExecLine) { // only use provided kmt-desktopfile:
0118         if (!isKmtDesktopfileProvided) {
0119             qCCritical(KMORETOOLS)
0120                 << "KMoreTools::registerServiceByDesktopEntryName for " << desktopEntryName
0121                 << ": If detectServiceExistenceViaProvidedExecLine is true then a kmt-desktopfile must be provided. Please fix. Return nullptr.";
0122             return nullptr;
0123         }
0124 
0125         const QString tryExec = kmtDesktopfile->property<QString>(QStringLiteral("TryExec"));
0126         isInstalled =
0127             (!tryExec.isEmpty() && !QStandardPaths::findExecutable(tryExec).isEmpty()) || !QStandardPaths::findExecutable(kmtDesktopfile->exec()).isEmpty();
0128     } else {
0129         Q_UNREACHABLE(); // case not handled
0130     }
0131 
0132     auto registeredService =
0133         new KMoreToolsService(d->kmtDesktopfileSubdirOrUniqueId(kmtDesktopfileSubdir), desktopEntryName, isInstalled, installedService, kmtDesktopfile);
0134 
0135     // add or replace item in serviceList
0136     auto foundService = std::find_if(d->serviceList.begin(), d->serviceList.end(), [&desktopEntryName](KMoreToolsService *service) {
0137         return service->desktopEntryName() == desktopEntryName;
0138     });
0139     if (foundService == d->serviceList.end()) {
0140         d->serviceList.append(registeredService);
0141     } else {
0142         KMoreToolsService *foundServicePtr = *foundService;
0143         int i = d->serviceList.indexOf(foundServicePtr);
0144         delete foundServicePtr;
0145         d->serviceList.replace(i, registeredService);
0146     }
0147 
0148     return registeredService;
0149 }
0150 
0151 KMoreToolsMenuBuilder *KMoreTools::menuBuilder(const QString &userConfigPostfix) const
0152 {
0153     if (d->menuBuilderMap.find(userConfigPostfix) == d->menuBuilderMap.end()) {
0154         d->menuBuilderMap.insert(userConfigPostfix, new KMoreToolsMenuBuilder(d->uniqueId, userConfigPostfix));
0155     }
0156     return d->menuBuilderMap[userConfigPostfix];
0157 }
0158 
0159 // ------------------------------------------------------------------------------------------------
0160 // ------------------------------------------------------------------------------------------------
0161 
0162 class KMoreToolsServicePrivate
0163 {
0164 public:
0165     QString kmtDesktopfileSubdir;
0166     QString desktopEntryName;
0167     KService::Ptr installedService;
0168     KService::Ptr kmtDesktopfile;
0169     QUrl homepageUrl;
0170     int maxUrlArgCount = 0;
0171     bool isInstalled = false;
0172     QString appstreamId;
0173 
0174 public:
0175     QString getServiceName()
0176     {
0177         if (installedService) {
0178             return installedService->name();
0179         } else {
0180             if (kmtDesktopfile) {
0181                 return kmtDesktopfile->name();
0182             } else {
0183                 return QString();
0184             }
0185         }
0186     }
0187 
0188     QString getServiceGenericName()
0189     {
0190         if (installedService) {
0191             return installedService->genericName();
0192         } else {
0193             if (kmtDesktopfile) {
0194                 return kmtDesktopfile->genericName();
0195             } else {
0196                 return QString();
0197             }
0198         }
0199     }
0200 
0201     /**
0202      * @return the provided icon or an empty icon if not kmtDesktopfile is available or the icon was not found
0203      */
0204     QIcon getKmtProvidedIcon()
0205     {
0206         if (!kmtDesktopfile) {
0207             return QIcon();
0208         }
0209 
0210         QString iconPath = KMoreToolsPrivate::findFileInKmtDesktopfilesDir(kmtDesktopfileSubdir, kmtDesktopfile->icon() + QLatin1String(".svg"));
0211         QIcon svgIcon(iconPath);
0212         if (!svgIcon.isNull()) {
0213             return svgIcon;
0214         }
0215 
0216         iconPath = KMoreToolsPrivate::findFileInKmtDesktopfilesDir(kmtDesktopfileSubdir, kmtDesktopfile->icon() + QLatin1String(".png"));
0217         QIcon pngIcon(iconPath);
0218         if (!pngIcon.isNull()) {
0219             return pngIcon;
0220         }
0221 
0222         return QIcon();
0223     }
0224 };
0225 
0226 KMoreToolsService::KMoreToolsService(const QString &kmtDesktopfileSubdir,
0227                                      const QString &desktopEntryName,
0228                                      bool isInstalled,
0229                                      KService::Ptr installedService,
0230                                      KService::Ptr kmtDesktopfile)
0231     : d(new KMoreToolsServicePrivate())
0232 {
0233     d->kmtDesktopfileSubdir = kmtDesktopfileSubdir;
0234     d->desktopEntryName = desktopEntryName;
0235     d->isInstalled = isInstalled;
0236     d->installedService = installedService;
0237     d->kmtDesktopfile = kmtDesktopfile;
0238 }
0239 
0240 KMoreToolsService::~KMoreToolsService() = default;
0241 
0242 QString KMoreToolsService::desktopEntryName() const
0243 {
0244     return d->desktopEntryName;
0245 }
0246 
0247 bool KMoreToolsService::isInstalled() const
0248 {
0249     return d->isInstalled;
0250 }
0251 
0252 KService::Ptr KMoreToolsService::installedService() const
0253 {
0254     return d->installedService;
0255 }
0256 
0257 KService::Ptr KMoreToolsService::kmtProvidedService() const
0258 {
0259     return d->kmtDesktopfile;
0260 }
0261 
0262 QIcon KMoreToolsService::kmtProvidedIcon() const
0263 {
0264     return d->getKmtProvidedIcon();
0265 }
0266 
0267 QUrl KMoreToolsService::homepageUrl() const
0268 {
0269     return d->homepageUrl;
0270 }
0271 
0272 void KMoreToolsService::setHomepageUrl(const QUrl &url)
0273 {
0274     d->homepageUrl = url;
0275 }
0276 
0277 int KMoreToolsService::maxUrlArgCount() const
0278 {
0279     return d->maxUrlArgCount;
0280 }
0281 
0282 void KMoreToolsService::setMaxUrlArgCount(int maxUrlArgCount)
0283 {
0284     d->maxUrlArgCount = maxUrlArgCount;
0285 }
0286 
0287 QString KMoreToolsService::formatString(const QString &formatString) const
0288 {
0289     QString result = formatString;
0290 
0291     QString genericName = d->getServiceGenericName();
0292     if (genericName.isEmpty()) {
0293         genericName = d->getServiceName();
0294         if (genericName.isEmpty()) {
0295             genericName = desktopEntryName();
0296         }
0297     }
0298 
0299     QString name = d->getServiceName();
0300     if (name.isEmpty()) {
0301         name = desktopEntryName();
0302     }
0303 
0304     result.replace(QLatin1String("$GenericName"), genericName);
0305     result.replace(QLatin1String("$Name"), name);
0306     result.replace(QLatin1String("$DesktopEntryName"), desktopEntryName());
0307 
0308     return result;
0309 }
0310 
0311 QIcon KMoreToolsService::icon() const
0312 {
0313     if (installedService() != nullptr) {
0314         return QIcon::fromTheme(installedService()->icon());
0315     } else if (kmtProvidedService() != nullptr) {
0316         return d->getKmtProvidedIcon();
0317     } else {
0318         return QIcon();
0319     }
0320 }
0321 
0322 void KMoreToolsService::setExec(const QString &exec)
0323 {
0324     auto service = installedService();
0325     if (service) {
0326         service->setExec(exec);
0327     }
0328 }
0329 
0330 QString KMoreToolsService::appstreamId() const
0331 {
0332     return d->appstreamId;
0333 }
0334 
0335 void KMoreToolsService::setAppstreamId(const QString &id)
0336 {
0337     d->appstreamId = id;
0338 }
0339 
0340 // ------------------------------------------------------------------------------------------------
0341 // ------------------------------------------------------------------------------------------------
0342 
0343 const QString configFile = QStringLiteral("kmoretoolsrc");
0344 const QString configKey = QStringLiteral("menu_structure");
0345 
0346 class KMoreToolsMenuBuilderPrivate
0347 {
0348 public:
0349     QString uniqueId;
0350     /**
0351      * default value is "", see KMoreTools::menuBuilder()
0352      */
0353     QString userConfigPostfix;
0354     QList<KMoreToolsMenuItem *> menuItems;
0355     KmtMenuItemIdGen menuItemIdGen;
0356     QString initialItemTextTemplate = QStringLiteral("$GenericName");
0357 
0358 public:
0359     KMoreToolsMenuBuilderPrivate()
0360     {
0361     }
0362 
0363     ~KMoreToolsMenuBuilderPrivate()
0364     {
0365     }
0366 
0367     void deleteAndClearMenuItems()
0368     {
0369         for (auto item : std::as_const(menuItems)) {
0370             delete item;
0371         }
0372 
0373         menuItems.clear();
0374     }
0375 
0376     KmtMenuStructureDto readUserConfig() const
0377     {
0378         KConfig config(configFile, KConfig::NoGlobals, QStandardPaths::ConfigLocation);
0379         auto configGroup = config.group(uniqueId + userConfigPostfix);
0380         QString json = configGroup.readEntry(configKey);
0381         KmtMenuStructureDto configuredStructure;
0382         configuredStructure.deserialize(json);
0383         return configuredStructure;
0384     }
0385 
0386     void writeUserConfig(const KmtMenuStructureDto &mstruct) const
0387     {
0388         KConfig config(configFile, KConfig::NoGlobals, QStandardPaths::ConfigLocation);
0389         auto configGroup = config.group(uniqueId + userConfigPostfix);
0390         auto configValue = mstruct.serialize();
0391         configGroup.writeEntry(configKey, configValue);
0392         configGroup.sync();
0393     }
0394 
0395     enum CreateMenuStructureOption {
0396         CreateMenuStructure_Default,
0397         CreateMenuStructure_MergeWithUserConfig,
0398     };
0399 
0400     /**
0401      * Merge strategy if createMenuStructureOption == CreateMenuStructure_MergeWithUserConfig
0402      * --------------------------------------------------------------------------------------
0403      * 1) For each 'main' section item from configStruct
0404      *      lookup in current structure (all installed items) and if found add to new structure
0405      *    This means items which are in configStruct but not in current structure will be discarded.
0406      *
0407      * 2) Add remaining 'main' section items from current to new structure
0408      *
0409      * 3) Do the 1) and 2) analogous for 'more' section
0410      *
0411      *
0412      * How default structure and DTOs play together
0413      * --------------------------------------------
0414      * Part 1:
0415      *
0416      *   defaultStruct (in memory, defined by application that uses KMoreTools)
0417      * + configuredStruct (DTO, loaded from disk, from json)
0418      * = currentStruct (in memory, used to create the actual menu)
0419      * This is done by KMoreToolsMenuBuilderPrivate::createMenuStructure(mergeWithUserConfig = true).
0420      *
0421      * Part 2:
0422      * defaultStruct => defaultStructDto
0423      * currentStruct => currentStructDto
0424      * Both DTOs go to the Configure dialog.
0425      * Users edits structure => new configuredStruct (DTO => to json => to disk)
0426      *
0427      *
0428      * If createMenuStructureOption == CreateMenuStructure_Default then the default menu structure is returned.
0429      */
0430     KmtMenuStructure createMenuStructure(CreateMenuStructureOption createMenuStructureOption) const
0431     {
0432         KmtMenuStructureDto configuredStructure; // if this stays empty then the default structure will not be changed
0433         if (createMenuStructureOption == CreateMenuStructure_MergeWithUserConfig) {
0434             // fill if should be merged
0435             configuredStructure = readUserConfig();
0436         }
0437 
0438         KmtMenuStructure mstruct;
0439 
0440         QList<KMoreToolsMenuItem *> menuItemsSource = menuItems;
0441         QList<KMoreToolsMenuItem *> menuItemsSortedAsConfigured;
0442 
0443         // presort as in configuredStructure
0444         for (const KmtMenuItemDto &item : std::as_const(configuredStructure.list)) {
0445             auto foundItem = std::find_if(menuItemsSource.begin(), menuItemsSource.end(), [&item](const KMoreToolsMenuItem *kMenuItem) {
0446                 return kMenuItem->id() == item.id;
0447             });
0448             if (foundItem != menuItemsSource.end()) {
0449                 menuItemsSortedAsConfigured.append(*foundItem); // add to final list
0450                 menuItemsSource.removeOne(*foundItem); // remove from source
0451             }
0452         }
0453         // Add remaining items from source. These may be main and more section items
0454         // so that the resulting list may have [ main items, more items, main items, more items ]
0455         // instead of only [ main items, more items ]
0456         // But in the next step this won't matter.
0457         menuItemsSortedAsConfigured.append(menuItemsSource);
0458 
0459         // build MenuStructure from presorted list
0460         for (auto item : std::as_const(menuItemsSortedAsConfigured)) {
0461             const auto registeredService = item->registeredService();
0462 
0463             if ((registeredService && registeredService->isInstalled()) || !registeredService) { // if a QAction was registered directly
0464                 std::optional<KmtMenuItemDto> confItem = configuredStructure.findInstalled(item->id());
0465                 if ((!confItem && item->defaultLocation() == KMoreTools::MenuSection_Main)
0466                     || (confItem && confItem->menuSection == KMoreTools::MenuSection_Main)) {
0467                     mstruct.mainItems.append(item);
0468                 } else if ((!confItem && item->defaultLocation() == KMoreTools::MenuSection_More)
0469                            || (confItem && confItem->menuSection == KMoreTools::MenuSection_More)) {
0470                     mstruct.moreItems.append(item);
0471                 } else {
0472                     Q_ASSERT_X(false,
0473                                "buildAndAppendToMenu",
0474                                "invalid enum"); // todo/later: apart from static programming error, if the config garbage this might happen
0475                 }
0476             } else {
0477                 if (!mstruct.notInstalledServices.contains(item->registeredService())) {
0478                     mstruct.notInstalledServices.append(item->registeredService());
0479                 }
0480             }
0481         }
0482 
0483         return mstruct;
0484     }
0485 
0486     /**
0487      * @param defaultStructure also contains the currently not-installed items
0488      */
0489     void showConfigDialog(KmtMenuStructureDto defaultStructureDto, const QString &title = QString()) const
0490     {
0491         // read from config
0492         auto currentStructure = createMenuStructure(CreateMenuStructure_MergeWithUserConfig);
0493         auto currentStructureDto = currentStructure.toDto();
0494 
0495         KMoreToolsConfigDialog *dlg = new KMoreToolsConfigDialog(defaultStructureDto, currentStructureDto, title);
0496         if (dlg->exec() == QDialog::Accepted) {
0497             currentStructureDto = dlg->currentStructure();
0498             writeUserConfig(currentStructureDto);
0499         }
0500 
0501         delete dlg;
0502     }
0503 
0504     /**
0505      * Create the 'More' menu with parent as parent
0506      * @param parent The parent of the menu
0507      */
0508     void createMoreMenu(const KmtMenuStructure &mstruct, QMenu *parent)
0509     {
0510         for (auto item : std::as_const(mstruct.moreItems)) {
0511             const auto action = item->action();
0512             action->setParent(parent);
0513             parent->addAction(action);
0514         }
0515 
0516         if (!mstruct.notInstalledServices.isEmpty()) {
0517             parent->addSection(i18nc("@action:inmenu", "Not installed:"));
0518 
0519             for (auto registeredService : std::as_const(mstruct.notInstalledServices)) {
0520                 QMenu *submenuForNotInstalled = KmtNotInstalledUtil::createSubmenuForNotInstalledApp(registeredService->formatString(QStringLiteral("$Name")),
0521                                                                                                      parent,
0522                                                                                                      registeredService->icon(),
0523                                                                                                      registeredService->homepageUrl(),
0524                                                                                                      registeredService->appstreamId());
0525                 parent->addMenu(submenuForNotInstalled);
0526             }
0527         }
0528     }
0529 };
0530 
0531 KMoreToolsMenuBuilder::KMoreToolsMenuBuilder()
0532 {
0533     Q_UNREACHABLE();
0534 }
0535 
0536 KMoreToolsMenuBuilder::KMoreToolsMenuBuilder(const QString &uniqueId, const QString &userConfigPostfix)
0537     : d(new KMoreToolsMenuBuilderPrivate())
0538 {
0539     d->uniqueId = uniqueId;
0540     d->userConfigPostfix = userConfigPostfix;
0541 }
0542 
0543 KMoreToolsMenuBuilder::~KMoreToolsMenuBuilder()
0544 {
0545     d->deleteAndClearMenuItems();
0546 }
0547 
0548 void KMoreToolsMenuBuilder::setInitialItemTextTemplate(const QString &templateText)
0549 {
0550     d->initialItemTextTemplate = templateText;
0551 }
0552 
0553 KMoreToolsMenuItem *KMoreToolsMenuBuilder::addMenuItem(KMoreToolsService *registeredService, KMoreTools::MenuSection defaultLocation)
0554 {
0555     auto kmtMenuItem = new KMoreToolsMenuItem(registeredService, defaultLocation, d->initialItemTextTemplate);
0556     kmtMenuItem->setId(d->menuItemIdGen.getId(registeredService->desktopEntryName()));
0557     d->menuItems.append(kmtMenuItem);
0558     return kmtMenuItem;
0559 }
0560 
0561 KMoreToolsMenuItem *KMoreToolsMenuBuilder::addMenuItem(QAction *action, const QString &itemId, KMoreTools::MenuSection defaultLocation)
0562 {
0563     auto kmtMenuItem = new KMoreToolsMenuItem(action, d->menuItemIdGen.getId(itemId), defaultLocation);
0564     d->menuItems.append(kmtMenuItem);
0565     return kmtMenuItem;
0566 }
0567 
0568 void KMoreToolsMenuBuilder::clear()
0569 {
0570     d->deleteAndClearMenuItems();
0571     d->menuItemIdGen.reset();
0572 }
0573 
0574 QString KMoreToolsMenuBuilder::menuStructureAsString(bool mergeWithUserConfig) const
0575 {
0576     KmtMenuStructure mstruct = d->createMenuStructure(mergeWithUserConfig ? KMoreToolsMenuBuilderPrivate::CreateMenuStructure_MergeWithUserConfig
0577                                                                           : KMoreToolsMenuBuilderPrivate::CreateMenuStructure_Default);
0578     QString s;
0579     s += QLatin1String("|main|:");
0580     for (auto item : std::as_const(mstruct.mainItems)) {
0581         s += item->registeredService()->desktopEntryName() + QLatin1Char('.');
0582     }
0583     s += QLatin1String("|more|:");
0584     for (auto item : std::as_const(mstruct.moreItems)) {
0585         s += item->registeredService()->desktopEntryName() + QLatin1Char('.');
0586     }
0587     s += QLatin1String("|notinstalled|:");
0588     for (auto regService : std::as_const(mstruct.notInstalledServices)) {
0589         s += regService->desktopEntryName() + QLatin1Char('.');
0590     }
0591     return s;
0592 }
0593 
0594 // TMP / for unit test
0595 void KMoreToolsMenuBuilder::showConfigDialog(const QString &title)
0596 {
0597     d->showConfigDialog(d->createMenuStructure(KMoreToolsMenuBuilderPrivate::CreateMenuStructure_Default).toDto(), title);
0598 }
0599 
0600 void KMoreToolsMenuBuilder::buildByAppendingToMenu(QMenu *menu,
0601                                                    KMoreTools::ConfigureDialogAccessibleSetting configureDialogAccessibleSetting,
0602                                                    QMenu **outMoreMenu)
0603 {
0604     KmtMenuStructure mstruct = d->createMenuStructure(KMoreToolsMenuBuilderPrivate::CreateMenuStructure_MergeWithUserConfig);
0605 
0606     for (auto item : std::as_const(mstruct.mainItems)) {
0607         const auto action = item->action();
0608         if (!action->parent()) { // if the action has no parent, set it to the menu to be filled
0609             action->setParent(menu);
0610         }
0611         menu->addAction(action);
0612     }
0613 
0614     QMenu *moreMenu = new QMenu(i18nc("@action:inmenu", "More"), menu);
0615 
0616     if (!mstruct.moreItems.isEmpty() || !mstruct.notInstalledServices.isEmpty()) {
0617         if (mstruct.mainItems.isEmpty()) {
0618             d->createMoreMenu(mstruct, menu);
0619         } else {
0620             menu->addSeparator();
0621             menu->addMenu(moreMenu);
0622             d->createMoreMenu(mstruct, moreMenu);
0623         }
0624     }
0625 
0626     if (moreMenu->isEmpty()) {
0627         if (outMoreMenu) {
0628             *outMoreMenu = nullptr;
0629         }
0630     } else {
0631         if (outMoreMenu) {
0632             *outMoreMenu = moreMenu;
0633         }
0634     }
0635 
0636     QMenu *baseMenu;
0637     // either the "Configure..." menu should be shown via setting or the Ctrl key is pressed
0638     if (configureDialogAccessibleSetting == KMoreTools::ConfigureDialogAccessible_Always || QApplication::keyboardModifiers() & Qt::ControlModifier
0639         || (configureDialogAccessibleSetting == KMoreTools::ConfigureDialogAccessible_Defensive && !mstruct.notInstalledServices.empty())) {
0640         if (moreMenu->isEmpty()) { // "more" menu was not created...
0641             // ...then we add the configure menu to the main menu
0642             baseMenu = menu;
0643         } else { // more menu has items
0644             // ...then it was added to main menu and has got at least on item
0645             baseMenu = moreMenu;
0646         }
0647 
0648         if (!baseMenu->isEmpty()) {
0649             baseMenu->addSeparator();
0650             auto configureAction = baseMenu->addAction(QIcon::fromTheme(QStringLiteral("configure")), i18nc("@action:inmenu", "Configure..."));
0651             configureAction->setData(QStringLiteral("configureItem")); // tag the action (currently only used in unit-test)
0652             KmtMenuStructure mstructDefault = d->createMenuStructure(KMoreToolsMenuBuilderPrivate::CreateMenuStructure_Default);
0653             KmtMenuStructureDto mstructDefaultDto = mstructDefault.toDto(); // makes sure the "Reset" button works as expected
0654             QObject::connect(configureAction, &QAction::triggered, configureAction, [this, mstructDefaultDto](bool) {
0655                 this->d->showConfigDialog(mstructDefaultDto);
0656             });
0657         }
0658     }
0659 }
0660 
0661 class KMoreToolsMenuItemPrivate
0662 {
0663 public:
0664     QString id;
0665     KMoreToolsService *registeredService = nullptr;
0666     QString initialItemText;
0667     QAction *action = nullptr;
0668     KMoreTools::MenuSection defaultLocation;
0669     bool actionAutoCreated = false; // action might stay nullptr even if actionCreated is true
0670 };
0671 
0672 KMoreToolsMenuItem::KMoreToolsMenuItem(KMoreToolsService *registeredService, KMoreTools::MenuSection defaultLocation, const QString &initialItemTextTemplate)
0673     : d(new KMoreToolsMenuItemPrivate())
0674 {
0675     d->registeredService = registeredService;
0676     d->defaultLocation = defaultLocation;
0677 
0678     // set menu item caption (text)
0679     QString defaultName = registeredService->formatString(initialItemTextTemplate); // e.g. "$GenericName", "$Name"
0680     d->initialItemText = registeredService->formatString(defaultName);
0681 }
0682 
0683 KMoreToolsMenuItem::KMoreToolsMenuItem(QAction *action, const QString &itemId, KMoreTools::MenuSection defaultLocation)
0684     : d(new KMoreToolsMenuItemPrivate())
0685 {
0686     d->action = action;
0687     d->id = itemId;
0688     d->defaultLocation = defaultLocation;
0689 }
0690 
0691 KMoreToolsMenuItem::~KMoreToolsMenuItem()
0692 {
0693     if (d->actionAutoCreated && d->action) { // Only do this if KMoreTools created the action. Other actions must be deleted by client.
0694         // d->action can already be nullptr in some cases.
0695         // Disconnects the 'connect' event (and potentially more; is this bad?)
0696         // that was connected in action() to detect action deletion.
0697         d->action->disconnect(d->action);
0698     }
0699 }
0700 
0701 QString KMoreToolsMenuItem::id() const
0702 {
0703     return d->id;
0704 }
0705 
0706 void KMoreToolsMenuItem::setId(const QString &id)
0707 {
0708     d->id = id;
0709 }
0710 
0711 KMoreToolsService *KMoreToolsMenuItem::registeredService() const
0712 {
0713     return d->registeredService;
0714 }
0715 
0716 KMoreTools::MenuSection KMoreToolsMenuItem::defaultLocation() const
0717 {
0718     return d->defaultLocation;
0719 }
0720 
0721 QString KMoreToolsMenuItem::initialItemText() const
0722 {
0723     return d->initialItemText;
0724 }
0725 
0726 void KMoreToolsMenuItem::setInitialItemText(const QString &itemText)
0727 {
0728     d->initialItemText = itemText;
0729 }
0730 
0731 QAction *KMoreToolsMenuItem::action() const
0732 {
0733     // currently we assume if a registeredService is given we auto-create the QAction once
0734     if (d->registeredService && !d->actionAutoCreated) {
0735         d->actionAutoCreated = true;
0736 
0737         if (d->registeredService->isInstalled()) {
0738             d->action = new QAction(d->registeredService->icon(), d->initialItemText, nullptr);
0739             // reset the action cache when action gets destroyed
0740             // this happens in unit-tests where menu.clear() is called before another buildByAppendingToMenu call
0741             // WARN: see also destructor! (might be a source of bugs?)
0742             QObject::connect(d->action, &QObject::destroyed, d->action, [this]() {
0743                 this->d->actionAutoCreated = false;
0744                 this->d->action = nullptr;
0745             });
0746         } else {
0747             d->action = nullptr;
0748         }
0749     }
0750     // else:
0751     // !d->registeredService => action will be provided by user
0752     // or d->actionAutoCreated => action was autocreated (or set to nullptr if service not installed)
0753 
0754     return d->action;
0755 }