Add desktop app service and VM dev session tooling
Introduces a new `core/applications` QML/C++ module (`Nebula.Applications`) to discover `.desktop` entries, expose filterable application models, and launch installed apps safely from the shell. The desktop launcher now uses real installed apps with themed icon loading and fallback glyphs, adds output-aware surface sizing via `OutputTracker`, and wires in image/icon providers. It also adds VMware-focused Sway/shell wrapper scripts, updates Sway config/session env defaults, and refreshes README docs to document the new development flow and Desktop 0.1 behavior.
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
#include "DesktopIconProvider.hpp"
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <QIcon>
|
||||
#include <QPixmap>
|
||||
#include <QUrl>
|
||||
|
||||
namespace {
|
||||
|
||||
QIcon resolveDesktopIcon(const QString &name)
|
||||
{
|
||||
if (name.isEmpty())
|
||||
return {};
|
||||
|
||||
const QFileInfo direct(name);
|
||||
if (direct.isAbsolute() && direct.exists())
|
||||
return QIcon(name);
|
||||
|
||||
QIcon themed = QIcon::fromTheme(name);
|
||||
if (!themed.isNull())
|
||||
return themed;
|
||||
|
||||
QString base = name;
|
||||
if (base.endsWith(QLatin1String(".png"), Qt::CaseInsensitive)
|
||||
|| base.endsWith(QLatin1String(".svg"), Qt::CaseInsensitive)
|
||||
|| base.endsWith(QLatin1String(".xpm"), Qt::CaseInsensitive)) {
|
||||
base = QFileInfo(base).completeBaseName();
|
||||
themed = QIcon::fromTheme(base);
|
||||
if (!themed.isNull())
|
||||
return themed;
|
||||
}
|
||||
|
||||
const QStringList pixmapDirs = {
|
||||
QStringLiteral("/usr/share/pixmaps"),
|
||||
QStringLiteral("/usr/share/icons/hicolor/48x48/apps"),
|
||||
QStringLiteral("/usr/share/icons/hicolor/scalable/apps"),
|
||||
};
|
||||
const QStringList suffixes = {
|
||||
QString(),
|
||||
QStringLiteral(".png"),
|
||||
QStringLiteral(".svg"),
|
||||
QStringLiteral(".xpm"),
|
||||
};
|
||||
|
||||
for (const QString &dir : pixmapDirs) {
|
||||
for (const QString &suffix : suffixes) {
|
||||
const QString candidate = dir + QLatin1Char('/') + (suffix.isEmpty() ? name : base) + suffix;
|
||||
if (QFileInfo::exists(candidate))
|
||||
return QIcon(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
DesktopIconProvider::DesktopIconProvider()
|
||||
: QQuickImageProvider(QQuickImageProvider::Pixmap)
|
||||
{
|
||||
}
|
||||
|
||||
QPixmap DesktopIconProvider::requestPixmap(const QString &id, QSize *size, const QSize &requestedSize)
|
||||
{
|
||||
const QString iconName = QUrl::fromPercentEncoding(id.toUtf8());
|
||||
const QIcon icon = resolveDesktopIcon(iconName);
|
||||
const QSize target = requestedSize.isValid() && requestedSize.width() > 0 && requestedSize.height() > 0
|
||||
? requestedSize
|
||||
: QSize(48, 48);
|
||||
|
||||
if (icon.isNull()) {
|
||||
if (size)
|
||||
*size = target;
|
||||
return {};
|
||||
}
|
||||
|
||||
const QPixmap pixmap = icon.pixmap(target);
|
||||
if (pixmap.isNull()) {
|
||||
if (size)
|
||||
*size = target;
|
||||
return {};
|
||||
}
|
||||
|
||||
if (size)
|
||||
*size = pixmap.size();
|
||||
return pixmap;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <QQuickImageProvider>
|
||||
|
||||
class DesktopIconProvider : public QQuickImageProvider
|
||||
{
|
||||
public:
|
||||
DesktopIconProvider();
|
||||
QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) override;
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
#include "OutputTracker.hpp"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QGuiApplication>
|
||||
#include <QJSEngine>
|
||||
#include <QRect>
|
||||
|
||||
OutputTracker::OutputTracker(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
if (auto *app = qobject_cast<QGuiApplication *>(QCoreApplication::instance())) {
|
||||
connect(app, &QGuiApplication::primaryScreenChanged, this, [this](QScreen *screen) {
|
||||
bindScreen(screen);
|
||||
});
|
||||
connect(app, &QGuiApplication::screenAdded, this, [this](QScreen *) {
|
||||
if (!m_screen)
|
||||
bindScreen(QGuiApplication::primaryScreen());
|
||||
});
|
||||
connect(app, &QGuiApplication::screenRemoved, this, [this](QScreen *screen) {
|
||||
if (m_screen == screen)
|
||||
bindScreen(QGuiApplication::primaryScreen());
|
||||
});
|
||||
bindScreen(app->primaryScreen());
|
||||
}
|
||||
}
|
||||
|
||||
OutputTracker *OutputTracker::create(QQmlEngine *engine, QJSEngine *)
|
||||
{
|
||||
auto *tracker = new OutputTracker(engine);
|
||||
QQmlEngine::setObjectOwnership(tracker, QQmlEngine::CppOwnership);
|
||||
return tracker;
|
||||
}
|
||||
|
||||
void OutputTracker::bindScreen(QScreen *screen)
|
||||
{
|
||||
if (m_screen == screen)
|
||||
return;
|
||||
|
||||
if (m_screen) {
|
||||
disconnect(m_screen, nullptr, this, nullptr);
|
||||
}
|
||||
|
||||
m_screen = screen;
|
||||
if (m_screen) {
|
||||
connect(m_screen, &QScreen::geometryChanged, this, [this](const QRect &) {
|
||||
logGeometry();
|
||||
emit geometryChanged();
|
||||
});
|
||||
connect(m_screen, &QScreen::physicalDotsPerInchChanged, this, &OutputTracker::geometryChanged);
|
||||
}
|
||||
|
||||
logGeometry();
|
||||
emit geometryChanged();
|
||||
}
|
||||
|
||||
void OutputTracker::logGeometry() const
|
||||
{
|
||||
if (!m_screen) {
|
||||
qWarning("nebula-shell: no Wayland output is available yet");
|
||||
return;
|
||||
}
|
||||
|
||||
const QRect geo = m_screen->geometry();
|
||||
qInfo("nebula-shell: output %s %dx%d+%d+%d scale %g",
|
||||
qPrintable(m_screen->name()),
|
||||
geo.width(),
|
||||
geo.height(),
|
||||
geo.x(),
|
||||
geo.y(),
|
||||
m_screen->devicePixelRatio());
|
||||
}
|
||||
|
||||
int OutputTracker::width() const
|
||||
{
|
||||
return m_screen ? m_screen->geometry().width() : 0;
|
||||
}
|
||||
|
||||
int OutputTracker::height() const
|
||||
{
|
||||
return m_screen ? m_screen->geometry().height() : 0;
|
||||
}
|
||||
|
||||
qreal OutputTracker::devicePixelRatio() const
|
||||
{
|
||||
return m_screen ? m_screen->devicePixelRatio() : 1.0;
|
||||
}
|
||||
|
||||
QString OutputTracker::name() const
|
||||
{
|
||||
return m_screen ? m_screen->name() : QString();
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QQmlEngine>
|
||||
#include <QScreen>
|
||||
#include <QtQml/qqmlregistration.h>
|
||||
|
||||
class QJSEngine;
|
||||
|
||||
class OutputTracker : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_SINGLETON
|
||||
Q_PROPERTY(int width READ width NOTIFY geometryChanged)
|
||||
Q_PROPERTY(int height READ height NOTIFY geometryChanged)
|
||||
Q_PROPERTY(qreal devicePixelRatio READ devicePixelRatio NOTIFY geometryChanged)
|
||||
Q_PROPERTY(QString name READ name NOTIFY geometryChanged)
|
||||
|
||||
public:
|
||||
explicit OutputTracker(QObject *parent = nullptr);
|
||||
static OutputTracker *create(QQmlEngine *engine, QJSEngine *scriptEngine);
|
||||
|
||||
int width() const;
|
||||
int height() const;
|
||||
qreal devicePixelRatio() const;
|
||||
QString name() const;
|
||||
|
||||
signals:
|
||||
void geometryChanged();
|
||||
|
||||
private:
|
||||
void bindScreen(QScreen *screen);
|
||||
void logGeometry() const;
|
||||
|
||||
QPointer<QScreen> m_screen;
|
||||
};
|
||||
@@ -1,11 +1,48 @@
|
||||
#include "DesktopIconProvider.hpp"
|
||||
|
||||
#include <LayerShellQt/Shell>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QGuiApplication>
|
||||
#include <QIcon>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQuickWindow>
|
||||
#include <QUrl>
|
||||
#include <QtGlobal>
|
||||
|
||||
namespace {
|
||||
|
||||
void initializeIconTheme()
|
||||
{
|
||||
if (QIcon::themeName().isEmpty()) {
|
||||
const QStringList candidates = {
|
||||
QStringLiteral("Adwaita"),
|
||||
QStringLiteral("Yaru"),
|
||||
QStringLiteral("ubuntu-mono-dark"),
|
||||
QStringLiteral("breeze"),
|
||||
QStringLiteral("hicolor"),
|
||||
};
|
||||
|
||||
const QStringList searchPaths = QIcon::themeSearchPaths();
|
||||
for (const QString &theme : candidates) {
|
||||
for (const QString &root : searchPaths) {
|
||||
if (QDir(root + QLatin1Char('/') + theme).exists()) {
|
||||
QIcon::setThemeName(theme);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!QIcon::themeName().isEmpty())
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (QIcon::fallbackThemeName().isEmpty())
|
||||
QIcon::setFallbackThemeName(QStringLiteral("hicolor"));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
LayerShellQt::Shell::useLayerShell();
|
||||
@@ -16,10 +53,12 @@ int main(int argc, char *argv[])
|
||||
app.setOrganizationName(QStringLiteral("NebulaOS"));
|
||||
app.setDesktopFileName(QStringLiteral("org.nebulaos.shell"));
|
||||
|
||||
initializeIconTheme();
|
||||
QQuickWindow::setDefaultAlphaBuffer(true);
|
||||
|
||||
QQmlApplicationEngine engine;
|
||||
engine.addImportPath(QStringLiteral("qrc:/qt/qml"));
|
||||
engine.addImageProvider(QStringLiteral("desktopicon"), new DesktopIconProvider);
|
||||
|
||||
QObject::connect(
|
||||
&engine,
|
||||
|
||||
Reference in New Issue
Block a user