File indexing completed on 2024-05-12 09:32:09

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