329 lines
12 KiB
C++
329 lines
12 KiB
C++
#include "platform.hpp"
|
|
|
|
#include <LayerShellQt/Window>
|
|
#include <QCoreApplication>
|
|
#include <QDBusConnection>
|
|
#include <QDBusInterface>
|
|
#include <QDBusMessage>
|
|
#include <QDBusReply>
|
|
#include <QDBusServiceWatcher>
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QGuiApplication>
|
|
#include <QScreen>
|
|
#include <QSettings>
|
|
#include <QtQml/qqml.h>
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
namespace {
|
|
constexpr auto scriptName = "jp.angeltech.yume4kde.WindowSurfaces";
|
|
constexpr auto applicationService = "jp.angeltech.yume4kde";
|
|
QString quotedScriptValue(const QString &value)
|
|
{
|
|
QString result = value;
|
|
result.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n").replace('\r', "\\r");
|
|
return result;
|
|
}
|
|
}
|
|
|
|
void yume4kde::prepare_application()
|
|
{
|
|
qputenv("QT_QPA_PLATFORM", "wayland");
|
|
qputenv("QT_QUICK_CONTROLS_STYLE", "Basic");
|
|
}
|
|
|
|
double yume4kde::load_size_factor()
|
|
{
|
|
QSettings settings(QSettings::IniFormat, QSettings::UserScope,
|
|
QStringLiteral("jp.angeltech"), QStringLiteral("yume4kde"));
|
|
bool ok = false;
|
|
const double factor = settings.value(QStringLiteral("size_factor"), 1.0).toDouble(&ok);
|
|
if (!ok || !std::isfinite(factor))
|
|
return 1.0;
|
|
return factor;
|
|
}
|
|
|
|
void yume4kde::save_size_factor(double factor)
|
|
{
|
|
if (!std::isfinite(factor)) {
|
|
qWarning() << "Cannot save non-finite size factor.";
|
|
return;
|
|
}
|
|
QSettings settings(QSettings::IniFormat, QSettings::UserScope,
|
|
QStringLiteral("jp.angeltech"), QStringLiteral("yume4kde"));
|
|
settings.setValue(QStringLiteral("size_factor"), factor);
|
|
settings.sync();
|
|
if (settings.status() != QSettings::NoError)
|
|
qWarning() << "Failed to save size factor:" << settings.status();
|
|
}
|
|
|
|
void yume4kde::register_platform_types()
|
|
{
|
|
QGuiApplication::setDesktopFileName(applicationService);
|
|
QGuiApplication::setQuitOnLastWindowClosed(false);
|
|
qmlRegisterType<DesktopIntegration>("Yume4kde.Platform", 1, 0, "DesktopIntegration");
|
|
}
|
|
|
|
DesktopIntegration::DesktopIntegration(QObject *parent) : QObject(parent)
|
|
{
|
|
m_inputUpdate.setSingleShot(true);
|
|
connect(&m_inputUpdate, &QTimer::timeout, this, &DesktopIntegration::updateInputRegion);
|
|
m_snapshotDeadline.setSingleShot(true);
|
|
connect(&m_snapshotDeadline, &QTimer::timeout, this, [this] {
|
|
fail(QStringLiteral("KWin did not provide window data. Check the KWin scripting service."));
|
|
});
|
|
connect(qGuiApp, &QGuiApplication::primaryScreenChanged, this, [this] { selectPrimaryScreen(); });
|
|
connect(qGuiApp, &QCoreApplication::aboutToQuit, this, &DesktopIntegration::unloadWindowScript);
|
|
}
|
|
|
|
DesktopIntegration::~DesktopIntegration()
|
|
{
|
|
unloadWindowScript();
|
|
if (m_registered) {
|
|
auto bus = QDBusConnection::sessionBus();
|
|
bus.unregisterObject(QStringLiteral("/Desktop"));
|
|
bus.unregisterService(applicationService);
|
|
}
|
|
}
|
|
|
|
void DesktopIntegration::fail(const QString &message)
|
|
{
|
|
qCritical().noquote() << message;
|
|
QTimer::singleShot(0, qGuiApp, [] { QCoreApplication::exit(1); });
|
|
}
|
|
|
|
void DesktopIntegration::setWindow(QQuickWindow *window)
|
|
{
|
|
if (m_window == window)
|
|
return;
|
|
m_window = window;
|
|
emit windowChanged();
|
|
if (!window)
|
|
return;
|
|
if (QGuiApplication::platformName() != QStringLiteral("wayland")) {
|
|
fail(QStringLiteral("yume4kde requires a KDE Wayland session."));
|
|
return;
|
|
}
|
|
auto bus = QDBusConnection::sessionBus();
|
|
if (!bus.registerService(applicationService)) {
|
|
fail(QStringLiteral("Another yume4kde instance is already running, or the session bus is unavailable."));
|
|
return;
|
|
}
|
|
m_registered = true;
|
|
if (!bus.registerObject(QStringLiteral("/Desktop"), this, QDBusConnection::ExportScriptableSlots)) {
|
|
fail(QStringLiteral("Cannot register the KWin callback interface."));
|
|
return;
|
|
}
|
|
auto *kwinWatcher = new QDBusServiceWatcher(QStringLiteral("org.kde.KWin"), bus,
|
|
QDBusServiceWatcher::WatchForOwnerChange, this);
|
|
connect(kwinWatcher, &QDBusServiceWatcher::serviceOwnerChanged, this,
|
|
[this](const QString &, const QString &, const QString &newOwner) {
|
|
m_ready = false;
|
|
emit readyChanged();
|
|
if (!newOwner.isEmpty())
|
|
loadWindowScript();
|
|
});
|
|
selectPrimaryScreen();
|
|
}
|
|
|
|
void DesktopIntegration::selectPrimaryScreen()
|
|
{
|
|
if (!m_window)
|
|
return;
|
|
QScreen *primary = QGuiApplication::primaryScreen();
|
|
if (!primary) {
|
|
fail(QStringLiteral("No primary screen is available."));
|
|
return;
|
|
}
|
|
const bool primaryScreenChanged = m_screen != primary;
|
|
if (m_screen)
|
|
disconnect(m_screen, nullptr, this, nullptr);
|
|
m_screen = primary;
|
|
m_screenGeometry = primary->geometry();
|
|
m_window->setScreen(primary);
|
|
m_window->setColor(Qt::transparent);
|
|
m_window->resize(m_screenGeometry.size().toSize());
|
|
auto layer = LayerShellQt::Window::get(m_window);
|
|
layer->setScope(QStringLiteral("utility"));
|
|
layer->setScreen(primary);
|
|
layer->setAnchors(LayerShellQt::Window::Anchors(LayerShellQt::Window::AnchorTop)
|
|
| LayerShellQt::Window::AnchorBottom | LayerShellQt::Window::AnchorLeft
|
|
| LayerShellQt::Window::AnchorRight);
|
|
layer->setExclusiveZone(-1);
|
|
layer->setKeyboardInteractivity(LayerShellQt::Window::KeyboardInteractivityNone);
|
|
layer->setLayer(LayerShellQt::Window::LayerOverlay);
|
|
layer->setCloseOnDismissed(false);
|
|
connect(primary, &QScreen::geometryChanged, this, [this] { selectPrimaryScreen(); });
|
|
emit screenGeometryChanged();
|
|
updateInputRegion();
|
|
if (primaryScreenChanged)
|
|
loadWindowScript();
|
|
}
|
|
|
|
void DesktopIntegration::loadWindowScript()
|
|
{
|
|
if (!m_registered || !m_screen)
|
|
return;
|
|
unloadWindowScript();
|
|
QFile resource(QStringLiteral(":/kwin/window-surfaces.js"));
|
|
if (!resource.open(QIODevice::ReadOnly)) {
|
|
fail(QStringLiteral("The embedded KWin helper is missing."));
|
|
return;
|
|
}
|
|
QByteArray script = resource.readAll();
|
|
script.replace("@CALLBACK_SERVICE@", quotedScriptValue(QString::fromLatin1(applicationService)).toUtf8());
|
|
script.replace("@OUTPUT_NAME@", quotedScriptValue(m_screen->name()).toUtf8());
|
|
QString scriptDirectory = QDir::tempPath();
|
|
const QString flatpakId = qEnvironmentVariable("FLATPAK_ID");
|
|
if (!flatpakId.isEmpty()) {
|
|
scriptDirectory = qEnvironmentVariable("XDG_RUNTIME_DIR") + QStringLiteral("/app/") + flatpakId;
|
|
if (!QDir(scriptDirectory).exists()) {
|
|
fail(QStringLiteral("The Flatpak runtime directory is unavailable."));
|
|
return;
|
|
}
|
|
}
|
|
m_scriptFile = std::make_unique<QTemporaryFile>(scriptDirectory + QStringLiteral("/yume4kde-XXXXXX.js"));
|
|
if (!m_scriptFile->open() || m_scriptFile->write(script) != script.size() || !m_scriptFile->flush()) {
|
|
fail(QStringLiteral("Cannot create the temporary KWin helper."));
|
|
return;
|
|
}
|
|
QDBusInterface scripting(QStringLiteral("org.kde.KWin"), QStringLiteral("/Scripting"),
|
|
QStringLiteral("org.kde.kwin.Scripting"));
|
|
scripting.call(QStringLiteral("unloadScript"), QString::fromLatin1(scriptName));
|
|
QDBusReply<int> loaded = scripting.call(QStringLiteral("loadScript"), m_scriptFile->fileName(),
|
|
QString::fromLatin1(scriptName));
|
|
if (!loaded.isValid() || loaded.value() < 0) {
|
|
fail(QStringLiteral("Cannot load the KWin helper: ") + loaded.error().message());
|
|
return;
|
|
}
|
|
QDBusInterface instance(QStringLiteral("org.kde.KWin"),
|
|
QStringLiteral("/Scripting/Script%1").arg(loaded.value()), QStringLiteral("org.kde.kwin.Script"));
|
|
const auto result = instance.call(QStringLiteral("run"));
|
|
if (result.type() == QDBusMessage::ErrorMessage) {
|
|
fail(QStringLiteral("Cannot start the KWin helper: ") + result.errorMessage());
|
|
return;
|
|
}
|
|
m_ready = true;
|
|
emit readyChanged();
|
|
m_snapshotDeadline.start(5000);
|
|
}
|
|
|
|
void DesktopIntegration::unloadWindowScript()
|
|
{
|
|
m_snapshotDeadline.stop();
|
|
if (!m_scriptFile)
|
|
return;
|
|
QDBusInterface scripting(QStringLiteral("org.kde.KWin"), QStringLiteral("/Scripting"),
|
|
QStringLiteral("org.kde.kwin.Scripting"));
|
|
scripting.call(QStringLiteral("unloadScript"), QString::fromLatin1(scriptName));
|
|
m_scriptFile.reset();
|
|
}
|
|
|
|
void DesktopIntegration::updateDesktop(const QString &snapshot)
|
|
{
|
|
m_snapshotDeadline.stop();
|
|
emit snapshotChanged(snapshot);
|
|
}
|
|
|
|
void DesktopIntegration::notifyCursorMovement()
|
|
{
|
|
emit cursorMoved();
|
|
}
|
|
|
|
const QImage &DesktopIntegration::image(const QString &source)
|
|
{
|
|
auto iterator = m_images.find(source);
|
|
if (iterator == m_images.end()) {
|
|
QString path = source;
|
|
if (path.startsWith(QStringLiteral("qrc:/")))
|
|
path.remove(0, 3);
|
|
iterator = m_images.insert(source, QImage(path).convertToFormat(QImage::Format_ARGB32));
|
|
}
|
|
return iterator.value();
|
|
}
|
|
|
|
bool DesktopIntegration::hitTest(const QString &source, double x, double y,
|
|
double width, double height, bool mirrored)
|
|
{
|
|
const QImage &pixels = image(source);
|
|
if (pixels.isNull() || width <= 0 || height <= 0 || x < 0 || y < 0 || x >= width || y >= height)
|
|
return false;
|
|
int imageX = std::min(pixels.width() - 1, int(x * pixels.width() / width));
|
|
const int imageY = std::min(pixels.height() - 1, int(y * pixels.height() / height));
|
|
if (mirrored)
|
|
imageX = pixels.width() - imageX - 1;
|
|
return qAlpha(pixels.pixel(imageX, imageY)) != 0;
|
|
}
|
|
|
|
QRegion DesktopIntegration::imageRegion(const ActorRegion &actor)
|
|
{
|
|
const QString key = actor.source + QStringLiteral("|%1,%2,%3").arg(actor.size.width()).arg(actor.size.height()).arg(actor.mirrored);
|
|
auto iterator = m_imageRegions.find(key);
|
|
if (iterator != m_imageRegions.end())
|
|
return iterator.value();
|
|
const QImage &original = image(actor.source);
|
|
if (original.isNull() || actor.size.isEmpty())
|
|
return {};
|
|
QImage pixels = original.scaled(actor.size, Qt::IgnoreAspectRatio, Qt::FastTransformation);
|
|
if (actor.mirrored)
|
|
pixels = pixels.flipped(Qt::Horizontal);
|
|
QRegion region;
|
|
for (int row = 0; row < pixels.height(); ++row) {
|
|
const auto *line = reinterpret_cast<const QRgb *>(pixels.constScanLine(row));
|
|
int column = 0;
|
|
while (column < pixels.width()) {
|
|
while (column < pixels.width() && qAlpha(line[column]) == 0)
|
|
++column;
|
|
const int start = column;
|
|
while (column < pixels.width() && qAlpha(line[column]) != 0)
|
|
++column;
|
|
if (column > start)
|
|
region += QRect(start, row, column - start, 1);
|
|
}
|
|
}
|
|
m_imageRegions.insert(key, region);
|
|
return region;
|
|
}
|
|
|
|
void DesktopIntegration::setActorRegion(int actorId, double x, double y, double width, double height,
|
|
const QString &source, bool mirrored)
|
|
{
|
|
m_actors.insert(actorId, {QPoint(qRound(x), qRound(y)), QSize(qRound(width), qRound(height)), source, mirrored});
|
|
scheduleInputRegion();
|
|
}
|
|
|
|
void DesktopIntegration::removeActorRegion(int actorId)
|
|
{
|
|
m_actors.remove(actorId);
|
|
scheduleInputRegion();
|
|
}
|
|
|
|
void DesktopIntegration::setMenuOpen(bool open)
|
|
{
|
|
m_menuOpen = open;
|
|
scheduleInputRegion();
|
|
}
|
|
|
|
void DesktopIntegration::scheduleInputRegion()
|
|
{
|
|
if (!m_inputUpdate.isActive())
|
|
m_inputUpdate.start(0);
|
|
}
|
|
|
|
void DesktopIntegration::updateInputRegion()
|
|
{
|
|
if (!m_window)
|
|
return;
|
|
QRegion region;
|
|
if (m_menuOpen) {
|
|
region = QRegion(QRect(QPoint(), m_window->size()));
|
|
} else {
|
|
for (const ActorRegion &actor : std::as_const(m_actors))
|
|
region += imageRegion(actor).translated(actor.position);
|
|
}
|
|
if (region.isEmpty())
|
|
region = QRegion(-2, -2, 1, 1);
|
|
m_window->setMask(region);
|
|
}
|