feat(applications): add shared desktop entry registry
parent
49123763a6
commit
795fa53c23
@ -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 = [¤tDesktops](const QStringList &desktops) {
|
||||
for (const QString &desktop : desktops) {
|
||||
for (const QString ¤t : 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;
|
||||
};
|
||||
Loading…
Reference in New Issue