Files

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);
}