1 /* 2 SPDX-FileCopyrightText: 2015 Gregor Mi <codestruct@posteo.org> 3 4 SPDX-License-Identifier: LGPL-2.1-or-later 5 */ 6 7 #ifndef KMORETOOLS_P_H 8 #define KMORETOOLS_P_H 9 10 #include "kmoretools.h" 11 12 #include <QDebug> 13 #include <QDesktopServices> 14 #include <QDir> 15 #include <QJsonArray> 16 #include <QJsonDocument> 17 #include <QRegularExpression> 18 #include <QTextCodec> 19 #include <QUrl> 20 21 #include <KLocalizedString> 22 23 #define _ QStringLiteral 24 25 /** 26 * Makes sure that if the same inputId is given more than once 27 * we will get unique IDs. 28 * 29 * See KMoreToolsTest::testMenuItemIdGen(). 30 */ 31 class KmtMenuItemIdGen 32 { 33 public: getId(const QString & inputId)34 QString getId(const QString &inputId) 35 { 36 int postFix = desktopEntryNameUsageMap[inputId]; 37 desktopEntryNameUsageMap[inputId] = postFix + 1; 38 return QStringLiteral("%1%2").arg(inputId).arg(postFix); 39 } 40 reset()41 void reset() 42 { 43 desktopEntryNameUsageMap.clear(); 44 } 45 46 private: 47 QMap<QString, int> desktopEntryNameUsageMap; 48 }; 49 50 /** 51 * A serializeable menu item 52 */ 53 class KmtMenuItemDto 54 { 55 public: 56 QString id; 57 58 /** 59 * @note that is might contain an ampersand (&) which may be used for menu items. 60 * Remove it with removeMenuAmpersand() 61 */ 62 QString text; 63 64 QIcon icon; 65 66 KMoreTools::MenuSection menuSection; 67 68 bool isInstalled = true; 69 70 /** 71 * only used if isInstalled == false 72 */ 73 QUrl homepageUrl; 74 75 QString appstreamId; 76 77 public: jsonRead(const QJsonObject & json)78 void jsonRead(const QJsonObject &json) 79 { 80 id = json[_("id")].toString(); 81 menuSection = json[_("menuSection")].toString() == _("main") ? KMoreTools::MenuSection_Main : KMoreTools::MenuSection_More; 82 isInstalled = json[_("isInstalled")].toBool(); 83 } 84 jsonWrite(QJsonObject & json)85 void jsonWrite(QJsonObject &json) const 86 { 87 json[_("id")] = id; 88 json[_("menuSection")] = menuSection == KMoreTools::MenuSection_Main ? _("main") : _("more"); 89 json[_("isInstalled")] = isInstalled; 90 } 91 92 bool operator==(const KmtMenuItemDto rhs) const 93 { 94 return this->id == rhs.id; 95 } 96 97 /** 98 * todo: is there a QT method that can be used instead of this? 99 */ removeMenuAmpersand(const QString & str)100 static QString removeMenuAmpersand(const QString &str) 101 { 102 QString newStr = str; 103 newStr.replace(QRegularExpression(QStringLiteral("\\&([^&])")), QStringLiteral("\\1")); // &Hallo --> Hallo 104 newStr.replace(_("&&"), _("&")); // &&Hallo --> &Hallo 105 return newStr; 106 } 107 }; 108 109 /** 110 * The serializeable menu structure. 111 * Used for working with user interaction for persisted configuration. 112 */ 113 class KmtMenuStructureDto 114 { 115 public: 116 QList<KmtMenuItemDto> list; 117 118 public: // should be private but we would like to unit test 119 /** 120 * NOT USED 121 */ itemsBySection(KMoreTools::MenuSection menuSection)122 QList<const KmtMenuItemDto *> itemsBySection(KMoreTools::MenuSection menuSection) const 123 { 124 QList<const KmtMenuItemDto *> r; 125 126 for (const auto &item : std::as_const(list)) { 127 if (item.menuSection == menuSection) { 128 r.append(&item); 129 } 130 } 131 132 return r; 133 } 134 135 /** 136 * don't store the returned pointer, but you can deref it which calls copy ctor 137 */ findInstalled(const QString & id)138 const KmtMenuItemDto *findInstalled(const QString &id) const 139 { 140 auto foundItem = std::find_if(list.begin(), list.end(), [id](const KmtMenuItemDto &item) { 141 return item.id == id && item.isInstalled; 142 }); 143 if (foundItem != list.end()) { 144 // deref iterator which is a const MenuItemDto& from which we get the pointer 145 // (todo: is this a good idea?) 146 return &(*foundItem); 147 } 148 149 return nullptr; 150 } 151 152 public: serialize()153 QString serialize() const 154 { 155 QJsonObject jObj; 156 jsonWrite(jObj); 157 QJsonDocument doc(jObj); 158 auto jByteArray = doc.toJson(QJsonDocument::Compact); 159 // http://stackoverflow.com/questions/14131127/qbytearray-to-qstring 160 // QJsonDocument uses UTF-8 => we use 106=UTF-8 161 // return QTextCodec::codecForMib(106)->toUnicode(jByteArray); 162 return QString::fromUtf8(jByteArray); // accidentally the ctor of QString takes an UTF-8 byte array 163 } 164 deserialize(const QString & text)165 void deserialize(const QString &text) 166 { 167 QJsonParseError parseError; 168 QJsonDocument doc(QJsonDocument::fromJson(text.toUtf8(), &parseError)); 169 jsonRead(doc.object()); 170 } 171 jsonRead(const QJsonObject & json)172 void jsonRead(const QJsonObject &json) 173 { 174 list.clear(); 175 auto jArr = json[_("menuitemlist")].toArray(); 176 for (int i = 0; i < jArr.size(); ++i) { 177 auto jObj = jArr[i].toObject(); 178 KmtMenuItemDto item; 179 item.jsonRead(jObj); 180 list.append(item); 181 } 182 } 183 jsonWrite(QJsonObject & json)184 void jsonWrite(QJsonObject &json) const 185 { 186 QJsonArray jArr; 187 for (const auto &item : std::as_const(list)) { 188 QJsonObject jObj; 189 item.jsonWrite(jObj); 190 jArr.append(jObj); 191 } 192 json[_("menuitemlist")] = jArr; 193 } 194 195 /** 196 * @returns true if there are any not-installed items 197 */ notInstalledServices()198 std::vector<KmtMenuItemDto> notInstalledServices() const 199 { 200 std::vector<KmtMenuItemDto> target; 201 std::copy_if(list.begin(), list.end(), std::back_inserter(target), [](const KmtMenuItemDto &item) { 202 return !item.isInstalled; 203 }); 204 return target; 205 } 206 207 public: // should be private but we would like to unit test 208 /** 209 * stable sorts: 210 * 1. main items 211 * 2. more items 212 * 3. not installed items 213 */ stableSortListBySection()214 void stableSortListBySection() 215 { 216 std::stable_sort(list.begin(), list.end(), [](const KmtMenuItemDto &i1, const KmtMenuItemDto &i2) { 217 return (i1.isInstalled && i1.menuSection == KMoreTools::MenuSection_Main && i2.isInstalled && i2.menuSection == KMoreTools::MenuSection_More) 218 || (i1.isInstalled && !i2.isInstalled); 219 }); 220 } 221 222 public: 223 /** 224 * moves an item up or down respecting its category 225 * @param direction: 1: down, -1: up 226 */ moveWithinSection(const QString & id,int direction)227 void moveWithinSection(const QString &id, int direction) 228 { 229 auto selItem = std::find_if(list.begin(), list.end(), [id](const KmtMenuItemDto &item) { 230 return item.id == id; 231 }); 232 233 if (selItem != list.end()) { // if found 234 if (direction == 1) { // "down" 235 auto itemAfter = std::find_if(selItem + 1, 236 list.end(), // find item where to insert after in the same category 237 [selItem](const KmtMenuItemDto &item) { 238 return item.menuSection == selItem->menuSection; 239 }); 240 241 if (itemAfter != list.end()) { 242 int prevIndex = list.indexOf(*selItem); 243 list.insert(list.indexOf(*itemAfter) + 1, *selItem); 244 list.removeAt(prevIndex); 245 } 246 } else if (direction == -1) { // "up" 247 // auto r_list = list; 248 // std::reverse(r_list.begin(), r_list.end()); // we need to search "up" 249 // auto itemBefore = std::find_if(selItem, list.begin(),// find item where to insert before in the same category 250 // [selItem](const MenuItemDto& item) { return item.menuSection == selItem->menuSection; }); 251 252 // todo: can't std::find_if be used instead of this loop? 253 QList<KmtMenuItemDto>::iterator itemBefore = list.end(); 254 auto it = selItem; 255 while (it != list.begin()) { 256 --it; 257 if (it->menuSection == selItem->menuSection) { 258 itemBefore = it; 259 break; 260 } 261 } 262 263 if (itemBefore != list.end()) { 264 int prevIndex = list.indexOf(*selItem); 265 list.insert(itemBefore, *selItem); 266 list.removeAt(prevIndex + 1); 267 } 268 } else { 269 Q_ASSERT(false); 270 } 271 } else { 272 qWarning() << "selItem != list.end() == false"; 273 } 274 275 stableSortListBySection(); 276 } 277 moveToOtherSection(const QString & id)278 void moveToOtherSection(const QString &id) 279 { 280 auto selItem = std::find_if(list.begin(), list.end(), [id](const KmtMenuItemDto &item) -> bool { 281 return item.id == id; 282 }); 283 284 if (selItem != list.end()) { // if found 285 if (selItem->menuSection == KMoreTools::MenuSection_Main) { 286 selItem->menuSection = KMoreTools::MenuSection_More; 287 } else if (selItem->menuSection == KMoreTools::MenuSection_More) { 288 selItem->menuSection = KMoreTools::MenuSection_Main; 289 } else { 290 Q_ASSERT(false); 291 } 292 } 293 294 stableSortListBySection(); 295 } 296 }; 297 298 /** 299 * In menu structure consisting of main section items, more section items 300 * and registered services which are not installed. 301 * In contrast to KmtMenuStructureDto we are dealing here with 302 * KMoreToolsMenuItem pointers instead of DTOs. 303 */ 304 class KmtMenuStructure 305 { 306 public: 307 QList<KMoreToolsMenuItem *> mainItems; 308 QList<KMoreToolsMenuItem *> moreItems; 309 310 /** 311 * contains each not installed registered service once 312 */ 313 QList<KMoreToolsService *> notInstalledServices; 314 315 public: toDto()316 KmtMenuStructureDto toDto() 317 { 318 KmtMenuStructureDto result; 319 320 for (auto item : std::as_const(mainItems)) { 321 const auto a = item->action(); 322 KmtMenuItemDto dto; 323 dto.id = item->id(); 324 dto.text = a->text(); // might be overridden, so we use directly from QAction 325 dto.icon = a->icon(); 326 dto.isInstalled = true; 327 dto.menuSection = KMoreTools::MenuSection_Main; 328 result.list << dto; 329 } 330 331 for (auto item : std::as_const(moreItems)) { 332 const auto a = item->action(); 333 KmtMenuItemDto dto; 334 dto.id = item->id(); 335 dto.text = a->text(); // might be overridden, so we use directly from QAction 336 dto.icon = a->icon(); 337 dto.isInstalled = true; 338 dto.menuSection = KMoreTools::MenuSection_More; 339 result.list << dto; 340 } 341 342 for (auto registeredService : std::as_const(notInstalledServices)) { 343 KmtMenuItemDto dto; 344 // dto.id = item->id(); // not used in this case 345 dto.text = registeredService->formatString(_("$Name")); 346 dto.icon = registeredService->icon(); 347 dto.isInstalled = false; 348 // dto.menuSection = // not used in this case 349 dto.homepageUrl = registeredService->homepageUrl(); 350 result.list << dto; 351 } 352 353 return result; 354 } 355 }; 356 357 /** 358 * Helper class that deals with creating the menu where all the not-installed 359 * services are listed. 360 */ 361 class KmtNotInstalledUtil 362 { 363 public: 364 /** 365 * For one given application/service which is named @p title a QMenu is 366 * created with the given @p icon and @p homepageUrl. 367 * It will be used as submenu for the menu that displays the not-installed 368 * services. 369 */ createSubmenuForNotInstalledApp(const QString & title,QWidget * parent,const QIcon & icon,const QUrl & homepageUrl,const QString & appstreamId)370 static QMenu *createSubmenuForNotInstalledApp(const QString &title, QWidget *parent, const QIcon &icon, const QUrl &homepageUrl, const QString &appstreamId) 371 { 372 QMenu *submenuForNotInstalled = new QMenu(title, parent); 373 submenuForNotInstalled->setIcon(icon); 374 375 if (homepageUrl.isValid()) { 376 auto websiteAction = submenuForNotInstalled->addAction(i18nc("@action:inmenu", "Visit homepage")); 377 websiteAction->setIcon(QIcon::fromTheme(QStringLiteral("internet-services"))); 378 auto url = homepageUrl; 379 // todo/review: is it ok to have sender and receiver the same object? 380 QObject::connect(websiteAction, &QAction::triggered, websiteAction, [url](bool) { 381 QDesktopServices::openUrl(url); 382 }); 383 } 384 385 QUrl appstreamUrl = QUrl(QStringLiteral("appstream://") % appstreamId); 386 387 if (!appstreamId.isEmpty()) { 388 auto installAction = submenuForNotInstalled->addAction(i18nc("@action:inmenu", "Install")); 389 installAction->setIcon(QIcon::fromTheme(QStringLiteral("download"))); 390 QObject::connect(installAction, &QAction::triggered, installAction, [appstreamUrl](bool) { 391 QDesktopServices::openUrl(appstreamUrl); 392 }); 393 } 394 395 if (!homepageUrl.isValid() && appstreamId.isEmpty()) { 396 submenuForNotInstalled->addAction(i18nc("@action:inmenu", "No further information available."))->setEnabled(false); 397 } 398 399 return submenuForNotInstalled; 400 } 401 }; 402 403 /** 404 * Url handling utils 405 */ 406 class KmtUrlUtil 407 { 408 public: 409 /** 410 * "file:///home/abc/hallo.txt" becomes "file:///home/abc" 411 */ localFileAbsoluteDir(const QUrl & url)412 static QUrl localFileAbsoluteDir(const QUrl &url) 413 { 414 if (!url.isLocalFile()) { 415 qWarning() << "localFileAbsoluteDir: url must be local file"; 416 } 417 QFileInfo fileInfo(url.toLocalFile()); 418 auto dir = QDir(fileInfo.absoluteDir()).absolutePath(); 419 return QUrl::fromLocalFile(dir); 420 } 421 }; 422 423 #endif 424