mirror of
https://git.outfoxxed.me/quickshell/quickshell.git
synced 2025-11-04 19:04:56 +11:00
core/desktopentry: watch for changes and rescan entries
This commit is contained in:
parent
49646e4407
commit
59f5744f30
6 changed files with 521 additions and 153 deletions
|
|
@ -23,6 +23,7 @@ qt_add_library(quickshell-core STATIC
|
||||||
model.cpp
|
model.cpp
|
||||||
elapsedtimer.cpp
|
elapsedtimer.cpp
|
||||||
desktopentry.cpp
|
desktopentry.cpp
|
||||||
|
desktopentrymonitor.cpp
|
||||||
objectrepeater.cpp
|
objectrepeater.cpp
|
||||||
platformmenu.cpp
|
platformmenu.cpp
|
||||||
qsmenu.cpp
|
qsmenu.cpp
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,27 @@
|
||||||
#include "desktopentry.hpp"
|
#include "desktopentry.hpp"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
#include <qcontainerfwd.h>
|
#include <qcontainerfwd.h>
|
||||||
#include <qdebug.h>
|
#include <qdebug.h>
|
||||||
#include <qdir.h>
|
#include <qdir.h>
|
||||||
|
#include <qfile.h>
|
||||||
#include <qfileinfo.h>
|
#include <qfileinfo.h>
|
||||||
#include <qhash.h>
|
|
||||||
#include <qlist.h>
|
|
||||||
#include <qlogging.h>
|
#include <qlogging.h>
|
||||||
#include <qloggingcategory.h>
|
#include <qloggingcategory.h>
|
||||||
#include <qnamespace.h>
|
#include <qnamespace.h>
|
||||||
#include <qobject.h>
|
#include <qobject.h>
|
||||||
|
#include <qobjectdefs.h>
|
||||||
#include <qpair.h>
|
#include <qpair.h>
|
||||||
#include <qstringview.h>
|
#include <qproperty.h>
|
||||||
|
#include <qscopeguard.h>
|
||||||
#include <qtenvironmentvariables.h>
|
#include <qtenvironmentvariables.h>
|
||||||
|
#include <qthreadpool.h>
|
||||||
|
#include <qtmetamacros.h>
|
||||||
#include <ranges>
|
#include <ranges>
|
||||||
|
|
||||||
#include "../io/processcore.hpp"
|
#include "../io/processcore.hpp"
|
||||||
|
#include "desktopentrymonitor.hpp"
|
||||||
#include "logcat.hpp"
|
#include "logcat.hpp"
|
||||||
#include "model.hpp"
|
#include "model.hpp"
|
||||||
#include "qmlglobal.hpp"
|
#include "qmlglobal.hpp"
|
||||||
|
|
@ -87,57 +92,60 @@ struct Locale {
|
||||||
QDebug operator<<(QDebug debug, const Locale& locale) {
|
QDebug operator<<(QDebug debug, const Locale& locale) {
|
||||||
auto saver = QDebugStateSaver(debug);
|
auto saver = QDebugStateSaver(debug);
|
||||||
debug.nospace() << "Locale(language=" << locale.language << ", territory=" << locale.territory
|
debug.nospace() << "Locale(language=" << locale.language << ", territory=" << locale.territory
|
||||||
<< ", modifier" << locale.modifier << ')';
|
<< ", modifier=" << locale.modifier << ')';
|
||||||
|
|
||||||
return debug;
|
return debug;
|
||||||
}
|
}
|
||||||
|
|
||||||
void DesktopEntry::parseEntry(const QString& text) {
|
ParsedDesktopEntryData DesktopEntry::parseText(const QString& id, const QString& text) {
|
||||||
|
ParsedDesktopEntryData data;
|
||||||
|
data.id = id;
|
||||||
const auto& system = Locale::system();
|
const auto& system = Locale::system();
|
||||||
|
|
||||||
auto groupName = QString();
|
auto groupName = QString();
|
||||||
auto entries = QHash<QString, QPair<Locale, QString>>();
|
auto entries = QHash<QString, QPair<Locale, QString>>();
|
||||||
|
|
||||||
auto finishCategory = [this, &groupName, &entries]() {
|
auto finishCategory = [&data, &groupName, &entries]() {
|
||||||
if (groupName == "Desktop Entry") {
|
if (groupName == "Desktop Entry") {
|
||||||
if (entries["Type"].second != "Application") return;
|
if (entries.value("Type").second != "Application") return;
|
||||||
if (entries.contains("Hidden") && entries["Hidden"].second == "true") return;
|
if (entries.value("Hidden").second == "true") return;
|
||||||
|
|
||||||
for (const auto& [key, pair]: entries.asKeyValueRange()) {
|
for (const auto& [key, pair]: entries.asKeyValueRange()) {
|
||||||
auto& [_, value] = pair;
|
auto& [_, value] = pair;
|
||||||
this->mEntries.insert(key, value);
|
data.entries.insert(key, value);
|
||||||
|
|
||||||
if (key == "Name") this->mName = value;
|
if (key == "Name") data.name = value;
|
||||||
else if (key == "GenericName") this->mGenericName = value;
|
else if (key == "GenericName") data.genericName = value;
|
||||||
else if (key == "StartupWMClass") this->mStartupClass = value;
|
else if (key == "StartupWMClass") data.startupClass = value;
|
||||||
else if (key == "NoDisplay") this->mNoDisplay = value == "true";
|
else if (key == "NoDisplay") data.noDisplay = value == "true";
|
||||||
else if (key == "Comment") this->mComment = value;
|
else if (key == "Comment") data.comment = value;
|
||||||
else if (key == "Icon") this->mIcon = value;
|
else if (key == "Icon") data.icon = value;
|
||||||
else if (key == "Exec") {
|
else if (key == "Exec") {
|
||||||
this->mExecString = value;
|
data.execString = value;
|
||||||
this->mCommand = DesktopEntry::parseExecString(value);
|
data.command = DesktopEntry::parseExecString(value);
|
||||||
} else if (key == "Path") this->mWorkingDirectory = value;
|
} else if (key == "Path") data.workingDirectory = value;
|
||||||
else if (key == "Terminal") this->mTerminal = value == "true";
|
else if (key == "Terminal") data.terminal = value == "true";
|
||||||
else if (key == "Categories") this->mCategories = value.split(u';', Qt::SkipEmptyParts);
|
else if (key == "Categories") data.categories = value.split(u';', Qt::SkipEmptyParts);
|
||||||
else if (key == "Keywords") this->mKeywords = value.split(u';', Qt::SkipEmptyParts);
|
else if (key == "Keywords") data.keywords = value.split(u';', Qt::SkipEmptyParts);
|
||||||
}
|
}
|
||||||
} else if (groupName.startsWith("Desktop Action ")) {
|
} else if (groupName.startsWith("Desktop Action ")) {
|
||||||
auto actionName = groupName.sliced(16);
|
auto actionName = groupName.sliced(16);
|
||||||
auto* action = new DesktopAction(actionName, this);
|
DesktopActionData action;
|
||||||
|
action.id = actionName;
|
||||||
|
|
||||||
for (const auto& [key, pair]: entries.asKeyValueRange()) {
|
for (const auto& [key, pair]: entries.asKeyValueRange()) {
|
||||||
const auto& [_, value] = pair;
|
const auto& [_, value] = pair;
|
||||||
action->mEntries.insert(key, value);
|
action.entries.insert(key, value);
|
||||||
|
|
||||||
if (key == "Name") action->mName = value;
|
if (key == "Name") action.name = value;
|
||||||
else if (key == "Icon") action->mIcon = value;
|
else if (key == "Icon") action.icon = value;
|
||||||
else if (key == "Exec") {
|
else if (key == "Exec") {
|
||||||
action->mExecString = value;
|
action.execString = value;
|
||||||
action->mCommand = DesktopEntry::parseExecString(value);
|
action.command = DesktopEntry::parseExecString(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this->mActions.insert(actionName, action);
|
data.actions.insert(actionName, action);
|
||||||
}
|
}
|
||||||
|
|
||||||
entries.clear();
|
entries.clear();
|
||||||
|
|
@ -183,14 +191,62 @@ void DesktopEntry::parseEntry(const QString& text) {
|
||||||
}
|
}
|
||||||
|
|
||||||
finishCategory();
|
finishCategory();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopEntry::updateState(const ParsedDesktopEntryData& newState) {
|
||||||
|
Qt::beginPropertyUpdateGroup();
|
||||||
|
this->bName = newState.name;
|
||||||
|
this->bGenericName = newState.genericName;
|
||||||
|
this->bStartupClass = newState.startupClass;
|
||||||
|
this->bNoDisplay = newState.noDisplay;
|
||||||
|
this->bComment = newState.comment;
|
||||||
|
this->bIcon = newState.icon;
|
||||||
|
this->bExecString = newState.execString;
|
||||||
|
this->bCommand = newState.command;
|
||||||
|
this->bWorkingDirectory = newState.workingDirectory;
|
||||||
|
this->bRunInTerminal = newState.terminal;
|
||||||
|
this->bCategories = newState.categories;
|
||||||
|
this->bKeywords = newState.keywords;
|
||||||
|
Qt::endPropertyUpdateGroup();
|
||||||
|
|
||||||
|
this->state = newState;
|
||||||
|
this->updateActions(newState.actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopEntry::updateActions(const QHash<QString, DesktopActionData>& newActions) {
|
||||||
|
auto old = this->mActions;
|
||||||
|
|
||||||
|
for (const auto& [key, d]: newActions.asKeyValueRange()) {
|
||||||
|
DesktopAction* act = nullptr;
|
||||||
|
if (auto found = old.find(key); found != old.end()) {
|
||||||
|
act = found.value();
|
||||||
|
old.erase(found);
|
||||||
|
} else {
|
||||||
|
act = new DesktopAction(d.id, this);
|
||||||
|
this->mActions.insert(key, act);
|
||||||
|
}
|
||||||
|
|
||||||
|
Qt::beginPropertyUpdateGroup();
|
||||||
|
act->bName = d.name;
|
||||||
|
act->bIcon = d.icon;
|
||||||
|
act->bExecString = d.execString;
|
||||||
|
act->bCommand = d.command;
|
||||||
|
Qt::endPropertyUpdateGroup();
|
||||||
|
|
||||||
|
act->mEntries = d.entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto* leftover: old) {
|
||||||
|
leftover->deleteLater();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void DesktopEntry::execute() const {
|
void DesktopEntry::execute() const {
|
||||||
DesktopEntry::doExec(this->mCommand, this->mWorkingDirectory);
|
DesktopEntry::doExec(this->bCommand.value(), this->bWorkingDirectory.value());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DesktopEntry::isValid() const { return !this->mName.isEmpty(); }
|
bool DesktopEntry::isValid() const { return !this->bName.value().isEmpty(); }
|
||||||
bool DesktopEntry::noDisplay() const { return this->mNoDisplay; }
|
|
||||||
|
|
||||||
QVector<DesktopAction*> DesktopEntry::actions() const { return this->mActions.values(); }
|
QVector<DesktopAction*> DesktopEntry::actions() const { return this->mActions.values(); }
|
||||||
|
|
||||||
|
|
@ -266,59 +322,44 @@ void DesktopEntry::doExec(const QList<QString>& execString, const QString& worki
|
||||||
}
|
}
|
||||||
|
|
||||||
void DesktopAction::execute() const {
|
void DesktopAction::execute() const {
|
||||||
DesktopEntry::doExec(this->mCommand, this->entry->mWorkingDirectory);
|
DesktopEntry::doExec(this->bCommand.value(), this->entry->bWorkingDirectory.value());
|
||||||
}
|
}
|
||||||
|
|
||||||
DesktopEntryManager::DesktopEntryManager() {
|
DesktopEntryScanner::DesktopEntryScanner(DesktopEntryManager* manager): manager(manager) {
|
||||||
this->scanDesktopEntries();
|
this->setAutoDelete(true);
|
||||||
this->populateApplications();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DesktopEntryManager::scanDesktopEntries() {
|
void DesktopEntryScanner::run() {
|
||||||
QList<QString> dataPaths;
|
const auto& desktopPaths = DesktopEntryManager::desktopPaths();
|
||||||
|
auto scanResults = QList<ParsedDesktopEntryData>();
|
||||||
|
|
||||||
if (qEnvironmentVariableIsSet("XDG_DATA_HOME")) {
|
for (const auto& path: desktopPaths | std::views::reverse) {
|
||||||
dataPaths.push_back(qEnvironmentVariable("XDG_DATA_HOME"));
|
auto file = QFileInfo(path);
|
||||||
} else if (qEnvironmentVariableIsSet("HOME")) {
|
if (!file.isDir()) continue;
|
||||||
dataPaths.push_back(qEnvironmentVariable("HOME") + "/.local/share");
|
|
||||||
|
this->scanDirectory(QDir(path), QString(), scanResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (qEnvironmentVariableIsSet("XDG_DATA_DIRS")) {
|
QMetaObject::invokeMethod(
|
||||||
auto var = qEnvironmentVariable("XDG_DATA_DIRS");
|
this->manager,
|
||||||
dataPaths += var.split(u':', Qt::SkipEmptyParts);
|
"onScanCompleted",
|
||||||
} else {
|
Qt::QueuedConnection,
|
||||||
dataPaths.push_back("/usr/local/share");
|
Q_ARG(QList<ParsedDesktopEntryData>, scanResults)
|
||||||
dataPaths.push_back("/usr/share");
|
);
|
||||||
}
|
|
||||||
|
|
||||||
qCDebug(logDesktopEntry) << "Creating desktop entry scanners";
|
|
||||||
|
|
||||||
for (auto& path: std::ranges::reverse_view(dataPaths)) {
|
|
||||||
auto p = QDir(path).filePath("applications");
|
|
||||||
auto file = QFileInfo(p);
|
|
||||||
|
|
||||||
if (!file.isDir()) {
|
|
||||||
qCDebug(logDesktopEntry) << "Not scanning path" << p << "as it is not a directory";
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
qCDebug(logDesktopEntry) << "Scanning path" << p;
|
|
||||||
this->scanPath(p);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DesktopEntryManager::populateApplications() {
|
void DesktopEntryScanner::scanDirectory(
|
||||||
for (auto& entry: this->desktopEntries.values()) {
|
const QDir& dir,
|
||||||
if (!entry->noDisplay()) this->mApplications.insertObject(entry);
|
const QString& idPrefix,
|
||||||
}
|
QList<ParsedDesktopEntryData>& entries
|
||||||
}
|
) {
|
||||||
|
auto dirEntries = dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
|
||||||
|
|
||||||
void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) {
|
for (auto& entry: dirEntries) {
|
||||||
auto entries = dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
|
if (entry.isDir()) {
|
||||||
|
auto subdirPrefix = idPrefix.isEmpty() ? entry.fileName() : idPrefix + '-' + entry.fileName();
|
||||||
for (auto& entry: entries) {
|
this->scanDirectory(QDir(entry.absoluteFilePath()), subdirPrefix, entries);
|
||||||
if (entry.isDir()) this->scanPath(entry.absoluteFilePath(), prefix + dir.dirName() + "-");
|
} else if (entry.isFile()) {
|
||||||
else if (entry.isFile()) {
|
|
||||||
auto path = entry.filePath();
|
auto path = entry.filePath();
|
||||||
if (!path.endsWith(".desktop")) {
|
if (!path.endsWith(".desktop")) {
|
||||||
qCDebug(logDesktopEntry) << "Skipping file" << path << "as it has no .desktop extension";
|
qCDebug(logDesktopEntry) << "Skipping file" << path << "as it has no .desktop extension";
|
||||||
|
|
@ -331,46 +372,42 @@ void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto id = prefix + entry.fileName().sliced(0, entry.fileName().length() - 8);
|
auto basename = QFileInfo(entry.fileName()).completeBaseName();
|
||||||
auto lowerId = id.toLower();
|
auto id = idPrefix.isEmpty() ? basename : idPrefix + '-' + basename;
|
||||||
|
auto content = QString::fromUtf8(file.readAll());
|
||||||
|
|
||||||
auto text = QString::fromUtf8(file.readAll());
|
auto data = DesktopEntry::parseText(id, content);
|
||||||
auto* dentry = new DesktopEntry(id, this);
|
entries.append(std::move(data));
|
||||||
dentry->parseEntry(text);
|
|
||||||
|
|
||||||
if (!dentry->isValid()) {
|
|
||||||
qCDebug(logDesktopEntry) << "Skipping desktop entry" << path;
|
|
||||||
delete dentry;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
qCDebug(logDesktopEntry) << "Found desktop entry" << id << "at" << path;
|
|
||||||
|
|
||||||
auto conflictingId = this->desktopEntries.contains(id);
|
|
||||||
|
|
||||||
if (conflictingId) {
|
|
||||||
qCDebug(logDesktopEntry) << "Replacing old entry for" << id;
|
|
||||||
delete this->desktopEntries.value(id);
|
|
||||||
this->desktopEntries.remove(id);
|
|
||||||
this->lowercaseDesktopEntries.remove(lowerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
this->desktopEntries.insert(id, dentry);
|
|
||||||
|
|
||||||
if (this->lowercaseDesktopEntries.contains(lowerId)) {
|
|
||||||
qCInfo(logDesktopEntry).nospace()
|
|
||||||
<< "Multiple desktop entries have the same lowercased id " << lowerId
|
|
||||||
<< ". This can cause ambiguity when byId requests are not made with the correct case "
|
|
||||||
"already.";
|
|
||||||
|
|
||||||
this->lowercaseDesktopEntries.remove(lowerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
this->lowercaseDesktopEntries.insert(lowerId, dentry);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DesktopEntryManager::DesktopEntryManager(): monitor(new DesktopEntryMonitor(this)) {
|
||||||
|
QObject::connect(
|
||||||
|
this->monitor,
|
||||||
|
&DesktopEntryMonitor::desktopEntriesChanged,
|
||||||
|
this,
|
||||||
|
&DesktopEntryManager::handleFileChanges
|
||||||
|
);
|
||||||
|
|
||||||
|
DesktopEntryScanner(this).run();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopEntryManager::scanDesktopEntries() {
|
||||||
|
qCDebug(logDesktopEntry) << "Starting desktop entry scan";
|
||||||
|
|
||||||
|
if (this->scanInProgress) {
|
||||||
|
qCDebug(logDesktopEntry) << "Scan already in progress, queuing another scan";
|
||||||
|
this->scanQueued = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->scanInProgress = true;
|
||||||
|
this->scanQueued = false;
|
||||||
|
auto* scanner = new DesktopEntryScanner(this);
|
||||||
|
QThreadPool::globalInstance()->start(scanner);
|
||||||
|
}
|
||||||
|
|
||||||
DesktopEntryManager* DesktopEntryManager::instance() {
|
DesktopEntryManager* DesktopEntryManager::instance() {
|
||||||
static auto* instance = new DesktopEntryManager(); // NOLINT
|
static auto* instance = new DesktopEntryManager(); // NOLINT
|
||||||
return instance;
|
return instance;
|
||||||
|
|
@ -391,14 +428,14 @@ DesktopEntry* DesktopEntryManager::heuristicLookup(const QString& name) {
|
||||||
|
|
||||||
auto list = this->desktopEntries.values();
|
auto list = this->desktopEntries.values();
|
||||||
|
|
||||||
auto iter = std::ranges::find_if(list, [&](const DesktopEntry* entry) {
|
auto iter = std::ranges::find_if(list, [&](DesktopEntry* entry) {
|
||||||
return name == entry->mStartupClass;
|
return name == entry->bStartupClass.value();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (iter != list.end()) return *iter;
|
if (iter != list.end()) return *iter;
|
||||||
|
|
||||||
iter = std::ranges::find_if(list, [&](const DesktopEntry* entry) {
|
iter = std::ranges::find_if(list, [&](DesktopEntry* entry) {
|
||||||
return name.toLower() == entry->mStartupClass.toLower();
|
return name.toLower() == entry->bStartupClass.value().toLower();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (iter != list.end()) return *iter;
|
if (iter != list.end()) return *iter;
|
||||||
|
|
@ -407,7 +444,123 @@ DesktopEntry* DesktopEntryManager::heuristicLookup(const QString& name) {
|
||||||
|
|
||||||
ObjectModel<DesktopEntry>* DesktopEntryManager::applications() { return &this->mApplications; }
|
ObjectModel<DesktopEntry>* DesktopEntryManager::applications() { return &this->mApplications; }
|
||||||
|
|
||||||
DesktopEntries::DesktopEntries() { DesktopEntryManager::instance(); }
|
void DesktopEntryManager::handleFileChanges() {
|
||||||
|
qCDebug(logDesktopEntry) << "Directory change detected, performing full rescan";
|
||||||
|
|
||||||
|
if (this->scanInProgress) {
|
||||||
|
qCDebug(logDesktopEntry) << "Scan already in progress, queuing another scan";
|
||||||
|
this->scanQueued = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->scanInProgress = true;
|
||||||
|
this->scanQueued = false;
|
||||||
|
auto* scanner = new DesktopEntryScanner(this);
|
||||||
|
QThreadPool::globalInstance()->start(scanner);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QStringList& DesktopEntryManager::desktopPaths() {
|
||||||
|
static const auto paths = []() {
|
||||||
|
auto dataPaths = QStringList();
|
||||||
|
|
||||||
|
auto dataHome = qEnvironmentVariable("XDG_DATA_HOME");
|
||||||
|
if (dataHome.isEmpty() && qEnvironmentVariableIsSet("HOME"))
|
||||||
|
dataHome = qEnvironmentVariable("HOME") + "/.local/share";
|
||||||
|
if (!dataHome.isEmpty()) dataPaths.append(dataHome + "/applications");
|
||||||
|
|
||||||
|
auto dataDirs = qEnvironmentVariable("XDG_DATA_DIRS");
|
||||||
|
if (dataDirs.isEmpty()) dataDirs = "/usr/local/share:/usr/share";
|
||||||
|
|
||||||
|
for (const auto& dir: dataDirs.split(':', Qt::SkipEmptyParts)) {
|
||||||
|
dataPaths.append(dir + "/applications");
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataPaths;
|
||||||
|
}();
|
||||||
|
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopEntryManager::onScanCompleted(const QList<ParsedDesktopEntryData>& scanResults) {
|
||||||
|
auto guard = qScopeGuard([this] {
|
||||||
|
this->scanInProgress = false;
|
||||||
|
if (this->scanQueued) {
|
||||||
|
this->scanQueued = false;
|
||||||
|
this->scanDesktopEntries();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
auto oldEntries = this->desktopEntries;
|
||||||
|
auto newEntries = QHash<QString, DesktopEntry*>();
|
||||||
|
auto newLowercaseEntries = QHash<QString, DesktopEntry*>();
|
||||||
|
|
||||||
|
for (const auto& data: scanResults) {
|
||||||
|
DesktopEntry* dentry = nullptr;
|
||||||
|
|
||||||
|
if (auto it = oldEntries.find(data.id); it != oldEntries.end()) {
|
||||||
|
dentry = it.value();
|
||||||
|
oldEntries.erase(it);
|
||||||
|
dentry->updateState(data);
|
||||||
|
} else {
|
||||||
|
dentry = new DesktopEntry(data.id, this);
|
||||||
|
dentry->updateState(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dentry->isValid()) {
|
||||||
|
qCDebug(logDesktopEntry) << "Skipping desktop entry" << data.id;
|
||||||
|
if (!oldEntries.contains(data.id)) {
|
||||||
|
dentry->deleteLater();
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
qCDebug(logDesktopEntry) << "Found desktop entry" << data.id;
|
||||||
|
|
||||||
|
auto lowerId = data.id.toLower();
|
||||||
|
auto conflictingId = newEntries.contains(data.id);
|
||||||
|
|
||||||
|
if (conflictingId) {
|
||||||
|
qCDebug(logDesktopEntry) << "Replacing old entry for" << data.id;
|
||||||
|
if (auto* victim = newEntries.take(data.id)) victim->deleteLater();
|
||||||
|
newLowercaseEntries.remove(lowerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
newEntries.insert(data.id, dentry);
|
||||||
|
|
||||||
|
if (newLowercaseEntries.contains(lowerId)) {
|
||||||
|
qCInfo(logDesktopEntry).nospace()
|
||||||
|
<< "Multiple desktop entries have the same lowercased id " << lowerId
|
||||||
|
<< ". This can cause ambiguity when byId requests are not made with the correct case "
|
||||||
|
"already.";
|
||||||
|
|
||||||
|
newLowercaseEntries.remove(lowerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
newLowercaseEntries.insert(lowerId, dentry);
|
||||||
|
}
|
||||||
|
|
||||||
|
this->desktopEntries = newEntries;
|
||||||
|
this->lowercaseDesktopEntries = newLowercaseEntries;
|
||||||
|
|
||||||
|
auto newApplications = QVector<DesktopEntry*>();
|
||||||
|
for (auto* entry: this->desktopEntries.values())
|
||||||
|
if (!entry->bNoDisplay) newApplications.append(entry);
|
||||||
|
|
||||||
|
this->mApplications.diffUpdate(newApplications);
|
||||||
|
|
||||||
|
emit this->applicationsChanged();
|
||||||
|
|
||||||
|
for (auto* e: oldEntries) e->deleteLater();
|
||||||
|
}
|
||||||
|
|
||||||
|
DesktopEntries::DesktopEntries() {
|
||||||
|
QObject::connect(
|
||||||
|
DesktopEntryManager::instance(),
|
||||||
|
&DesktopEntryManager::applicationsChanged,
|
||||||
|
this,
|
||||||
|
&DesktopEntries::applicationsChanged
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
DesktopEntry* DesktopEntries::byId(const QString& id) {
|
DesktopEntry* DesktopEntries::byId(const QString& id) {
|
||||||
return DesktopEntryManager::instance()->byId(id);
|
return DesktopEntryManager::instance()->byId(id);
|
||||||
|
|
|
||||||
|
|
@ -6,35 +6,67 @@
|
||||||
#include <qdir.h>
|
#include <qdir.h>
|
||||||
#include <qhash.h>
|
#include <qhash.h>
|
||||||
#include <qobject.h>
|
#include <qobject.h>
|
||||||
|
#include <qproperty.h>
|
||||||
#include <qqmlintegration.h>
|
#include <qqmlintegration.h>
|
||||||
|
#include <qrunnable.h>
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
|
#include "desktopentrymonitor.hpp"
|
||||||
#include "doc.hpp"
|
#include "doc.hpp"
|
||||||
#include "model.hpp"
|
#include "model.hpp"
|
||||||
|
|
||||||
class DesktopAction;
|
class DesktopAction;
|
||||||
|
class DesktopEntryMonitor;
|
||||||
|
|
||||||
|
struct DesktopActionData {
|
||||||
|
QString id;
|
||||||
|
QString name;
|
||||||
|
QString icon;
|
||||||
|
QString execString;
|
||||||
|
QVector<QString> command;
|
||||||
|
QHash<QString, QString> entries;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ParsedDesktopEntryData {
|
||||||
|
QString id;
|
||||||
|
QString name;
|
||||||
|
QString genericName;
|
||||||
|
QString startupClass;
|
||||||
|
bool noDisplay = false;
|
||||||
|
QString comment;
|
||||||
|
QString icon;
|
||||||
|
QString execString;
|
||||||
|
QVector<QString> command;
|
||||||
|
QString workingDirectory;
|
||||||
|
bool terminal = false;
|
||||||
|
QVector<QString> categories;
|
||||||
|
QVector<QString> keywords;
|
||||||
|
QHash<QString, QString> entries;
|
||||||
|
QHash<QString, DesktopActionData> actions;
|
||||||
|
};
|
||||||
|
|
||||||
/// A desktop entry. See @@DesktopEntries for details.
|
/// A desktop entry. See @@DesktopEntries for details.
|
||||||
class DesktopEntry: public QObject {
|
class DesktopEntry: public QObject {
|
||||||
Q_OBJECT;
|
Q_OBJECT;
|
||||||
Q_PROPERTY(QString id MEMBER mId CONSTANT);
|
Q_PROPERTY(QString id MEMBER mId CONSTANT);
|
||||||
/// Name of the specific application, such as "Firefox".
|
/// Name of the specific application, such as "Firefox".
|
||||||
Q_PROPERTY(QString name MEMBER mName CONSTANT);
|
// clang-format off
|
||||||
|
Q_PROPERTY(QString name READ default WRITE default NOTIFY nameChanged BINDABLE bindableName);
|
||||||
/// Short description of the application, such as "Web Browser". May be empty.
|
/// Short description of the application, such as "Web Browser". May be empty.
|
||||||
Q_PROPERTY(QString genericName MEMBER mGenericName CONSTANT);
|
Q_PROPERTY(QString genericName READ default WRITE default NOTIFY genericNameChanged BINDABLE bindableGenericName);
|
||||||
/// Initial class or app id the app intends to use. May be useful for matching running apps
|
/// Initial class or app id the app intends to use. May be useful for matching running apps
|
||||||
/// to desktop entries.
|
/// to desktop entries.
|
||||||
Q_PROPERTY(QString startupClass MEMBER mStartupClass CONSTANT);
|
Q_PROPERTY(QString startupClass READ default WRITE default NOTIFY startupClassChanged BINDABLE bindableStartupClass);
|
||||||
/// If true, this application should not be displayed in menus and launchers.
|
/// If true, this application should not be displayed in menus and launchers.
|
||||||
Q_PROPERTY(bool noDisplay MEMBER mNoDisplay CONSTANT);
|
Q_PROPERTY(bool noDisplay READ default WRITE default NOTIFY noDisplayChanged BINDABLE bindableNoDisplay);
|
||||||
/// Long description of the application, such as "View websites on the internet". May be empty.
|
/// Long description of the application, such as "View websites on the internet". May be empty.
|
||||||
Q_PROPERTY(QString comment MEMBER mComment CONSTANT);
|
Q_PROPERTY(QString comment READ default WRITE default NOTIFY commentChanged BINDABLE bindableComment);
|
||||||
/// Name of the icon associated with this application. May be empty.
|
/// Name of the icon associated with this application. May be empty.
|
||||||
Q_PROPERTY(QString icon MEMBER mIcon CONSTANT);
|
Q_PROPERTY(QString icon READ default WRITE default NOTIFY iconChanged BINDABLE bindableIcon);
|
||||||
/// The raw `Exec` string from the desktop entry.
|
/// The raw `Exec` string from the desktop entry.
|
||||||
///
|
///
|
||||||
/// > [!WARNING] This cannot be reliably run as a command. See @@command for one you can run.
|
/// > [!WARNING] This cannot be reliably run as a command. See @@command for one you can run.
|
||||||
Q_PROPERTY(QString execString MEMBER mExecString CONSTANT);
|
Q_PROPERTY(QString execString READ default WRITE default NOTIFY execStringChanged BINDABLE bindableExecString);
|
||||||
/// The parsed `Exec` command in the desktop entry.
|
/// The parsed `Exec` command in the desktop entry.
|
||||||
///
|
///
|
||||||
/// The entry can be run with @@execute(), or by using this command in
|
/// The entry can be run with @@execute(), or by using this command in
|
||||||
|
|
@ -43,13 +75,14 @@ class DesktopEntry: public QObject {
|
||||||
/// the invoked process. See @@execute() for details.
|
/// the invoked process. See @@execute() for details.
|
||||||
///
|
///
|
||||||
/// > [!NOTE] The provided command does not invoke a terminal even if @@runInTerminal is true.
|
/// > [!NOTE] The provided command does not invoke a terminal even if @@runInTerminal is true.
|
||||||
Q_PROPERTY(QVector<QString> command MEMBER mCommand CONSTANT);
|
Q_PROPERTY(QVector<QString> command READ default WRITE default NOTIFY commandChanged BINDABLE bindableCommand);
|
||||||
/// The working directory to execute from.
|
/// The working directory to execute from.
|
||||||
Q_PROPERTY(QString workingDirectory MEMBER mWorkingDirectory CONSTANT);
|
Q_PROPERTY(QString workingDirectory READ default WRITE default NOTIFY workingDirectoryChanged BINDABLE bindableWorkingDirectory);
|
||||||
/// If the application should run in a terminal.
|
/// If the application should run in a terminal.
|
||||||
Q_PROPERTY(bool runInTerminal MEMBER mTerminal CONSTANT);
|
Q_PROPERTY(bool runInTerminal READ default WRITE default NOTIFY runInTerminalChanged BINDABLE bindableRunInTerminal);
|
||||||
Q_PROPERTY(QVector<QString> categories MEMBER mCategories CONSTANT);
|
Q_PROPERTY(QVector<QString> categories READ default WRITE default NOTIFY categoriesChanged BINDABLE bindableCategories);
|
||||||
Q_PROPERTY(QVector<QString> keywords MEMBER mKeywords CONSTANT);
|
Q_PROPERTY(QVector<QString> keywords READ default WRITE default NOTIFY keywordsChanged BINDABLE bindableKeywords);
|
||||||
|
// clang-format on
|
||||||
Q_PROPERTY(QVector<DesktopAction*> actions READ actions CONSTANT);
|
Q_PROPERTY(QVector<DesktopAction*> actions READ actions CONSTANT);
|
||||||
QML_ELEMENT;
|
QML_ELEMENT;
|
||||||
QML_UNCREATABLE("DesktopEntry instances must be retrieved from DesktopEntries");
|
QML_UNCREATABLE("DesktopEntry instances must be retrieved from DesktopEntries");
|
||||||
|
|
@ -57,7 +90,8 @@ class DesktopEntry: public QObject {
|
||||||
public:
|
public:
|
||||||
explicit DesktopEntry(QString id, QObject* parent): QObject(parent), mId(std::move(id)) {}
|
explicit DesktopEntry(QString id, QObject* parent): QObject(parent), mId(std::move(id)) {}
|
||||||
|
|
||||||
void parseEntry(const QString& text);
|
static ParsedDesktopEntryData parseText(const QString& id, const QString& text);
|
||||||
|
void updateState(const ParsedDesktopEntryData& newState);
|
||||||
|
|
||||||
/// Run the application. Currently ignores @@runInTerminal and field codes.
|
/// Run the application. Currently ignores @@runInTerminal and field codes.
|
||||||
///
|
///
|
||||||
|
|
@ -73,30 +107,65 @@ public:
|
||||||
Q_INVOKABLE void execute() const;
|
Q_INVOKABLE void execute() const;
|
||||||
|
|
||||||
[[nodiscard]] bool isValid() const;
|
[[nodiscard]] bool isValid() const;
|
||||||
[[nodiscard]] bool noDisplay() const;
|
|
||||||
[[nodiscard]] QVector<DesktopAction*> actions() const;
|
[[nodiscard]] QVector<DesktopAction*> actions() const;
|
||||||
|
|
||||||
|
[[nodiscard]] QBindable<QString> bindableName() const { return &this->bName; }
|
||||||
|
[[nodiscard]] QBindable<QString> bindableGenericName() const { return &this->bGenericName; }
|
||||||
|
[[nodiscard]] QBindable<QString> bindableStartupClass() const { return &this->bStartupClass; }
|
||||||
|
[[nodiscard]] QBindable<bool> bindableNoDisplay() const { return &this->bNoDisplay; }
|
||||||
|
[[nodiscard]] QBindable<QString> bindableComment() const { return &this->bComment; }
|
||||||
|
[[nodiscard]] QBindable<QString> bindableIcon() const { return &this->bIcon; }
|
||||||
|
[[nodiscard]] QBindable<QString> bindableExecString() const { return &this->bExecString; }
|
||||||
|
[[nodiscard]] QBindable<QVector<QString>> bindableCommand() const { return &this->bCommand; }
|
||||||
|
[[nodiscard]] QBindable<QString> bindableWorkingDirectory() const {
|
||||||
|
return &this->bWorkingDirectory;
|
||||||
|
}
|
||||||
|
[[nodiscard]] QBindable<bool> bindableRunInTerminal() const { return &this->bRunInTerminal; }
|
||||||
|
[[nodiscard]] QBindable<QVector<QString>> bindableCategories() const {
|
||||||
|
return &this->bCategories;
|
||||||
|
}
|
||||||
|
[[nodiscard]] QBindable<QVector<QString>> bindableKeywords() const { return &this->bKeywords; }
|
||||||
|
|
||||||
// currently ignores all field codes.
|
// currently ignores all field codes.
|
||||||
static QVector<QString> parseExecString(const QString& execString);
|
static QVector<QString> parseExecString(const QString& execString);
|
||||||
static void doExec(const QList<QString>& execString, const QString& workingDirectory);
|
static void doExec(const QList<QString>& execString, const QString& workingDirectory);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void nameChanged();
|
||||||
|
void genericNameChanged();
|
||||||
|
void startupClassChanged();
|
||||||
|
void noDisplayChanged();
|
||||||
|
void commentChanged();
|
||||||
|
void iconChanged();
|
||||||
|
void execStringChanged();
|
||||||
|
void commandChanged();
|
||||||
|
void workingDirectoryChanged();
|
||||||
|
void runInTerminalChanged();
|
||||||
|
void categoriesChanged();
|
||||||
|
void keywordsChanged();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
QString mId;
|
QString mId;
|
||||||
QString mName;
|
|
||||||
QString mGenericName;
|
// clang-format off
|
||||||
QString mStartupClass;
|
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bName, &DesktopEntry::nameChanged);
|
||||||
bool mNoDisplay = false;
|
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bGenericName, &DesktopEntry::genericNameChanged);
|
||||||
QString mComment;
|
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bStartupClass, &DesktopEntry::startupClassChanged);
|
||||||
QString mIcon;
|
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, bool, bNoDisplay, &DesktopEntry::noDisplayChanged);
|
||||||
QString mExecString;
|
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bComment, &DesktopEntry::commentChanged);
|
||||||
QVector<QString> mCommand;
|
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bIcon, &DesktopEntry::iconChanged);
|
||||||
QString mWorkingDirectory;
|
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bExecString, &DesktopEntry::execStringChanged);
|
||||||
bool mTerminal = false;
|
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QVector<QString>, bCommand, &DesktopEntry::commandChanged);
|
||||||
QVector<QString> mCategories;
|
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bWorkingDirectory, &DesktopEntry::workingDirectoryChanged);
|
||||||
QVector<QString> mKeywords;
|
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, bool, bRunInTerminal, &DesktopEntry::runInTerminalChanged);
|
||||||
|
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QVector<QString>, bCategories, &DesktopEntry::categoriesChanged);
|
||||||
|
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QVector<QString>, bKeywords, &DesktopEntry::keywordsChanged);
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QHash<QString, QString> mEntries;
|
void updateActions(const QHash<QString, DesktopActionData>& newActions);
|
||||||
|
|
||||||
|
ParsedDesktopEntryData state;
|
||||||
QHash<QString, DesktopAction*> mActions;
|
QHash<QString, DesktopAction*> mActions;
|
||||||
|
|
||||||
friend class DesktopAction;
|
friend class DesktopAction;
|
||||||
|
|
@ -106,12 +175,13 @@ private:
|
||||||
class DesktopAction: public QObject {
|
class DesktopAction: public QObject {
|
||||||
Q_OBJECT;
|
Q_OBJECT;
|
||||||
Q_PROPERTY(QString id MEMBER mId CONSTANT);
|
Q_PROPERTY(QString id MEMBER mId CONSTANT);
|
||||||
Q_PROPERTY(QString name MEMBER mName CONSTANT);
|
// clang-format off
|
||||||
Q_PROPERTY(QString icon MEMBER mIcon CONSTANT);
|
Q_PROPERTY(QString name READ default WRITE default NOTIFY nameChanged BINDABLE bindableName);
|
||||||
|
Q_PROPERTY(QString icon READ default WRITE default NOTIFY iconChanged BINDABLE bindableIcon);
|
||||||
/// The raw `Exec` string from the action.
|
/// The raw `Exec` string from the action.
|
||||||
///
|
///
|
||||||
/// > [!WARNING] This cannot be reliably run as a command. See @@command for one you can run.
|
/// > [!WARNING] This cannot be reliably run as a command. See @@command for one you can run.
|
||||||
Q_PROPERTY(QString execString MEMBER mExecString CONSTANT);
|
Q_PROPERTY(QString execString READ default WRITE default NOTIFY execStringChanged BINDABLE bindableExecString);
|
||||||
/// The parsed `Exec` command in the action.
|
/// The parsed `Exec` command in the action.
|
||||||
///
|
///
|
||||||
/// The entry can be run with @@execute(), or by using this command in
|
/// The entry can be run with @@execute(), or by using this command in
|
||||||
|
|
@ -120,7 +190,8 @@ class DesktopAction: public QObject {
|
||||||
/// the invoked process.
|
/// the invoked process.
|
||||||
///
|
///
|
||||||
/// > [!NOTE] The provided command does not invoke a terminal even if @@runInTerminal is true.
|
/// > [!NOTE] The provided command does not invoke a terminal even if @@runInTerminal is true.
|
||||||
Q_PROPERTY(QVector<QString> command MEMBER mCommand CONSTANT);
|
Q_PROPERTY(QVector<QString> command READ default WRITE default NOTIFY commandChanged BINDABLE bindableCommand);
|
||||||
|
// clang-format on
|
||||||
QML_ELEMENT;
|
QML_ELEMENT;
|
||||||
QML_UNCREATABLE("DesktopAction instances must be retrieved from a DesktopEntry");
|
QML_UNCREATABLE("DesktopAction instances must be retrieved from a DesktopEntry");
|
||||||
|
|
||||||
|
|
@ -136,18 +207,47 @@ public:
|
||||||
/// and @@DesktopEntry.workingDirectory.
|
/// and @@DesktopEntry.workingDirectory.
|
||||||
Q_INVOKABLE void execute() const;
|
Q_INVOKABLE void execute() const;
|
||||||
|
|
||||||
|
[[nodiscard]] QBindable<QString> bindableName() const { return &this->bName; }
|
||||||
|
[[nodiscard]] QBindable<QString> bindableIcon() const { return &this->bIcon; }
|
||||||
|
[[nodiscard]] QBindable<QString> bindableExecString() const { return &this->bExecString; }
|
||||||
|
[[nodiscard]] QBindable<QVector<QString>> bindableCommand() const { return &this->bCommand; }
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void nameChanged();
|
||||||
|
void iconChanged();
|
||||||
|
void execStringChanged();
|
||||||
|
void commandChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
DesktopEntry* entry;
|
DesktopEntry* entry;
|
||||||
QString mId;
|
QString mId;
|
||||||
QString mName;
|
|
||||||
QString mIcon;
|
|
||||||
QString mExecString;
|
|
||||||
QVector<QString> mCommand;
|
|
||||||
QHash<QString, QString> mEntries;
|
QHash<QString, QString> mEntries;
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QString, bName, &DesktopAction::nameChanged);
|
||||||
|
Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QString, bIcon, &DesktopAction::iconChanged);
|
||||||
|
Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QString, bExecString, &DesktopAction::execStringChanged);
|
||||||
|
Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QVector<QString>, bCommand, &DesktopAction::commandChanged);
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
friend class DesktopEntry;
|
friend class DesktopEntry;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class DesktopEntryManager;
|
||||||
|
|
||||||
|
class DesktopEntryScanner: public QRunnable {
|
||||||
|
public:
|
||||||
|
explicit DesktopEntryScanner(DesktopEntryManager* manager);
|
||||||
|
|
||||||
|
void run() override;
|
||||||
|
// clang-format off
|
||||||
|
void scanDirectory(const QDir& dir, const QString& idPrefix, QList<ParsedDesktopEntryData>& entries);
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
|
private:
|
||||||
|
DesktopEntryManager* manager;
|
||||||
|
};
|
||||||
|
|
||||||
class DesktopEntryManager: public QObject {
|
class DesktopEntryManager: public QObject {
|
||||||
Q_OBJECT;
|
Q_OBJECT;
|
||||||
|
|
||||||
|
|
@ -161,15 +261,26 @@ public:
|
||||||
|
|
||||||
static DesktopEntryManager* instance();
|
static DesktopEntryManager* instance();
|
||||||
|
|
||||||
|
static const QStringList& desktopPaths();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void applicationsChanged();
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void handleFileChanges();
|
||||||
|
void onScanCompleted(const QList<ParsedDesktopEntryData>& scanResults);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
explicit DesktopEntryManager();
|
explicit DesktopEntryManager();
|
||||||
|
|
||||||
void populateApplications();
|
|
||||||
void scanPath(const QDir& dir, const QString& prefix = QString());
|
|
||||||
|
|
||||||
QHash<QString, DesktopEntry*> desktopEntries;
|
QHash<QString, DesktopEntry*> desktopEntries;
|
||||||
QHash<QString, DesktopEntry*> lowercaseDesktopEntries;
|
QHash<QString, DesktopEntry*> lowercaseDesktopEntries;
|
||||||
ObjectModel<DesktopEntry> mApplications {this};
|
ObjectModel<DesktopEntry> mApplications {this};
|
||||||
|
DesktopEntryMonitor* monitor = nullptr;
|
||||||
|
bool scanInProgress = false;
|
||||||
|
bool scanQueued = false;
|
||||||
|
|
||||||
|
friend class DesktopEntryScanner;
|
||||||
};
|
};
|
||||||
|
|
||||||
///! Desktop entry index.
|
///! Desktop entry index.
|
||||||
|
|
@ -201,4 +312,7 @@ public:
|
||||||
Q_INVOKABLE [[nodiscard]] static DesktopEntry* heuristicLookup(const QString& name);
|
Q_INVOKABLE [[nodiscard]] static DesktopEntry* heuristicLookup(const QString& name);
|
||||||
|
|
||||||
[[nodiscard]] static ObjectModel<DesktopEntry>* applications();
|
[[nodiscard]] static ObjectModel<DesktopEntry>* applications();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void applicationsChanged();
|
||||||
};
|
};
|
||||||
|
|
|
||||||
68
src/core/desktopentrymonitor.cpp
Normal file
68
src/core/desktopentrymonitor.cpp
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
#include "desktopentrymonitor.hpp"
|
||||||
|
|
||||||
|
#include <qdir.h>
|
||||||
|
#include <qfileinfo.h>
|
||||||
|
#include <qfilesystemwatcher.h>
|
||||||
|
#include <qobject.h>
|
||||||
|
#include <qstring.h>
|
||||||
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
|
#include "desktopentry.hpp"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
void addPathAndParents(QFileSystemWatcher& watcher, const QString& path) {
|
||||||
|
watcher.addPath(path);
|
||||||
|
|
||||||
|
auto p = QFileInfo(path).absolutePath();
|
||||||
|
while (!p.isEmpty()) {
|
||||||
|
watcher.addPath(p);
|
||||||
|
const auto parent = QFileInfo(p).dir().absolutePath();
|
||||||
|
if (parent == p) break;
|
||||||
|
p = parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
DesktopEntryMonitor::DesktopEntryMonitor(QObject* parent): QObject(parent) {
|
||||||
|
this->debounceTimer.setSingleShot(true);
|
||||||
|
this->debounceTimer.setInterval(100);
|
||||||
|
|
||||||
|
QObject::connect(
|
||||||
|
&this->watcher,
|
||||||
|
&QFileSystemWatcher::directoryChanged,
|
||||||
|
this,
|
||||||
|
&DesktopEntryMonitor::onDirectoryChanged
|
||||||
|
);
|
||||||
|
QObject::connect(
|
||||||
|
&this->debounceTimer,
|
||||||
|
&QTimer::timeout,
|
||||||
|
this,
|
||||||
|
&DesktopEntryMonitor::processChanges
|
||||||
|
);
|
||||||
|
|
||||||
|
this->startMonitoring();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopEntryMonitor::startMonitoring() {
|
||||||
|
for (const auto& path: DesktopEntryManager::desktopPaths()) {
|
||||||
|
if (!QDir(path).exists()) continue;
|
||||||
|
addPathAndParents(this->watcher, path);
|
||||||
|
this->scanAndWatch(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopEntryMonitor::scanAndWatch(const QString& dirPath) {
|
||||||
|
auto dir = QDir(dirPath);
|
||||||
|
if (!dir.exists()) return;
|
||||||
|
|
||||||
|
this->watcher.addPath(dirPath);
|
||||||
|
|
||||||
|
auto subdirs = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks);
|
||||||
|
for (const auto& subdir: subdirs) this->watcher.addPath(subdir.absoluteFilePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopEntryMonitor::onDirectoryChanged(const QString& /*path*/) {
|
||||||
|
this->debounceTimer.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopEntryMonitor::processChanges() { emit this->desktopEntriesChanged(); }
|
||||||
32
src/core/desktopentrymonitor.hpp
Normal file
32
src/core/desktopentrymonitor.hpp
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <qfilesystemwatcher.h>
|
||||||
|
#include <qobject.h>
|
||||||
|
#include <qstringlist.h>
|
||||||
|
#include <qtimer.h>
|
||||||
|
|
||||||
|
class DesktopEntryMonitor: public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit DesktopEntryMonitor(QObject* parent = nullptr);
|
||||||
|
~DesktopEntryMonitor() override = default;
|
||||||
|
DesktopEntryMonitor(const DesktopEntryMonitor&) = delete;
|
||||||
|
DesktopEntryMonitor& operator=(const DesktopEntryMonitor&) = delete;
|
||||||
|
DesktopEntryMonitor(DesktopEntryMonitor&&) = delete;
|
||||||
|
DesktopEntryMonitor& operator=(DesktopEntryMonitor&&) = delete;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void desktopEntriesChanged();
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onDirectoryChanged(const QString& path);
|
||||||
|
void processChanges();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void startMonitoring();
|
||||||
|
void scanAndWatch(const QString& dirPath);
|
||||||
|
|
||||||
|
QFileSystemWatcher watcher;
|
||||||
|
QTimer debounceTimer;
|
||||||
|
};
|
||||||
|
|
@ -127,7 +127,7 @@ void Notification::updateProperties(
|
||||||
|
|
||||||
if (appIcon.isEmpty() && !this->bDesktopEntry.value().isEmpty()) {
|
if (appIcon.isEmpty() && !this->bDesktopEntry.value().isEmpty()) {
|
||||||
if (auto* entry = DesktopEntryManager::instance()->byId(this->bDesktopEntry.value())) {
|
if (auto* entry = DesktopEntryManager::instance()->byId(this->bDesktopEntry.value())) {
|
||||||
appIcon = entry->mIcon;
|
appIcon = entry->bIcon.value();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue