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"