/* SPDX-FileCopyrightText: 2012 Alejandro Fiestas Olivares SPDX-FileCopyrightText: 2016 Sebastian Kügler Work sponsored by the LiMux project of the city of Munich: SPDX-FileCopyrightText: 2018 Kai Uwe Broulik SPDX-License-Identifier: GPL-2.0-or-later */ #include "daemon.h" #include "config.h" #include "device.h" #include "generator.h" #include "kscreen_daemon_debug.h" #include "osdservice_interface.h" #include #include #include #include #include #include #include #include #include #include #include #if WITH_X11 #include #include #include #include #include #endif K_PLUGIN_CLASS_WITH_JSON(KScreenDaemon, "kscreen.json") #if WITH_X11 struct DeviceListDeleter { void operator()(XDeviceInfo *p) { if (p) { XFreeDeviceList(p); } } }; struct XDeleter { void operator()(void *p) { if (p) { XFree(p); } } }; #endif KScreenDaemon::KScreenDaemon(QObject *parent, const QList &) : KDEDModule(parent) , m_monitoring(false) , m_changeCompressor(new QTimer(this)) , m_saveTimer(nullptr) , m_lidClosedTimer(new QTimer(this)) { KScreen::Log::instance(); qMetaTypeId(); QMetaObject::invokeMethod(this, "getInitialConfig", Qt::QueuedConnection); } void KScreenDaemon::getInitialConfig() { connect(new KScreen::GetConfigOperation, &KScreen::GetConfigOperation::finished, this, [this](KScreen::ConfigOperation *op) { if (op->hasError()) { qCDebug(KSCREEN_KDED) << "Error getting initial configuration" << op->errorString(); return; } m_monitoredConfig = std::unique_ptr(new Config(qobject_cast(op)->config())); m_monitoredConfig->setValidityFlags(KScreen::Config::ValidityFlag::RequireAtLeastOneEnabledScreen); qCDebug(KSCREEN_KDED) << "Config" << m_monitoredConfig->data().data() << "is ready"; KScreen::ConfigMonitor::instance()->addConfig(m_monitoredConfig->data()); init(); }); } KScreenDaemon::~KScreenDaemon() { Generator::destroy(); Device::destroy(); } void KScreenDaemon::init() { const QString osdService = QStringLiteral("org.kde.kscreen.osdService"); const QString osdPath = QStringLiteral("/org/kde/kscreen/osdService"); m_osdServiceInterface = new OrgKdeKscreenOsdServiceInterface(osdService, osdPath, QDBusConnection::sessionBus(), this); // Set a longer timeout to not assume timeout while the osd is still shown m_osdServiceInterface->setTimeout(std::chrono::duration_cast(std::chrono::seconds(60)).count()); m_changeCompressor->setInterval(10); m_changeCompressor->setSingleShot(true); connect(m_changeCompressor, &QTimer::timeout, this, &KScreenDaemon::applyConfig); m_lidClosedTimer->setInterval(1000); m_lidClosedTimer->setSingleShot(true); connect(m_lidClosedTimer, &QTimer::timeout, this, &KScreenDaemon::disableLidOutput); connect(Device::self(), &Device::lidClosedChanged, this, &KScreenDaemon::lidClosedChanged); connect(Device::self(), &Device::resumingFromSuspend, this, [this]() { KScreen::Log::instance()->setContext(QStringLiteral("resuming")); qCDebug(KSCREEN_KDED) << "Resumed from suspend, checking for screen changes"; // We don't care about the result, we just want to force the backend // to query XRandR so that it will detect possible changes that happened // while the computer was suspended, and will emit the change events. new KScreen::GetConfigOperation(KScreen::GetConfigOperation::NoEDID, this); }); connect(Device::self(), &Device::aboutToSuspend, this, [this]() { qCDebug(KSCREEN_KDED) << "System is going to suspend, won't be changing config (waited for " << (m_lidClosedTimer->interval() - m_lidClosedTimer->remainingTime()) << "ms)"; m_lidClosedTimer->stop(); }); connect(Generator::self(), &Generator::ready, this, [this] { applyConfig(); if (Device::self()->isLaptop() && Device::self()->isLidClosed()) { disableLidOutput(); } m_startingUp = false; }); Generator::self()->setCurrentConfig(m_monitoredConfig->data()); monitorConnectedChange(); } void KScreenDaemon::doApplyConfig(const KScreen::ConfigPtr &config) { qCDebug(KSCREEN_KDED) << "Do set and apply specific config"; auto configWrapper = std::unique_ptr(new Config(config)); configWrapper->setValidityFlags(KScreen::Config::ValidityFlag::RequireAtLeastOneEnabledScreen); doApplyConfig(std::move(configWrapper)); } void KScreenDaemon::doApplyConfig(std::unique_ptr config) { m_monitoredConfig = std::move(config); m_monitoredConfig->activateControlWatching(); refreshConfig(); } void KScreenDaemon::refreshConfig() { setMonitorForChanges(false); m_configDirty = false; KScreen::ConfigMonitor::instance()->addConfig(m_monitoredConfig->data()); connect(new KScreen::SetConfigOperation(m_monitoredConfig->data()), &KScreen::SetConfigOperation::finished, this, [this]() { qCDebug(KSCREEN_KDED) << "Config applied"; if (m_configDirty) { // Config changed in the meantime again, apply. doApplyConfig(m_monitoredConfig->data()); } else { setMonitorForChanges(true); } }); } void KScreenDaemon::applyConfig() { qCDebug(KSCREEN_KDED) << "Applying config"; if (m_monitoredConfig->fileExists()) { applyKnownConfig(); return; } applyIdealConfig(); } void KScreenDaemon::applyKnownConfig() { qCDebug(KSCREEN_KDED) << "Applying known config"; std::unique_ptr readInConfig = m_monitoredConfig->readFile(); if (readInConfig) { doApplyConfig(std::move(readInConfig)); } else { qCDebug(KSCREEN_KDED) << "Loading failed, falling back to the ideal config" << m_monitoredConfig->id(); applyIdealConfig(); } } void KScreenDaemon::showOSD() { QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kscreen.osdService"), QStringLiteral("/org/kde/kscreen/osdService"), QStringLiteral("org.kde.kscreen.osdService"), QStringLiteral("showActionSelector")); QDBusConnection::sessionBus().asyncCall(message); } void KScreenDaemon::applyIdealConfig() { const bool showOsd = m_monitoredConfig->data()->connectedOutputs().count() > 1 && !m_startingUp; doApplyConfig(Generator::self()->idealConfig(m_monitoredConfig->data())); if (showOsd) { qCDebug(KSCREEN_KDED) << "Getting ideal config from user via OSD..."; showOSD(); } else { m_osdServiceInterface->hideOsd(); } } void KScreenDaemon::configChanged() { qCDebug(KSCREEN_KDED) << "Change detected"; m_monitoredConfig->log(); // Modes may have changed, fix-up current mode id bool changed = false; const auto outputs = m_monitoredConfig->data()->outputs(); for (const KScreen::OutputPtr &output : outputs) { if (output->isConnected() && output->isEnabled() && (output->currentMode().isNull() || (output->followPreferredMode() && output->currentModeId() != output->preferredModeId()))) { qCDebug(KSCREEN_KDED) << "Current mode was" << output->currentModeId() << ", setting preferred mode" << output->preferredModeId(); output->setCurrentModeId(output->preferredModeId()); changed = true; } } if (changed) { refreshConfig(); } // Reset timer, delay the writeback if (!m_saveTimer) { m_saveTimer = new QTimer(this); m_saveTimer->setInterval(300); m_saveTimer->setSingleShot(true); connect(m_saveTimer, &QTimer::timeout, this, &KScreenDaemon::saveCurrentConfig); } m_saveTimer->start(); #if WITH_X11 alignX11TouchScreen(); #endif } #if WITH_X11 void KScreenDaemon::alignX11TouchScreen() { if (qGuiApp->platformName() != QStringLiteral("xcb")) { return; } auto *display = QX11Info::display(); if (!display) { return; } auto *connection = QX11Info::connection(); if (!connection) { return; } const QRect totalRect(QPoint(0, 0), m_monitoredConfig->data()->screen()->currentSize()); QRect internalOutputRect; int touchScreenRotationAngle = 0; for (const auto &output : m_monitoredConfig->data()->connectedOutputs()) { if (output->isEnabled() && output->type() == KScreen::Output::Panel) { internalOutputRect = output->geometry(); switch (output->rotation()) { case KScreen::Output::Left: touchScreenRotationAngle = 90; break; case KScreen::Output::Right: touchScreenRotationAngle = 270; break; case KScreen::Output::Inverted: touchScreenRotationAngle = 180; break; default: touchScreenRotationAngle = 0; } } } // Compute the transformation matrix for the QTransform transform; transform = transform.translate(float(internalOutputRect.x()) / float(totalRect.width()), float(internalOutputRect.y()) / float(totalRect.height())); transform = transform.scale(float(internalOutputRect.width()) / float(totalRect.width()), float(internalOutputRect.height()) / float(totalRect.height())); transform = transform.rotate(touchScreenRotationAngle); // After rotation we need to make the matrix origin aligned with the workspace again // ____ ___ // |__| -> 90° clockwise -> ___ -> needs to be moved up -> | | // | | |_| // |_| switch (touchScreenRotationAngle) { case 90: transform = transform.translate(0, -1); break; case 270: transform = transform.translate(-1, 0); break; case 180: transform = transform.translate(-1, -1); break; default: break; } auto getAtom = [](xcb_connection_t *connection, const char *name) { auto cookie = xcb_intern_atom(connection, true, strlen(name), name); auto reply = xcb_intern_atom_reply(connection, cookie, nullptr); if (reply) { return reply->atom; } else { return xcb_atom_t(0); } }; int nDevices = 0; std::unique_ptr deviceInfo(XListInputDevices(display, &nDevices)); auto touchScreenAtom = getAtom(connection, XI_TOUCHSCREEN); if (touchScreenAtom == 0) { return; } auto matrixAtom = getAtom(connection, "Coordinate Transformation Matrix"); if (matrixAtom == 0) { return; } auto floatAtom = getAtom(connection, "FLOAT"); if (floatAtom == 0) { return; } auto setMatrixAtom = [display, floatAtom](XDeviceInfo *info, Atom atom, const QTransform &transform) { Atom type; int format = 0; unsigned long nItems, bytesAfter; unsigned char *dataPtr = nullptr; std::unique_ptr data(dataPtr); XIGetProperty(display, info->id, atom, 0, 1000, False, AnyPropertyType, &type, &format, &nItems, &bytesAfter, &dataPtr); if (nItems != 9) { return; } if (format != sizeof(float) * CHAR_BIT || type != floatAtom) { return; } float *fData = reinterpret_cast(dataPtr); fData[0] = transform.m11(); fData[1] = transform.m21(); fData[2] = transform.m31(); fData[3] = transform.m12(); fData[4] = transform.m22(); fData[5] = transform.m32(); fData[6] = transform.m13(); fData[7] = transform.m23(); fData[8] = transform.m33(); XIChangeProperty(display, info->id, atom, type, format, PropModeReplace, dataPtr, nItems); }; for (XDeviceInfo *info = deviceInfo.get(); info < deviceInfo.get() + nDevices; info++) { // Make sure device is touchscreen if (info->type != touchScreenAtom) { continue; } int nProperties = 0; std::unique_ptr properties(XIListProperties(display, info->id, &nProperties)); bool matrixAtomFound = false; Atom *atom = properties.get(); Atom *atomEnd = properties.get() + nProperties; for (; atom != atomEnd; atom++) { if (!internalOutputRect.isEmpty() && *atom == matrixAtom) { matrixAtomFound = true; } } if (matrixAtomFound) { setMatrixAtom(info, matrixAtom, transform); } // For now we assume there is only one touchscreen XFlush(display); break; } } #endif void KScreenDaemon::saveCurrentConfig() { qCDebug(KSCREEN_KDED) << "Saving current config to file"; // We assume the config is valid, since it's what we got, but we are interested // in the "at least one enabled screen" check if (m_monitoredConfig->canBeApplied()) { m_monitoredConfig->writeFile(); m_monitoredConfig->log(); } else { qCWarning(KSCREEN_KDED) << "Config does not have at least one screen enabled, WILL NOT save this config, this is not what user wants."; m_monitoredConfig->log(); } } void KScreenDaemon::lidClosedChanged(bool lidIsClosed) { // Ignore this when we don't have any external monitors, we can't turn off our // only screen if (m_monitoredConfig->data()->connectedOutputs().count() == 1) { return; } if (lidIsClosed) { // Lid is closed, now we wait for couple seconds to find out whether it // will trigger a suspend (see Device::aboutToSuspend), or whether we should // turn off the screen qCDebug(KSCREEN_KDED) << "Lid closed, waiting to see if the computer goes to sleep..."; m_lidClosedTimer->start(); return; } else { qCDebug(KSCREEN_KDED) << "Lid opened!"; // We should have a config with "_lidOpened" suffix lying around. If not, // then the configuration has changed while the lid was closed and we just // use applyConfig() and see what we can do ... if (auto openCfg = m_monitoredConfig->readOpenLidFile()) { doApplyConfig(std::move(openCfg)); } } } void KScreenDaemon::disableLidOutput() { // Make sure nothing has changed in the past second... :-) if (!Device::self()->isLidClosed()) { return; } // If we are here, it means that closing the lid did not result in suspend // action. // FIXME: This could be because the suspend took longer than m_lidClosedTimer // timeout. Ideally we need to be able to look into PowerDevil config to see // what's the configured action for lid events, but there's no API to do that // and I'm not parsing PowerDevil's configs... qCDebug(KSCREEN_KDED) << "Lid closed, finding lid to disable"; for (KScreen::OutputPtr &output : m_monitoredConfig->data()->outputs()) { if (output->type() == KScreen::Output::Panel) { if (output->isConnected() && output->isEnabled()) { // Save the current config with opened lid, just so that we know // how to restore it later m_monitoredConfig->writeOpenLidFile(); disableOutput(output); refreshConfig(); return; } } } } void KScreenDaemon::outputConnectedChanged() { if (!m_changeCompressor->isActive()) { m_changeCompressor->start(); } KScreen::Output *output = qobject_cast(sender()); qCDebug(KSCREEN_KDED) << "outputConnectedChanged():" << output->name(); } void KScreenDaemon::outputAddedSlot(const KScreen::OutputPtr &output) { if (output->isConnected()) { m_changeCompressor->start(); } connect(output.data(), &KScreen::Output::isConnectedChanged, this, &KScreenDaemon::outputConnectedChanged, Qt::UniqueConnection); } void KScreenDaemon::monitorConnectedChange() { const KScreen::OutputList outputs = m_monitoredConfig->data()->outputs(); for (const KScreen::OutputPtr &output : outputs) { connect(output.data(), &KScreen::Output::isConnectedChanged, this, &KScreenDaemon::outputConnectedChanged, Qt::UniqueConnection); } connect(m_monitoredConfig->data().data(), &KScreen::Config::outputAdded, this, &KScreenDaemon::outputAddedSlot, Qt::UniqueConnection); connect(m_monitoredConfig->data().data(), &KScreen::Config::outputRemoved, this, &KScreenDaemon::applyConfig, static_cast(Qt::QueuedConnection | Qt::UniqueConnection)); } void KScreenDaemon::setMonitorForChanges(bool enabled) { if (m_monitoring == enabled) { return; } qCDebug(KSCREEN_KDED) << "Monitor for changes: " << enabled; m_monitoring = enabled; if (m_monitoring) { connect(KScreen::ConfigMonitor::instance(), &KScreen::ConfigMonitor::configurationChanged, this, &KScreenDaemon::configChanged, Qt::UniqueConnection); } else { disconnect(KScreen::ConfigMonitor::instance(), &KScreen::ConfigMonitor::configurationChanged, this, &KScreenDaemon::configChanged); } } void KScreenDaemon::disableOutput(const KScreen::OutputPtr &output) { const QRect geom = output->geometry(); qCDebug(KSCREEN_KDED) << "Laptop geometry:" << geom << output->pos() << (output->currentMode() ? output->currentMode()->size() : QSize()); // Move all outputs right from the @p output to left for (KScreen::OutputPtr &otherOutput : m_monitoredConfig->data()->outputs()) { if (otherOutput == output || !otherOutput->isConnected() || !otherOutput->isEnabled()) { continue; } QPoint otherPos = otherOutput->pos(); if (otherPos.x() >= geom.right() && otherPos.y() >= geom.top() && otherPos.y() <= geom.bottom()) { otherPos.setX(otherPos.x() - geom.width()); } qCDebug(KSCREEN_KDED) << "Moving" << otherOutput->name() << "from" << otherOutput->pos() << "to" << otherPos; otherOutput->setPos(otherPos); } // Disable the output output->setEnabled(false); } #include "daemon.moc"