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