You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
434 lines
14 KiB
C++
434 lines
14 KiB
C++
#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();
|
|
}
|
|
|
|
} // 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 QStringList currentDesktops = DesktopEntry::currentDesktops();
|
|
|
|
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 (entry->shouldShow(currentDesktops))
|
|
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"));
|
|
}
|