Add floating traffic-light window chrome
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.
This commit is contained in:
@@ -38,6 +38,7 @@ public:
|
||||
virtual bool resizeWindow(qint64 windowId, int width, int height) = 0;
|
||||
virtual bool snapWindow(qint64 windowId, SnapEdge edge) = 0;
|
||||
virtual bool switchWorkspace(const QString &workspaceId) = 0;
|
||||
virtual bool updateFocusedGeometry() { return false; }
|
||||
|
||||
signals:
|
||||
void connectedChanged();
|
||||
|
||||
@@ -27,11 +27,15 @@ WindowService::WindowService(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)
|
||||
@@ -97,6 +101,51 @@ 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));
|
||||
@@ -197,15 +246,24 @@ void WindowService::syncFromBackend()
|
||||
QString appId;
|
||||
QString appName;
|
||||
bool maximized = false;
|
||||
const WindowInfo *focusedInfo = nullptr;
|
||||
|
||||
if (const WindowInfo *focused = m_model->windowById(focusedId)) {
|
||||
if (!focused->minimized) {
|
||||
windowId = QString::number(focused->id);
|
||||
title = focused->title;
|
||||
appId = focused->appId.isEmpty() ? focused->windowClass : focused->appId;
|
||||
appName = focused->displayName;
|
||||
maximized = focused->maximized;
|
||||
}
|
||||
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
|
||||
@@ -220,6 +278,86 @@ void WindowService::syncFromBackend()
|
||||
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
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <QJSEngine>
|
||||
#include <QObject>
|
||||
#include <QQmlEngine>
|
||||
#include <QTimer>
|
||||
#include <QVector>
|
||||
#include <QtQml/qqmlregistration.h>
|
||||
|
||||
@@ -27,6 +28,15 @@ class WindowService : public QObject
|
||||
Q_PROPERTY(QString focusedApplicationId READ focusedApplicationId NOTIFY focusChanged)
|
||||
Q_PROPERTY(QString focusedApplicationName READ focusedApplicationName NOTIFY focusChanged)
|
||||
Q_PROPERTY(bool focusedWindowMaximized READ focusedWindowMaximized NOTIFY focusChanged)
|
||||
Q_PROPERTY(bool focusedWindowFullscreen READ focusedWindowFullscreen NOTIFY focusChanged)
|
||||
Q_PROPERTY(int focusedWindowX READ focusedWindowX NOTIFY focusedWindowGeometryChanged)
|
||||
Q_PROPERTY(int focusedWindowY READ focusedWindowY NOTIFY focusedWindowGeometryChanged)
|
||||
Q_PROPERTY(int focusedWindowWidth READ focusedWindowWidth NOTIFY focusedWindowGeometryChanged)
|
||||
Q_PROPERTY(int focusedWindowHeight READ focusedWindowHeight NOTIFY focusedWindowGeometryChanged)
|
||||
Q_PROPERTY(int focusedWindowDecoX READ focusedWindowDecoX NOTIFY focusedWindowGeometryChanged)
|
||||
Q_PROPERTY(int focusedWindowDecoY READ focusedWindowDecoY NOTIFY focusedWindowGeometryChanged)
|
||||
Q_PROPERTY(int focusedWindowDecoWidth READ focusedWindowDecoWidth NOTIFY focusedWindowGeometryChanged)
|
||||
Q_PROPERTY(int focusedWindowDecoHeight READ focusedWindowDecoHeight NOTIFY focusedWindowGeometryChanged)
|
||||
|
||||
public:
|
||||
enum SnapEdge {
|
||||
@@ -50,6 +60,15 @@ public:
|
||||
QString focusedApplicationId() const;
|
||||
QString focusedApplicationName() const;
|
||||
bool focusedWindowMaximized() const;
|
||||
bool focusedWindowFullscreen() const;
|
||||
int focusedWindowX() const;
|
||||
int focusedWindowY() const;
|
||||
int focusedWindowWidth() const;
|
||||
int focusedWindowHeight() const;
|
||||
int focusedWindowDecoX() const;
|
||||
int focusedWindowDecoY() const;
|
||||
int focusedWindowDecoWidth() const;
|
||||
int focusedWindowDecoHeight() const;
|
||||
|
||||
Q_INVOKABLE bool focusWindow(const QString &windowId);
|
||||
Q_INVOKABLE bool closeWindow(const QString &windowId);
|
||||
@@ -69,9 +88,12 @@ signals:
|
||||
void connectedChanged();
|
||||
void windowCountChanged();
|
||||
void focusChanged();
|
||||
void focusedWindowGeometryChanged();
|
||||
|
||||
private:
|
||||
void syncFromBackend();
|
||||
void syncFocusedGeometry();
|
||||
void publishFocusedGeometry(const WindowInfo *focused);
|
||||
void resolveNames(QVector<WindowInfo> &windows) const;
|
||||
void bindApplicationService();
|
||||
static qint64 parseWindowId(const QString &windowId);
|
||||
@@ -87,4 +109,14 @@ private:
|
||||
QString m_focusedApplicationId;
|
||||
QString m_focusedApplicationName;
|
||||
bool m_focusedWindowMaximized = false;
|
||||
bool m_focusedWindowFullscreen = false;
|
||||
int m_focusedWindowX = 0;
|
||||
int m_focusedWindowY = 0;
|
||||
int m_focusedWindowWidth = 0;
|
||||
int m_focusedWindowHeight = 0;
|
||||
int m_focusedWindowDecoX = 0;
|
||||
int m_focusedWindowDecoY = 0;
|
||||
int m_focusedWindowDecoWidth = 0;
|
||||
int m_focusedWindowDecoHeight = 0;
|
||||
QTimer *m_geometryTimer = nullptr;
|
||||
};
|
||||
|
||||
@@ -22,6 +22,10 @@ struct WindowInfo
|
||||
int y = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int decoX = 0;
|
||||
int decoY = 0;
|
||||
int decoWidth = 0;
|
||||
int decoHeight = 0;
|
||||
};
|
||||
|
||||
struct WorkspaceInfo
|
||||
|
||||
@@ -236,6 +236,52 @@ bool SwayWindowBackend::switchWorkspace(const QString &workspaceId)
|
||||
return run(QStringLiteral("workspace \"%1\"").arg(quoteCriteriaValue(workspaceId)));
|
||||
}
|
||||
|
||||
bool SwayWindowBackend::updateFocusedGeometry()
|
||||
{
|
||||
if (!m_ipc->isConnected() || m_focusedWindowId == 0)
|
||||
return false;
|
||||
|
||||
const QJsonDocument tree = m_ipc->request(SwayIpcClient::GetTree);
|
||||
if (!tree.isObject())
|
||||
return false;
|
||||
|
||||
QVector<WindowInfo> windows;
|
||||
QVector<WorkspaceInfo> workspaces;
|
||||
collect(tree.object(), {}, {}, false, &windows, &workspaces);
|
||||
|
||||
for (WindowInfo &window : windows) {
|
||||
if (window.id != m_focusedWindowId)
|
||||
continue;
|
||||
for (WindowInfo &cached : m_windows) {
|
||||
if (cached.id != window.id)
|
||||
continue;
|
||||
if (cached.x == window.x
|
||||
&& cached.y == window.y
|
||||
&& cached.width == window.width
|
||||
&& cached.height == window.height
|
||||
&& cached.decoX == window.decoX
|
||||
&& cached.decoY == window.decoY
|
||||
&& cached.decoWidth == window.decoWidth
|
||||
&& cached.decoHeight == window.decoHeight
|
||||
&& cached.fullscreen == window.fullscreen)
|
||||
return false;
|
||||
cached.x = window.x;
|
||||
cached.y = window.y;
|
||||
cached.width = window.width;
|
||||
cached.height = window.height;
|
||||
cached.decoX = window.decoX;
|
||||
cached.decoY = window.decoY;
|
||||
cached.decoWidth = window.decoWidth;
|
||||
cached.decoHeight = window.decoHeight;
|
||||
cached.fullscreen = window.fullscreen;
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void SwayWindowBackend::tryConnect()
|
||||
{
|
||||
if (m_ipc->isConnected())
|
||||
@@ -422,7 +468,8 @@ bool SwayWindowBackend::isShellChrome(const QJsonObject &node)
|
||||
const QString name = node.value(QStringLiteral("name")).toString();
|
||||
if (name == QLatin1String("Nebula Desktop")
|
||||
|| name == QLatin1String("Nebula Top Bar")
|
||||
|| name == QLatin1String("Nebula Launcher")) {
|
||||
|| name == QLatin1String("Nebula Launcher")
|
||||
|| name == QLatin1String("Nebula Window Chrome")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -491,6 +538,11 @@ void SwayWindowBackend::collect(const QJsonObject &node,
|
||||
window.y = rect.value(QStringLiteral("y")).toInt();
|
||||
window.width = rect.value(QStringLiteral("width")).toInt();
|
||||
window.height = rect.value(QStringLiteral("height")).toInt();
|
||||
const QJsonObject deco = node.value(QStringLiteral("deco_rect")).toObject();
|
||||
window.decoX = deco.value(QStringLiteral("x")).toInt();
|
||||
window.decoY = deco.value(QStringLiteral("y")).toInt();
|
||||
window.decoWidth = deco.value(QStringLiteral("width")).toInt();
|
||||
window.decoHeight = deco.value(QStringLiteral("height")).toInt();
|
||||
windows->append(window);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ public:
|
||||
bool resizeWindow(qint64 windowId, int width, int height) override;
|
||||
bool snapWindow(qint64 windowId, SnapEdge edge) override;
|
||||
bool switchWorkspace(const QString &workspaceId) override;
|
||||
bool updateFocusedGeometry() override;
|
||||
|
||||
private:
|
||||
void tryConnect();
|
||||
|
||||
Reference in New Issue
Block a user