KService

kservice.cpp
1 /*
2  This file is part of the KDE libraries
3  SPDX-FileCopyrightText: 1999-2001 Waldo Bastian <[email protected]>
4  SPDX-FileCopyrightText: 1999-2005 David Faure <[email protected]>
5  SPDX-FileCopyrightText: 2022 Harald Sitter <[email protected]>
6 
7  SPDX-License-Identifier: LGPL-2.0-only
8 */
9 
10 #include "kservice.h"
11 #include "kmimetypefactory_p.h"
12 #include "kservice_p.h"
13 #include "ksycoca.h"
14 #include "ksycoca_p.h"
15 
16 #include <qplatformdefs.h>
17 
18 #include <QDir>
19 #include <QMap>
20 #include <QMimeDatabase>
21 
22 #include <KAuthorized>
23 #include <KConfigGroup>
24 #include <KDesktopFile>
25 #include <KLocalizedString>
26 #include <KShell>
27 
28 #include <QDebug>
29 #include <QStandardPaths>
30 
31 #include "kservicefactory_p.h"
32 #include "kservicetypefactory_p.h"
33 #include "kserviceutil_p.h"
34 #include "servicesdebug.h"
35 
36 QDataStream &operator<<(QDataStream &s, const KService::ServiceTypeAndPreference &st)
37 {
38  s << st.preference << st.serviceType;
39  return s;
40 }
41 QDataStream &operator>>(QDataStream &s, KService::ServiceTypeAndPreference &st)
42 {
43  s >> st.preference >> st.serviceType;
44  return s;
45 }
46 
47 void KServicePrivate::init(const KDesktopFile *config, KService *q)
48 {
49  const QString entryPath = q->entryPath();
50  if (entryPath.isEmpty()) {
51  // We are opening a "" service, this means whatever warning we might get is going to be misleading
52  m_bValid = false;
53  return;
54  }
55 
56  bool absPath = !QDir::isRelativePath(entryPath);
57 
58  // TODO: it makes sense to have a KConstConfigGroup I guess
59  const KConfigGroup desktopGroup = const_cast<KDesktopFile *>(config)->desktopGroup();
60  QMap<QString, QString> entryMap = desktopGroup.entryMap();
61 
62  entryMap.remove(QStringLiteral("Encoding")); // reserved as part of Desktop Entry Standard
63  entryMap.remove(QStringLiteral("Version")); // reserved as part of Desktop Entry Standard
64 
65  q->setDeleted(desktopGroup.readEntry("Hidden", false));
66  entryMap.remove(QStringLiteral("Hidden"));
67  if (q->isDeleted()) {
68  m_bValid = false;
69  return;
70  }
71 
72  m_strName = config->readName();
73  entryMap.remove(QStringLiteral("Name"));
74  if (m_strName.isEmpty()) {
75  // Try to make up a name.
76  m_strName = entryPath;
77  int i = m_strName.lastIndexOf(QLatin1Char('/'));
78  m_strName = m_strName.mid(i + 1);
79  i = m_strName.lastIndexOf(QLatin1Char('.'));
80  if (i != -1) {
81  m_strName.truncate(i);
82  }
83  }
84 
85  m_strType = config->readType();
86  entryMap.remove(QStringLiteral("Type"));
87  if (m_strType.isEmpty()) {
88  /*kWarning(servicesDebugArea()) << "The desktop entry file " << entryPath
89  << " has no Type=... entry."
90  << " It should be \"Application\" or \"Service\"";
91  m_bValid = false;
92  return;*/
93  m_strType = QStringLiteral("Application");
94  } else if (m_strType != QLatin1String("Application") && m_strType != QLatin1String("Service")) {
95  qCWarning(SERVICES) << "The desktop entry file" << entryPath << "has Type=" << m_strType << "instead of \"Application\" or \"Service\"";
96  m_bValid = false;
97  return;
98  }
99 
100  // NOT readPathEntry, it is not XDG-compliant: it performs
101  // various expansions, like $HOME. Note that the expansion
102  // behaviour still happens if the "e" flag is set, maintaining
103  // backwards compatibility.
104  m_strExec = desktopGroup.readEntry("Exec", QString());
105  entryMap.remove(QStringLiteral("Exec"));
106 
107  if (m_strType == QLatin1String("Application")) {
108  // It's an application? Should have an Exec line then, otherwise we can't run it
109  if (m_strExec.isEmpty()) {
110  qCWarning(SERVICES) << "The desktop entry file" << entryPath << "has Type=" << m_strType << "but no Exec line";
111  m_bValid = false;
112  return;
113  }
114  }
115 
116  // In case Try Exec is set, check if the application is available
117  if (!config->tryExec()) {
118  q->setDeleted(true);
119  m_bValid = false;
120  return;
121  }
122 
123  const QStandardPaths::StandardLocation locationType = config->locationType();
124 
125  if ((m_strType == QLatin1String("Application")) && (locationType != QStandardPaths::ApplicationsLocation) && !absPath) {
126  qCWarning(SERVICES) << "The desktop entry file" << entryPath << "has Type=" << m_strType << "but is located under \""
127  << QStandardPaths::displayName(locationType) << "\" instead of \"Applications\"";
128  m_bValid = false;
129  return;
130  }
131 
132  if ((m_strType == QLatin1String("Service")) && (locationType != QStandardPaths::GenericDataLocation) && !absPath) {
133  qCWarning(SERVICES) << "The desktop entry file" << entryPath << "has Type=" << m_strType << "but is located under \""
134  << QStandardPaths::displayName(locationType) << "\" instead of \"Shared Data\"/kservices5";
135  m_bValid = false;
136  return;
137  }
138 
139  // entryPath To desktopEntryName
140  // (e.g. "/home/x/.qttest/share/kservices5/fakepart2.desktop" --> "fakepart2")
141  QString _name = KServiceUtilPrivate::completeBaseName(entryPath);
142 
143  m_strIcon = config->readIcon();
144  entryMap.remove(QStringLiteral("Icon"));
145  m_bTerminal = desktopGroup.readEntry("Terminal", false); // should be a property IMHO
146  entryMap.remove(QStringLiteral("Terminal"));
147  m_strTerminalOptions = desktopGroup.readEntry("TerminalOptions"); // should be a property IMHO
148  entryMap.remove(QStringLiteral("TerminalOptions"));
149  m_strWorkingDirectory = KShell::tildeExpand(config->readPath());
150  entryMap.remove(QStringLiteral("Path"));
151  m_strComment = config->readComment();
152  entryMap.remove(QStringLiteral("Comment"));
153  m_strGenName = config->readGenericName();
154  entryMap.remove(QStringLiteral("GenericName"));
155  QString _untranslatedGenericName = desktopGroup.readEntryUntranslated("GenericName");
156  if (!_untranslatedGenericName.isEmpty()) {
157  entryMap.insert(QStringLiteral("UntranslatedGenericName"), _untranslatedGenericName);
158  }
159 
160  m_lstFormFactors = desktopGroup.readEntry("X-KDE-FormFactors", QStringList());
161  entryMap.remove(QStringLiteral("X-KDE-FormFactors"));
162 
163  m_lstKeywords = desktopGroup.readXdgListEntry("Keywords", QStringList());
164  entryMap.remove(QStringLiteral("Keywords"));
165  m_lstKeywords += desktopGroup.readEntry("X-KDE-Keywords", QStringList());
166  entryMap.remove(QStringLiteral("X-KDE-Keywords"));
167  categories = desktopGroup.readXdgListEntry("Categories");
168  entryMap.remove(QStringLiteral("Categories"));
169  // TODO KDE5: only care for X-KDE-Library in Type=Service desktop files
170  // This will prevent people defining a part and an app in the same desktop file
171  // which makes user-preference handling difficult.
172  m_strLibrary = desktopGroup.readEntry("X-KDE-Library");
173  entryMap.remove(QStringLiteral("X-KDE-Library"));
174  if (!m_strLibrary.isEmpty() && m_strType == QLatin1String("Application")) {
175  qCWarning(SERVICES) << "The desktop entry file" << entryPath << "has Type=" << m_strType
176  << "but also has a X-KDE-Library key. This works for now,"
177  " but makes user-preference handling difficult, so support for this might"
178  " be removed at some point. Consider splitting it into two desktop files.";
179  }
180 
181  QStringList lstServiceTypes = desktopGroup.readEntry("ServiceTypes", QStringList());
182  entryMap.remove(QStringLiteral("ServiceTypes"));
183  lstServiceTypes += desktopGroup.readEntry("X-KDE-ServiceTypes", QStringList());
184  entryMap.remove(QStringLiteral("X-KDE-ServiceTypes"));
185  lstServiceTypes += desktopGroup.readXdgListEntry("MimeType");
186  entryMap.remove(QStringLiteral("MimeType"));
187 
188  if (m_strType == QLatin1String("Application") && !lstServiceTypes.contains(QLatin1String("Application")))
189  // Applications implement the service type "Application" ;-)
190  {
191  lstServiceTypes += QStringLiteral("Application");
192  }
193 
194  m_initialPreference = desktopGroup.readEntry("InitialPreference", 1);
195  entryMap.remove(QStringLiteral("InitialPreference"));
196 
197  // Assign the "initial preference" to each mimetype/servicetype
198  // (and to set such preferences in memory from kbuildsycoca)
199  m_serviceTypes.reserve(lstServiceTypes.size());
200  QListIterator<QString> st_it(lstServiceTypes);
201  while (st_it.hasNext()) {
202  const QString st = st_it.next();
203  if (st.isEmpty()) {
204  qCWarning(SERVICES) << "The desktop entry file" << entryPath << "has an empty MimeType!";
205  continue;
206  }
207  int initialPreference = m_initialPreference;
208  if (st_it.hasNext()) {
209  // TODO better syntax - separate group with mimetype=number entries?
210  bool isNumber;
211  const int val = st_it.peekNext().toInt(&isNumber);
212  if (isNumber) {
213  initialPreference = val;
214  st_it.next();
215  }
216  }
217  m_serviceTypes.push_back(KService::ServiceTypeAndPreference(initialPreference, st));
218  }
219 
220  if (entryMap.contains(QLatin1String("Actions"))) {
221  parseActions(config, q);
222  }
223 
224 #if KSERVICE_BUILD_DEPRECATED_SINCE(5, 102)
225  QString dbusStartupType = desktopGroup.readEntry("X-DBUS-StartupType").toLower();
226  entryMap.remove(QStringLiteral("X-DBUS-StartupType"));
227  if (dbusStartupType == QLatin1String("unique")) {
228  m_DBUSStartusType = KService::DBusUnique;
229  } else if (dbusStartupType == QLatin1String("multi")) {
230  m_DBUSStartusType = KService::DBusMulti;
231  } else {
232  m_DBUSStartusType = KService::DBusNone;
233  }
234 #endif
235 
236  m_strDesktopEntryName = _name;
237 
238  // Exec lines from the KCMs always have the pattern "<program> m_strDesktopEntryName", see https://phabricator.kde.org/T13729
239  const static bool hasSystemsettings = !QStandardPaths::findExecutable(QStringLiteral("systemsettings5")).isEmpty();
240  if (m_strExec.isEmpty() && serviceTypes().contains(QStringLiteral("KCModule"))) {
241  if (desktopGroup.readEntry("X-KDE-ParentApp") == QLatin1String("kinfocenter")) {
242  m_strExec = QStringLiteral("kinfocenter ") + m_strDesktopEntryName;
243  } else if (desktopGroup.readEntry("X-KDE-ParentApp") == QLatin1String("kcontrol")) {
244  if (!desktopGroup.readEntry("X-KDE-System-Settings-Parent-Category").isEmpty() && hasSystemsettings) {
245  m_strExec = QStringLiteral("systemsettings5 ") + m_strDesktopEntryName;
246  } else {
247  m_strExec = QStringLiteral("kcmshell5 ") + m_strDesktopEntryName;
248  }
249  }
250  }
251  m_bAllowAsDefault = desktopGroup.readEntry("AllowDefault", true);
252  entryMap.remove(QStringLiteral("AllowDefault"));
253 
254  // allow plugin users to translate categories without needing a separate key
255  auto entryIt = entryMap.find(QStringLiteral("X-KDE-PluginInfo-Category"));
256  if (entryIt != entryMap.end()) {
257  const QString &key = entryIt.key();
258  m_mapProps.insert(key, QVariant(desktopGroup.readEntryUntranslated(key)));
259  m_mapProps.insert(key + QLatin1String("-Translated"), QVariant(entryIt.value()));
260  entryMap.erase(entryIt);
261  }
262 
263  // Store all additional entries in the property map.
264  // A QMap<QString,QString> would be easier for this but we can't
265  // break BC, so we have to store it in m_mapProps.
266  // qDebug("Path = %s", entryPath.toLatin1().constData());
267  auto it = entryMap.constBegin();
268  for (; it != entryMap.constEnd(); ++it) {
269  const QString key = it.key();
270  // do not store other translations like Name[fr]; kbuildsycoca will rerun if we change languages anyway
271  if (!key.contains(QLatin1Char('['))) {
272  // qCDebug(SERVICES) << " Key =" << key << " Data =" << it.value();
273  if (key == QLatin1String("X-Flatpak-RenamedFrom")) {
274  m_mapProps.insert(key, desktopGroup.readXdgListEntry(key));
275  } else {
276  m_mapProps.insert(key, QVariant(it.value()));
277  }
278  }
279  }
280 }
281 
282 void KServicePrivate::parseActions(const KDesktopFile *config, KService *q)
283 {
284  const QStringList keys = config->readActions();
285  if (keys.isEmpty()) {
286  return;
287  }
288 
289  KService::Ptr serviceClone(new KService(*q));
290 
291  for (const QString &group : keys) {
292  if (group == QLatin1String("_SEPARATOR_")) {
293  m_actions.append(KServiceAction(group, QString(), QString(), QString(), false, serviceClone));
294  continue;
295  }
296 
297  if (config->hasActionGroup(group)) {
298  const KConfigGroup cg = config->actionGroup(group);
299  if (!cg.hasKey("Name") || !cg.hasKey("Exec")) {
300  qCWarning(SERVICES) << "The action" << group << "in the desktop file" << q->entryPath() << "has no Name or no Exec key";
301  } else {
302  const QMap<QString, QString> entries = cg.entryMap();
303 
304  QVariantMap entriesVariants;
305 
306  for (auto it = entries.constKeyValueBegin(); it != entries.constKeyValueEnd(); ++it) {
307  // Those are stored separately
308  if (it->first == QLatin1String("Name") || it->first == QLatin1String("Icon") || it->first == QLatin1String("Exec")
309  || it->first == QLatin1String("NoDisplay")) {
310  continue;
311  }
312 
313  entriesVariants.insert(it->first, it->second);
314  }
315 
316  KServiceAction action(group, cg.readEntry("Name"), cg.readEntry("Icon"), cg.readEntry("Exec"), cg.readEntry("NoDisplay", false), serviceClone);
317  action.setData(QVariant::fromValue(entriesVariants));
318  m_actions.append(action);
319  }
320  } else {
321  qCWarning(SERVICES) << "The desktop file" << q->entryPath() << "references the action" << group << "but doesn't define it";
322  }
323  }
324 }
325 
326 void KServicePrivate::load(QDataStream &s)
327 {
328  qint8 def;
329  qint8 term;
330  qint8 dst;
331  qint8 initpref;
332 
333  // WARNING: THIS NEEDS TO REMAIN COMPATIBLE WITH PREVIOUS KService 5.x VERSIONS!
334  // !! This data structure should remain binary compatible at all times !!
335  // You may add new fields at the end. Make sure to update KSYCOCA_VERSION
336  // number in ksycoca.cpp
337  // clang-format off
338  s >> m_strType >> m_strName >> m_strExec >> m_strIcon
339  >> term >> m_strTerminalOptions
340  >> m_strWorkingDirectory >> m_strComment >> def >> m_mapProps
341  >> m_strLibrary
342  >> dst
343  >> m_strDesktopEntryName
344  >> initpref
345  >> m_lstKeywords >> m_strGenName
346  >> categories >> menuId >> m_actions >> m_serviceTypes
347  >> m_lstFormFactors;
348  // clang-format on
349 
350  m_bAllowAsDefault = bool(def);
351  m_bTerminal = bool(term);
352 #if KSERVICE_BUILD_DEPRECATED_SINCE(5, 102)
353  m_DBUSStartusType = static_cast<KService::DBusStartupType>(dst);
354 #endif
355  m_initialPreference = initpref;
356 
357  m_bValid = true;
358 }
359 
360 void KServicePrivate::save(QDataStream &s)
361 {
362  KSycocaEntryPrivate::save(s);
363  qint8 def = m_bAllowAsDefault;
364  qint8 initpref = m_initialPreference;
365  qint8 term = m_bTerminal;
366 #if KSERVICE_BUILD_DEPRECATED_SINCE(5, 102)
367  qint8 dst = qint8(m_DBUSStartusType);
368 #else
369  qint8 dst = 0;
370 #endif
371 
372  // WARNING: THIS NEEDS TO REMAIN COMPATIBLE WITH PREVIOUS KService 5.x VERSIONS!
373  // !! This data structure should remain binary compatible at all times !!
374  // You may add new fields at the end. Make sure to update KSYCOCA_VERSION
375  // number in ksycoca.cpp
376  s << m_strType << m_strName << m_strExec << m_strIcon << term << m_strTerminalOptions << m_strWorkingDirectory << m_strComment << def << m_mapProps
377  << m_strLibrary << dst << m_strDesktopEntryName << initpref << m_lstKeywords << m_strGenName << categories << menuId << m_actions << m_serviceTypes
378  << m_lstFormFactors;
379 }
380 
381 ////
382 
383 KService::KService(const QString &_name, const QString &_exec, const QString &_icon)
384  : KSycocaEntry(*new KServicePrivate(QString()))
385 {
386  Q_D(KService);
387  d->m_strType = QStringLiteral("Application");
388  d->m_strName = _name;
389  d->m_strExec = _exec;
390  d->m_strIcon = _icon;
391  d->m_bTerminal = false;
392  d->m_bAllowAsDefault = true;
393  d->m_initialPreference = 10;
394 }
395 
396 KService::KService(const QString &_fullpath)
397  : KSycocaEntry(*new KServicePrivate(_fullpath))
398 {
399  Q_D(KService);
400 
401  KDesktopFile config(_fullpath);
402  d->init(&config, this);
403 }
404 
405 KService::KService(const KDesktopFile *config, const QString &entryPath)
406  : KSycocaEntry(*new KServicePrivate(entryPath.isEmpty() ? config->fileName() : entryPath))
407 {
408  Q_D(KService);
409 
410  d->init(config, this);
411 }
412 
413 KService::KService(QDataStream &_str, int _offset)
414  : KSycocaEntry(*new KServicePrivate(_str, _offset))
415 {
416  Q_D(KService);
417  KService::Ptr serviceClone(new KService(*this));
418  for (KServiceAction &action : d->m_actions) {
419  action.setService(serviceClone);
420  }
421 }
422 
423 KService::KService(const KService &other)
424  : KSycocaEntry(*new KServicePrivate(*other.d_func()))
425 {
426 }
427 
428 KService::~KService()
429 {
430 }
431 
432 bool KService::hasServiceType(const QString &serviceType) const
433 {
434  Q_D(const KService);
435 
436  if (!d->m_bValid) {
437  return false; // (useless) safety test
438  }
439  const KServiceType::Ptr ptr = KServiceType::serviceType(serviceType);
440  if (!ptr) {
441  return false;
442  }
443  const int serviceOffset = offset();
444  // doesn't seem to work:
445  // if ( serviceOffset == 0 )
446  // serviceOffset = serviceByStorageId( storageId() );
447  if (serviceOffset) {
449  return KSycocaPrivate::self()->serviceFactory()->hasOffer(ptr->offset(), ptr->serviceOffersOffset(), serviceOffset);
450  }
451 
452  // fall-back code for services that are NOT from ksycoca
453  // For each service type we are associated with, if it doesn't
454  // match then we try its parent service types.
455  const QString serviceTypeName = ptr->name();
456  auto matchFunc = [&serviceTypeName](const ServiceTypeAndPreference &typePref) {
457  const QString &st = typePref.serviceType;
458  // qCDebug(SERVICES) << " has " << typePref;
459  if (st == serviceTypeName) {
460  return true;
461  }
462 
463  // also the case of parent servicetypes
465  if (p && p->inherits(serviceTypeName)) {
466  return true;
467  }
468 
469  return false;
470  };
471 
472  return std::any_of(d->m_serviceTypes.cbegin(), d->m_serviceTypes.cend(), matchFunc);
473 }
474 
475 bool KService::hasMimeType(const QString &mimeType) const
476 {
477  Q_D(const KService);
478  QMimeDatabase db;
479  const QString mime = db.mimeTypeForName(mimeType).name();
480  if (mime.isEmpty()) {
481  return false;
482  }
483  int serviceOffset = offset();
484  if (serviceOffset) {
486  KMimeTypeFactory *factory = KSycocaPrivate::self()->mimeTypeFactory();
487  const int mimeOffset = factory->entryOffset(mime);
488  const int serviceOffersOffset = factory->serviceOffersOffset(mime);
489  if (serviceOffersOffset == -1) {
490  return false;
491  }
492  return KSycocaPrivate::self()->serviceFactory()->hasOffer(mimeOffset, serviceOffersOffset, serviceOffset);
493  }
494 
495  auto matchFunc = [&mime](const ServiceTypeAndPreference &typePref) {
496  // qCDebug(SERVICES) << " has " << typePref;
497  if (typePref.serviceType == mime) {
498  return true;
499  }
500  // TODO: should we handle inherited MIME types here?
501  // KMimeType was in kio when this code was written, this is the only reason it's not done.
502  // But this should matter only in a very rare case, since most code gets KServices from ksycoca.
503  // Warning, change hasServiceType if you implement this here (and check kbuildservicefactory).
504  return false;
505  };
506 
507  // fall-back code for services that are NOT from ksycoca
508  return std::any_of(d->m_serviceTypes.cbegin(), d->m_serviceTypes.cend(), matchFunc);
509 }
510 
511 QVariant KServicePrivate::property(const QString &_name) const
512 {
513  return property(_name, QMetaType::UnknownType);
514 }
515 
516 // Return a string QVariant if string isn't null, and invalid variant otherwise
517 // (the variant must be invalid if the field isn't in the .desktop file)
518 // This allows trader queries like "exist Library" to work.
519 static QVariant makeStringVariant(const QString &string)
520 {
521  // Using isEmpty here would be wrong.
522  // Empty is "specified but empty", null is "not specified" (in the .desktop file)
523  return string.isNull() ? QVariant() : QVariant(string);
524 }
525 
526 #if KSERVICE_BUILD_DEPRECATED_SINCE(5, 102)
528 {
529  Q_D(const KService);
530  return d->property(_name, (QMetaType::Type)t);
531 }
532 #endif
533 
535 {
536  Q_D(const KService);
537  return d->property(_name, t);
538 }
539 
540 QVariant KServicePrivate::property(const QString &_name, QMetaType::Type t) const
541 {
542  if (_name == QLatin1String("Type")) {
543  return QVariant(m_strType); // can't be null
544  } else if (_name == QLatin1String("Name")) {
545  return QVariant(m_strName); // can't be null
546  } else if (_name == QLatin1String("Exec")) {
547  return makeStringVariant(m_strExec);
548  } else if (_name == QLatin1String("Icon")) {
549  return makeStringVariant(m_strIcon);
550  } else if (_name == QLatin1String("Terminal")) {
551  return QVariant(m_bTerminal);
552  } else if (_name == QLatin1String("TerminalOptions")) {
553  return makeStringVariant(m_strTerminalOptions);
554  } else if (_name == QLatin1String("Path")) {
555  return makeStringVariant(m_strWorkingDirectory);
556  } else if (_name == QLatin1String("Comment")) {
557  return makeStringVariant(m_strComment);
558  } else if (_name == QLatin1String("GenericName")) {
559  return makeStringVariant(m_strGenName);
560  } else if (_name == QLatin1String("ServiceTypes")) {
561  return QVariant(serviceTypes());
562  } else if (_name == QLatin1String("AllowAsDefault")) {
563  return QVariant(m_bAllowAsDefault);
564  } else if (_name == QLatin1String("InitialPreference")) {
565  return QVariant(m_initialPreference);
566  } else if (_name == QLatin1String("Library")) {
567  return makeStringVariant(m_strLibrary);
568  } else if (_name == QLatin1String("DesktopEntryPath")) { // can't be null
569  return QVariant(path);
570  } else if (_name == QLatin1String("DesktopEntryName")) {
571  return QVariant(m_strDesktopEntryName); // can't be null
572  } else if (_name == QLatin1String("Categories")) {
573  return QVariant(categories);
574  } else if (_name == QLatin1String("Keywords")) {
575  return QVariant(m_lstKeywords);
576  } else if (_name == QLatin1String("FormFactors")) {
577  return QVariant(m_lstFormFactors);
578  }
579 
580  // Ok we need to convert the property from a QString to its real type.
581  // Maybe the caller helped us.
582  if (t == QMetaType::UnknownType) {
583  // No luck, let's ask KServiceTypeFactory what the type of this property
584  // is supposed to be.
585  // ######### this looks in all servicetypes, not just the ones this service supports!
587  t = KSycocaPrivate::self()->serviceTypeFactory()->findPropertyTypeByName(_name);
588  if (t == QMetaType::UnknownType) {
589  qCDebug(SERVICES) << "Request for unknown property" << _name;
590  return QVariant(); // Unknown property: Invalid variant.
591  }
592  }
593 
594  auto it = m_mapProps.constFind(_name);
595  if (it == m_mapProps.cend() || !it.value().isValid()) {
596  // qCDebug(SERVICES) << "Property not found " << _name;
597  return QVariant(); // No property set.
598  }
599 
600  if (t == QMetaType::QString) {
601  return it.value(); // no conversion necessary
602  } else {
603  // All others
604  // For instance properties defined as StringList, like MimeTypes.
605  // XXX This API is accessible only through a friend declaration.
606 
607 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
608  return KConfigGroup::convertToQVariant(_name.toUtf8().constData(), it.value().toString().toUtf8(), QVariant(static_cast<QVariant::Type>(t)));
609 #else
610  return KConfigGroup::convertToQVariant(_name.toUtf8().constData(), it.value().toString().toUtf8(), QVariant(QMetaType(t)));
611 #endif
612  }
613 }
614 
615 QStringList KServicePrivate::propertyNames() const
616 {
617  static const QStringList defaultKeys = {
618  QStringLiteral("Type"),
619  QStringLiteral("Name"),
620  QStringLiteral("Comment"),
621  QStringLiteral("GenericName"),
622  QStringLiteral("Icon"),
623  QStringLiteral("Exec"),
624  QStringLiteral("Terminal"),
625  QStringLiteral("TerminalOptions"),
626  QStringLiteral("Path"),
627  QStringLiteral("ServiceTypes"),
628  QStringLiteral("AllowAsDefault"),
629  QStringLiteral("InitialPreference"),
630  QStringLiteral("Library"),
631  QStringLiteral("DesktopEntryPath"),
632  QStringLiteral("DesktopEntryName"),
633  QStringLiteral("Keywords"),
634  QStringLiteral("FormFactors"),
635  QStringLiteral("Categories"),
636  };
637 
638  return m_mapProps.keys() + defaultKeys;
639 }
640 
642 {
644  return KSycocaPrivate::self()->serviceFactory()->allServices();
645 }
646 
648 {
650  return KSycocaPrivate::self()->serviceFactory()->findServiceByDesktopPath(_name);
651 }
652 
654 {
656  return KSycocaPrivate::self()->serviceFactory()->findServiceByDesktopName(_name);
657 }
658 
660 {
662  return KSycocaPrivate::self()->serviceFactory()->findServiceByMenuId(_name);
663 }
664 
666 {
668  return KSycocaPrivate::self()->serviceFactory()->findServiceByStorageId(_storageId);
669 }
670 
672 {
673  QVariant v = property(QStringLiteral("X-KDE-SubstituteUID"), QMetaType::Bool);
674  return v.isValid() && v.toBool();
675 }
676 
678 {
679  // See also KDesktopFile::tryExec()
680  QString user;
681  QVariant v = property(QStringLiteral("X-KDE-Username"), QMetaType::QString);
682  user = v.isValid() ? v.toString() : QString();
683  if (user.isEmpty()) {
684  user = QString::fromLocal8Bit(qgetenv("ADMIN_ACCOUNT"));
685  }
686  if (user.isEmpty()) {
687  user = QStringLiteral("root");
688  }
689  return user;
690 }
691 
693 {
694  Q_D(const KService);
695 
696  const QString envVar = QString::fromLatin1(qgetenv("XDG_CURRENT_DESKTOP"));
697 
698 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
699  QVector<QStringView> currentDesktops = QStringView(envVar).split(QLatin1Char(':'), Qt::SkipEmptyParts);
700 #else
701  QVector<QStringRef> currentDesktops = envVar.splitRef(QLatin1Char(':'), Qt::SkipEmptyParts);
702 #endif
703 
704  const QString kde = QStringLiteral("KDE");
705  if (currentDesktops.isEmpty()) {
706  // This could be an old display manager, or e.g. a failsafe session with no desktop name
707  // In doubt, let's say we show KDE stuff.
708 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
709  currentDesktops.append(kde);
710 #else
711  currentDesktops.append(&kde);
712 #endif
713  }
714 
715  // This algorithm is described in the desktop entry spec
716 
717  auto it = d->m_mapProps.constFind(QStringLiteral("OnlyShowIn"));
718  if (it != d->m_mapProps.cend()) {
719  const QVariant &val = it.value();
720  if (val.isValid()) {
721  const QStringList aList = val.toString().split(QLatin1Char(';'));
722  return std::any_of(currentDesktops.cbegin(), currentDesktops.cend(), [&aList](const auto desktop) {
723  return aList.contains(desktop);
724  });
725  }
726  }
727 
728  it = d->m_mapProps.constFind(QStringLiteral("NotShowIn"));
729  if (it != d->m_mapProps.cend()) {
730  const QVariant &val = it.value();
731  if (val.isValid()) {
732  const QStringList aList = val.toString().split(QLatin1Char(';'));
733  return std::none_of(currentDesktops.cbegin(), currentDesktops.cend(), [&aList](const auto desktop) {
734  return aList.contains(desktop);
735  });
736  }
737  }
738 
739  return true;
740 }
741 
743 {
744  Q_D(const KService);
745  const QString platform = QCoreApplication::instance()->property("platformName").toString();
746  if (platform.isEmpty()) {
747  return true;
748  }
749 
750  auto it = d->m_mapProps.find(QStringLiteral("X-KDE-OnlyShowOnQtPlatforms"));
751  if ((it != d->m_mapProps.end()) && (it->isValid())) {
752  const QStringList aList = it->toString().split(QLatin1Char(';'));
753  if (!aList.contains(platform)) {
754  return false;
755  }
756  }
757 
758  it = d->m_mapProps.find(QStringLiteral("X-KDE-NotShowOnQtPlatforms"));
759  if ((it != d->m_mapProps.end()) && (it->isValid())) {
760  const QStringList aList = it->toString().split(QLatin1Char(';'));
761  if (aList.contains(platform)) {
762  return false;
763  }
764  }
765  return true;
766 }
767 
769 {
770  if (qvariant_cast<bool>(property(QStringLiteral("NoDisplay"), QMetaType::Bool))) {
771  return true;
772  }
773 
774  if (!showInCurrentDesktop()) {
775  return true;
776  }
777 
778  if (!showOnCurrentPlatform()) {
779  return true;
780  }
781 
783  return true;
784  }
785 
786  return false;
787 }
788 
790 {
791  QVariant v = property(QStringLiteral("UntranslatedGenericName"), QMetaType::QString);
792  return v.isValid() ? v.toString() : QString();
793 }
794 
795 #if KSERVICE_BUILD_DEPRECATED_SINCE(5, 87)
797 {
798  Q_D(const KService);
799  auto it = d->m_mapProps.constFind(QStringLiteral("X-KDE-ParentApp"));
800  if (it != d->m_mapProps.cend()) {
801  const QVariant &val = it.value();
802  if (val.isValid()) {
803  return val.toString();
804  }
805  }
806 
807  return {};
808 }
809 #endif
810 
811 #if KSERVICE_BUILD_DEPRECATED_SINCE(5, 87)
813 {
814  Q_D(const KService);
815  QMap<QString, QVariant>::ConstIterator it = d->m_mapProps.find(QStringLiteral("X-KDE-PluginKeyword"));
816  if ((it == d->m_mapProps.end()) || (!it->isValid())) {
817  return QString();
818  }
819 
820  return it->toString();
821 }
822 #endif
823 
825 {
826  Q_D(const KService);
827 
828  for (const QString &str : {QStringLiteral("X-DocPath"), QStringLiteral("DocPath")}) {
829  auto it = d->m_mapProps.constFind(str);
830  if (it != d->m_mapProps.cend()) {
831  const QVariant variant = it.value();
832  Q_ASSERT(variant.isValid());
833  const QString path = variant.toString();
834  if (!path.isEmpty()) {
835  return path;
836  }
837  }
838  }
839 
840  return {};
841 }
842 
844 {
845  Q_D(const KService);
846  // Can we pass multiple files on the command line or do we have to start the application for every single file ?
847  return (d->m_strExec.contains(QLatin1String("%F")) //
848  || d->m_strExec.contains(QLatin1String("%U")) //
849  || d->m_strExec.contains(QLatin1String("%N")) //
850  || d->m_strExec.contains(QLatin1String("%D")));
851 }
852 
854 {
855  Q_D(const KService);
856  return d->categories;
857 }
858 
860 {
861  Q_D(const KService);
862  return d->menuId;
863 }
864 
865 void KService::setMenuId(const QString &_menuId)
866 {
867  Q_D(KService);
868  d->menuId = _menuId;
869 }
870 
872 {
873  Q_D(const KService);
874  return d->storageId();
875 }
876 
877 // not sure this is still used anywhere...
879 {
880  Q_D(const KService);
881  if (d->menuId.isEmpty() //
882  || entryPath().startsWith(QLatin1String(".hidden")) //
883  || (QDir::isRelativePath(entryPath()) && d->categories.isEmpty())) {
885  }
886 
888 }
889 
890 QString KService::newServicePath(bool showInMenu, const QString &suggestedName, QString *menuId, const QStringList *reservedMenuIds)
891 {
892  Q_UNUSED(showInMenu); // TODO KDE5: remove argument
893 
894  QString base = suggestedName;
895  QString result;
896  for (int i = 1; true; i++) {
897  if (i == 1) {
898  result = base + QStringLiteral(".desktop");
899  } else {
900  result = base + QStringLiteral("-%1.desktop").arg(i);
901  }
902 
903  if (reservedMenuIds && reservedMenuIds->contains(result)) {
904  continue;
905  }
906 
907  // Lookup service by menu-id
908  KService::Ptr s = serviceByMenuId(result);
909  if (s) {
910  continue;
911  }
912 
913  if (!QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("applications/") + result).isEmpty()) {
914  continue;
915  }
916 
917  break;
918  }
919  if (menuId) {
920  *menuId = result;
921  }
922 
924 }
925 
927 {
928  Q_D(const KService);
929  return d->m_strType == QLatin1String("Application");
930 }
931 
933 {
934  Q_D(const KService);
935  if (d->m_strType == QLatin1String("Application") && d->m_strExec.isEmpty()) {
936  qCWarning(SERVICES) << "The desktop entry file" << entryPath() << "has Type=" << d->m_strType << "but has no Exec field.";
937  }
938  return d->m_strExec;
939 }
940 
942 {
943  Q_D(const KService);
944  return d->m_strLibrary;
945 }
946 
948 {
949  Q_D(const KService);
950  return d->m_strIcon;
951 }
952 
954 {
955  Q_D(const KService);
956  return d->m_strTerminalOptions;
957 }
958 
959 bool KService::terminal() const
960 {
961  Q_D(const KService);
962  return d->m_bTerminal;
963 }
964 
966 {
967  QVariant prop = property(QStringLiteral("PrefersNonDefaultGPU"), QMetaType::Bool);
968  if (!prop.isValid()) {
969  // For backwards compatibility
970  prop = property(QStringLiteral("X-KDE-RunOnDiscreteGpu"), QMetaType::Bool);
971  }
972 
973  return prop.isValid() && prop.toBool();
974 }
975 
977 {
978  Q_D(const KService);
979  return d->m_strDesktopEntryName;
980 }
981 
982 #if KSERVICE_BUILD_DEPRECATED_SINCE(5, 102)
984 {
985  Q_D(const KService);
986  return d->m_DBUSStartusType;
987 }
988 #endif
989 
990 #if KSERVICE_BUILD_DEPRECATED_SINCE(5, 63)
992 {
993  Q_D(const KService);
994  return d->m_strWorkingDirectory;
995 }
996 #endif
997 
999 {
1000  Q_D(const KService);
1001  return d->m_strWorkingDirectory;
1002 }
1003 
1005 {
1006  Q_D(const KService);
1007  return d->m_strComment;
1008 }
1009 
1011 {
1012  Q_D(const KService);
1013  return d->m_strGenName;
1014 }
1015 
1017 {
1018  Q_D(const KService);
1019  return d->m_lstKeywords;
1020 }
1021 
1022 QStringList KServicePrivate::serviceTypes() const
1023 {
1024  QStringList ret;
1025  ret.reserve(m_serviceTypes.size());
1026 
1027  std::transform(m_serviceTypes.cbegin(), m_serviceTypes.cend(), std::back_inserter(ret), [](const KService::ServiceTypeAndPreference &typePref) {
1028  Q_ASSERT(!typePref.serviceType.isEmpty());
1029  return typePref.serviceType;
1030  });
1031 
1032  return ret;
1033 }
1034 
1036 {
1037  Q_D(const KService);
1038  return d->serviceTypes();
1039 }
1040 
1042 {
1043  Q_D(const KService);
1044 
1045  QMimeDatabase db;
1046  QStringList ret;
1047 
1048  for (const KService::ServiceTypeAndPreference &s : d->m_serviceTypes) {
1049  const QString servType = s.serviceType;
1050  if (db.mimeTypeForName(servType).isValid()) { // keep only mimetypes, filter out servicetypes
1051  ret.append(servType);
1052  }
1053  }
1054  return ret;
1055 }
1056 
1057 #if KSERVICE_BUILD_DEPRECATED_SINCE(5, 67)
1059 {
1060  Q_D(const KService);
1061  return d->m_bAllowAsDefault;
1062 }
1063 #endif
1064 
1066 {
1067  Q_D(const KService);
1068  return d->m_initialPreference;
1069 }
1070 
1072 {
1073  Q_D(KService);
1074  d->m_bTerminal = b;
1075 }
1076 
1078 {
1079  Q_D(KService);
1080  d->m_strTerminalOptions = options;
1081 }
1082 
1083 void KService::setExec(const QString &exec)
1084 {
1085  Q_D(KService);
1086 
1087  if (!exec.isEmpty()) {
1088  d->m_strExec = exec;
1089  d->path.clear();
1090  }
1091 }
1092 
1094 {
1095  Q_D(KService);
1096 
1097  if (!workingDir.isEmpty()) {
1098  d->m_strWorkingDirectory = workingDir;
1099  d->path.clear();
1100  }
1101 }
1102 
1103 QVector<KService::ServiceTypeAndPreference> &KService::_k_accessServiceTypes()
1104 {
1105  Q_D(KService);
1106  return d->m_serviceTypes;
1107 }
1108 
1110 {
1111  Q_D(const KService);
1112  return d->m_actions;
1113 }
1114 
1115 #if KCOREADDONS_BUILD_DEPRECATED_SINCE(5, 86)
1116 #if KSERVICE_BUILD_DEPRECATED_SINCE(5, 86)
1117 KService::operator KPluginName() const
1118 {
1119  if (!isValid()) {
1120  return KPluginName::fromErrorString(i18n("The provided service is not valid"));
1121  }
1122 
1123  if (library().isEmpty()) {
1124  return KPluginName::fromErrorString(i18n("The service '%1' provides no library or the Library key is missing", entryPath()));
1125  }
1126 
1127  return KPluginName(library());
1128 }
1129 #endif
1130 #endif
1131 
1133 {
1134  return KServiceUtilPrivate::completeBaseName(property(QStringLiteral("X-KDE-AliasFor"), QMetaType::QString).toString());
1135 }
static KSycoca * self()
Get or create the only instance of KSycoca (read-only)
Definition: ksycoca.cpp:379
void append(const T &value)
bool isNull() const const
QMap::const_iterator constBegin() const const
QString readEntry(const char *key, const char *aDefault=nullptr) const
QString desktopEntryName() const
Returns the filename of the service desktop entry without any extension.
Definition: kservice.cpp:976
bool isEmpty() const const
bool isValid() const const
void ensureCacheValid()
Ensures the ksycoca database is up to date.
Definition: ksycoca.cpp:826
bool contains(const Key &key) const const
QMap::const_key_value_iterator constKeyValueBegin() const const
QString comment() const
Returns the descriptive comment for the service, if there is one.
Definition: kservice.cpp:1004
QVariant fromValue(const T &value)
void setDeleted(bool deleted)
Sets whether or not this service is deleted.
QString parentApp() const
Name of the application this service belongs to.
Definition: kservice.cpp:796
QVector::const_iterator cend() const const
QStringList serviceTypes() const
Returns the service types that this service supports.
Definition: kservice.cpp:1035
QStringList split(const QString &sep, QString::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
T value() const const
Base class for all Sycoca entries.
Definition: ksycocaentry.h:32
QString storageId() const
Returns a normalized ID suitable for storing in configuration files.
Definition: kservice.cpp:871
bool contains(const QString &str, Qt::CaseSensitivity cs) const const
void clear()
void append(const T &value)
bool hasServiceType(const QString &serviceTypePtr) const
Checks whether the service supports this service type.
Definition: kservice.cpp:432
QString entryPath() const
QString readEntryUntranslated(const char *key, const QString &aDefault=QString()) const
void setMenuId(const QString &menuId)
Definition: kservice.cpp:865
QDataStream & operator<<(QDataStream &out, const KDateTime &dateTime)
QString genericName() const
Returns the generic name for the service, if there is one (e.g.
Definition: kservice.cpp:1010
QString untranslatedGenericName() const
Returns the untranslated (US English) generic name for the service, if there is one (e....
Definition: kservice.cpp:789
QString writableLocation(QStandardPaths::StandardLocation type)
KService(const QString &name, const QString &exec, const QString &icon)
Construct a temporary service with a given name, exec-line and icon.
Definition: kservice.cpp:383
QStringList categories() const
Returns a list of VFolder categories.
Definition: kservice.cpp:853
int lastIndexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
QString locate(QStandardPaths::StandardLocation type, const QString &fileName, QStandardPaths::LocateOptions options)
QStringList mimeTypes() const
Returns the list of MIME types that this service supports.
Definition: kservice.cpp:1041
QMap< QString, QString > entryMap() const
QVariant property(const QString &_name, QVariant::Type t) const
Returns the requested property.
Definition: kservice.cpp:527
bool hasMimeType(const QString &mimeType) const
Checks whether the service supports this MIME type.
Definition: kservice.cpp:475
int initialPreference() const
What preference to associate with this service initially (before the user has had any chance to defin...
Definition: kservice.cpp:1065
QString findExecutable(const QString &executableName, const QStringList &paths)
void reserve(int alloc)
int remove(const Key &key)
QMap::iterator insert(const Key &key, const T &value)
static Ptr serviceByMenuId(const QString &_menuId)
Find a service by its menu-id.
Definition: kservice.cpp:659
int size() const const
QMap::iterator end()
void setWorkingDirectory(const QString &workingDir)
Overrides the "Path=" line of the service.
Definition: kservice.cpp:1093
DBusStartupType dbusStartupType() const
Returns the DBUSStartupType supported by this service.
Definition: kservice.cpp:983
QString i18n(const char *text, const TYPE &arg...)
QStringList readXdgListEntry(const char *key, const QStringList &aDefault=QStringList()) const
QMap::iterator find(const Key &key)
static QString newServicePath(bool showInMenu, const QString &suggestedName, QString *menuId=nullptr, const QStringList *reservedMenuIds=nullptr)
Returns a path that can be used to create a new KService based on suggestedName.
Definition: kservice.cpp:890
QString fromLocal8Bit(const char *str, int size)
static KPluginName fromErrorString(const QString &errorString)
QString terminalOptions() const
Returns any options associated with the terminal the service runs in, if it requires a terminal.
Definition: kservice.cpp:953
QMimeType mimeTypeForName(const QString &nameOrAlias) const const
QMap::const_iterator constEnd() const const
bool isApplication() const
Services are either applications (executables) or dlopened libraries (plugins).
Definition: kservice.cpp:926
QVector::const_iterator cbegin() const const
bool isEmpty() const const
SkipEmptyParts
void setTerminal(bool b)
Definition: kservice.cpp:1071
QString icon() const
Returns the name of the icon.
Definition: kservice.cpp:947
bool isEmpty() const const
bool allowAsDefault() const
Set to true if it is allowed to use this service as the default (main) action for the files it suppor...
Definition: kservice.cpp:1058
QVector< QStringRef > splitRef(const QString &sep, QString::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
QByteArray toUtf8() const const
QMap::iterator erase(QMap::iterator pos)
QCoreApplication * instance()
static Ptr serviceByDesktopPath(const QString &_path)
Find a service based on its path as returned by entryPath().
Definition: kservice.cpp:647
KCOREADDONS_EXPORT QString tildeExpand(const QString &path)
QString path() const
Definition: kservice.cpp:991
QString docPath() const
The path to the documentation for this service.
Definition: kservice.cpp:824
int toInt(bool *ok, int base) const const
QString library() const
Returns the name of the service's library.
Definition: kservice.cpp:941
QStringList keywords() const
Returns a list of descriptive keywords the service, if there are any.
Definition: kservice.cpp:1016
void setTerminalOptions(const QString &options)
Definition: kservice.cpp:1077
bool isEmpty() const const
QString workingDirectory() const
Definition: kservice.cpp:998
KCONFIGCORE_EXPORT bool authorizeControlModule(const QString &menuId)
void setExec(const QString &exec)
Overrides the "Exec=" line of the service.
Definition: kservice.cpp:1083
bool isValid() const const
bool hasKey(const char *key) const
static List allServices()
Returns the whole list of services.
Definition: kservice.cpp:641
KSharedConfigPtr config()
QString aliasFor() const
A desktop file name that this service is an alias for.
Definition: kservice.cpp:1132
static Ptr serviceType(const QString &_name)
Returns a pointer to the servicetype '_name' or nullptr if the service type is unknown.
bool toBool() const const
QString menuId() const
Returns the menu ID of the service desktop entry.
Definition: kservice.cpp:859
QList< KServiceAction > actions() const
Returns the actions defined in this desktop file.
Definition: kservice.cpp:1109
QString toLower() const const
bool showOnCurrentPlatform() const
Whether the service should be shown on the current platform (e.g.
Definition: kservice.cpp:742
bool allowMultipleFiles() const
Checks whether this service can handle several files as startup arguments.
Definition: kservice.cpp:843
const char * constData() const const
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
static Ptr serviceByStorageId(const QString &_storageId)
Find a service by its storage-id or desktop-file path.
Definition: kservice.cpp:665
QString & insert(int position, QChar ch)
QString fromLatin1(const char *str, int size)
bool substituteUid() const
Checks whether the service runs with a different user id.
Definition: kservice.cpp:671
bool runOnDiscreteGpu() const
Returns true if the service inidicates that it's preferred to run the application on a discrete graph...
Definition: kservice.cpp:965
QString locateLocal() const
Returns a path that can be used for saving changes to this service.
Definition: kservice.cpp:878
bool terminal() const
Checks whether the service should be run in a terminal.
Definition: kservice.cpp:959
bool isRelativePath(const QString &path)
QString username() const
Returns the user name, if the service runs with a different user id.
Definition: kservice.cpp:677
QString displayName(QStandardPaths::StandardLocation type)
DBusStartupType
Describes the D-Bus Startup type of the service.
Definition: kservice.h:192
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
QDataStream & operator>>(QDataStream &in, KDateTime &dateTime)
static Ptr serviceByDesktopName(const QString &_name)
Find a service by the name of its desktop file, not depending on its actual location (as long as it's...
Definition: kservice.cpp:653
bool showInCurrentDesktop() const
Whether the service should be shown in the current desktop (including in context menus).
Definition: kservice.cpp:692
bool isDeleted() const
static QString locateLocal(const QString &path)
QString pluginKeyword() const
The keyword to be used when constructing the plugin using KPluginFactory.
Definition: kservice.cpp:812
Q_D(Todo)
QString exec() const
Returns the executable.
Definition: kservice.cpp:932
bool noDisplay() const
Whether the entry should be suppressed in the K menu.
Definition: kservice.cpp:768
QString toString() const const
QVariant property(const char *name) const const
QMap::const_key_value_iterator constKeyValueEnd() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Tue Feb 7 2023 04:00:52 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.