Add Nebula.Windows with Sway IPC backend
Introduces a new `core/windows` module (`Nebula.Windows`) with `WindowService`, window/workspace models, and a Sway IPC backend for live window tracking and control (focus, close, move/resize, snap, maximize/restore, minimize, workspace switch). The desktop shell now links this module, uses compositor focus for top-bar active app text, and adds an Open Windows section in the launcher with focus/close actions. Also updates application metadata handling to parse `StartupWMClass` and map window `app_id`/class to friendly names and icons, and revises Sway config/docs for the new floating-first Desktop 0.2 behavior with temporary compositor-provided decorations.
This commit is contained in:
@@ -91,3 +91,49 @@ const DesktopEntry *ApplicationModel::entryById(const QString &desktopId) const
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const DesktopEntry *ApplicationModel::entryMatchingWindow(const QString &appId, const QString &windowClass) const
|
||||
{
|
||||
auto stem = [](QString value) {
|
||||
if (value.endsWith(QLatin1String(".desktop"), Qt::CaseInsensitive))
|
||||
value.chop(8);
|
||||
return value.toLower();
|
||||
};
|
||||
|
||||
const QString app = stem(appId);
|
||||
const QString cls = stem(windowClass);
|
||||
const QString appLast = app.contains(QLatin1Char('.')) ? app.section(QLatin1Char('.'), -1) : app;
|
||||
const QString classLast = cls.contains(QLatin1Char('.')) ? cls.section(QLatin1Char('.'), -1) : cls;
|
||||
|
||||
const DesktopEntry *wmClassMatch = nullptr;
|
||||
const DesktopEntry *lastComponentMatch = nullptr;
|
||||
|
||||
for (const DesktopEntry &entry : m_entries) {
|
||||
const QString id = stem(entry.desktopId);
|
||||
if (!app.isEmpty() && id == app)
|
||||
return &entry;
|
||||
if (!cls.isEmpty() && id == cls)
|
||||
return &entry;
|
||||
|
||||
if (!entry.startupWmClass.isEmpty()) {
|
||||
const QString wm = stem(entry.startupWmClass);
|
||||
if ((!app.isEmpty() && wm == app) || (!cls.isEmpty() && wm == cls)
|
||||
|| (!appLast.isEmpty() && wm == appLast) || (!classLast.isEmpty() && wm == classLast)) {
|
||||
if (!wmClassMatch)
|
||||
wmClassMatch = &entry;
|
||||
}
|
||||
}
|
||||
|
||||
if (!lastComponentMatch) {
|
||||
const QString idLast = id.contains(QLatin1Char('.')) ? id.section(QLatin1Char('.'), -1) : id;
|
||||
if (appLast.size() >= 3 && (id == appLast || idLast == appLast))
|
||||
lastComponentMatch = &entry;
|
||||
else if (classLast.size() >= 3 && (id == classLast || idLast == classLast))
|
||||
lastComponentMatch = &entry;
|
||||
}
|
||||
}
|
||||
|
||||
if (wmClassMatch)
|
||||
return wmClassMatch;
|
||||
return lastComponentMatch;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ public:
|
||||
void setEntries(QVector<DesktopEntry> entries);
|
||||
const DesktopEntry *entryAt(int row) const;
|
||||
const DesktopEntry *entryById(const QString &desktopId) const;
|
||||
const DesktopEntry *entryMatchingWindow(const QString &appId, const QString &windowClass) const;
|
||||
|
||||
signals:
|
||||
void countChanged();
|
||||
|
||||
@@ -7,13 +7,21 @@
|
||||
#include <QFileInfo>
|
||||
#include <QSet>
|
||||
#include <QVector>
|
||||
#include <QtGlobal>
|
||||
#include <utility>
|
||||
|
||||
namespace {
|
||||
ApplicationService *s_instance = nullptr;
|
||||
}
|
||||
|
||||
ApplicationService::ApplicationService(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_model(new ApplicationModel(this))
|
||||
, m_filtered(new ApplicationFilterModel(this))
|
||||
{
|
||||
if (!s_instance)
|
||||
s_instance = this;
|
||||
|
||||
m_filtered->setSourceModel(m_model);
|
||||
m_filtered->setSortRole(ApplicationModel::NameRole);
|
||||
m_filtered->sort(0, Qt::AscendingOrder);
|
||||
@@ -24,6 +32,17 @@ ApplicationService::ApplicationService(QObject *parent)
|
||||
refresh();
|
||||
}
|
||||
|
||||
ApplicationService::~ApplicationService()
|
||||
{
|
||||
if (s_instance == this)
|
||||
s_instance = nullptr;
|
||||
}
|
||||
|
||||
ApplicationService *ApplicationService::instance()
|
||||
{
|
||||
return s_instance;
|
||||
}
|
||||
|
||||
ApplicationService *ApplicationService::create(QQmlEngine *engine, QJSEngine *)
|
||||
{
|
||||
auto *service = new ApplicationService(engine);
|
||||
@@ -66,6 +85,22 @@ bool ApplicationService::launch(const QString &desktopId) const
|
||||
return entry->launch();
|
||||
}
|
||||
|
||||
QString ApplicationService::displayNameForWindow(const QString &appId, const QString &windowClass) const
|
||||
{
|
||||
const DesktopEntry *entry = m_model->entryMatchingWindow(appId, windowClass);
|
||||
if (entry && !entry->name.isEmpty())
|
||||
return entry->name;
|
||||
return {};
|
||||
}
|
||||
|
||||
QString ApplicationService::iconNameForWindow(const QString &appId, const QString &windowClass) const
|
||||
{
|
||||
const DesktopEntry *entry = m_model->entryMatchingWindow(appId, windowClass);
|
||||
if (entry)
|
||||
return entry->icon;
|
||||
return {};
|
||||
}
|
||||
|
||||
void ApplicationService::refresh()
|
||||
{
|
||||
QVector<DesktopEntry> entries;
|
||||
|
||||
@@ -20,7 +20,10 @@ class ApplicationService : public QObject
|
||||
|
||||
public:
|
||||
explicit ApplicationService(QObject *parent = nullptr);
|
||||
~ApplicationService() override;
|
||||
|
||||
static ApplicationService *create(QQmlEngine *engine, QJSEngine *scriptEngine);
|
||||
static ApplicationService *instance();
|
||||
|
||||
ApplicationModel *model() const;
|
||||
ApplicationFilterModel *filteredModel() const;
|
||||
@@ -30,6 +33,8 @@ public:
|
||||
int count() const;
|
||||
|
||||
Q_INVOKABLE bool launch(const QString &desktopId) const;
|
||||
Q_INVOKABLE QString displayNameForWindow(const QString &appId, const QString &windowClass) const;
|
||||
Q_INVOKABLE QString iconNameForWindow(const QString &appId, const QString &windowClass) const;
|
||||
Q_INVOKABLE void refresh();
|
||||
|
||||
signals:
|
||||
|
||||
@@ -353,6 +353,8 @@ DesktopEntry parseDesktopEntryFile(const QString &filePath, const QString &deskt
|
||||
entry.exec = value;
|
||||
else if (key == QLatin1String("TryExec"))
|
||||
entry.tryExec = value;
|
||||
else if (key == QLatin1String("StartupWMClass"))
|
||||
entry.startupWmClass = value;
|
||||
else if (key == QLatin1String("Icon"))
|
||||
entry.icon = value;
|
||||
else if (key == QLatin1String("Path"))
|
||||
|
||||
@@ -13,6 +13,7 @@ struct DesktopEntry
|
||||
QString icon;
|
||||
QString exec;
|
||||
QString tryExec;
|
||||
QString startupWmClass;
|
||||
QString workingDirectory;
|
||||
QStringList categories;
|
||||
bool terminal = false;
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
Shared C++ service for discovering and launching installed Linux applications from `.desktop` entries.
|
||||
|
||||
Desktop and the future Bigscreen shell should both use this library. It has no Desktop-specific QML.
|
||||
Desktop and the future Bigscreen shell should both use this library. It has no Desktop-specific QML. Window naming helpers (`displayNameForWindow`, `iconNameForWindow`) are intended for `WindowService`.
|
||||
|
||||
## Behaviour
|
||||
|
||||
- Scans XDG application directories (`XDG_DATA_HOME`, `XDG_DATA_DIRS`, with the usual `~/.local/share` and `/usr/share` defaults).
|
||||
- Hides entries with `Hidden=true` or `NoDisplay=true`, and respects `OnlyShowIn` / `NotShowIn` / `TryExec`.
|
||||
- Exposes a `QAbstractListModel` to QML (`desktopId`, `name`, `genericName`, `comment`, `iconName`, `categories`, `terminal`).
|
||||
- Parses `StartupWMClass` and can map a compositor `app_id` / window class to a friendly `Name` (`displayNameForWindow`, `iconNameForWindow`) for `WindowService`.
|
||||
- Launches with `QProcess::startDetached` and `QProcess::splitCommand`. Does not use a shell.
|
||||
- Strips or substitutes common Exec field codes when launching without files/URLs.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user