File indexing completed on 2024-04-21 15:02:22

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