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