Files
Nebula-OS/core/windows/WindowService.cpp
T
andrew 08e67845a5 Add focused window controls to top bar
Introduces a new `WindowControls` component in the desktop top bar with minimize, maximize/restore, and close actions for the focused window. `WindowService` now exposes focused-window presence/maximized state plus focused-window convenience actions to support this UI.

Also extends Nebula UI with a reusable `NebulaToolTip`, keyboard/pressed-state support in `NebulaIconButton`, new window-control glyphs in `NebulaIcon`, and theme tokens for pressed/danger hover states, with CMake and README updates to register/document the new components.
2026-08-26 22:26:29 +12:00

304 lines
8.2 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))
{
connect(m_backend, &WindowBackend::connectedChanged, this, &WindowService::connectedChanged);
connect(m_backend, &WindowBackend::connectedChanged, this, &WindowService::syncFromBackend);
connect(m_backend, &WindowBackend::windowsChanged, this, &WindowService::syncFromBackend);
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::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;
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 (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();
}
}
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(' '));
}