#include "WindowService.hpp" #include "ApplicationService.hpp" #include "SwayWindowBackend.hpp" #include #include #include 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(edge)); } bool WindowService::switchWorkspace(const QString &workspaceId) { return m_backend->switchWorkspace(workspaceId); } void WindowService::syncFromBackend() { bindApplicationService(); QVector 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 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 &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 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(' ')); }