diff --git a/src/kmoretools/kmoretools.cpp b/src/kmoretools/kmoretools.cpp index 86821c6e..a1647392 100644 --- a/src/kmoretools/kmoretools.cpp +++ b/src/kmoretools/kmoretools.cpp @@ -1,794 +1,806 @@ /* Copyright 2015 by Gregor Mi This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . */ #include "kmoretools.h" #include "kmoretools_p.h" #include "kmoretoolsconfigdialog_p.h" #include #include #include #include #include #include #include #include class KMoreToolsPrivate { public: QString uniqueId; // allocated via new, don't forget to delete QList serviceList; QMap menuBuilderMap; public: KMoreToolsPrivate(const QString& uniqueId) : uniqueId(uniqueId) { } ~KMoreToolsPrivate() { qDeleteAll(menuBuilderMap); qDeleteAll(serviceList); } /** * @return uniqueId if kmtDesktopfileSubdir is empty * else kmtDesktopfileSubdir */ QString kmtDesktopfileSubdirOrUniqueId(const QString& kmtDesktopfileSubdir) { if (kmtDesktopfileSubdir.isEmpty()) { return uniqueId; } return kmtDesktopfileSubdir; } /** * Finds a file in the '/usr/share'/kf5/kmoretools/'uniqueId'/ directory. * '/usr/share' = "~/.local/share", "/usr/local/share", "/usr/share" (see QStandardPaths::GenericDataLocation) * 'uniqueId' = @see uniqueId() * * @param can be a filename with or without relative path. But no absolute path. * @returns the first occurence if there are more than one found */ QString findFileInKmtDesktopfilesDir(const QString& filename) { return findFileInKmtDesktopfilesDir(uniqueId, filename); } static QString findFileInKmtDesktopfilesDir(const QString& kmtDesktopfileSubdir, const QString& filename) { //qDebug() << "--search locations:" << QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); // /usr/share etc. const QString kmtDesktopfilesFilename = QLatin1String("kf5/kmoretools/") + kmtDesktopfileSubdir + QLatin1Char('/') + filename; //qDebug() << "---search for:" << kmtDesktopfilesFilename; const QString foundKmtFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, kmtDesktopfilesFilename); //qDebug() << "----QStandardPaths::locate(QStandardPaths::GenericDataLocation, kmtDesktopfilesFilename) -> foundKmtFile" << foundKmtFile; return foundKmtFile; } }; KMoreTools::KMoreTools(const QString& uniqueId) : d(new KMoreToolsPrivate(uniqueId)) { } KMoreTools::~KMoreTools() { delete d; } KMoreToolsService* KMoreTools::registerServiceByDesktopEntryName( const QString& desktopEntryName, const QString& kmtDesktopfileSubdir, KMoreTools::ServiceLocatingMode serviceLocatingMode) { //qDebug() << "* registerServiceByDesktopEntryName(desktopEntryName=" << desktopEntryName; const QString foundKmtDesktopfilePath = d->findFileInKmtDesktopfilesDir( d->kmtDesktopfileSubdirOrUniqueId(kmtDesktopfileSubdir), desktopEntryName + QLatin1String(".desktop")); const bool isKmtDesktopfileProvided = !foundKmtDesktopfilePath.isEmpty(); KService::Ptr kmtDesktopfile; if (isKmtDesktopfileProvided) { kmtDesktopfile = KService::Ptr(new KService(foundKmtDesktopfilePath)); // todo later: what exactly does "isValid" mean? Valid syntax? Or installed in system? // right now we cannot use it //Q_ASSERT_X(kmtDesktopfile->isValid(), "addServiceByDesktopFile", "the kmt-desktopfile is provided but not valid. This must be fixed."); //qDebug() << " INFO: kmt-desktopfile provided and valid."; if (kmtDesktopfile->exec().isEmpty()) { qCritical() << "KMoreTools::registerServiceByDesktopEntryName: the kmt-desktopfile " << desktopEntryName << " is provided but no Exec line is specified. The desktop file is probably faulty. Please fix. Return nullptr."; return nullptr; } //qDebug() << " INFO: kmt-desktopfile provided."; } else { qWarning() << "KMoreTools::registerServiceByDesktopEntryName: desktopEntryName " << desktopEntryName << " (kmtDesktopfileSubdir=" << kmtDesktopfileSubdir << ") not provided (or at the wrong place) in the installed kmt-desktopfiles directory. If the service is also not installed on the system the user won't get nice translated app name and description."; qDebug() << "`-- More info at findFileInKmtDesktopfilesDir, QStandardPaths::standardLocations = " << QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); // /usr/share etc. } bool isInstalled = false; KService::Ptr installedService; if (serviceLocatingMode == KMoreTools::ServiceLocatingMode_Default) { // == default behaviour: search for installed services installedService = KService::serviceByDesktopName(desktopEntryName); isInstalled = installedService != nullptr; //qDebug() << "----- isInstalled: " << isInstalled; } else if (serviceLocatingMode == KMoreTools::ServiceLocatingMode_ByProvidedExecLine) { // only use provided kmt-desktopfile: if (!isKmtDesktopfileProvided) { qCritical() << "KMoreTools::registerServiceByDesktopEntryName for " << desktopEntryName << ": If detectServiceExistenceViaProvidedExecLine is true then a kmt-desktopfile must be provided. Please fix. Return nullptr."; return nullptr; } auto tryExecProp = kmtDesktopfile->property(QStringLiteral("TryExec"), QVariant::String); isInstalled = (tryExecProp.isValid() && !QStandardPaths::findExecutable(tryExecProp.toString()).isEmpty()) || !QStandardPaths::findExecutable(kmtDesktopfile->exec()).isEmpty(); } else { Q_ASSERT(false); // case not handled } // if (isInstalled) { // qDebug() << "registerServiceByDesktopEntryName:" << desktopEntryName << ": installed."; // } else { // qDebug() << "registerServiceByDesktopEntryName:" << desktopEntryName << ": NOT installed."; // } auto registeredService = new KMoreToolsService( d->kmtDesktopfileSubdirOrUniqueId(kmtDesktopfileSubdir), desktopEntryName, isInstalled, installedService, kmtDesktopfile); // add or replace item in serviceList auto foundService = std::find_if(d->serviceList.begin(), d->serviceList.end(), [desktopEntryName](KMoreToolsService* service) { return service->desktopEntryName() == desktopEntryName; }); if (foundService == d->serviceList.end()) { //qDebug() << "not found, add new service"; d->serviceList.append(registeredService); } else { KMoreToolsService* foundServicePtr = *foundService; int i = d->serviceList.indexOf(foundServicePtr); //qDebug() << "found: replace it with new service, index=" << i; delete foundServicePtr; //qDebug() << " deleted"; d->serviceList.replace(i, registeredService); //qDebug() << " replaced in list"; } return registeredService; } KMoreToolsMenuBuilder* KMoreTools::menuBuilder(const QString& userConfigPostfix) const { if (d->menuBuilderMap.find(userConfigPostfix) == d->menuBuilderMap.end()) { d->menuBuilderMap.insert(userConfigPostfix, new KMoreToolsMenuBuilder(d->uniqueId, userConfigPostfix)); } return d->menuBuilderMap[userConfigPostfix]; } // ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------ class KMoreToolsServicePrivate { public: QString kmtDesktopfileSubdir; QString desktopEntryName; KService::Ptr installedService; KService::Ptr kmtDesktopfile; QUrl homepageUrl; int maxUrlArgCount = 0; bool isInstalled = false; + QString appstreamId; public: QString getServiceName() { if (installedService) { return installedService->name(); } else { if (kmtDesktopfile) { return kmtDesktopfile->name(); } else { return QString(); } } } QString getServiceGenericName() { if (installedService) { return installedService->genericName(); } else { if (kmtDesktopfile) { return kmtDesktopfile->genericName(); } else { return QString(); } } } /** * @return the provided icon or an empty icon if not kmtDesktopfile is available or the icon was not found */ QIcon getKmtProvidedIcon() { if (kmtDesktopfile == nullptr) { return QIcon(); } QString iconPath = KMoreToolsPrivate::findFileInKmtDesktopfilesDir(kmtDesktopfileSubdir, kmtDesktopfile->icon() + QLatin1String(".svg")); //qDebug() << "kmt iconPath" << iconPath; QIcon svgIcon(iconPath); if (!svgIcon.isNull()) { return svgIcon; } iconPath = KMoreToolsPrivate::findFileInKmtDesktopfilesDir(kmtDesktopfileSubdir, kmtDesktopfile->icon() + QLatin1String(".png")); //qDebug() << "kmt iconPath" << iconPath; QIcon pngIcon(iconPath); if (!pngIcon.isNull()) { return pngIcon; } return QIcon(); } }; KMoreToolsService::KMoreToolsService(const QString& kmtDesktopfileSubdir, const QString& desktopEntryName, bool isInstalled, KService::Ptr installedService, KService::Ptr kmtDesktopfile) : d(new KMoreToolsServicePrivate()) { d->kmtDesktopfileSubdir = kmtDesktopfileSubdir; d->desktopEntryName = desktopEntryName; d->isInstalled = isInstalled; d->installedService = installedService; d->kmtDesktopfile = kmtDesktopfile; } KMoreToolsService::~KMoreToolsService() { delete d; } QString KMoreToolsService::desktopEntryName() const { return d->desktopEntryName; } bool KMoreToolsService::isInstalled() const { return d->isInstalled; } KService::Ptr KMoreToolsService::installedService() const { return d->installedService; } KService::Ptr KMoreToolsService::kmtProvidedService() const { return d->kmtDesktopfile; } QIcon KMoreToolsService::kmtProvidedIcon() const { return d->getKmtProvidedIcon(); } QUrl KMoreToolsService::homepageUrl() const { return d->homepageUrl; } void KMoreToolsService::setHomepageUrl(const QUrl& url) { d->homepageUrl = url; } int KMoreToolsService::maxUrlArgCount() const { return d->maxUrlArgCount; } void KMoreToolsService::setMaxUrlArgCount(int maxUrlArgCount) { d->maxUrlArgCount = maxUrlArgCount; } QString KMoreToolsService::formatString(const QString& formatString) const { QString result = formatString; QString genericName = d->getServiceGenericName(); if (genericName.isEmpty()) { genericName = d->getServiceName(); if (genericName.isEmpty()) { genericName = desktopEntryName(); } } QString name = d->getServiceName(); if (name.isEmpty()) { name = desktopEntryName(); } result.replace(QLatin1String("$GenericName"), genericName); result.replace(QLatin1String("$Name"), name); result.replace(QLatin1String("$DesktopEntryName"), desktopEntryName()); return result; } QIcon KMoreToolsService::icon() const { if (installedService() != nullptr) { return QIcon::fromTheme(installedService()->icon()); } else if (kmtProvidedService() != nullptr) { return d->getKmtProvidedIcon(); } else { return QIcon(); } } void KMoreToolsService::setExec(const QString& exec) { auto service = installedService(); if (service) { service->setExec(exec); } } +QString KMoreToolsService::appstreamId() const +{ + return d->appstreamId; +} + +void KMoreToolsService::setAppstreamId(const QString& id) +{ + d->appstreamId = id; +} + + // ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------ const QString configFile = QStringLiteral("kmoretoolsrc"); const QString configKey = QStringLiteral("menu_structure"); class KMoreToolsMenuBuilderPrivate { public: QString uniqueId; /** * default value is "", see KMoreTools::menuBuilder() */ QString userConfigPostfix; QList menuItems; KmtMenuItemIdGen menuItemIdGen; QString initialItemTextTemplate = QStringLiteral("$GenericName"); public: KMoreToolsMenuBuilderPrivate() { } ~KMoreToolsMenuBuilderPrivate() { } void deleteAndClearMenuItems() { Q_FOREACH (auto item, menuItems) { //qDebug() << item; delete item; } menuItems.clear(); } KmtMenuStructureDto readUserConfig() const { KConfig config(configFile, KConfig::NoGlobals, QStandardPaths::ConfigLocation); auto configGroup = config.group(uniqueId + userConfigPostfix); QString json = configGroup.readEntry(configKey, ""); KmtMenuStructureDto configuredStructure; //qDebug() << "read from config: " << json; configuredStructure.deserialize(json); return configuredStructure; } void writeUserConfig(const KmtMenuStructureDto& mstruct) const { KConfig config(configFile, KConfig::NoGlobals, QStandardPaths::ConfigLocation); auto configGroup = config.group(uniqueId + userConfigPostfix); auto configValue = mstruct.serialize(); //qDebug() << "write to config: " << configValue; configGroup.writeEntry(configKey, configValue); configGroup.sync(); } enum CreateMenuStructureOption { CreateMenuStructure_Default, CreateMenuStructure_MergeWithUserConfig }; /** * Merge strategy if createMenuStructureOption == CreateMenuStructure_MergeWithUserConfig * -------------------------------------------------------------------------------------- * 1) For each 'main' section item from configStruct * lookup in current structure (all installed items) and if found add to new structure * This means items which are in configStruct but not in current structure will be discarded. * * 2) Add remaining 'main' section items from current to new structure * * 3) Do the 1) and 2) analogous for 'more' section * * * How default structure and DTOs play together * -------------------------------------------- * Part 1: * * defaultStruct (in memory, defined by application that uses KMoreTools) * + configuredStruct (DTO, loaded from disk, from json) * = currentStruct (in memory, used to create the actual menu) * This is done by KMoreToolsMenuBuilderPrivate::createMenuStructure(mergeWithUserConfig = true). * * Part 2: * defaultStruct => defaultStructDto * currentStruct => currentStructDto * Both DTOs go to the Configure dialog. * Users edits structure => new configuredStruct (DTO => to json => to disk) * * * If createMenuStructureOption == CreateMenuStructure_Default then the default menu structure is returned. */ KmtMenuStructure createMenuStructure(CreateMenuStructureOption createMenuStructureOption) const { KmtMenuStructureDto configuredStructure; // if this stays empty then the default structure will not be changed if (createMenuStructureOption == CreateMenuStructure_MergeWithUserConfig) { // fill if should be merged configuredStructure = readUserConfig(); } KmtMenuStructure mstruct; QList menuItemsSource = menuItems; QList menuItemsSortedAsConfigured; // presort as in configuredStructure // Q_FOREACH (const auto& item, configuredStructure.list) { auto foundItem = std::find_if(menuItemsSource.begin(), menuItemsSource.end(), [item](const KMoreToolsMenuItem* kMenuItem) { return kMenuItem->id() == item.id; }); if (foundItem != menuItemsSource.end()) { menuItemsSortedAsConfigured.append(*foundItem); // add to final list menuItemsSource.removeOne(*foundItem); // remove from source } } // Add remaining items from source. These may be main and more section items // so that the resulting list may have [ main items, more items, main items, more items ] // instead of only [ main items, more items ] // But in the next step this won't matter. menuItemsSortedAsConfigured.append(menuItemsSource); // build MenuStructure from presorted list // Q_FOREACH (auto item, menuItemsSortedAsConfigured) { const auto registeredService = item->registeredService(); if ((registeredService && registeredService->isInstalled()) || !registeredService) { // if a QAction was registered directly auto confItem = configuredStructure.findInstalled(item->id()); if ((!confItem && item->defaultLocation() == KMoreTools::MenuSection_Main) || (confItem && confItem->menuSection == KMoreTools::MenuSection_Main)) { mstruct.mainItems.append(item); } else if ((!confItem && item->defaultLocation() == KMoreTools::MenuSection_More) || (confItem && confItem->menuSection == KMoreTools::MenuSection_More)) { mstruct.moreItems.append(item); } else { Q_ASSERT_X(false, "buildAndAppendToMenu", "invalid enum"); // todo/later: apart from static programming error, if the config garbage this might happen } } else { if (!mstruct.notInstalledServices.contains(item->registeredService())) { mstruct.notInstalledServices.append(item->registeredService()); } } } return mstruct; } /** * @param defaultStructure also contains the currently not-installed items */ void showConfigDialog(KmtMenuStructureDto defaultStructureDto, const QString& title = QString()) const { // read from config // auto currentStructure = createMenuStructure(CreateMenuStructure_MergeWithUserConfig); auto currentStructureDto = currentStructure.toDto(); KMoreToolsConfigDialog *dlg = new KMoreToolsConfigDialog(defaultStructureDto, currentStructureDto, title); if (dlg->exec() == QDialog::Accepted) { currentStructureDto = dlg->currentStructure(); writeUserConfig(currentStructureDto); } delete dlg; } }; KMoreToolsMenuBuilder::KMoreToolsMenuBuilder() { Q_ASSERT(false); } KMoreToolsMenuBuilder::KMoreToolsMenuBuilder(const QString& uniqueId, const QString& userConfigPostfix) : d(new KMoreToolsMenuBuilderPrivate()) { d->uniqueId = uniqueId; d->userConfigPostfix = userConfigPostfix; } KMoreToolsMenuBuilder::~KMoreToolsMenuBuilder() { d->deleteAndClearMenuItems(); delete d; } void KMoreToolsMenuBuilder::setInitialItemTextTemplate(const QString& templateText) { d->initialItemTextTemplate = templateText; } KMoreToolsMenuItem* KMoreToolsMenuBuilder::addMenuItem(KMoreToolsService* registeredService, KMoreTools::MenuSection defaultLocation) { auto kmtMenuItem = new KMoreToolsMenuItem(registeredService, defaultLocation, d->initialItemTextTemplate); kmtMenuItem->setId(d->menuItemIdGen.getId(registeredService->desktopEntryName())); d->menuItems.append(kmtMenuItem); return kmtMenuItem; } KMoreToolsMenuItem* KMoreToolsMenuBuilder::addMenuItem(QAction* action, const QString& itemId, KMoreTools::MenuSection defaultLocation) { auto kmtMenuItem = new KMoreToolsMenuItem(action, d->menuItemIdGen.getId(itemId), defaultLocation); d->menuItems.append(kmtMenuItem); return kmtMenuItem; } void KMoreToolsMenuBuilder::clear() { //qDebug() << "----KMoreToolsMenuBuilder::clear()"; //qDebug() << "d" << d; //qDebug() << "d->menuItems" << d->menuItems.count(); d->deleteAndClearMenuItems(); //qDebug() << "----after d->menuItems.clear();"; d->menuItemIdGen.reset(); } QString KMoreToolsMenuBuilder::menuStructureAsString(bool mergeWithUserConfig) const { KmtMenuStructure mstruct = d->createMenuStructure(mergeWithUserConfig ? KMoreToolsMenuBuilderPrivate::CreateMenuStructure_MergeWithUserConfig : KMoreToolsMenuBuilderPrivate::CreateMenuStructure_Default); QString s; s += QLatin1String("|main|:"); Q_FOREACH (auto item, mstruct.mainItems) { s += item->registeredService()->desktopEntryName() + "."; } s += QLatin1String("|more|:"); Q_FOREACH (auto item, mstruct.moreItems) { s += item->registeredService()->desktopEntryName() + "."; } s += QLatin1String("|notinstalled|:"); Q_FOREACH (auto regService, mstruct.notInstalledServices) { s += regService->desktopEntryName() + "."; } return s; } // TMP / for unit test void KMoreToolsMenuBuilder::showConfigDialog(const QString& title) { d->showConfigDialog(d->createMenuStructure(KMoreToolsMenuBuilderPrivate::CreateMenuStructure_Default).toDto(), title); } void KMoreToolsMenuBuilder::buildByAppendingToMenu(QMenu* menu, KMoreTools::ConfigureDialogAccessibleSetting configureDialogAccessibleSetting, QMenu** outMoreMenu) { KmtMenuStructure mstruct = d->createMenuStructure(KMoreToolsMenuBuilderPrivate::CreateMenuStructure_MergeWithUserConfig); Q_FOREACH (auto item, mstruct.mainItems) { const auto action = item->action(); if (!action->parent()) { // if the action has no parent, set it to the menu to be filled action->setParent(menu); } menu->addAction(action); } QMenu* moreMenu = new QMenu(i18nc("@action:inmenu", "More"), menu); if (!mstruct.moreItems.isEmpty() || !mstruct.notInstalledServices.isEmpty()) { menu->addSeparator(); menu->addMenu(moreMenu); Q_FOREACH (auto item, mstruct.moreItems) { const auto action = item->action(); action->setParent(menu); moreMenu->addAction(action); } if (!mstruct.notInstalledServices.isEmpty()) { //qDebug() << "notInstalledItems not empty => build 'Not installed' section"; moreMenu->addSection(i18nc("@action:inmenu", "Not installed:")); Q_FOREACH (auto registeredService, mstruct.notInstalledServices) { QMenu* submenuForNotInstalled = KmtNotInstalledUtil::createSubmenuForNotInstalledApp( - registeredService->formatString(QStringLiteral("$Name")), menu, registeredService->icon(), registeredService->homepageUrl()); + registeredService->formatString(QStringLiteral("$Name")), menu, registeredService->icon(), registeredService->homepageUrl(), registeredService->appstreamId()); moreMenu->addMenu(submenuForNotInstalled); } } } if (moreMenu->isEmpty()) { if (outMoreMenu) { *outMoreMenu = nullptr; } } else { if (outMoreMenu) { *outMoreMenu = moreMenu; } } QMenu* baseMenu; // either the "Configure..." menu should be shown via setting or the Ctrl key is pressed if (configureDialogAccessibleSetting == KMoreTools::ConfigureDialogAccessible_Always || QApplication::keyboardModifiers() & Qt::ControlModifier || (configureDialogAccessibleSetting == KMoreTools::ConfigureDialogAccessible_Defensive && !mstruct.notInstalledServices.empty())) { if (moreMenu->isEmpty()) { // "more" menu was not created... // ...then we add the configure menu to the main menu baseMenu = menu; } else { // more menu has items // ...then it was added to main menu and has got at least on item baseMenu = moreMenu; } if (!baseMenu->isEmpty()) { baseMenu->addSeparator(); auto configureAction = baseMenu->addAction(i18nc("@action:inmenu", "Configure...")); configureAction->setData("configureItem"); // tag the action (currently only used in unit-test) KmtMenuStructure mstructDefault = d->createMenuStructure(KMoreToolsMenuBuilderPrivate::CreateMenuStructure_Default); KmtMenuStructureDto mstructDefaultDto = mstructDefault.toDto(); // makes sure the "Reset" button works as expected QObject::connect(configureAction, &QAction::triggered, configureAction, [this, mstructDefaultDto](bool) { this->d->showConfigDialog(mstructDefaultDto); }); } } } // ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------ class KMoreToolsMenuItemPrivate { public: QString id; KMoreToolsService* registeredService = nullptr; QString initialItemText; QAction* action = nullptr; KMoreTools::MenuSection defaultLocation; bool actionAutoCreated = false; // action might stay nullptr even if actionCreated is true }; KMoreToolsMenuItem::KMoreToolsMenuItem(KMoreToolsService* registeredService, KMoreTools::MenuSection defaultLocation, const QString& initialItemTextTemplate) : d(new KMoreToolsMenuItemPrivate()) { d->registeredService = registeredService; d->defaultLocation = defaultLocation; // set menu item caption (text) QString defaultName = registeredService->formatString(initialItemTextTemplate); // e.g. "$GenericName", "$Name" d->initialItemText = registeredService->formatString(defaultName); } KMoreToolsMenuItem::KMoreToolsMenuItem(QAction* action, const QString& itemId, KMoreTools::MenuSection defaultLocation) : d(new KMoreToolsMenuItemPrivate()) { d->action = action; d->id = itemId; d->defaultLocation = defaultLocation; } KMoreToolsMenuItem::~KMoreToolsMenuItem() { if (d->actionAutoCreated && d->action) { // Only do this if KMoreTools created the action. Other actions must be deleted by client. // d->action can already be nullptr in some cases. // Disconnects the 'connect' event (and potentially more; is this bad?) // that was connected in action() to detect action deletion. d->action->disconnect(d->action); delete d; } } QString KMoreToolsMenuItem::id() const { return d->id; } void KMoreToolsMenuItem::setId(const QString& id) { d->id = id; } KMoreToolsService* KMoreToolsMenuItem::registeredService() const { return d->registeredService; } KMoreTools::MenuSection KMoreToolsMenuItem::defaultLocation() const { return d->defaultLocation; } QString KMoreToolsMenuItem::initialItemText() const { return d->initialItemText; } void KMoreToolsMenuItem::setInitialItemText(const QString& itemText) { d->initialItemText = itemText; } QAction* KMoreToolsMenuItem::action() const { // currently we assume if a registeredService is given we auto-create the QAction once if (d->registeredService && !d->actionAutoCreated) { d->actionAutoCreated = true; if (d->registeredService->isInstalled()) { d->action = new QAction(d->registeredService->icon(), d->initialItemText, nullptr); // reset the action cache when action gets destroyed // this happens in unit-tests where menu.clear() is called before another buildByAppendingToMenu call // WARN: see also destructor! (might be a source of bugs?) QObject::connect(d->action, &QObject::destroyed, d->action, [this]() { this->d->actionAutoCreated = false; this->d->action = nullptr; }); } else { d->action = nullptr; } } // else: // !d->registeredService => action will be provided by user // or d->actionAutoCreated => action was autocreated (or set to nullptr if service not installed) return d->action; } diff --git a/src/kmoretools/kmoretools.h b/src/kmoretools/kmoretools.h index 9e7825d9..4d7f1dbb 100644 --- a/src/kmoretools/kmoretools.h +++ b/src/kmoretools/kmoretools.h @@ -1,765 +1,787 @@ /* Copyright 2015 by Gregor Mi This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . */ #ifndef KMORETOOLS_H #define KMORETOOLS_H #include #include #include #include #include "knewstuff_export.h" class KMoreToolsService; class KMoreToolsMenuBuilder; class KMoreToolsPrivate; /** * Helps to create user-configurable menus with tools which are potentially not yet installed. * * This class is one entry point of the KMoreTools API. * See also KMoreToolsMenuFactory. * * @note This is a new API (published within KNewStuff since April 2015). Its current * target are KDE applications which are part of the kdesrcbuild infrastructure. * Here, it is possible to find all usages and to adapt to API changes when needed. * So, if you use this in your own application, beware that there might be API * changes when more use cases are developed. * * * Introduction * ------------ * KMoreTools helps to to build user-configurable menus with tools which * might not installed yet. These tools may also take URL arguments supplied * by the application. * * The user will see a menu item for a tool even if it is not installed (in the * 'More' section). Furthermore, it makes long menus shorter by providing a * main and more section. * It provides a 'Configure menu' dialog to make the menu user-configurable. * * It does this in the following ways: * - Provide an API to define external applications for a given context. * - If a defined application is not installed (yet) the application is (optionally) * still presented to the user with a hint that it is not installed and a link * to the homepage (later with integration to package managment). * This increases the discoverability of useful applications the user never * heard about yet. * - In case of many applications for a given context, it provides a GUI to the * user to hand-pick favorite tools. * This makes it easier for application developers to add alternative * application/tool suggestions without worrying about cluttered menus. * - Menu items can be (automatically) moved to the "More" submenu. * - Reduce translation effort by re-using .desktop files of the services added * to the menu. * * * Details * ------- * The term "kmt-desktopfile" refers to a 1:1 copy of a .desktop file. The * kmt-desktopfile is provided by the application that uses KMoreTools * and must be installed to subdirectories of /usr/share/kf5/kmoretools/ * - e.g. /usr/share/kf5/kmoretools/dolphin/statusbar-diskspace-menu/ * - e.g. /usr/share/kf5/kmoretools/kate/addons/project/git-tools/ * - generally, 'QStandardPaths::GenericDataLocation'/kf5/kmoretools/'uniqueId' * * See KMoreTools::KMoreTools for hints of how to install this correctly * using cmake. * * The kmt-desktopfiles are used to get ready-made translations for application * name and description even if the application is not installed. You can * also provide an icon which is used in the not-installed section when the * application is not installed yet. * * For details about the resulting menu structure, see KMoreToolsMenuBuilder. * * See also https://community.kde.org/Scratchpad/KMoreToolsFramework (outdated) * * * Rationale for the "Not installed" section * ----------------------------------------- * - Increase discoverability and visibility of useful free software that have * inherently low budget for marketing. * - Make interconnection of different free software packages as effortless as * possible (in terms of creating and maintaining the menu). * - Provide expert (i.e. your) knowledge to useful free software alternatives * to solve a certain task. * - Give novice users hints about tools that are useful in a particular * context even if they are not installed. * - Improve self-documentation of applications. * * * Presets * ------- * Before installing desktop files in your application you might take a look * at KMoreToolsPresets or KMoreToolsMenuFactory which might already contain * the needed tools. * * * Screenshots * ----------- * This section shows screenshots of usage examples. * * ### KSnapshot's Send To... menu * * Last updated: 2015-04-17, uncommited demo, source code: * src/kde/kdegraphics/ksnapshot/ksnapshotsendtoactions.cpp * * Note, that the last item in the 'More' menu in the following screenshot was * added by KSnapshot's code. * * \image html kmoretools-ksnapshot-sendto-1.png "Send To menu" width=100px * * ### Dolphins's Space info menu * * Last updated: 2015-04-17, uncommited demo, source code: src/kde/applications/dolphin/src/statusbar/spaceinfotoolsmenu.cpp * * \image html kmoretools-dolphin-spaceinfo-1.png "Space info menu" width=100px * * ### Kate's Project plugin git menu * * Last updated: 2015-03-25, uncommited demo, source code: * src/kde/applications/kate/addons/project/kateprojecttreeviewcontextmenu.cpp * * \image html kmoretools-kate-project-1-all-installed.png "All git tools installed" width=100px * * \image html kmoretools-kate-project-2-two-not-installed.png "Not all git tools installed" width=100px * * \image html kmoretools-kate-project-3-config-dialog-all-installed.png "'Configure menu' dialog" width=100px * * ### Kate's Project plugin git menu * * Last updated: 2015-04-17, source code: src/frameworks/knewstuff/tests/kmoretools/kmoretoolstest.cpp * * \image html kmoretools-tests-configure-dialog-notinstalledapps.png "Configure dialog when there are non-installed apps" width=100px * * * FAQ * --- * ### Why is everything based on desktopfiles? * * - With desktopfiles translation can be reused. * - Definition of application icon can be reused. * - They provide a unified interface for dealing with program arguments. * * * todo later * ---------- * - question: KMoreTools::registerServiceByDesktopEntryName(): * - warn if service is not of Type=Application (KService::isApplication()) or just leave it? * Add support for package managers to install software (e.g. muon discover) * - maybe: kmt-desktopfiles: add a config file that can configure the homepage URLs * and e.g. the package name if needed for package manager support * * @since 5.10 */ class KNEWSTUFF_EXPORT KMoreTools { friend class KMoreToolsService; friend class KMoreToolsServicePrivate; public: /** * Specify how should be determined if a service is installed or not */ enum ServiceLocatingMode { /** * by existence of desktop file (discoverable by KService) */ ServiceLocatingMode_Default, /** * by existence of executable defined in the TryExec or Exec line of * the provided kmt-desktopfile */ ServiceLocatingMode_ByProvidedExecLine }; /** * Specify where a menu item be placed by default */ enum MenuSection { /** * The item is placed in the main section (default) */ MenuSection_Main, /** * The item is placed in the "More" submenu. */ MenuSection_More }; // /* * // * todo/later: introduce when needed // */ // enum NotInstalledSectionOption // { // /* * // * default // */ // NotInstalledSection_Show, // // /* * // * Even if there are non-installed apps the Not-Installed section will // * not be shown // */ // NotInstalledSection_Hide // }; /** * Specify if the Configure dialog be accessible from the menu * (via a "Configure..." menu item) */ enum ConfigureDialogAccessibleSetting { /** * Always show the "Configure..." menu item * (default) */ ConfigureDialogAccessible_Always, /** * Defensively show the "Configure..." menu item * * The "Configure..." menu item will only be shown if there are non-installed * apps. * Rationale (suggestion): Do not clutter menu more than needed in standard * cases. But when there are not-installed apps the configure dialog can * be used to find out more about these apps. * * Note, that the "Configure..." menu item still becomes visible when the * user holds the Ctrl key while opening the menu. */ ConfigureDialogAccessible_Defensive }; public: /** * @param uniqueId defines two things * 1) the config section name where the user settings done by the Configure * dialog will be stored. * 2) the location where the kmt-desktopfiles should be installed because * there they will be searched by default. * If @p uniqueId contains slashes they will result in subdirectories. * The default location can be overriden by * registerServiceByDesktopEntryName's kmtDesktopfileSubdir parameter. * This is currently used in KMoreToolsPresets implementation to * separate the kmt-desktopfiles location from the user's config section * name. * * Install Desktopfiles * -------------------- * Example 1 (CMakeLists.txt if uniqueId = "dolphin/statusbar-diskspace-menu"): * \verbatim # note the trailing slash ------------. (it makes sure only the contents of the directory is copied) # | ----fix--- # v ------ uniqueId----------------- install(DIRECTORY statusbar/kmt-desktopfiles/ DESTINATION ${KDE_INSTALL_DATADIR_KF5}/kmoretools/dolphin/statusbar-diskspace-menu) \endverbatim Example 2: \verbatim ------ uniqueId-------------- install(DIRECTORY kmt-desktopfiles/ DESTINATION ${KDE_INSTALL_DATADIR_KF5}/kmoretools/kate/addons/project/git-tools) \endverbatim * * ### About ${KDE_INSTALL_DATADIR_KF5} * * In general, ${KDE_INSTALL_DATADIR_KF5}/kmoretools/hallo ends up in /usr/share/kf5/kmoretools/hallo. * * To use it, you need to add \verbatim include(KDEInstallDirs) \endverbatim to your CMakeLists.txt. */ explicit KMoreTools(const QString& uniqueId); ~KMoreTools(); /** * Registers a service with KMoreTools. * * If the method is called more than once for the same desktopEntryName * the service is located again and the old service is replaced with the * new one. * * @param desktopEntryName is the name of the desktopfile (without the * .desktop extension) * The desktop file is * 1. either already installed. Then the information of the installed file * is used. * 2. or not installed and kmt-desktopfile is present. Then the information * of the app-local copy of desktopfile located in the kmt-desktopfiles * directory is used * 3. or not installed and no kmt-desktopfile provided. In this case * KMoreToolsService::setHomepageUrl should be used so that at least a * website link can be displayed. * * @param kmtDesktopfileSubdir when not empty overrides the @p uniqueId * parameter from the ctor when it comes to searching a kmt-desktopfile. * Default value is the empty string. * * @param serviceLocatingMode == ServiceLocatingMode_ByProvidedExecLine: * Some programs don't install a desktop file of their own (e.g. gitk). * If set to true then installed desktop files are not searched * but the provided in kmt-desktopfiles will be used to extract exec line. * The exec line will be used to determine if the executable is installed. * * @return a KMoreToolsService pointer which lives as long as KMoreTools, so * do not store it for later use. * @return nullptr if the kmt provided desktop file is faulty. * This kind of error must be fixed before you ship your application. * This case is only used for unit tests. */ KMoreToolsService* registerServiceByDesktopEntryName( const QString& desktopEntryName, const QString& kmtDesktopfileSubdir = QString(), ServiceLocatingMode serviceLocatingMode = ServiceLocatingMode_Default); /** * @returns the interface to build the menu. It is a singleton instance * for each different @p userConfigPostfix (which is "" by default). * So repeated calls with same parameter will return the same object. * * The pointer lives as long as KMoreTools. * * @param userConfigPostfix is empty by default. You can use it to specify * a postfix for the user config section. So you can build different menus * which can be configured separately. (This is used in unit tests to * separated test cases.) * * @sa KMoreToolsMenuBuilder::clear() */ KMoreToolsMenuBuilder* menuBuilder(const QString& userConfigPostfix = QString()) const; private: /** * No copy semantic => private and no implementation */ KMoreTools(const KMoreTools&); private: KMoreToolsPrivate* d; }; // -------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------- class KMoreToolsServicePrivate; /** * A service described in a .desktop file (kmt-desktopfile) which will be * called "registered service". * * A registered service can either be installed (isInstalled() == true) * or - if not found on the system - not installed (isInstalled() == false). * * @since 5.10 */ class KNEWSTUFF_EXPORT KMoreToolsService { friend class KMoreTools; friend class KMoreToolsPrivate; public: ~KMoreToolsService(); /** * @return the desktop entry name which the service is identified by and with which * it was registered (see registerServiceByDesktopEntryName). * * Filename without .desktop: e.g. if the desktop file is named * "org.kde.ksnapshot.desktop" then the desktop entry name is * "org.kde.ksnapshot". */ QString desktopEntryName() const; /** * @returns true if the desktopfile with the given * desktopname (name of the .desktop file without the .desktop) * is installed on the system */ bool isInstalled() const; /** * @returns the KService represented by an installed desktop file. * * @note that this might be nullptr even if isInstalled() is true. * This can only happen when ServiceLocatingMode::ServiceLocatingMode_ByProvidedExecLine * is used in registerServiceByDesktopEntryName. (Then the kmt-desktopfile's * Exec line is used to determine if a program is installed) */ KService::Ptr installedService() const; /** * @returns a non-null KService::Ptr if app-local kmt-desktopfile is * found and valid */ KService::Ptr kmtProvidedService() const; /** * @return the icon provided by the KMoreTools' user and not the installed one. * (e.g. QGit currently has got a blank icon installed) */ QIcon kmtProvidedIcon() const; /** * @see setHomepageUrl() */ QUrl homepageUrl() const; /** * Sets the homepage url the user is shown when a service is not installed. * This way the user gets some information of how to install the * application. */ void setHomepageUrl(const QUrl& url); /** * @see setMaxUrlArgCount() */ int maxUrlArgCount() const; /** * In KMoreToolsMenuFactory some minor magic is done. In the context of * connecting the action trigger signal we need to know the maximum number * of URL arguments a given service can accept. Usaually a number between * 0 and 1. Sometimes 2. * E.g. kdf must not be called with any positional argument. * E.g. gitg can be called with zero or one arguments. */ void setMaxUrlArgCount(int maxUrlArgCount); /** * @param formatString supports the following placeholders: * * 1. $GenericName * 2. $Name * 3. $DesktopEntryName * * which are replaced by the corresponding desktop file entries. * * If a value for a placeholder is not available (or empty) * (e.g. if no desktop file is available (not installed or not provided * via kmt-desktopfiles)) then the next one is used until 3. is reached which * is always available. Example: the formatString is "$GenericName", but * the GenericName field is not available. So $Name is used. If this is * also not available, $DesktopEntryName is used. * * @sa KMoreToolsMenuItem::setInitialItemText * @sa KMoreToolsMenuBuilder::setInitialItemTextTemplate */ QString formatString(const QString& formatString) const; /** * 1. Icon from installed desktop file * If 1. is not found not found then... * 2. icon from kmt desktop file (which is then searched in the kmt-desktopfiles * directory, must have extension .svg or .png) * If 2. is not not found then... * 3. no icon */ QIcon icon() const; /** * Will override the "Exec=" line of the service. Will only apply if the * service is installed. * * @see KService::setExec(...) */ void setExec(const QString& exec); + /** + * Returns the associated appstream id that was previously set with setAppstreamId(). + * If no appstream id was set, an empty string is returned. + * + * @return The service's appstream id. + * + * @since 5.48 + */ + QString appstreamId() const; + + /** + * Sets the appstream id of the service. This is used to create a + * appstream url for installing the service via a software store + * (e.g. Discover). For instance, the appstream id for filelight is + * "org.kde.filelight.desktop". + * + * @param id the appstream id + * + * @since 5.48 + */ + void setAppstreamId(const QString&); + private: /** * @param kmtDesktopfileSubdir where to find kmt-desktopfiles * @param desktopEntryName name of the desktopfile without the .desktop extension * @param isInstalled true if desktop file is installed * @param installedService not nullptr if @p isInstalled is true * @param kmtDesktopfile not null if app-local kmt-desktopfile is found and valid */ KMoreToolsService(const QString& kmtDesktopfileSubdir, const QString& desktopEntryName, bool isInstalled, KService::Ptr installedService, KService::Ptr kmtDesktopfile); /** * No copy semantic => private and no implementation */ KMoreToolsService(const KMoreTools&); KMoreToolsServicePrivate* d; }; // -------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------- class KMoreToolsMenuItem; class KMoreToolsMenuBuilderPrivate; /** * Define how the default structure of the menu should look like. * * Depending on if the added service is installed or not a "Not installed" section * will be automatically added to the generated menu. * * @since 5.10 */ class KNEWSTUFF_EXPORT KMoreToolsMenuBuilder { friend class KMoreToolsPrivate; friend class KMoreTools; friend class KMoreToolsTest; friend class KMoreToolsTest2; friend class KMoreToolsTestInteractive; public: ~KMoreToolsMenuBuilder(); /** * Affects addMenuItem() if called before it. * * see KMoreToolsService::formatString, see KMoreToolsMenuItem::setInitialItemText * * The default template text is "$GenericName". */ void setInitialItemTextTemplate(const QString& templateText); /** * Adds a registered service (which can installed or not) to the menu. * If the service is not installed it will be shown in the "Not installed" * section. * * @param registeredService will be added to a the menu. A unique menu * itemId will be generated automatically from the desktopEntryName. * See also KMoreToolsMenuItem::id(). * * @param defaultLocation is KMoreTools::MenuSection_Main by default. * * The registeredService->isInstalled() result will be respected. E.g. if the service * is not installed it will be placed in the "Not installed" section in the more * location of the menu even if @p defaultLocation was main location. * * See also KMoreToolsMenuItem ctor * * @sa KMoreToolsMenuItem::action() */ KMoreToolsMenuItem* addMenuItem(KMoreToolsService* registeredService, KMoreTools::MenuSection defaultLocation = KMoreTools::MenuSection_Main); /** * Adds an action to the menu which is created and managed by the caller. * * @param action to be added to the menu. * * @param itemId is a unique (for this menu) id for the item. The itemId * _may_ be not unique. Then a unique id is generated automatically by * using some postfix. But it is better if you specify something sensible * because the itemId is used to find the items in the user config. * Otherwise the user config can be messed up if the order or number * of default menu items changes. NOTE, that the QAction::text is NOT * used to generate the unique id because the text is translated and * therefore not stable. * * @sa KMoreToolsMenuItem::action() */ KMoreToolsMenuItem* addMenuItem(QAction* action, const QString& itemId, KMoreTools::MenuSection defaultLocation = KMoreTools::MenuSection_Main); /** * Clears all added menu items. This can be useful if the menuBuilder is reused more than once. * * @sa KMoreToolsService::menuBuilder */ void clear(); /** * Builds the actual menu and appends all items (main items, * more submenu with a potential "not installed" section) to the @p menu. * * @param menu the menu where the items should be appended to * * @param configureDialogAccessibleSetting determines when the * "Configure..." menu item should be added to the menu * * @param moreMenu if not nullptr then it will be set to the pointer to the * "More" menu in case it was created. * Otherwise the pointer will set to nullptr. * This can be used to add some custom items to the @p menu. */ void buildByAppendingToMenu( QMenu* menu, KMoreTools::ConfigureDialogAccessibleSetting configureDialogAccessibleSetting = KMoreTools::ConfigureDialogAccessible_Always, QMenu** outMoreMenu = nullptr); private: /** * for unit testing / get as debug string */ QString menuStructureAsString(bool mergeWithUserConfig) const; /** * for unit testing */ void showConfigDialog(const QString& title); /** * (needed because QMap needs a default ctor) */ KMoreToolsMenuBuilder(); /** * internal usage */ KMoreToolsMenuBuilder(const QString& uniqueId, const QString& userConfigPostfix); /** * No copy semantic => private and no implementation */ KMoreToolsMenuBuilder(const KMoreTools&); KMoreToolsMenuBuilderPrivate* d; }; // -------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------- class KMoreToolsMenuItemPrivate; /** * Represents a menu item of a service (application, tool or variant of the same * service with different parameters). * * The service might be installed or not. * * The corresponding QAction will be created for installed services. * * @note that for not-installed services action() returns nullptr. * * @since 5.10 */ class KNEWSTUFF_EXPORT KMoreToolsMenuItem { friend class KMoreToolsMenuBuilderPrivate; friend class KMoreToolsMenuBuilder; public: /** * Auto-generated unique id that tries to be as stable as possible even if the * menu gets restructured after the user did some customization that was * persisted in a config file. * * @note It is possible to add the same service more than once (and then * hopefully change the action text). When the order of those are changed, * the id will not be consistent (because internally an increasing number is used) * If you have issues with this you can solve this by manually * calling setId (e.g. 'desktopEntryName' + 'x'). */ QString id() const; /** * (Optional) to help with stable ids (see id()) * * todo: make sure that if this is called, uniqueness of ids will be assured. * todo: make sure to show error if the id contains characters other than * alphanumerica, dashes and underscores etc. */ void setId(const QString& id); /** * @return the underlying KMoreToolsService instance, * see KMoreToolsMenuBuilder::addMenuItem (with KKmoreToolsService* argument). * Or nullptr when KMoreToolsMenuBuilder::addMenuItem (with QAction* argument * was used). */ KMoreToolsService* registeredService() const; /** * see KMoreToolsMenuBuilder::addMenuItem */ KMoreTools::MenuSection defaultLocation() const; /** * see setInitialItemText() */ QString initialItemText() const; /** * Sets the initial text of a menu item. * * Menu items of a non-installed service will get this text. * If the service is installed and you would like to change the item text, * you can retrieve the created QAction (action()) * and modify the text using QAction's methods (QAction::setText()). * * @see * - initialItemText() * - action() * - You can use the static method KMoreToolsService::formatString here. */ void setInitialItemText(const QString& itemText); /** * Case 1 * ------ * KMoreToolsMenuBuilder::addMenuItem was called with KKmoreToolsService* argument. * * the corresponding QAction which will be added to the actual menu when * underlying service is installed or else - if not installed - nullptr. * * So you can change the created action as you desire. * * We return nullptr because not-installed services will get a submenu with * other items like opening a website instead of an single action. * * To change the item's text even for not-installed services use initialItemText() * * Note, that once the method was invoked the first time the action is created * an then reused. * * Case 2 * ------ * KMoreToolsMenuBuilder::addMenuItem was called with QAction* argument. * The added action will be returned. * * @see KMoreToolsService::isInstalled */ QAction* action() const; private: // internal usage /** * Sets the initial item text. */ KMoreToolsMenuItem(KMoreToolsService* registeredService, KMoreTools::MenuSection defaultLocation, const QString& initialItemTextTemplate); KMoreToolsMenuItem(QAction* action, const QString& itemId, KMoreTools::MenuSection defaultLocation); ~KMoreToolsMenuItem(); private: /** * No copy semantic => private and no implementation */ KMoreToolsMenuItem(const KMoreTools&); private: KMoreToolsMenuItemPrivate* d; }; #endif // KMORETOOLS_H diff --git a/src/kmoretools/kmoretools_p.h b/src/kmoretools/kmoretools_p.h index 3d53b5ad..3dce441a 100644 --- a/src/kmoretools/kmoretools_p.h +++ b/src/kmoretools/kmoretools_p.h @@ -1,419 +1,432 @@ /* Copyright 2015 by Gregor Mi This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . */ #ifndef KMORETOOLS_P_H #define KMORETOOLS_P_H #include "kmoretools.h" #include #include #include #include #include #include #include #include #define _ QLatin1String /** * Makes sure that if the same inputId is given more than once * we will get unique IDs. * * See KMoreToolsTest::testMenuItemIdGen(). */ class KmtMenuItemIdGen { public: QString getId(const QString& inputId) { int postFix = desktopEntryNameUsageMap[inputId]; desktopEntryNameUsageMap[inputId] = postFix + 1; return QStringLiteral("%1%2").arg(inputId).arg(postFix); } void reset() { desktopEntryNameUsageMap.clear(); } private: QMap desktopEntryNameUsageMap; }; /** * A serializeable menu item */ class KmtMenuItemDto { public: QString id; /** * @note that is might contain an ampersand (&) which may be used for menu items. * Remove it with removeMenuAmpersand() */ QString text; QIcon icon; KMoreTools::MenuSection menuSection; bool isInstalled = true; /** * only used if isInstalled == false */ QUrl homepageUrl; + QString appstreamId; + public: void jsonRead(const QJsonObject &json) { id = json[_("id")].toString(); menuSection = json[_("menuSection")].toString() == _("main") ? KMoreTools::MenuSection_Main : KMoreTools::MenuSection_More; isInstalled = json[_("isInstalled")].toBool(); } void jsonWrite(QJsonObject &json) const { json[_("id")] = id; json[_("menuSection")] = menuSection == KMoreTools::MenuSection_Main ? _("main") : _("more"); json[_("isInstalled")] = isInstalled; } bool operator==(const KmtMenuItemDto rhs) const { return this->id == rhs.id; } /** * todo: is there a QT method that can be used insted of this? */ static QString removeMenuAmpersand(const QString& str) { QString newStr = str; newStr.replace(QRegExp(_("\\&([^&])")), _("\\1")); // &Hallo --> Hallo newStr.replace(_("&&"), _("&")); // &&Hallo --> &Hallo return newStr; } }; /** * The serializeable menu structure. * Used for working with user interaction for persisted configuration. */ class KmtMenuStructureDto { public: QList list; public: // should be private but we would like to unit test /** * NOT USED */ QList itemsBySection(KMoreTools::MenuSection menuSection) const { QList r; Q_FOREACH (const auto& item, list) { if (item.menuSection == menuSection) { r.append(&item); } } return r; } /** * don't store the returned pointer, but you can deref it which calls copy ctor */ const KmtMenuItemDto* findInstalled(const QString& id) const { auto foundItem = std::find_if(list.begin(), list.end(), [id](const KmtMenuItemDto& item) { return item.id == id && item.isInstalled; }); if (foundItem != list.end()) { // deref iterator which is a const MenuItemDto& from which we get the pointer // (todo: is this a good idea?) return &(*foundItem); } return nullptr; } public: QString serialize() const { QJsonObject jObj; jsonWrite(jObj); QJsonDocument doc(jObj); auto jByteArray = doc.toJson(QJsonDocument::Compact); // http://stackoverflow.com/questions/14131127/qbytearray-to-qstring // QJsonDocument uses UTF-8 => we use 106=UTF-8 //return QTextCodec::codecForMib(106)->toUnicode(jByteArray); return _(jByteArray); // accidently the ctor of QString takes an UTF-8 byte array } void deserialize(const QString& text) { QJsonParseError parseError; QJsonDocument doc(QJsonDocument::fromJson(text.toUtf8(), &parseError)); jsonRead(doc.object()); } void jsonRead(const QJsonObject &json) { list.clear(); auto jArr = json[_("menuitemlist")].toArray(); for (int i = 0; i < jArr.size(); ++i) { auto jObj = jArr[i].toObject(); KmtMenuItemDto item; item.jsonRead(jObj); list.append(item); } } void jsonWrite(QJsonObject &json) const { QJsonArray jArr; Q_FOREACH (const auto item, list) { QJsonObject jObj; item.jsonWrite(jObj); jArr.append(jObj); } json[_("menuitemlist")] = jArr; } /** * @returns true if there are any not-installed items */ std::vector notInstalledServices() const { std::vector target; std::copy_if(list.begin(), list.end(), std::back_inserter(target), [](const KmtMenuItemDto& item) { return !item.isInstalled; }); return target; } public: // should be private but we would like to unit test /** * stable sorts: * 1. main items * 2. more items * 3. not installed items */ void stableSortListBySection() { std::stable_sort(list.begin(), list.end(), [](const KmtMenuItemDto& i1, const KmtMenuItemDto& i2) { return (i1.isInstalled && i1.menuSection == KMoreTools::MenuSection_Main && i2.isInstalled && i2.menuSection == KMoreTools::MenuSection_More) || (i1.isInstalled && !i2.isInstalled); }); } public: /** * moves an item up or down respecting its catgory * @param direction: 1: down, -1: up */ void moveWithinSection(const QString& id, int direction) { auto selItem = std::find_if(list.begin(), list.end(), [id](const KmtMenuItemDto& item) { return item.id == id; }); if (selItem != list.end()) { // if found if (direction == 1) { // "down" auto itemAfter = std::find_if(selItem + 1, list.end(), // find item where to insert after in the same category [selItem](const KmtMenuItemDto& item) { return item.menuSection == selItem->menuSection; }); if (itemAfter != list.end()) { int prevIndex = list.indexOf(*selItem); list.insert(list.indexOf(*itemAfter) + 1, *selItem); list.removeAt(prevIndex); } } else if (direction == -1) { // "up" //auto r_list = list; //std::reverse(r_list.begin(), r_list.end()); // we need to search "up" //auto itemBefore = std::find_if(selItem, list.begin(),// find item where to insert before in the same category // [selItem](const MenuItemDto& item) { return item.menuSection == selItem->menuSection; }); // todo: can't std::find_if be used instead of this loop? QList::iterator itemBefore = list.end(); auto it = selItem; while(it != list.begin()) { --it; if (it->menuSection == selItem->menuSection) { itemBefore = it; break; } } if (itemBefore != list.end()) { int prevIndex = list.indexOf(*selItem); list.insert(itemBefore, *selItem); list.removeAt(prevIndex + 1); } } else { Q_ASSERT(false); } } else { qWarning() << "selItem != list.end() == false"; } stableSortListBySection(); } void moveToOtherSection(const QString& id) { auto selItem = std::find_if(list.begin(), list.end(), [id](const KmtMenuItemDto& item) -> bool { return item.id == id; }); if (selItem != list.end()) { // if found if (selItem->menuSection == KMoreTools::MenuSection_Main) { selItem->menuSection = KMoreTools::MenuSection_More; } else if (selItem->menuSection == KMoreTools::MenuSection_More) { selItem->menuSection = KMoreTools::MenuSection_Main; } else { Q_ASSERT(false); } } stableSortListBySection(); } }; /** * In menu structure consisting of main section items, more section items * and registered services which are not installed. * In contrast to KmtMenuStructureDto we are dealing here with * KMoreToolsMenuItem pointers instead of DTOs. */ class KmtMenuStructure { public: QList mainItems; QList moreItems; /** * contains each not installed registered service once */ QList notInstalledServices; public: KmtMenuStructureDto toDto() { KmtMenuStructureDto result; Q_FOREACH (auto item, mainItems) { const auto a = item->action(); KmtMenuItemDto dto; dto.id = item->id(); dto.text = a->text(); // might be overriden, so we use directly from QAction dto.icon = a->icon(); dto.isInstalled = true; dto.menuSection = KMoreTools::MenuSection_Main; result.list << dto; } Q_FOREACH (auto item, moreItems) { const auto a = item->action(); KmtMenuItemDto dto; dto.id = item->id(); dto.text = a->text(); // might be overriden, so we use directly from QAction dto.icon = a->icon(); dto.isInstalled = true; dto.menuSection = KMoreTools::MenuSection_More; result.list << dto; } Q_FOREACH (auto registeredService, notInstalledServices) { KmtMenuItemDto dto; //dto.id = item->id(); // not used in this case dto.text = registeredService->formatString(_("$Name")); dto.icon = registeredService->icon(); dto.isInstalled = false; // dto.menuSection = // not used in this case dto.homepageUrl = registeredService->homepageUrl(); result.list << dto; } return result; } }; /** * Helper class that deals with creating the menu where all the not-installed * services are listed. */ class KmtNotInstalledUtil { public: /** * For one given application/service which is named @p title a QMenu is * created with the given @p icon and @p homepageUrl. * It will be used as submenu for the menu that displays the not-installed * services. */ - static QMenu* createSubmenuForNotInstalledApp(const QString& title, QWidget* parent, const QIcon& icon, const QUrl& homepageUrl) + static QMenu* createSubmenuForNotInstalledApp(const QString& title, QWidget* parent, const QIcon& icon, const QUrl& homepageUrl, const QString& appstreamId) { QMenu* submenuForNotInstalled = new QMenu(title, parent); submenuForNotInstalled->setIcon(icon); if (homepageUrl.isValid()) { auto websiteAction = submenuForNotInstalled->addAction(i18nc("@action:inmenu", "Visit homepage")); auto url = homepageUrl; // todo/review: is it ok to have sender and receiver the same object? QObject::connect(websiteAction, &QAction::triggered, websiteAction, [url](bool) { QDesktopServices::openUrl(url); }); - } else { + } + + QUrl appstreamUrl = QUrl(QStringLiteral("appstream://") % appstreamId); + + if (!appstreamId.isEmpty()) { + auto installAction = submenuForNotInstalled->addAction(i18nc("@action:inmenu", "Install")); + QObject::connect(installAction, &QAction::triggered, installAction, [appstreamUrl](bool) { + QDesktopServices::openUrl(appstreamUrl); + }); + } + + if (!homepageUrl.isValid() && appstreamId.isEmpty()) { submenuForNotInstalled->addAction(i18nc("@action:inmenu", "No further information available.")) ->setEnabled(false); } return submenuForNotInstalled; } }; /** * Url handling utils */ class KmtUrlUtil { public: /** * "file:///home/abc/hallo.txt" becomes "file:///home/abc" */ static QUrl localFileAbsoluteDir(const QUrl& url) { if (!url.isLocalFile()) { qWarning() << "localFileAbsoluteDir: url must be local file"; } QFileInfo fileInfo(url.toLocalFile()); auto dir = QDir(fileInfo.absoluteDir()).absolutePath(); return QUrl::fromLocalFile(dir); } }; #endif diff --git a/src/kmoretools/kmoretoolsconfigdialog_p.cpp b/src/kmoretools/kmoretoolsconfigdialog_p.cpp index 64da1b27..7375e1b9 100644 --- a/src/kmoretools/kmoretoolsconfigdialog_p.cpp +++ b/src/kmoretools/kmoretoolsconfigdialog_p.cpp @@ -1,338 +1,338 @@ /* Copyright 2015 by Gregor Mi This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . */ #include "kmoretoolsconfigdialog_p.h" #include "ui_kmoretoolsconfigwidget.h" #include #include #include #include class KMoreToolsConfigDialogPrivate { public: /** * menu defined by code */ KmtMenuStructureDto defaultStructure; /** * resulting menu (default merged with configured) and then maybe edited via GUI */ KmtMenuStructureDto currentStructure; Ui::KMoreToolsConfigWidget* configUi = nullptr; QAction* moveUpAction = nullptr; QAction* moveDownAction = nullptr; QAction* moveToMoreSectionAction = nullptr; QAction* moveToMainSectionAction = nullptr; public: QAction* createActionForButton(QAbstractButton* button, QObject* parent) { auto action = new QAction(button->icon(), button->text(), parent); return action; } QListWidgetItem* selectedItemMainSection() { auto items = configUi->listMainSection->selectedItems(); if (items.count() == 0) { return nullptr; } else { return items[0]; } } QListWidgetItem* selectedItemMoreSection() { auto items = configUi->listMoreSection->selectedItems(); if (items.count() == 0) { return nullptr; } else { return items[0]; } } /** * Only one section has a selection at a time. * @return the id of the item in one of the sections (main or more) or empty string */ QString uiSelectedItemId() { auto mainItem = selectedItemMainSection(); auto moreItem = selectedItemMoreSection(); if (mainItem) { return mainItem->data(Qt::UserRole).toString(); } else if (moreItem) { return moreItem->data(Qt::UserRole).toString(); } else { return QString(); } } void updateMoveButtonsState() { bool hasSelectedMain = selectedItemMainSection(); if (hasSelectedMain) { auto listMain = configUi->listMainSection; moveUpAction->setEnabled(hasSelectedMain && listMain->currentRow() > 0); moveDownAction->setEnabled(hasSelectedMain && listMain->currentRow() < listMain->count() - 1); } bool hasSelectedMore = selectedItemMoreSection(); if (hasSelectedMore) { auto listMore = configUi->listMoreSection; moveUpAction->setEnabled(hasSelectedMore && listMore->currentRow() > 0); moveDownAction->setEnabled(hasSelectedMore && listMore->currentRow() < listMore->count() - 1); } moveToMoreSectionAction->setEnabled(hasSelectedMain); moveToMainSectionAction->setEnabled(hasSelectedMore); } /** * refill lists and restore selection */ void updateListViews(QString idToSelect = QString()) { configUi->listMainSection->clear(); configUi->listMoreSection->clear(); // restore item selection QListWidgetItem* mainSelItem = nullptr; QListWidgetItem* moreSelItem = nullptr; foreach (auto item, currentStructure.list) { QIcon icon = item.icon; if (icon.isNull()) { QPixmap pix(16, 16); // TODO: should same size as other icons in the listview pix.fill(QColor(0, 0, 0, 0)); // transparent icon = QIcon(pix); } if (item.isInstalled) { auto listItem = new QListWidgetItem(icon, KmtMenuItemDto::removeMenuAmpersand(item.text) /*+ " - " + item.id*/); listItem->setData(Qt::UserRole, item.id); if (item.menuSection == KMoreTools::MenuSection_Main) { //qDebug() << item.text << item.icon << item.icon.isNull() << item.icon.availableSizes(); //itemModel->appendRow(new QStandardItem(icon, item.text /*+ " - " + item.id*/)); configUi->listMainSection->addItem(listItem); if (item.id == idToSelect) { mainSelItem = listItem; } } else if (item.menuSection == KMoreTools::MenuSection_More) { configUi->listMoreSection->addItem(listItem); //configUi->listMoreSection->addItem("test"); // DND copies item instead of moving it if (item.id == idToSelect) { moreSelItem = listItem; } } else { Q_ASSERT(false); } } } // // restore selection // "current vs. selected?" see http://doc.qt.digia.com/4.6/model-view-selection.html // if (mainSelItem) { mainSelItem->setSelected(true); configUi->listMainSection->setCurrentItem(mainSelItem); // for focus and keyboard handling configUi->listMainSection->setFocus(); } if (moreSelItem) { moreSelItem->setSelected(true); configUi->listMoreSection->setCurrentItem(moreSelItem); // for focus and keyboard handling configUi->listMoreSection->setFocus(); } updateMoveButtonsState(); } }; /** * for merging strategy see KMoreToolsMenuBuilderPrivate::createMenuStructure(mergeWithUserConfig=true) */ KMoreToolsConfigDialog::KMoreToolsConfigDialog(const KmtMenuStructureDto& defaultStructure, const KmtMenuStructureDto& currentStructure, const QString& title) : d(new KMoreToolsConfigDialogPrivate()) { d->defaultStructure = defaultStructure; d->currentStructure = currentStructure; QWidget *configPage = new QWidget(); if (title.isEmpty()) { addPage(configPage, i18n("Configure menu")); } else { addPage(configPage, i18n("Configure menu - %1", title)); } d->configUi = new Ui::KMoreToolsConfigWidget(); d->configUi->setupUi(configPage); // // show or don't show not-installed section depending if there are any // auto notInstalledServices = defaultStructure.notInstalledServices(); d->configUi->frameNotInstalledTools->setVisible(!notInstalledServices.empty()); if (!notInstalledServices.empty()) { auto menu = new QMenu(this); for (const KmtMenuItemDto& registeredService : notInstalledServices) { QMenu* submenuForNotInstalled = KmtNotInstalledUtil::createSubmenuForNotInstalledApp( - registeredService.text, menu, registeredService.icon, registeredService.homepageUrl); + registeredService.text, menu, registeredService.icon, registeredService.homepageUrl, registeredService.appstreamId); menu->addMenu(submenuForNotInstalled); } d->configUi->buttonNotInstalledTools->setMenu(menu); } //auto itemModel = new QStandardItemModel(); //configUi->listMainSection->setModel(itemModel); //configUi->listMainSection->setDragDropMode(QAbstractItemView::DragOnly); //configUi->listMainSection->setDragEnabled(true); // QWARN : KMoreToolsTest::testConfigDialog() Trying to construct an instance of an invalid type, type id: 872443648 //configUi->listMainSection->setAcceptDrops(true); // ...and crash when doing to much DND. Why? ... //configUi->listMainSection->setDropIndicatorShown(true); //configUi->listMoreSection->setDragEnabled(true); // crashes on DND. Why? //configUi->listMoreSection->setAcceptDrops(true); // // connect signals // { auto configUi = d->configUi; // // actions // d->moveUpAction = d->createActionForButton(configUi->buttonMoveUp, this); d->moveUpAction->setEnabled(false); configUi->buttonMoveUp->setDefaultAction(d->moveUpAction); connect(d->moveUpAction, &QAction::triggered, this, [this]() { QString selectedItemId = d->uiSelectedItemId(); if (!selectedItemId.isEmpty()) { d->currentStructure.moveWithinSection(selectedItemId, -1); d->updateListViews(selectedItemId); } }); d->moveDownAction = d->createActionForButton(configUi->buttonMoveDown, this); d->moveDownAction->setEnabled(false); configUi->buttonMoveDown->setDefaultAction(d->moveDownAction); connect(d->moveDownAction, &QAction::triggered, this, [this]() { QString selectedItemId = d->uiSelectedItemId(); if (!selectedItemId.isEmpty()) { d->currentStructure.moveWithinSection(selectedItemId, 1); d->updateListViews(selectedItemId); } }); d->moveToMoreSectionAction = d->createActionForButton(configUi->buttonMoveToMore, this); d->moveToMoreSectionAction->setEnabled(false); configUi->buttonMoveToMore->setDefaultAction(d->moveToMoreSectionAction); connect(d->moveToMoreSectionAction, &QAction::triggered, this, [this]() { QString selectedItemId = d->selectedItemMainSection()->data(Qt::UserRole).toString(); d->currentStructure.moveToOtherSection(selectedItemId); d->selectedItemMainSection()->setSelected(false); d->updateListViews(selectedItemId); }); d->moveToMainSectionAction = d->createActionForButton(configUi->buttonMoveToMain, this); d->moveToMainSectionAction->setEnabled(false); configUi->buttonMoveToMain->setDefaultAction(d->moveToMainSectionAction); connect(d->moveToMainSectionAction, &QAction::triggered, this, [this]() { QString selectedItemId = d->selectedItemMoreSection()->data(Qt::UserRole).toString(); d->currentStructure.moveToOtherSection(selectedItemId); d->selectedItemMoreSection()->setSelected(false); d->updateListViews(selectedItemId); }); connect(configUi->buttonReset, &QAbstractButton::clicked, this, [this]() { d->currentStructure = d->defaultStructure; d->updateListViews(); }); // // widgets enabled or not // connect(configUi->listMainSection, &QListWidget::itemSelectionChanged, this, [this]() { if (!d->selectedItemMainSection()) { d->moveToMoreSectionAction->setEnabled(false); d->moveUpAction->setEnabled(false); d->moveDownAction->setEnabled(false); return; } else { d->moveToMoreSectionAction->setEnabled(true); } d->updateMoveButtonsState(); }); connect(configUi->listMainSection, &QListWidget::currentItemChanged, this, [this, configUi](QListWidgetItem* current, QListWidgetItem* previous) { Q_UNUSED(previous) if (current && d->selectedItemMoreSection()) { d->selectedItemMoreSection()->setSelected(false); configUi->listMoreSection->setCurrentItem(nullptr); } d->updateMoveButtonsState(); }); connect(configUi->listMoreSection, &QListWidget::itemSelectionChanged, this, [this]() { if (!d->selectedItemMoreSection()) { d->moveToMainSectionAction->setEnabled(false); d->moveUpAction->setEnabled(false); d->moveDownAction->setEnabled(false); return; } else { d->moveToMainSectionAction->setEnabled(true); } d->updateMoveButtonsState(); }); connect(configUi->listMoreSection, &QListWidget::currentItemChanged, this, [this, configUi](QListWidgetItem* current, QListWidgetItem* previous) { Q_UNUSED(previous) if (current && d->selectedItemMainSection()) { d->selectedItemMainSection()->setSelected(false); configUi->listMainSection->setCurrentItem(nullptr); } d->updateMoveButtonsState(); }); } d->updateListViews(); } KMoreToolsConfigDialog::~KMoreToolsConfigDialog() { delete d; } KmtMenuStructureDto KMoreToolsConfigDialog::currentStructure() { return d->currentStructure; } diff --git a/src/kmoretools/kmoretoolspresets.cpp b/src/kmoretools/kmoretoolspresets.cpp index 5896dbfa..e8434c03 100644 --- a/src/kmoretools/kmoretoolspresets.cpp +++ b/src/kmoretools/kmoretoolspresets.cpp @@ -1,200 +1,202 @@ /* Copyright 2015 by Gregor Mi This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . */ #include "kmoretoolspresets.h" #include "kmoretoolspresets_p.h" #include "knewstuff_debug.h" #include #include #include #define _ QStringLiteral class KmtServiceInfo { public: - KmtServiceInfo(const QString &desktopEntryName, const QString &homepageUrl, int maxUrlArgCount) - : desktopEntryName(desktopEntryName), homepageUrl(homepageUrl), maxUrlArgCount(maxUrlArgCount) + KmtServiceInfo(const QString &desktopEntryName, const QString &homepageUrl, int maxUrlArgCount, const QString &appstreamId) + : desktopEntryName(desktopEntryName), homepageUrl(homepageUrl), maxUrlArgCount(maxUrlArgCount), appstreamId(appstreamId) { } public: QString desktopEntryName; QString homepageUrl; int maxUrlArgCount; + QString appstreamId; }; // // todo later: add a property "maturity" with values "stable" > "new" > "incubating" or similar // KMoreToolsService* KMoreToolsPresets::registerServiceByDesktopEntryName(KMoreTools* kmt, const QString& desktopEntryName) { static QHash dict; -#define ADD_ENTRY(desktopEntryName, maxUrlArgCount, homepageUrl) dict.insert(desktopEntryName, KmtServiceInfo(desktopEntryName, QLatin1String(homepageUrl), maxUrlArgCount)); +#define ADD_ENTRY(desktopEntryName, maxUrlArgCount, homepageUrl, appstreamUrl) dict.insert(desktopEntryName, KmtServiceInfo(desktopEntryName, QLatin1String(homepageUrl), maxUrlArgCount, appstreamUrl)); // // definitions begin (sorted alphabetically): // .------ If one gives more URL arguments as // | specified here the program will not work. // | Note, that there are some desktop files where _too few_ // | arguments also lead to errors. Watch the console // v output for messages from the program. // - ADD_ENTRY("angrysearch", 0, "https://github.com/DoTheEvo/ANGRYsearch"); - ADD_ENTRY("com.uploadedlobster.peek", 0, "https://github.com/phw/peek"); // easy to use screen recorder, creates gif - ADD_ENTRY("catfish", 1, "http://www.twotoasts.de/index.php/catfish/"); - ADD_ENTRY("ding", 0, "https://www-user.tu-chemnitz.de/~fri/ding/"); // Offline dict, Online: http://dict.tu-chemnitz.de/dings.cgi - ADD_ENTRY("disk", 0, "https://en.opensuse.org/YaST_Disk_Controller"); - ADD_ENTRY("fontinst", 0, "https://docs.kde.org/trunk5/en/kde-workspace/kcontrol/fontinst/"); // good for previewing many fonts at once - ADD_ENTRY("fontmatrix", 0, "https://github.com/fontmatrix/fontmatrix"); - ADD_ENTRY("fsearch", 0, "http://www.fsearch.org/"); - ADD_ENTRY("giggle", 1, "https://wiki.gnome.org/Apps/giggle/"); // good for searching in history - ADD_ENTRY("git-cola-folder-handler", 1, "https://git-cola.github.io"); - ADD_ENTRY("git-cola-view-history.kmt-edition", 1, "https://git-cola.github.io"); - ADD_ENTRY("gitk.kmt-edition", 1, "http://git-scm.com/docs/gitk"); - ADD_ENTRY("qgit.kmt-edition", 1, "http://libre.tibirna.org/projects/qgit"); - ADD_ENTRY("gitg", 1, "https://wiki.gnome.org/action/show/Apps/Gitg?action=show&redirect=Gitg"); - ADD_ENTRY("gnome-search-tool", 0, "https://help.gnome.org/users/gnome-search-tool/"); // has good filtering options - ADD_ENTRY("gucharmap", 0, "https://wiki.gnome.org/action/show/Apps/Gucharmap"); - ADD_ENTRY("gparted", 0, "http://gparted.org"); - ADD_ENTRY("htop", 0, "http://hisham.hm/htop/"); - ADD_ENTRY("hotshots", 1, "http://sourceforge.net/projects/hotshots/"); - ADD_ENTRY("kaption", 0, "http://kde-apps.org/content/show.php/?content=139302"); - ADD_ENTRY("kding", 0, ""); // Offline dict; unmaintained? - ADD_ENTRY("org.kde.kmousetool", 0, "https://www.kde.org/applications/utilities/kmousetool/"); - ADD_ENTRY("org.gnome.clocks", 0, "https://wiki.gnome.org/Apps/Clocks"); - ADD_ENTRY("org.kde.filelight", 1, "https://utils.kde.org/projects/filelight"); - ADD_ENTRY("org.kde.kcharselect", 0, "https://utils.kde.org/projects/kcharselect/"); - ADD_ENTRY("org.kde.kdf", 0, "https://www.kde.org/applications/system/kdiskfree"); - ADD_ENTRY("org.kde.kfind", 1, "https://www.kde.org/applications/utilities/kfind/"); // has good filtering options - ADD_ENTRY("org.kde.partitionmanager", 0, "https://www.kde.org/applications/system/kdepartitionmanager/"); - ADD_ENTRY("org.kde.plasma.cuttlefish.kmt-edition", 0, "http://vizzzion.org/blog/2015/02/say-hi-to-cuttlefish/"); - ADD_ENTRY("org.kde.ksysguard", 0, "https://userbase.kde.org/KSysGuard"); - ADD_ENTRY("org.kde.ksystemlog", 0, "https://www.kde.org/applications/system/ksystemlog/"); - ADD_ENTRY("org.kde.ktimer", 0, "https://www.kde.org/applications/utilities/ktimer/"); - ADD_ENTRY("org.kde.spectacle", 0, "https://www.kde.org/applications/graphics/spectacle"); - ADD_ENTRY("simplescreenrecorder", 0, "http://www.maartenbaert.be/simplescreenrecorder/"); - ADD_ENTRY("shutter", 0, "http://shutter-project.org"); // good for edit screenshot after capture - ADD_ENTRY("vokoscreen", 0, "https://github.com/vkohaupt/vokoscreen"); // feature-rich screen recorder - ADD_ENTRY("xfce4-taskmanager", 0, "http://goodies.xfce.org/projects/applications/xfce4-taskmanager"); + ADD_ENTRY("angrysearch", 0, "https://github.com/DoTheEvo/ANGRYsearch", ""); + ADD_ENTRY("com.uploadedlobster.peek", 0, "https://github.com/phw/peek", "com.uploadedlobster.peek.desktop"); // easy to use screen recorder, creates gif + ADD_ENTRY("catfish", 1, "http://www.twotoasts.de/index.php/catfish/", "catfish"); + ADD_ENTRY("ding", 0, "https://www-user.tu-chemnitz.de/~fri/ding/", ""); // Offline dict, Online: http://dict.tu-chemnitz.de/dings.cgi + ADD_ENTRY("disk", 0, "https://en.opensuse.org/YaST_Disk_Controller", ""); + ADD_ENTRY("fontinst", 0, "https://docs.kde.org/trunk5/en/kde-workspace/kcontrol/fontinst/", ""); // good for previewing many fonts at once + ADD_ENTRY("fontmatrix", 0, "https://github.com/fontmatrix/fontmatrix", ""); + ADD_ENTRY("fsearch", 0, "http://www.fsearch.org/", ""); + ADD_ENTRY("giggle", 1, "https://wiki.gnome.org/Apps/giggle/", "giggle.desktop"); // good for searching in history + ADD_ENTRY("git-cola-folder-handler", 1, "https://git-cola.github.io", "git-cola.desktop"); + ADD_ENTRY("git-cola-view-history.kmt-edition", 1, "https://git-cola.github.io", "git-cola.desktop"); + ADD_ENTRY("gitk.kmt-edition", 1, "http://git-scm.com/docs/gitk", ""); + ADD_ENTRY("qgit.kmt-edition", 1, "http://libre.tibirna.org/projects/qgit", ""); + ADD_ENTRY("gitg", 1, "https://wiki.gnome.org/action/show/Apps/Gitg?action=show&redirect=Gitg", "gitg.desktop"); + ADD_ENTRY("gnome-search-tool", 0, "https://help.gnome.org/users/gnome-search-tool/", "gnome-search-tool.desktop"); // has good filtering options + ADD_ENTRY("gucharmap", 0, "https://wiki.gnome.org/action/show/Apps/Gucharmap", "gucharmap.desktop"); + ADD_ENTRY("gparted", 0, "http://gparted.org", "gparted.desktop"); + ADD_ENTRY("htop", 0, "http://hisham.hm/htop/", "htop.desktop"); + ADD_ENTRY("hotshots", 1, "http://sourceforge.net/projects/hotshots/", ""); + ADD_ENTRY("kaption", 0, "http://kde-apps.org/content/show.php/?content=139302", ""); + ADD_ENTRY("kding", 0, "", ""); // Offline dict; unmaintained? + ADD_ENTRY("org.kde.kmousetool", 0, "https://www.kde.org/applications/utilities/kmousetool/", "org.kde.kmousetool"); + ADD_ENTRY("org.gnome.clocks", 0, "https://wiki.gnome.org/Apps/Clocks", "org.gnome.clocks.desktop"); + ADD_ENTRY("org.kde.filelight", 1, "https://utils.kde.org/projects/filelight", "org.kde.filelight.desktop"); + ADD_ENTRY("org.kde.kcharselect", 0, "https://utils.kde.org/projects/kcharselect/", "org.kde.kcharselect"); + ADD_ENTRY("org.kde.kdf", 0, "https://www.kde.org/applications/system/kdiskfree", "org.kde.kdf"); + ADD_ENTRY("org.kde.kfind", 1, "https://www.kde.org/applications/utilities/kfind/", "org.kde.kfind.desktop"); // has good filtering options + ADD_ENTRY("org.kde.partitionmanager", 0, "https://www.kde.org/applications/system/kdepartitionmanager/", "org.kde.partitionmanager.desktop"); + ADD_ENTRY("org.kde.plasma.cuttlefish.kmt-edition", 0, "http://vizzzion.org/blog/2015/02/say-hi-to-cuttlefish/", "org.kde.plasma.cuttlefish"); + ADD_ENTRY("org.kde.ksysguard", 0, "https://userbase.kde.org/KSysGuard", "org.kde.ksysguard"); + ADD_ENTRY("org.kde.ksystemlog", 0, "https://www.kde.org/applications/system/ksystemlog/", "org.kde.ksystemlog"); + ADD_ENTRY("org.kde.ktimer", 0, "https://www.kde.org/applications/utilities/ktimer/", "org.kde.ktimer"); + ADD_ENTRY("org.kde.spectacle", 0, "https://www.kde.org/applications/graphics/spectacle", "org.kde.spectacle.desktop"); + ADD_ENTRY("simplescreenrecorder", 0, "http://www.maartenbaert.be/simplescreenrecorder/", "simplescreenrecorder.desktop"); + ADD_ENTRY("shutter", 0, "http://shutter-project.org", "org.shutterproject.shutter"); // good for edit screenshot after capture + ADD_ENTRY("vokoscreen", 0, "https://github.com/vkohaupt/vokoscreen", ""); // feature-rich screen recorder + ADD_ENTRY("xfce4-taskmanager", 0, "http://goodies.xfce.org/projects/applications/xfce4-taskmanager", "xfce4-taskmanager.desktop"); // // ...definitions end // #undef ADD_ENTRY auto iter = dict.constFind(desktopEntryName); if (iter != dict.constEnd()) { auto kmtServiceInfo = *iter; const QString subdir = QStringLiteral("presets-kmoretools"); auto serviceLocatingMode = desktopEntryName.endsWith(QLatin1String(".kmt-edition")) ? KMoreTools::ServiceLocatingMode_ByProvidedExecLine : KMoreTools::ServiceLocatingMode_Default; auto service = kmt->registerServiceByDesktopEntryName(desktopEntryName, subdir, serviceLocatingMode); if (service) { // We might get nullptr in case of missing or broken .desktop files service->setHomepageUrl(QUrl(kmtServiceInfo.homepageUrl)); service->setMaxUrlArgCount(kmtServiceInfo.maxUrlArgCount); + service->setAppstreamId(kmtServiceInfo.appstreamId); } return service; } else { qCDebug(KNEWSTUFF) << "KMoreToolsPresets::registerServiceByDesktopEntryName: " << desktopEntryName << "was not found. Return nullptr."; return nullptr; } } QList KMoreToolsPresets::registerServicesByGroupingNames(KMoreTools* kmt, const QStringList& groupingNames) { QString firstMoreSectionDesktopEntryName; return KMoreToolsPresetsPrivate::registerServicesByGroupingNames(&firstMoreSectionDesktopEntryName, kmt, groupingNames); } QList KMoreToolsPresetsPrivate::registerServicesByGroupingNames(QString* firstMoreSectionDesktopEntryName, KMoreTools* kmt, const QStringList& groupingNames) { static QHash> dict; // The corresponding desktop files are located here: // 'knewstuff/data/kmoretools-desktopfiles/' // Use KMoreToolsTest2::testDialogForGroupingNames to see if the settings // here are correct. // NOTE that the desktopentry names must be registered in // registerServiceByDesktopEntryName above. // For special handlings about naming in the menu etc. see kmoretoolsmenufactory.cpp/addItemsForGroupingNameWithSpecialHandling // // grouping definitions begin (sorted alphabetically): // dict.insert(_("disk-usage"), { _("org.kde.kdf"), _("org.kde.filelight") }); dict.insert(_("disk-partitions"), { _("gparted"), _("org.kde.partitionmanager"), _("disk") }); dict.insert(_("files-find"), { _("org.kde.kfind"), _("fsearch"), _("more:"), _("gnome-search-tool"), _("catfish"), _("angrysearch") }); dict.insert(_("font-tools"), { _("fontinst"), _("gucharmap"), _("more:"), _("org.kde.kcharselect"), _("fontmatrix") }); dict.insert(_("git-clients-for-folder"), { _("git-cola-folder-handler"), _("gitk.kmt-edition"), _("giggle"), _("qgit.kmt-edition"), _("gitg") }); dict.insert(_("git-clients-and-actions"), { _("git-cola-folder-handler"), _("git-cola-view-history.kmt-edition"), _("giggle"), _("gitk.kmt-edition"), _("qgit.kmt-edition"), _("gitg") }); dict.insert(_("icon-browser"), { _("org.kde.plasma.cuttlefish.kmt-edition") }); dict.insert(_("language-dictionary"), { _("ding"), _("kding") }); dict.insert(_("mouse-tools"), { _("org.kde.kmousetool") }); // todo: add program "xbanish" to remove mouse cursor while typing dict.insert(_("screenrecorder"), { _("com.uploadedlobster.peek"), _("simplescreenrecorder"), _("vokoscreen") }); dict.insert(_("screenshot-take"), { _("org.kde.spectacle"), _("shutter"), _("kaption"), _("hotshots") }); dict.insert(_("system-monitor-processes"), { _("org.kde.ksysguard"), _("more:"), _("htop"), _("xfce4-taskmanager") }); dict.insert(_("system-monitor-logs"), { _("org.kde.ksystemlog") }); dict.insert(_("time-countdown"), { _("org.gnome.clocks"), _("org.kde.ktimer") }); // // ...grouping definitions end // QList resultList; QSet alreadyUsedDesktopEntryNames; // including the "more:" keyword bool nextIsMore = false; Q_FOREACH (const QString &groupingName, groupingNames) { auto iter = dict.constFind(groupingName); if (iter != dict.constEnd()) { Q_FOREACH(const QString &desktopEntryName, *iter) { if (!alreadyUsedDesktopEntryNames.contains(desktopEntryName)) { if (desktopEntryName == _("more:")) { nextIsMore = true; } else { if (nextIsMore) { // this will be only set once *firstMoreSectionDesktopEntryName = desktopEntryName; nextIsMore = false; } KMoreToolsService *kmtService = KMoreToolsPresets::registerServiceByDesktopEntryName(kmt, desktopEntryName); if (kmtService) { // Do not add null pointers caused by missing or broken .desktop files resultList << kmtService; } } } else { alreadyUsedDesktopEntryNames.insert(desktopEntryName); } } } else { qWarning() << "KMoreToolsPresets::registerServicesByGroupingName: groupingName not found: " << groupingName; } } if (resultList.isEmpty()) { qWarning() << "KMoreToolsPresets::registerServicesByGroupingName: " << groupingNames << ". Nothing found in this groupings. HINT: check for invalid grouping names."; } return resultList; }