File indexing completed on 2024-05-05 05:30:25

0001 /*
0002     SPDX-FileCopyrightText: 2012 Alejandro Fiestas Olivares <afiestas@kde.org>
0003     SPDX-FileCopyrightText: 2016 Sebastian Kügler <sebas@kde.org>
0004 
0005     Work sponsored by the LiMux project of the city of Munich:
0006     SPDX-FileCopyrightText: 2018 Kai Uwe Broulik <kde@broulik.de>
0007 
0008     SPDX-License-Identifier: GPL-2.0-or-later
0009 */
0010 #include "daemon.h"
0011 
0012 #include "config.h"
0013 #include "device.h"
0014 #include "generator.h"
0015 #include "kscreen_daemon_debug.h"
0016 #include "osdservice_interface.h"
0017 
0018 #include <kscreen/configmonitor.h>
0019 #include <kscreen/getconfigoperation.h>
0020 #include <kscreen/log.h>
0021 #include <kscreen/mode.h>
0022 #include <kscreen/output.h>
0023 #include <kscreen/screen.h>
0024 #include <kscreen/setconfigoperation.h>
0025 
0026 #include <KPluginFactory>
0027 
0028 #include <QGuiApplication>
0029 #include <QTimer>
0030 #include <QTransform>
0031 
0032 #if HAVE_X11
0033 #include <QtGui/private/qtx11extras_p.h>
0034 #include <X11/Xatom.h>
0035 #include <X11/Xlib-xcb.h>
0036 #include <X11/extensions/XInput.h>
0037 #include <X11/extensions/XInput2.h>
0038 #endif
0039 
0040 K_PLUGIN_CLASS_WITH_JSON(KScreenDaemon, "kscreen.json")
0041 
0042 #if HAVE_X11
0043 struct DeviceListDeleter {
0044     void operator()(XDeviceInfo *p)
0045     {
0046         if (p) {
0047             XFreeDeviceList(p);
0048         }
0049     }
0050 };
0051 
0052 struct XDeleter {
0053     void operator()(void *p)
0054     {
0055         if (p) {
0056             XFree(p);
0057         }
0058     }
0059 };
0060 #endif
0061 
0062 KScreenDaemon::KScreenDaemon(QObject *parent, const QList<QVariant> &)
0063     : KDEDModule(parent)
0064     , m_monitoring(false)
0065     , m_changeCompressor(new QTimer(this))
0066     , m_saveTimer(nullptr)
0067     , m_lidClosedTimer(new QTimer(this))
0068 {
0069     KScreen::Log::instance();
0070     qMetaTypeId<KScreen::OsdAction>();
0071     QMetaObject::invokeMethod(this, "getInitialConfig", Qt::QueuedConnection);
0072 }
0073 
0074 void KScreenDaemon::getInitialConfig()
0075 {
0076     connect(new KScreen::GetConfigOperation, &KScreen::GetConfigOperation::finished, this, [this](KScreen::ConfigOperation *op) {
0077         if (op->hasError()) {
0078             qCDebug(KSCREEN_KDED) << "Error getting initial configuration" << op->errorString();
0079             return;
0080         }
0081 
0082         m_monitoredConfig = std::unique_ptr<Config>(new Config(qobject_cast<KScreen::GetConfigOperation *>(op)->config()));
0083         m_monitoredConfig->setValidityFlags(KScreen::Config::ValidityFlag::RequireAtLeastOneEnabledScreen);
0084         qCDebug(KSCREEN_KDED) << "Config" << m_monitoredConfig->data().data() << "is ready";
0085         KScreen::ConfigMonitor::instance()->addConfig(m_monitoredConfig->data());
0086 
0087         init();
0088     });
0089 }
0090 
0091 KScreenDaemon::~KScreenDaemon()
0092 {
0093     Generator::destroy();
0094     Device::destroy();
0095 }
0096 
0097 void KScreenDaemon::init()
0098 {
0099     const QString osdService = QStringLiteral("org.kde.kscreen.osdService");
0100     const QString osdPath = QStringLiteral("/org/kde/kscreen/osdService");
0101     m_osdServiceInterface = new OrgKdeKscreenOsdServiceInterface(osdService, osdPath, QDBusConnection::sessionBus(), this);
0102     // Set a longer timeout to not assume timeout while the osd is still shown
0103     m_osdServiceInterface->setTimeout(std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::seconds(60)).count());
0104 
0105     m_changeCompressor->setInterval(10);
0106     m_changeCompressor->setSingleShot(true);
0107     connect(m_changeCompressor, &QTimer::timeout, this, &KScreenDaemon::applyConfig);
0108 
0109     m_lidClosedTimer->setInterval(1000);
0110     m_lidClosedTimer->setSingleShot(true);
0111     connect(m_lidClosedTimer, &QTimer::timeout, this, &KScreenDaemon::disableLidOutput);
0112 
0113     connect(Device::self(), &Device::lidClosedChanged, this, &KScreenDaemon::lidClosedChanged);
0114     connect(Device::self(), &Device::resumingFromSuspend, this, [this]() {
0115         KScreen::Log::instance()->setContext(QStringLiteral("resuming"));
0116         qCDebug(KSCREEN_KDED) << "Resumed from suspend, checking for screen changes";
0117         // We don't care about the result, we just want to force the backend
0118         // to query XRandR so that it will detect possible changes that happened
0119         // while the computer was suspended, and will emit the change events.
0120         new KScreen::GetConfigOperation(KScreen::GetConfigOperation::NoEDID, this);
0121     });
0122     connect(Device::self(), &Device::aboutToSuspend, this, [this]() {
0123         qCDebug(KSCREEN_KDED) << "System is going to suspend, won't be changing config (waited for "
0124                               << (m_lidClosedTimer->interval() - m_lidClosedTimer->remainingTime()) << "ms)";
0125         m_lidClosedTimer->stop();
0126     });
0127 
0128     connect(Generator::self(), &Generator::ready, this, [this] {
0129         applyConfig();
0130 
0131         if (Device::self()->isLaptop() && Device::self()->isLidClosed()) {
0132             disableLidOutput();
0133         }
0134 
0135         m_startingUp = false;
0136     });
0137 
0138     Generator::self()->setCurrentConfig(m_monitoredConfig->data());
0139     monitorConnectedChange();
0140 }
0141 
0142 void KScreenDaemon::doApplyConfig(const KScreen::ConfigPtr &config)
0143 {
0144     qCDebug(KSCREEN_KDED) << "Do set and apply specific config";
0145     auto configWrapper = std::unique_ptr<Config>(new Config(config));
0146     configWrapper->setValidityFlags(KScreen::Config::ValidityFlag::RequireAtLeastOneEnabledScreen);
0147 
0148     doApplyConfig(std::move(configWrapper));
0149 }
0150 
0151 void KScreenDaemon::doApplyConfig(std::unique_ptr<Config> config)
0152 {
0153     m_monitoredConfig = std::move(config);
0154 
0155     m_monitoredConfig->activateControlWatching();
0156 
0157     refreshConfig();
0158 }
0159 
0160 void KScreenDaemon::refreshConfig()
0161 {
0162     setMonitorForChanges(false);
0163     m_configDirty = false;
0164     KScreen::ConfigMonitor::instance()->addConfig(m_monitoredConfig->data());
0165 
0166     connect(new KScreen::SetConfigOperation(m_monitoredConfig->data()), &KScreen::SetConfigOperation::finished, this, [this]() {
0167         qCDebug(KSCREEN_KDED) << "Config applied";
0168         if (m_configDirty) {
0169             // Config changed in the meantime again, apply.
0170             doApplyConfig(m_monitoredConfig->data());
0171         } else {
0172             setMonitorForChanges(true);
0173         }
0174     });
0175 }
0176 
0177 void KScreenDaemon::applyConfig()
0178 {
0179     qCDebug(KSCREEN_KDED) << "Applying config";
0180     if (m_monitoredConfig->fileExists()) {
0181         applyKnownConfig();
0182         return;
0183     }
0184     applyIdealConfig();
0185 }
0186 
0187 void KScreenDaemon::applyKnownConfig()
0188 {
0189     qCDebug(KSCREEN_KDED) << "Applying known config";
0190 
0191     std::unique_ptr<Config> readInConfig = m_monitoredConfig->readFile();
0192     if (readInConfig) {
0193         doApplyConfig(std::move(readInConfig));
0194     } else {
0195         qCDebug(KSCREEN_KDED) << "Loading failed, falling back to the ideal config" << m_monitoredConfig->id();
0196         applyIdealConfig();
0197     }
0198 }
0199 
0200 void KScreenDaemon::showOSD()
0201 {
0202     QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kscreen.osdService"),
0203                                                           QStringLiteral("/org/kde/kscreen/osdService"),
0204                                                           QStringLiteral("org.kde.kscreen.osdService"),
0205                                                           QStringLiteral("showActionSelector"));
0206     QDBusConnection::sessionBus().asyncCall(message);
0207 }
0208 
0209 void KScreenDaemon::applyIdealConfig()
0210 {
0211     const bool showOsd = m_monitoredConfig->data()->connectedOutputs().count() > 1 && !m_startingUp;
0212 
0213     doApplyConfig(Generator::self()->idealConfig(m_monitoredConfig->data()));
0214 
0215     if (showOsd) {
0216         qCDebug(KSCREEN_KDED) << "Getting ideal config from user via OSD...";
0217         showOSD();
0218     } else {
0219         m_osdServiceInterface->hideOsd();
0220     }
0221 }
0222 
0223 void KScreenDaemon::configChanged()
0224 {
0225     qCDebug(KSCREEN_KDED) << "Change detected";
0226     m_monitoredConfig->log();
0227 
0228     // Modes may have changed, fix-up current mode id
0229     bool changed = false;
0230     const auto outputs = m_monitoredConfig->data()->outputs();
0231     for (const KScreen::OutputPtr &output : outputs) {
0232         if (output->isConnected() && output->isEnabled()
0233             && (output->currentMode().isNull() || (output->followPreferredMode() && output->currentModeId() != output->preferredModeId()))) {
0234             qCDebug(KSCREEN_KDED) << "Current mode was" << output->currentModeId() << ", setting preferred mode" << output->preferredModeId();
0235             output->setCurrentModeId(output->preferredModeId());
0236             changed = true;
0237         }
0238     }
0239     if (changed) {
0240         refreshConfig();
0241     }
0242 
0243     // Reset timer, delay the writeback
0244     if (!m_saveTimer) {
0245         m_saveTimer = new QTimer(this);
0246         m_saveTimer->setInterval(300);
0247         m_saveTimer->setSingleShot(true);
0248         connect(m_saveTimer, &QTimer::timeout, this, &KScreenDaemon::saveCurrentConfig);
0249     }
0250     m_saveTimer->start();
0251 #if HAVE_X11
0252     alignX11TouchScreen();
0253 #endif
0254 }
0255 
0256 #if HAVE_X11
0257 void KScreenDaemon::alignX11TouchScreen()
0258 {
0259     if (qGuiApp->platformName() != QStringLiteral("xcb")) {
0260         return;
0261     }
0262     auto *display = QX11Info::display();
0263     if (!display) {
0264         return;
0265     }
0266     auto *connection = QX11Info::connection();
0267     if (!connection) {
0268         return;
0269     }
0270 
0271     const QRect totalRect(QPoint(0, 0), m_monitoredConfig->data()->screen()->currentSize());
0272     QRect internalOutputRect;
0273     int touchScreenRotationAngle = 0;
0274 
0275     for (const auto &output : m_monitoredConfig->data()->connectedOutputs()) {
0276         if (output->isEnabled() && output->type() == KScreen::Output::Panel) {
0277             internalOutputRect = output->geometry();
0278 
0279             switch (output->rotation()) {
0280             case KScreen::Output::Left:
0281                 touchScreenRotationAngle = 90;
0282                 break;
0283             case KScreen::Output::Right:
0284                 touchScreenRotationAngle = 270;
0285                 break;
0286             case KScreen::Output::Inverted:
0287                 touchScreenRotationAngle = 180;
0288                 break;
0289             default:
0290                 touchScreenRotationAngle = 0;
0291             }
0292         }
0293     }
0294 
0295     // Compute the transformation matrix for the
0296     QTransform transform;
0297     transform = transform.translate(float(internalOutputRect.x()) / float(totalRect.width()), float(internalOutputRect.y()) / float(totalRect.height()));
0298     transform = transform.scale(float(internalOutputRect.width()) / float(totalRect.width()), float(internalOutputRect.height()) / float(totalRect.height()));
0299     transform = transform.rotate(touchScreenRotationAngle);
0300 
0301     // After rotation we need to make the matrix origin aligned with the workspace again
0302     // ____                                                      ___
0303     // |__|  -> 90° clockwise -> ___  -> needs to be moved up -> | |
0304     //                           | |                             |_|
0305     //                           |_|
0306     switch (touchScreenRotationAngle) {
0307     case 90:
0308         transform = transform.translate(0, -1);
0309         break;
0310     case 270:
0311         transform = transform.translate(-1, 0);
0312         break;
0313     case 180:
0314         transform = transform.translate(-1, -1);
0315         break;
0316     default:
0317         break;
0318     }
0319 
0320     auto getAtom = [](xcb_connection_t *connection, const char *name) {
0321         auto cookie = xcb_intern_atom(connection, true, strlen(name), name);
0322         auto reply = xcb_intern_atom_reply(connection, cookie, nullptr);
0323         if (reply) {
0324             return reply->atom;
0325         } else {
0326             return xcb_atom_t(0);
0327         }
0328     };
0329 
0330     int nDevices = 0;
0331     std::unique_ptr<XDeviceInfo, DeviceListDeleter> deviceInfo(XListInputDevices(display, &nDevices));
0332     auto touchScreenAtom = getAtom(connection, XI_TOUCHSCREEN);
0333     if (touchScreenAtom == 0) {
0334         return;
0335     }
0336     auto matrixAtom = getAtom(connection, "Coordinate Transformation Matrix");
0337     if (matrixAtom == 0) {
0338         return;
0339     }
0340     auto floatAtom = getAtom(connection, "FLOAT");
0341     if (floatAtom == 0) {
0342         return;
0343     }
0344 
0345     auto setMatrixAtom = [display, floatAtom](XDeviceInfo *info, Atom atom, const QTransform &transform) {
0346         Atom type;
0347         int format = 0;
0348         unsigned long nItems, bytesAfter;
0349         unsigned char *dataPtr = nullptr;
0350 
0351         std::unique_ptr<unsigned char, XDeleter> data(dataPtr);
0352         XIGetProperty(display, info->id, atom, 0, 1000, False, AnyPropertyType, &type, &format, &nItems, &bytesAfter, &dataPtr);
0353 
0354         if (nItems != 9) {
0355             return;
0356         }
0357         if (format != sizeof(float) * CHAR_BIT || type != floatAtom) {
0358             return;
0359         }
0360 
0361         float *fData = reinterpret_cast<float *>(dataPtr);
0362 
0363         fData[0] = transform.m11();
0364         fData[1] = transform.m21();
0365         fData[2] = transform.m31();
0366 
0367         fData[3] = transform.m12();
0368         fData[4] = transform.m22();
0369         fData[5] = transform.m32();
0370 
0371         fData[6] = transform.m13();
0372         fData[7] = transform.m23();
0373         fData[8] = transform.m33();
0374 
0375         XIChangeProperty(display, info->id, atom, type, format, PropModeReplace, dataPtr, nItems);
0376     };
0377 
0378     for (XDeviceInfo *info = deviceInfo.get(); info < deviceInfo.get() + nDevices; info++) {
0379         // Make sure device is touchscreen
0380         if (info->type != touchScreenAtom) {
0381             continue;
0382         }
0383 
0384         int nProperties = 0;
0385         std::unique_ptr<Atom, XDeleter> properties(XIListProperties(display, info->id, &nProperties));
0386 
0387         bool matrixAtomFound = false;
0388 
0389         Atom *atom = properties.get();
0390         Atom *atomEnd = properties.get() + nProperties;
0391         for (; atom != atomEnd; atom++) {
0392             if (!internalOutputRect.isEmpty() && *atom == matrixAtom) {
0393                 matrixAtomFound = true;
0394             }
0395         }
0396 
0397         if (matrixAtomFound) {
0398             setMatrixAtom(info, matrixAtom, transform);
0399         }
0400 
0401         // For now we assume there is only one touchscreen
0402         XFlush(display);
0403         break;
0404     }
0405 }
0406 #endif
0407 
0408 void KScreenDaemon::saveCurrentConfig()
0409 {
0410     qCDebug(KSCREEN_KDED) << "Saving current config to file";
0411 
0412     // We assume the config is valid, since it's what we got, but we are interested
0413     // in the "at least one enabled screen" check
0414 
0415     if (m_monitoredConfig->canBeApplied()) {
0416         m_monitoredConfig->writeFile();
0417         m_monitoredConfig->log();
0418     } else {
0419         qCWarning(KSCREEN_KDED) << "Config does not have at least one screen enabled, WILL NOT save this config, this is not what user wants.";
0420         m_monitoredConfig->log();
0421     }
0422 }
0423 
0424 void KScreenDaemon::lidClosedChanged(bool lidIsClosed)
0425 {
0426     // Ignore this when we don't have any external monitors, we can't turn off our
0427     // only screen
0428     if (m_monitoredConfig->data()->connectedOutputs().count() == 1) {
0429         return;
0430     }
0431 
0432     if (lidIsClosed) {
0433         // Lid is closed, now we wait for couple seconds to find out whether it
0434         // will trigger a suspend (see Device::aboutToSuspend), or whether we should
0435         // turn off the screen
0436         qCDebug(KSCREEN_KDED) << "Lid closed, waiting to see if the computer goes to sleep...";
0437         m_lidClosedTimer->start();
0438         return;
0439     } else {
0440         qCDebug(KSCREEN_KDED) << "Lid opened!";
0441         // We should have a config with "_lidOpened" suffix lying around. If not,
0442         // then the configuration has changed while the lid was closed and we just
0443         // use applyConfig() and see what we can do ...
0444         if (auto openCfg = m_monitoredConfig->readOpenLidFile()) {
0445             doApplyConfig(std::move(openCfg));
0446         }
0447     }
0448 }
0449 
0450 void KScreenDaemon::disableLidOutput()
0451 {
0452     // Make sure nothing has changed in the past second... :-)
0453     if (!Device::self()->isLidClosed()) {
0454         return;
0455     }
0456 
0457     // If we are here, it means that closing the lid did not result in suspend
0458     // action.
0459     // FIXME: This could be because the suspend took longer than m_lidClosedTimer
0460     // timeout. Ideally we need to be able to look into PowerDevil config to see
0461     // what's the configured action for lid events, but there's no API to do that
0462     // and I'm not parsing PowerDevil's configs...
0463 
0464     qCDebug(KSCREEN_KDED) << "Lid closed, finding lid to disable";
0465     for (KScreen::OutputPtr &output : m_monitoredConfig->data()->outputs()) {
0466         if (output->type() == KScreen::Output::Panel) {
0467             if (output->isConnected() && output->isEnabled()) {
0468                 // Save the current config with opened lid, just so that we know
0469                 // how to restore it later
0470                 m_monitoredConfig->writeOpenLidFile();
0471                 disableOutput(output);
0472                 refreshConfig();
0473                 return;
0474             }
0475         }
0476     }
0477 }
0478 
0479 void KScreenDaemon::outputConnectedChanged()
0480 {
0481     if (!m_changeCompressor->isActive()) {
0482         m_changeCompressor->start();
0483     }
0484 
0485     KScreen::Output *output = qobject_cast<KScreen::Output *>(sender());
0486     qCDebug(KSCREEN_KDED) << "outputConnectedChanged():" << output->name();
0487 }
0488 
0489 void KScreenDaemon::outputAddedSlot(const KScreen::OutputPtr &output)
0490 {
0491     if (output->isConnected()) {
0492         m_changeCompressor->start();
0493     }
0494     connect(output.data(), &KScreen::Output::isConnectedChanged, this, &KScreenDaemon::outputConnectedChanged, Qt::UniqueConnection);
0495 }
0496 
0497 void KScreenDaemon::monitorConnectedChange()
0498 {
0499     const KScreen::OutputList outputs = m_monitoredConfig->data()->outputs();
0500     for (const KScreen::OutputPtr &output : outputs) {
0501         connect(output.data(), &KScreen::Output::isConnectedChanged, this, &KScreenDaemon::outputConnectedChanged, Qt::UniqueConnection);
0502     }
0503     connect(m_monitoredConfig->data().data(), &KScreen::Config::outputAdded, this, &KScreenDaemon::outputAddedSlot, Qt::UniqueConnection);
0504     connect(m_monitoredConfig->data().data(),
0505             &KScreen::Config::outputRemoved,
0506             this,
0507             &KScreenDaemon::applyConfig,
0508             static_cast<Qt::ConnectionType>(Qt::QueuedConnection | Qt::UniqueConnection));
0509 }
0510 
0511 void KScreenDaemon::setMonitorForChanges(bool enabled)
0512 {
0513     if (m_monitoring == enabled) {
0514         return;
0515     }
0516 
0517     qCDebug(KSCREEN_KDED) << "Monitor for changes: " << enabled;
0518     m_monitoring = enabled;
0519     if (m_monitoring) {
0520         connect(KScreen::ConfigMonitor::instance(), &KScreen::ConfigMonitor::configurationChanged, this, &KScreenDaemon::configChanged, Qt::UniqueConnection);
0521     } else {
0522         disconnect(KScreen::ConfigMonitor::instance(), &KScreen::ConfigMonitor::configurationChanged, this, &KScreenDaemon::configChanged);
0523     }
0524 }
0525 
0526 void KScreenDaemon::disableOutput(const KScreen::OutputPtr &output)
0527 {
0528     const QRect geom = output->geometry();
0529     qCDebug(KSCREEN_KDED) << "Laptop geometry:" << geom << output->pos() << (output->currentMode() ? output->currentMode()->size() : QSize());
0530 
0531     // Move all outputs right from the @p output to left
0532     for (KScreen::OutputPtr &otherOutput : m_monitoredConfig->data()->outputs()) {
0533         if (otherOutput == output || !otherOutput->isConnected() || !otherOutput->isEnabled()) {
0534             continue;
0535         }
0536 
0537         QPoint otherPos = otherOutput->pos();
0538         if (otherPos.x() >= geom.right() && otherPos.y() >= geom.top() && otherPos.y() <= geom.bottom()) {
0539             otherPos.setX(otherPos.x() - geom.width());
0540         }
0541         qCDebug(KSCREEN_KDED) << "Moving" << otherOutput->name() << "from" << otherOutput->pos() << "to" << otherPos;
0542         otherOutput->setPos(otherPos);
0543     }
0544 
0545     // Disable the output
0546     output->setEnabled(false);
0547 }
0548 
0549 #include "daemon.moc"