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.
140 lines
4.1 KiB
C++
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;
|
|
}
|