Introduces a dedicated layer-shell `WindowChromeSurface` that renders macOS-style traffic-light controls over the focused Sway window titlebar, replacing the old top-bar window controls. Adds theme tokens and a reusable `TrafficLightButton` component, updates shell QML/CMake wiring, and adjusts Sway titlebar padding/title format so window titles do not overlap the overlay. Window tracking was extended with focused-window geometry/deco/fullscreen properties plus backend refresh support so the chrome follows window movement and state changes in near real time.
442 lines
12 KiB
C++
442 lines
12 KiB
C++
#include "WindowService.hpp"
|
|
|
|
#include "ApplicationService.hpp"
|
|
#include "SwayWindowBackend.hpp"
|
|
|
|
#include <QHash>
|
|
#include <QStringList>
|
|
#include <QTimer>
|
|
|
|
namespace {
|
|
|
|
bool looksTechnicalTitle(const QString &title)
|
|
{
|
|
if (title.isEmpty())
|
|
return true;
|
|
if (title.contains(QLatin1Char('@')) || title.contains(QLatin1Char('/')) || title.contains(QLatin1Char('\\')))
|
|
return true;
|
|
if (title.contains(QLatin1String("://")))
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
WindowService::WindowService(QObject *parent)
|
|
: QObject(parent)
|
|
, m_backend(new SwayWindowBackend(this))
|
|
, m_model(new WindowModel(this))
|
|
, m_workspaces(new WorkspaceModel(this))
|
|
, m_geometryTimer(new QTimer(this))
|
|
{
|
|
connect(m_backend, &WindowBackend::connectedChanged, this, &WindowService::connectedChanged);
|
|
connect(m_backend, &WindowBackend::connectedChanged, this, &WindowService::syncFromBackend);
|
|
connect(m_backend, &WindowBackend::windowsChanged, this, &WindowService::syncFromBackend);
|
|
|
|
m_geometryTimer->setInterval(33);
|
|
connect(m_geometryTimer, &QTimer::timeout, this, &WindowService::syncFocusedGeometry);
|
|
|
|
QTimer::singleShot(0, this, [this]() {
|
|
bindApplicationService();
|
|
if (m_applications)
|
|
syncFromBackend();
|
|
});
|
|
syncFromBackend();
|
|
}
|
|
|
|
WindowService *WindowService::create(QQmlEngine *engine, QJSEngine *)
|
|
{
|
|
auto *service = new WindowService(engine);
|
|
QQmlEngine::setObjectOwnership(service, QQmlEngine::CppOwnership);
|
|
return service;
|
|
}
|
|
|
|
bool WindowService::connected() const
|
|
{
|
|
return m_backend && m_backend->isConnected();
|
|
}
|
|
|
|
WindowModel *WindowService::model() const
|
|
{
|
|
return m_model;
|
|
}
|
|
|
|
WorkspaceModel *WindowService::workspaceModel() const
|
|
{
|
|
return m_workspaces;
|
|
}
|
|
|
|
int WindowService::windowCount() const
|
|
{
|
|
return m_model->count();
|
|
}
|
|
|
|
bool WindowService::hasFocusedWindow() const
|
|
{
|
|
return !m_focusedWindowId.isEmpty();
|
|
}
|
|
|
|
QString WindowService::focusedWindowId() const
|
|
{
|
|
return m_focusedWindowId;
|
|
}
|
|
|
|
QString WindowService::focusedWindowTitle() const
|
|
{
|
|
return m_focusedWindowTitle;
|
|
}
|
|
|
|
QString WindowService::focusedApplicationId() const
|
|
{
|
|
return m_focusedApplicationId;
|
|
}
|
|
|
|
QString WindowService::focusedApplicationName() const
|
|
{
|
|
return m_focusedApplicationName;
|
|
}
|
|
|
|
bool WindowService::focusedWindowMaximized() const
|
|
{
|
|
return m_focusedWindowMaximized;
|
|
}
|
|
|
|
bool WindowService::focusedWindowFullscreen() const
|
|
{
|
|
return m_focusedWindowFullscreen;
|
|
}
|
|
|
|
int WindowService::focusedWindowX() const
|
|
{
|
|
return m_focusedWindowX;
|
|
}
|
|
|
|
int WindowService::focusedWindowY() const
|
|
{
|
|
return m_focusedWindowY;
|
|
}
|
|
|
|
int WindowService::focusedWindowWidth() const
|
|
{
|
|
return m_focusedWindowWidth;
|
|
}
|
|
|
|
int WindowService::focusedWindowHeight() const
|
|
{
|
|
return m_focusedWindowHeight;
|
|
}
|
|
|
|
int WindowService::focusedWindowDecoX() const
|
|
{
|
|
return m_focusedWindowDecoX;
|
|
}
|
|
|
|
int WindowService::focusedWindowDecoY() const
|
|
{
|
|
return m_focusedWindowDecoY;
|
|
}
|
|
|
|
int WindowService::focusedWindowDecoWidth() const
|
|
{
|
|
return m_focusedWindowDecoWidth;
|
|
}
|
|
|
|
int WindowService::focusedWindowDecoHeight() const
|
|
{
|
|
return m_focusedWindowDecoHeight;
|
|
}
|
|
|
|
bool WindowService::focusWindow(const QString &windowId)
|
|
{
|
|
return m_backend->focusWindow(parseWindowId(windowId));
|
|
}
|
|
|
|
bool WindowService::closeWindow(const QString &windowId)
|
|
{
|
|
return m_backend->closeWindow(parseWindowId(windowId));
|
|
}
|
|
|
|
bool WindowService::maximizeWindow(const QString &windowId)
|
|
{
|
|
return m_backend->maximizeWindow(parseWindowId(windowId));
|
|
}
|
|
|
|
bool WindowService::restoreWindow(const QString &windowId)
|
|
{
|
|
return m_backend->restoreWindow(parseWindowId(windowId));
|
|
}
|
|
|
|
bool WindowService::minimizeWindow(const QString &windowId)
|
|
{
|
|
return m_backend->minimizeWindow(parseWindowId(windowId));
|
|
}
|
|
|
|
bool WindowService::minimizeFocusedWindow()
|
|
{
|
|
if (m_focusedWindowId.isEmpty())
|
|
return false;
|
|
return minimizeWindow(m_focusedWindowId);
|
|
}
|
|
|
|
bool WindowService::toggleMaximizeFocusedWindow()
|
|
{
|
|
if (m_focusedWindowId.isEmpty())
|
|
return false;
|
|
|
|
const bool restore = m_focusedWindowMaximized;
|
|
const bool ok = restore ? restoreWindow(m_focusedWindowId)
|
|
: maximizeWindow(m_focusedWindowId);
|
|
if (!ok)
|
|
return false;
|
|
|
|
if (m_focusedWindowMaximized == restore) {
|
|
m_focusedWindowMaximized = !restore;
|
|
emit focusChanged();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool WindowService::closeFocusedWindow()
|
|
{
|
|
if (m_focusedWindowId.isEmpty())
|
|
return false;
|
|
return closeWindow(m_focusedWindowId);
|
|
}
|
|
|
|
bool WindowService::setWindowFloating(const QString &windowId, bool floating)
|
|
{
|
|
return m_backend->setWindowFloating(parseWindowId(windowId), floating);
|
|
}
|
|
|
|
bool WindowService::moveWindow(const QString &windowId, int x, int y)
|
|
{
|
|
return m_backend->moveWindow(parseWindowId(windowId), x, y);
|
|
}
|
|
|
|
bool WindowService::resizeWindow(const QString &windowId, int width, int height)
|
|
{
|
|
return m_backend->resizeWindow(parseWindowId(windowId), width, height);
|
|
}
|
|
|
|
bool WindowService::snapWindow(const QString &windowId, SnapEdge edge)
|
|
{
|
|
return m_backend->snapWindow(parseWindowId(windowId), static_cast<WindowBackend::SnapEdge>(edge));
|
|
}
|
|
|
|
bool WindowService::switchWorkspace(const QString &workspaceId)
|
|
{
|
|
return m_backend->switchWorkspace(workspaceId);
|
|
}
|
|
|
|
void WindowService::syncFromBackend()
|
|
{
|
|
bindApplicationService();
|
|
|
|
QVector<WindowInfo> windows = m_backend->windows();
|
|
resolveNames(windows);
|
|
const int previousCount = m_model->count();
|
|
m_model->setWindows(windows);
|
|
m_workspaces->setWorkspaces(m_backend->workspaces());
|
|
if (m_model->count() != previousCount)
|
|
emit windowCountChanged();
|
|
|
|
const qint64 focusedId = m_backend->focusedWindowId();
|
|
QString windowId;
|
|
QString title;
|
|
QString appId;
|
|
QString appName;
|
|
bool maximized = false;
|
|
const WindowInfo *focusedInfo = nullptr;
|
|
|
|
if (const WindowInfo *focused = m_model->windowById(focusedId)) {
|
|
if (!focused->minimized)
|
|
focusedInfo = focused;
|
|
}
|
|
if (!focusedInfo && !m_focusedWindowId.isEmpty()) {
|
|
const WindowInfo *previous = m_model->windowById(parseWindowId(m_focusedWindowId));
|
|
if (previous && !previous->minimized)
|
|
focusedInfo = previous;
|
|
}
|
|
|
|
if (focusedInfo) {
|
|
windowId = QString::number(focusedInfo->id);
|
|
title = focusedInfo->title;
|
|
appId = focusedInfo->appId.isEmpty() ? focusedInfo->windowClass : focusedInfo->appId;
|
|
appName = focusedInfo->displayName;
|
|
maximized = focusedInfo->maximized;
|
|
}
|
|
|
|
if (m_focusedWindowId != windowId
|
|
|| m_focusedWindowTitle != title
|
|
|| m_focusedApplicationId != appId
|
|
|| m_focusedApplicationName != appName
|
|
|| m_focusedWindowMaximized != maximized) {
|
|
m_focusedWindowId = windowId;
|
|
m_focusedWindowTitle = title;
|
|
m_focusedApplicationId = appId;
|
|
m_focusedApplicationName = appName;
|
|
m_focusedWindowMaximized = maximized;
|
|
emit focusChanged();
|
|
}
|
|
|
|
publishFocusedGeometry(focusedInfo);
|
|
|
|
if (hasFocusedWindow()) {
|
|
if (!m_geometryTimer->isActive())
|
|
m_geometryTimer->start();
|
|
} else if (m_geometryTimer->isActive()) {
|
|
m_geometryTimer->stop();
|
|
}
|
|
}
|
|
|
|
void WindowService::syncFocusedGeometry()
|
|
{
|
|
if (m_focusedWindowId.isEmpty()) {
|
|
m_geometryTimer->stop();
|
|
publishFocusedGeometry(nullptr);
|
|
return;
|
|
}
|
|
|
|
m_backend->updateFocusedGeometry();
|
|
|
|
const QVector<WindowInfo> windows = m_backend->windows();
|
|
const qint64 id = parseWindowId(m_focusedWindowId);
|
|
const WindowInfo *focused = nullptr;
|
|
for (const WindowInfo &window : windows) {
|
|
if (window.id == id) {
|
|
focused = &window;
|
|
break;
|
|
}
|
|
}
|
|
publishFocusedGeometry(focused);
|
|
}
|
|
|
|
void WindowService::publishFocusedGeometry(const WindowInfo *focused)
|
|
{
|
|
int x = 0;
|
|
int y = 0;
|
|
int width = 0;
|
|
int height = 0;
|
|
int decoX = 0;
|
|
int decoY = 0;
|
|
int decoWidth = 0;
|
|
int decoHeight = 0;
|
|
bool fullscreen = false;
|
|
|
|
if (focused && !focused->minimized) {
|
|
x = focused->x;
|
|
y = focused->y;
|
|
width = focused->width;
|
|
height = focused->height;
|
|
decoX = focused->decoX;
|
|
decoY = focused->decoY;
|
|
decoWidth = focused->decoWidth;
|
|
decoHeight = focused->decoHeight;
|
|
fullscreen = focused->fullscreen;
|
|
}
|
|
|
|
if (m_focusedWindowFullscreen != fullscreen) {
|
|
m_focusedWindowFullscreen = fullscreen;
|
|
emit focusChanged();
|
|
}
|
|
|
|
if (m_focusedWindowX != x
|
|
|| m_focusedWindowY != y
|
|
|| m_focusedWindowWidth != width
|
|
|| m_focusedWindowHeight != height
|
|
|| m_focusedWindowDecoX != decoX
|
|
|| m_focusedWindowDecoY != decoY
|
|
|| m_focusedWindowDecoWidth != decoWidth
|
|
|| m_focusedWindowDecoHeight != decoHeight) {
|
|
m_focusedWindowX = x;
|
|
m_focusedWindowY = y;
|
|
m_focusedWindowWidth = width;
|
|
m_focusedWindowHeight = height;
|
|
m_focusedWindowDecoX = decoX;
|
|
m_focusedWindowDecoY = decoY;
|
|
m_focusedWindowDecoWidth = decoWidth;
|
|
m_focusedWindowDecoHeight = decoHeight;
|
|
emit focusedWindowGeometryChanged();
|
|
}
|
|
}
|
|
|
|
void WindowService::resolveNames(QVector<WindowInfo> &windows) const
|
|
{
|
|
for (WindowInfo &window : windows) {
|
|
QString name;
|
|
if (m_applications)
|
|
name = m_applications->displayNameForWindow(window.appId, window.windowClass);
|
|
if (name.isEmpty())
|
|
name = humanizeIdentifier(window.appId, window.windowClass);
|
|
if (name.isEmpty() && !looksTechnicalTitle(window.title))
|
|
name = window.title;
|
|
window.displayName = name;
|
|
|
|
if (m_applications)
|
|
window.iconName = m_applications->iconNameForWindow(window.appId, window.windowClass);
|
|
}
|
|
}
|
|
|
|
void WindowService::bindApplicationService()
|
|
{
|
|
if (m_applications)
|
|
return;
|
|
|
|
m_applications = ApplicationService::instance();
|
|
if (!m_applications)
|
|
return;
|
|
|
|
connect(m_applications, &ApplicationService::countChanged, this, &WindowService::syncFromBackend);
|
|
}
|
|
|
|
qint64 WindowService::parseWindowId(const QString &windowId)
|
|
{
|
|
bool ok = false;
|
|
const qint64 id = windowId.trimmed().toLongLong(&ok);
|
|
return ok ? id : 0;
|
|
}
|
|
|
|
QString WindowService::humanizeIdentifier(const QString &appId, const QString &windowClass)
|
|
{
|
|
static const QHash<QString, QString> aliases = {
|
|
{QStringLiteral("foot"), QStringLiteral("Foot")},
|
|
{QStringLiteral("footclient"), QStringLiteral("Foot")},
|
|
{QStringLiteral("footserver"), QStringLiteral("Foot")},
|
|
{QStringLiteral("org.mozilla.firefox"), QStringLiteral("Firefox")},
|
|
{QStringLiteral("firefox"), QStringLiteral("Firefox")},
|
|
{QStringLiteral("firefox-esr"), QStringLiteral("Firefox")},
|
|
};
|
|
|
|
const QString raw = appId.isEmpty() ? windowClass : appId;
|
|
const QString lower = raw.toLower();
|
|
if (aliases.contains(lower))
|
|
return aliases.value(lower);
|
|
|
|
return titleCaseIdentifier(raw);
|
|
}
|
|
|
|
QString WindowService::titleCaseIdentifier(QString id)
|
|
{
|
|
if (id.endsWith(QLatin1String(".desktop"), Qt::CaseInsensitive))
|
|
id.chop(8);
|
|
if (id.contains(QLatin1Char('.')))
|
|
id = id.section(QLatin1Char('.'), -1);
|
|
if (id.isEmpty())
|
|
return {};
|
|
|
|
id.replace(QLatin1Char('-'), QLatin1Char(' '));
|
|
id.replace(QLatin1Char('_'), QLatin1Char(' '));
|
|
|
|
const QStringList parts = id.split(QLatin1Char(' '), Qt::SkipEmptyParts);
|
|
QStringList titled;
|
|
titled.reserve(parts.size());
|
|
for (QString part : parts) {
|
|
if (part.size() == 1)
|
|
part = part.toUpper();
|
|
else if (!part.isEmpty())
|
|
part = part.left(1).toUpper() + part.mid(1);
|
|
titled.append(part);
|
|
}
|
|
return titled.join(QLatin1Char(' '));
|
|
}
|