feat(applications): add shared desktop entry registry

main
reionwong 3 weeks ago
parent 49123763a6
commit 795fa53c23

@ -32,6 +32,7 @@ else()
endif()
# -------------------------------------------
add_subdirectory(applications)
add_subdirectory(accounts)
add_subdirectory(bluez)
add_subdirectory(mpris)

@ -0,0 +1,21 @@
set(CUTEFISH_APPLICATIONS_SRCS
applicationlauncher.cpp
applicationlauncher.h
applicationregistry.cpp
applicationregistry.h
desktopentry.cpp
desktopentry.h
)
add_library(cutefishapplications STATIC ${CUTEFISH_APPLICATIONS_SRCS})
set_target_properties(cutefishapplications PROPERTIES POSITION_INDEPENDENT_CODE ON)
set_target_properties(cutefishapplications PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON)
target_include_directories(cutefishapplications PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(cutefishapplications
PUBLIC
Qt6::Core
Qt6::DBus
Qt6::Gui
Qt6::Qml
)

@ -0,0 +1,26 @@
#include "applicationlauncher.h"
#include <QDBusConnection>
#include <QDBusInterface>
#include <QProcess>
bool ApplicationLauncher::startDetached(const QStringList &command,
const QString &workingDirectory)
{
if (command.isEmpty() || command.first().isEmpty())
return false;
const QString program = command.first();
const QStringList arguments = command.mid(1);
// Keep using the session launch service when it is available. It applies
// the desktop session's environment and startup handling consistently.
QDBusInterface session("com.cutefish.Session", "/Session",
"com.cutefish.Session", QDBusConnection::sessionBus());
if (session.isValid()) {
session.asyncCall("launch", program, arguments);
return true;
}
return QProcess::startDetached(program, arguments, workingDirectory);
}

@ -0,0 +1,11 @@
#pragma once
#include <QString>
#include <QStringList>
class ApplicationLauncher
{
public:
static bool startDetached(const QStringList &command,
const QString &workingDirectory = QString());
};

@ -0,0 +1,454 @@
#include "applicationregistry.h"
#include <QDir>
#include <QDirIterator>
#include <QFile>
#include <QFileInfo>
#include <QJSEngine>
#include <QPointer>
#include <QQmlEngine>
#include <QSet>
#include <QThreadPool>
#include <algorithm>
#include <utility>
namespace {
class DesktopEntryScanner : public QRunnable
{
public:
explicit DesktopEntryScanner(ApplicationRegistry *registry)
: m_registry(registry)
{
setAutoDelete(true);
}
void run() override
{
QList<DesktopEntryData> results;
QSet<QString> selectedIds;
for (const QString &rootPath : ApplicationRegistry::desktopPaths()) {
QDir root(rootPath);
if (!root.exists())
continue;
QStringList files;
QDirIterator iterator(rootPath, {QStringLiteral("*.desktop")},
QDir::Files | QDir::Readable,
QDirIterator::Subdirectories);
while (iterator.hasNext())
files.append(iterator.next());
std::sort(files.begin(), files.end());
for (const QString &path : files) {
QFile file(path);
if (!file.open(QIODevice::ReadOnly))
continue;
QString id = root.relativeFilePath(path);
id.replace(QLatin1Char('/'), QLatin1Char('-'));
if (id.endsWith(QStringLiteral(".desktop")))
id.chop(QStringLiteral(".desktop").size());
if (selectedIds.contains(id))
continue;
DesktopEntryData data;
if (DesktopEntry::parse(id, path, file.readAll(), &data)) {
selectedIds.insert(id);
results.append(std::move(data));
}
}
}
QPointer<ApplicationRegistry> registry = m_registry;
QMetaObject::invokeMethod(m_registry, [registry, results]() {
if (registry)
registry->applyScan(results);
}, Qt::QueuedConnection);
}
private:
ApplicationRegistry *m_registry;
};
QString normalized(const QString &value)
{
return value.trimmed().toLower();
}
bool desktopMatches(const DesktopEntryData &entry, const QByteArray &desktop)
{
const QStringList currentDesktops = QString::fromLocal8Bit(desktop)
.split(QLatin1Char(':'), Qt::SkipEmptyParts);
auto containsDesktop = [&currentDesktops](const QStringList &desktops) {
for (const QString &desktop : desktops) {
for (const QString &current : currentDesktops) {
if (desktop.compare(current, Qt::CaseInsensitive) == 0)
return true;
}
}
return false;
};
if (!entry.onlyShowIn.isEmpty() && !containsDesktop(entry.onlyShowIn))
return false;
if (containsDesktop(entry.notShowIn))
return false;
return true;
}
} // namespace
DesktopEntryModel::DesktopEntryModel(QObject *parent)
: QAbstractListModel(parent)
{
}
int DesktopEntryModel::rowCount(const QModelIndex &parent) const
{
return parent.isValid() ? 0 : m_entries.size();
}
QVariant DesktopEntryModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() < 0 || index.row() >= m_entries.size())
return {};
DesktopEntry *entry = m_entries.at(index.row());
switch (role) {
case IdRole: return entry->id();
case NameRole: return entry->name();
case GenericNameRole: return entry->genericName();
case CommentRole: return entry->comment();
case IconRole: return entry->icon();
case PathRole: return entry->path();
case ExecRole: return entry->exec();
case CommandRole: return entry->command();
case StartupWMClassRole: return entry->startupWMClass();
case CategoriesRole: return entry->categories();
case KeywordsRole: return entry->keywords();
case MimeTypesRole: return entry->mimeTypes();
case TerminalRole: return entry->terminal();
case EntryRole: return QVariant::fromValue(entry);
default: return {};
}
}
QHash<int, QByteArray> DesktopEntryModel::roleNames() const
{
return {
{IdRole, "id"},
{NameRole, "name"},
{GenericNameRole, "genericName"},
{CommentRole, "comment"},
{IconRole, "icon"},
{PathRole, "path"},
{ExecRole, "exec"},
{CommandRole, "command"},
{StartupWMClassRole, "startupWMClass"},
{CategoriesRole, "categories"},
{KeywordsRole, "keywords"},
{MimeTypesRole, "mimeTypes"},
{TerminalRole, "terminal"},
{EntryRole, "entry"}
};
}
void DesktopEntryModel::setEntries(const QList<DesktopEntry *> &entries)
{
for (DesktopEntry *entry : std::as_const(m_entries))
disconnect(entry, nullptr, this, nullptr);
beginResetModel();
m_entries = entries;
endResetModel();
for (DesktopEntry *entry : std::as_const(m_entries)) {
connect(entry, &DesktopEntry::changed, this, [this, entry]() {
const int row = m_entries.indexOf(entry);
if (row >= 0)
emit dataChanged(index(row), index(row));
});
}
}
QList<DesktopEntry *> DesktopEntryModel::entries() const
{
return m_entries;
}
ApplicationRegistry *ApplicationRegistry::instance()
{
static ApplicationRegistry *registry = new ApplicationRegistry;
return registry;
}
ApplicationRegistry::ApplicationRegistry(QObject *parent)
: QObject(parent)
, m_model(new DesktopEntryModel(this))
, m_watcher(new QFileSystemWatcher(this))
{
m_debounceTimer.setSingleShot(true);
m_debounceTimer.setInterval(100);
connect(m_watcher, &QFileSystemWatcher::directoryChanged,
this, &ApplicationRegistry::scheduleScan);
connect(m_watcher, &QFileSystemWatcher::fileChanged,
this, &ApplicationRegistry::scheduleScan);
connect(&m_debounceTimer, &QTimer::timeout,
this, &ApplicationRegistry::startScan);
monitorPaths();
startScan();
}
QAbstractItemModel *ApplicationRegistry::applications() const
{
return m_model;
}
bool ApplicationRegistry::loading() const
{
return m_loading;
}
DesktopEntry *ApplicationRegistry::byId(const QString &id) const
{
if (DesktopEntry *entry = m_entriesById.value(id))
return entry;
const QString lower = normalized(id);
for (auto it = m_entriesById.cbegin(); it != m_entriesById.cend(); ++it) {
if (normalized(it.key()) == lower)
return it.value();
}
return nullptr;
}
DesktopEntry *ApplicationRegistry::byPath(const QString &path) const
{
if (path.isEmpty())
return nullptr;
return m_entriesByPath.value(QFileInfo(path).absoluteFilePath());
}
DesktopEntry *ApplicationRegistry::resolveWindow(const QString &appId,
const QString &windowClass,
const QString &executablePath,
const QString &executableName) const
{
const QString wantedAppId = normalized(appId);
const QString wantedClass = normalized(windowClass);
const QString wantedExecutable = normalized(executablePath);
const QString wantedName = normalized(executableName);
DesktopEntry *best = nullptr;
int bestScore = 0;
for (DesktopEntry *entry : applicationEntries()) {
const QString id = normalized(entry->id());
const QString startupClass = normalized(entry->startupWMClass());
const QString program = normalized(entry->command().value(0));
const QString programName = normalized(QFileInfo(entry->command().value(0)).fileName());
const QString icon = normalized(entry->icon());
const QString name = normalized(entry->name());
const QString fileName = normalized(QFileInfo(entry->path()).completeBaseName());
int score = 0;
if (!wantedAppId.isEmpty() && id == wantedAppId)
score = qMax(score, 100);
if (!wantedClass.isEmpty() && id == wantedClass)
score = qMax(score, 90);
if (!wantedClass.isEmpty() && startupClass == wantedClass)
score = qMax(score, 95);
if (!wantedAppId.isEmpty() && startupClass == wantedAppId)
score = qMax(score, 95);
if (!wantedExecutable.isEmpty() && program == wantedExecutable)
score = qMax(score, 85);
if (!wantedName.isEmpty() && programName == wantedName)
score = qMax(score, 80);
if (!wantedExecutable.isEmpty() && fileName == wantedExecutable)
score = qMax(score, 75);
if (!wantedName.isEmpty() && fileName == wantedName)
score = qMax(score, 75);
if (!wantedExecutable.isEmpty() && icon == wantedExecutable)
score = qMax(score, 75);
if (!wantedName.isEmpty() && icon == wantedName)
score = qMax(score, 75);
if (!wantedAppId.isEmpty() && !startupClass.isEmpty()
&& startupClass.startsWith(wantedAppId))
score = qMax(score, 70);
if (!wantedClass.isEmpty() && !startupClass.isEmpty()
&& startupClass.startsWith(wantedClass))
score = qMax(score, 70);
if (!wantedClass.isEmpty() && id.startsWith(wantedClass))
score = qMax(score, 60);
if (!wantedClass.isEmpty() && fileName.startsWith(wantedClass))
score = qMax(score, 60);
if (!wantedClass.isEmpty() && program.startsWith(wantedClass))
score = qMax(score, 55);
if (!wantedClass.isEmpty() && icon.startsWith(wantedClass))
score = qMax(score, 55);
if (!wantedClass.isEmpty() && name.startsWith(wantedClass))
score = qMax(score, 50);
if (!wantedExecutable.isEmpty() && !program.isEmpty()
&& wantedExecutable.contains(program))
score = qMax(score, 45);
if (score > bestScore) {
best = entry;
bestScore = score;
}
}
return best;
}
QList<DesktopEntry *> ApplicationRegistry::entries() const
{
return m_entries;
}
QList<DesktopEntry *> ApplicationRegistry::applicationEntries() const
{
return m_model->entries();
}
QStringList ApplicationRegistry::desktopPaths()
{
QString dataHome = qEnvironmentVariable("XDG_DATA_HOME");
if (dataHome.isEmpty())
dataHome = QDir::homePath() + QStringLiteral("/.local/share");
QString dataDirs = qEnvironmentVariable("XDG_DATA_DIRS");
if (dataDirs.isEmpty())
dataDirs = QStringLiteral("/usr/local/share:/usr/share");
QStringList paths;
paths.append(QDir::cleanPath(dataHome + QStringLiteral("/applications")));
for (const QString &dir : dataDirs.split(QLatin1Char(':'), Qt::SkipEmptyParts))
paths.append(QDir::cleanPath(dir + QStringLiteral("/applications")));
paths.removeDuplicates();
return paths;
}
void ApplicationRegistry::scheduleScan()
{
m_debounceTimer.start();
}
void ApplicationRegistry::startScan()
{
if (m_scanInProgress) {
m_scanQueued = true;
return;
}
m_scanInProgress = true;
if (!m_loading) {
m_loading = true;
emit loadingChanged();
}
QThreadPool::globalInstance()->start(new DesktopEntryScanner(this));
}
void ApplicationRegistry::applyScan(const QList<DesktopEntryData> &results)
{
const bool wasLoading = m_loading;
m_loading = false;
m_scanInProgress = false;
QHash<QString, DesktopEntry *> oldEntries = m_entriesById;
QHash<QString, DesktopEntry *> newEntries;
QHash<QString, DesktopEntry *> newEntriesByPath;
QList<DesktopEntry *> allEntries;
QList<DesktopEntry *> visibleEntries;
const QByteArray currentDesktop = qgetenv("XDG_CURRENT_DESKTOP");
for (const DesktopEntryData &data : results) {
DesktopEntry *entry = oldEntries.take(data.id);
if (!entry)
entry = new DesktopEntry(data.id, this);
entry->update(data);
newEntries.insert(data.id, entry);
newEntriesByPath.insert(QFileInfo(data.path).absoluteFilePath(), entry);
allEntries.append(entry);
if (!data.hidden && !data.noDisplay && desktopMatches(data, currentDesktop))
visibleEntries.append(entry);
}
std::sort(visibleEntries.begin(), visibleEntries.end(), [](DesktopEntry *a, DesktopEntry *b) {
const int nameCompare = QString::localeAwareCompare(a->name(), b->name());
return nameCompare == 0 ? a->id() < b->id() : nameCompare < 0;
});
m_entriesById = newEntries;
m_entriesByPath = newEntriesByPath;
m_entries = allEntries;
m_model->setEntries(visibleEntries);
monitorPaths();
for (DesktopEntry *entry : std::as_const(oldEntries))
entry->deleteLater();
if (wasLoading)
emit loadingChanged();
emit applicationsChanged();
if (m_scanQueued) {
m_scanQueued = false;
startScan();
}
}
void ApplicationRegistry::monitorPaths()
{
for (const QString &rootPath : desktopPaths()) {
QDir root(rootPath);
if (!root.exists()) {
QString parentPath = rootPath;
while (!QDir(parentPath).exists()) {
const QString parent = QFileInfo(parentPath).absolutePath();
if (parent == parentPath)
break;
parentPath = parent;
}
if (QDir(parentPath).exists())
m_watcher->addPath(parentPath);
continue;
}
m_watcher->addPath(rootPath);
QDirIterator iterator(rootPath, QDir::Dirs | QDir::NoDotAndDotDot,
QDirIterator::Subdirectories);
while (iterator.hasNext())
m_watcher->addPath(iterator.next());
QDirIterator files(rootPath, {QStringLiteral("*.desktop")},
QDir::Files | QDir::Readable,
QDirIterator::Subdirectories);
while (files.hasNext())
m_watcher->addPath(files.next());
}
}
void registerApplicationsQmlTypes()
{
static bool registered = false;
if (registered)
return;
registered = true;
qmlRegisterSingletonType<ApplicationRegistry>(
"Cutefish.Applications", 1, 0, "DesktopEntries",
[](QQmlEngine *, QJSEngine *) -> QObject * {
return ApplicationRegistry::instance();
});
qmlRegisterUncreatableType<DesktopEntry>(
"Cutefish.Applications", 1, 0, "DesktopEntry",
QStringLiteral("DesktopEntry objects are provided by DesktopEntries"));
}

@ -0,0 +1,95 @@
#pragma once
#include "desktopentry.h"
#include <QAbstractListModel>
#include <QFileSystemWatcher>
#include <QHash>
#include <QObject>
#include <QTimer>
class DesktopEntryModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles {
IdRole = Qt::UserRole + 1,
NameRole,
GenericNameRole,
CommentRole,
IconRole,
PathRole,
ExecRole,
CommandRole,
StartupWMClassRole,
CategoriesRole,
KeywordsRole,
MimeTypesRole,
TerminalRole,
EntryRole
};
Q_ENUM(Roles)
explicit DesktopEntryModel(QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override;
void setEntries(const QList<DesktopEntry *> &entries);
QList<DesktopEntry *> entries() const;
private:
QList<DesktopEntry *> m_entries;
};
class ApplicationRegistry : public QObject
{
Q_OBJECT
Q_PROPERTY(QAbstractItemModel *applications READ applications CONSTANT)
Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)
public:
static ApplicationRegistry *instance();
explicit ApplicationRegistry(QObject *parent = nullptr);
QAbstractItemModel *applications() const;
bool loading() const;
Q_INVOKABLE DesktopEntry *byId(const QString &id) const;
Q_INVOKABLE DesktopEntry *byPath(const QString &path) const;
DesktopEntry *resolveWindow(const QString &appId,
const QString &windowClass,
const QString &executablePath,
const QString &executableName) const;
QList<DesktopEntry *> applicationEntries() const;
QList<DesktopEntry *> entries() const;
static QStringList desktopPaths();
signals:
void applicationsChanged();
void loadingChanged();
public slots:
void scheduleScan();
void startScan();
void applyScan(const QList<DesktopEntryData> &results);
private:
void monitorPaths();
DesktopEntryModel *m_model;
QFileSystemWatcher *m_watcher;
QTimer m_debounceTimer;
QHash<QString, DesktopEntry *> m_entriesById;
QHash<QString, DesktopEntry *> m_entriesByPath;
QList<DesktopEntry *> m_entries;
bool m_loading = false;
bool m_scanInProgress = false;
bool m_scanQueued = false;
};
void registerApplicationsQmlTypes();

@ -0,0 +1,364 @@
#include "desktopentry.h"
#include "applicationlauncher.h"
#include <QFile>
#include <QFileInfo>
#include <QLocale>
#include <QMap>
#include <QRegularExpression>
namespace {
QStringList splitList(const QString &value)
{
QStringList result;
for (const QString &item : value.split(QLatin1Char(';'), Qt::SkipEmptyParts))
result.append(item.trimmed());
return result;
}
QString unescapeValue(QString value)
{
value.replace(QStringLiteral("\\n"), QStringLiteral("\n"));
value.replace(QStringLiteral("\\t"), QStringLiteral("\t"));
value.replace(QStringLiteral("\\r"), QStringLiteral("\r"));
value.replace(QStringLiteral("\\s"), QStringLiteral(" "));
value.replace(QStringLiteral("\\;"), QStringLiteral(";"));
value.replace(QStringLiteral("\\\\"), QStringLiteral("\\"));
return value;
}
bool parseBool(const QString &value)
{
return value.compare(QStringLiteral("true"), Qt::CaseInsensitive) == 0;
}
QString localizedValue(const QMap<QString, QString> &values,
const QMap<QString, QString> &localized,
const QString &key)
{
const QLocale locale = QLocale::system();
const QStringList candidates = {
locale.name(),
locale.bcp47Name(),
locale.name().section(QLatin1Char('_'), 0, 0)
};
for (const QString &candidate : candidates) {
if (candidate.isEmpty())
continue;
const auto it = localized.constFind(key + QLatin1Char('[') + candidate
+ QLatin1Char(']'));
if (it != localized.constEnd())
return *it;
}
return values.value(key);
}
QStringList tokenizeExec(const QString &exec)
{
QStringList result;
QString token;
bool quoted = false;
QChar quote;
for (int i = 0; i < exec.size(); ++i) {
const QChar ch = exec.at(i);
if (ch == QLatin1Char('\\') && i + 1 < exec.size()) {
token.append(exec.at(++i));
continue;
}
if ((ch == QLatin1Char('"') || ch == QLatin1Char('\''))) {
if (!quoted) {
quoted = true;
quote = ch;
} else if (quote == ch) {
quoted = false;
} else {
token.append(ch);
}
continue;
}
if (!quoted && ch.isSpace()) {
if (!token.isEmpty()) {
result.append(token);
token.clear();
}
continue;
}
token.append(ch);
}
if (!token.isEmpty())
result.append(token);
return result;
}
QString substituteToken(const QString &token, const DesktopEntry *entry,
const QStringList &arguments)
{
QString result;
for (int i = 0; i < token.size(); ++i) {
if (token.at(i) != QLatin1Char('%') || i + 1 >= token.size()) {
result.append(token.at(i));
continue;
}
const QChar field = token.at(++i);
if (field == QLatin1Char('%')) {
result.append(QLatin1Char('%'));
} else if (field == QLatin1Char('c')) {
result.append(entry->name());
} else if (field == QLatin1Char('k')) {
result.append(entry->path());
} else if (field == QLatin1Char('i')) {
// %i is handled as two arguments by commandForArguments().
} else if (field == QLatin1Char('f') || field == QLatin1Char('u')) {
if (!arguments.isEmpty())
result.append(arguments.first());
} else if (field == QLatin1Char('F') || field == QLatin1Char('U')) {
if (!arguments.isEmpty())
result.append(arguments.first());
}
}
return result;
}
} // namespace
DesktopEntry::DesktopEntry(const QString &id, QObject *parent)
: QObject(parent)
, m_id(id)
{
}
QString DesktopEntry::id() const { return m_id; }
QString DesktopEntry::path() const { return m_path; }
QString DesktopEntry::name() const { return m_name; }
QString DesktopEntry::genericName() const { return m_genericName; }
QString DesktopEntry::comment() const { return m_comment; }
QString DesktopEntry::icon() const { return m_icon; }
QString DesktopEntry::exec() const { return m_exec; }
QStringList DesktopEntry::command() const { return m_command; }
QString DesktopEntry::workingDirectory() const { return m_workingDirectory; }
QString DesktopEntry::startupWMClass() const { return m_startupWMClass; }
QStringList DesktopEntry::categories() const { return m_categories; }
QStringList DesktopEntry::keywords() const { return m_keywords; }
QStringList DesktopEntry::mimeTypes() const { return m_mimeTypes; }
bool DesktopEntry::terminal() const { return m_terminal; }
bool DesktopEntry::noDisplay() const { return m_noDisplay; }
bool DesktopEntry::hidden() const { return m_hidden; }
QStringList DesktopEntry::commandForArguments(const QStringList &arguments) const
{
QStringList result;
const QStringList tokens = tokenizeExec(m_exec);
for (const QString &token : tokens) {
if (token == QStringLiteral("%F") || token == QStringLiteral("%U")) {
result.append(arguments);
continue;
}
if (token == QStringLiteral("%f") || token == QStringLiteral("%u")) {
if (!arguments.isEmpty())
result.append(arguments.first());
continue;
}
if (token == QStringLiteral("%i")) {
if (!m_icon.isEmpty())
result << QStringLiteral("--icon") << m_icon;
continue;
}
const QString substituted = substituteToken(token, this, arguments);
if (!substituted.isEmpty())
result.append(substituted);
}
return result;
}
bool DesktopEntry::launch(const QStringList &arguments) const
{
return ApplicationLauncher::startDetached(commandForArguments(arguments),
m_workingDirectory);
}
QStringList DesktopEntry::parseExec(const QString &exec)
{
// This intentionally returns a command without field-code arguments. The
// complete expansion is available through commandForArguments().
DesktopEntry entry(QStringLiteral("temporary"));
entry.m_exec = exec;
return entry.commandForArguments(QStringList());
}
bool DesktopEntry::parse(const QString &id, const QString &path,
const QByteArray &contents, DesktopEntryData *result)
{
if (!result)
return false;
DesktopEntryData data;
data.id = id;
data.path = path;
QString group;
QMap<QString, QString> values;
QMap<QString, QString> localized;
QMap<QString, QMap<QString, QString>> actionValues;
auto finishGroup = [&]() {
if (group == QStringLiteral("Desktop Entry")) {
data.type = values.value(QStringLiteral("Type"));
data.name = localizedValue(values, localized, QStringLiteral("Name"));
data.genericName = localizedValue(values, localized, QStringLiteral("GenericName"));
data.comment = localizedValue(values, localized, QStringLiteral("Comment"));
data.icon = values.value(QStringLiteral("Icon"));
data.exec = values.value(QStringLiteral("Exec"));
data.workingDirectory = values.value(QStringLiteral("Path"));
data.startupWMClass = values.value(QStringLiteral("StartupWMClass"));
data.categories = splitList(values.value(QStringLiteral("Categories")));
data.keywords = splitList(values.value(QStringLiteral("Keywords")));
data.mimeTypes = splitList(values.value(QStringLiteral("MimeType")));
data.onlyShowIn = splitList(values.value(QStringLiteral("OnlyShowIn")));
data.notShowIn = splitList(values.value(QStringLiteral("NotShowIn")));
data.terminal = parseBool(values.value(QStringLiteral("Terminal")));
data.noDisplay = parseBool(values.value(QStringLiteral("NoDisplay")));
data.hidden = parseBool(values.value(QStringLiteral("Hidden")));
} else if (group.startsWith(QStringLiteral("Desktop Action "))) {
const QString actionId = group.mid(QStringLiteral("Desktop Action ").size());
const auto action = actionValues.value(actionId);
if (!action.isEmpty()) {
DesktopActionData item;
item.id = actionId;
item.name = action.value(QStringLiteral("Name"));
item.icon = action.value(QStringLiteral("Icon"));
item.exec = action.value(QStringLiteral("Exec"));
data.actions.append(item);
}
}
values.clear();
localized.clear();
};
const QStringList lines = QString::fromUtf8(contents).split(QLatin1Char('\n'));
for (QString line : lines) {
line.remove(QLatin1Char('\r'));
line = line.trimmed();
if (line.isEmpty() || line.startsWith(QLatin1Char('#')))
continue;
if (line.startsWith(QLatin1Char('[')) && line.endsWith(QLatin1Char(']'))) {
finishGroup();
group = line.mid(1, line.size() - 2);
continue;
}
const int equals = line.indexOf(QLatin1Char('='));
if (equals <= 0)
continue;
QString key = line.left(equals).trimmed();
const QString value = unescapeValue(line.mid(equals + 1).trimmed());
if (group.startsWith(QStringLiteral("Desktop Action "))) {
actionValues[group.mid(QStringLiteral("Desktop Action ").size())][key] = value;
} else if (group == QStringLiteral("Desktop Entry")) {
const int localeStart = key.indexOf(QLatin1Char('['));
if (localeStart > 0 && key.endsWith(QLatin1Char(']'))) {
localized.insert(key, value);
key = key.left(localeStart);
} else {
values.insert(key, value);
}
}
}
finishGroup();
if (data.type != QStringLiteral("Application") || data.name.isEmpty())
return false;
*result = data;
return true;
}
void DesktopEntry::update(const DesktopEntryData &data)
{
if (m_path != data.path) {
m_path = data.path;
emit pathChanged();
}
if (m_name != data.name) {
m_name = data.name;
emit nameChanged();
}
if (m_genericName != data.genericName) {
m_genericName = data.genericName;
emit genericNameChanged();
}
if (m_comment != data.comment) {
m_comment = data.comment;
emit commentChanged();
}
if (m_icon != data.icon) {
m_icon = data.icon;
emit iconChanged();
}
const bool hasExecChanged = m_exec != data.exec;
if (hasExecChanged) {
m_exec = data.exec;
emit execChanged();
}
if (m_workingDirectory != data.workingDirectory) {
m_workingDirectory = data.workingDirectory;
emit workingDirectoryChanged();
}
if (m_startupWMClass != data.startupWMClass) {
m_startupWMClass = data.startupWMClass;
emit startupWMClassChanged();
}
if (m_categories != data.categories) {
m_categories = data.categories;
emit categoriesChanged();
}
if (m_keywords != data.keywords) {
m_keywords = data.keywords;
emit keywordsChanged();
}
if (m_mimeTypes != data.mimeTypes) {
m_mimeTypes = data.mimeTypes;
emit mimeTypesChanged();
}
if (m_terminal != data.terminal) {
m_terminal = data.terminal;
emit terminalChanged();
}
if (m_noDisplay != data.noDisplay) {
m_noDisplay = data.noDisplay;
emit noDisplayChanged();
}
if (m_hidden != data.hidden) {
m_hidden = data.hidden;
emit hiddenChanged();
}
const QStringList command = commandForArguments(QStringList());
if (m_command != command) {
m_command = command;
emit commandChanged();
}
emit changed();
}

@ -0,0 +1,126 @@
#pragma once
#include <QMetaType>
#include <QObject>
#include <QStringList>
struct DesktopActionData
{
QString id;
QString name;
QString icon;
QString exec;
};
struct DesktopEntryData
{
QString id;
QString path;
QString type;
QString name;
QString genericName;
QString comment;
QString icon;
QString exec;
QString workingDirectory;
QString startupWMClass;
QStringList categories;
QStringList keywords;
QStringList mimeTypes;
QStringList onlyShowIn;
QStringList notShowIn;
QList<DesktopActionData> actions;
bool terminal = false;
bool noDisplay = false;
bool hidden = false;
};
Q_DECLARE_METATYPE(DesktopEntryData)
Q_DECLARE_METATYPE(QList<DesktopEntryData>)
class DesktopEntry : public QObject
{
Q_OBJECT
Q_PROPERTY(QString id READ id CONSTANT)
Q_PROPERTY(QString path READ path NOTIFY pathChanged)
Q_PROPERTY(QString name READ name NOTIFY nameChanged)
Q_PROPERTY(QString genericName READ genericName NOTIFY genericNameChanged)
Q_PROPERTY(QString comment READ comment NOTIFY commentChanged)
Q_PROPERTY(QString icon READ icon NOTIFY iconChanged)
Q_PROPERTY(QString exec READ exec NOTIFY execChanged)
Q_PROPERTY(QStringList command READ command NOTIFY commandChanged)
Q_PROPERTY(QString workingDirectory READ workingDirectory NOTIFY workingDirectoryChanged)
Q_PROPERTY(QString startupWMClass READ startupWMClass NOTIFY startupWMClassChanged)
Q_PROPERTY(QStringList categories READ categories NOTIFY categoriesChanged)
Q_PROPERTY(QStringList keywords READ keywords NOTIFY keywordsChanged)
Q_PROPERTY(QStringList mimeTypes READ mimeTypes NOTIFY mimeTypesChanged)
Q_PROPERTY(bool terminal READ terminal NOTIFY terminalChanged)
Q_PROPERTY(bool noDisplay READ noDisplay NOTIFY noDisplayChanged)
Q_PROPERTY(bool hidden READ hidden NOTIFY hiddenChanged)
public:
explicit DesktopEntry(const QString &id, QObject *parent = nullptr);
QString id() const;
QString path() const;
QString name() const;
QString genericName() const;
QString comment() const;
QString icon() const;
QString exec() const;
QStringList command() const;
QString workingDirectory() const;
QString startupWMClass() const;
QStringList categories() const;
QStringList keywords() const;
QStringList mimeTypes() const;
bool terminal() const;
bool noDisplay() const;
bool hidden() const;
Q_INVOKABLE bool launch(const QStringList &arguments = QStringList()) const;
QStringList commandForArguments(const QStringList &arguments) const;
static bool parse(const QString &id, const QString &path,
const QByteArray &contents, DesktopEntryData *result);
static QStringList parseExec(const QString &exec);
void update(const DesktopEntryData &data);
signals:
void pathChanged();
void nameChanged();
void genericNameChanged();
void commentChanged();
void iconChanged();
void execChanged();
void commandChanged();
void workingDirectoryChanged();
void startupWMClassChanged();
void categoriesChanged();
void keywordsChanged();
void mimeTypesChanged();
void terminalChanged();
void noDisplayChanged();
void hiddenChanged();
void changed();
private:
QString m_id;
QString m_path;
QString m_name;
QString m_genericName;
QString m_comment;
QString m_icon;
QString m_exec;
QStringList m_command;
QString m_workingDirectory;
QString m_startupWMClass;
QStringList m_categories;
QStringList m_keywords;
QStringList m_mimeTypes;
bool m_terminal = false;
bool m_noDisplay = false;
bool m_hidden = false;
};

@ -12,13 +12,30 @@
#include <QSettings>
#include <QTimer>
static bool isDynamicDisplay(const KScreen::OutputPtr &output)
{
if (!output) {
return false;
}
const QString identity = QStringLiteral("%1 %2 %3")
.arg(output->name(), output->vendor(), output->model());
return identity.contains(QStringLiteral("virtual"), Qt::CaseInsensitive)
|| identity.contains(QStringLiteral("qemu"), Qt::CaseInsensitive)
|| identity.contains(QStringLiteral("spice"), Qt::CaseInsensitive)
|| identity.contains(QStringLiteral("virtio"), Qt::CaseInsensitive);
}
static QString displayOutputKey(const KScreen::OutputPtr &output)
{
if (!output) {
return QString();
}
QString key = output->hashMd5();
// Virtual outputs can change their EDID/mode list whenever the host
// resizes the guest window. Their hash is therefore not a stable
// identity; use the compositor's output name instead.
QString key = isDynamicDisplay(output) ? output->name() : output->hashMd5();
if (key.isEmpty()) {
key = output->name();
}

Loading…
Cancel
Save