Files
andrew 2f59d3c115 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.
2026-08-26 18:20:56 +12:00

140 lines
4.1 KiB
C++

#include "ApplicationModel.hpp"
#include <algorithm>
#include <utility>
ApplicationModel::ApplicationModel(QObject *parent)
: QAbstractListModel(parent)
{
}
int ApplicationModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid())
return 0;
return m_entries.size();
}
int ApplicationModel::count() const
{
return m_entries.size();
}
QVariant ApplicationModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() < 0 || index.row() >= m_entries.size())
return {};
const DesktopEntry &entry = m_entries.at(index.row());
switch (role) {
case DesktopIdRole:
case Qt::UserRole:
return entry.desktopId;
case NameRole:
case Qt::DisplayRole:
return entry.name;
case GenericNameRole:
return entry.genericName;
case CommentRole:
return entry.comment;
case IconNameRole:
return entry.icon;
case CategoriesRole:
return entry.categories;
case TerminalRole:
return entry.terminal;
default:
return {};
}
}
QHash<int, QByteArray> ApplicationModel::roleNames() const
{
return {
{DesktopIdRole, QByteArrayLiteral("desktopId")},
{NameRole, QByteArrayLiteral("name")},
{GenericNameRole, QByteArrayLiteral("genericName")},
{CommentRole, QByteArrayLiteral("comment")},
{IconNameRole, QByteArrayLiteral("iconName")},
{CategoriesRole, QByteArrayLiteral("categories")},
{TerminalRole, QByteArrayLiteral("terminal")},
};
}
void ApplicationModel::setEntries(QVector<DesktopEntry> entries)
{
std::sort(entries.begin(), entries.end(), [](const DesktopEntry &left, const DesktopEntry &right) {
const int order = QString::localeAwareCompare(left.name, right.name);
if (order != 0)
return order < 0;
return left.desktopId < right.desktopId;
});
beginResetModel();
m_entries = std::move(entries);
endResetModel();
emit countChanged();
}
const DesktopEntry *ApplicationModel::entryAt(int row) const
{
if (row < 0 || row >= m_entries.size())
return nullptr;
return &m_entries.at(row);
}
const DesktopEntry *ApplicationModel::entryById(const QString &desktopId) const
{
for (const DesktopEntry &entry : m_entries) {
if (entry.desktopId == desktopId)
return &entry;
}
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;
}