File indexing completed on 2024-05-12 09:32:08
0001 /* 0002 SPDX-FileCopyrightText: 2019 Roman Gilg <subdiff@gmail.com> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 #include "kcm.h" 0007 0008 #include "../common/control.h" 0009 #include "../common/orientation_sensor.h" 0010 #include "config_handler.h" 0011 #include "globalscalesettings.h" 0012 #include "kcm_screen_debug.h" 0013 #include "kwincompositing_setting.h" 0014 0015 #include <kscreen/config.h> 0016 #include <kscreen/configmonitor.h> 0017 #include <kscreen/getconfigoperation.h> 0018 #include <kscreen/log.h> 0019 #include <kscreen/mode.h> 0020 #include <kscreen/output.h> 0021 #include <kscreen/setconfigoperation.h> 0022 0023 #include <KConfigGroup> 0024 #include <KLocalizedString> 0025 #include <KPluginFactory> 0026 #include <KSharedConfig> 0027 0028 #include <QDBusConnection> 0029 #include <QDBusMessage> 0030 #include <QDBusPendingReply> 0031 #include <QProcess> 0032 #include <QTimer> 0033 0034 K_PLUGIN_CLASS_WITH_JSON(KCMKScreen, "kcm_kscreen.json") 0035 0036 using namespace KScreen; 0037 0038 KCMKScreen::KCMKScreen(QObject *parent, const KPluginMetaData &data) 0039 : KQuickManagedConfigModule(parent, data) 0040 { 0041 qmlRegisterAnonymousType<OutputModel>("org.kde.private.kcm.screen", 1); 0042 qmlRegisterType<KScreen::Output>("org.kde.private.kcm.kscreen", 1, 0, "Output"); 0043 qmlRegisterUncreatableType<KCMKScreen>("org.kde.private.kcm.kscreen", 1, 0, "KCMKScreen", QStringLiteral("For InvalidConfig enum")); 0044 Log::instance(); 0045 0046 setButtons(Apply); 0047 0048 m_loadCompressor = new QTimer(this); 0049 m_loadCompressor->setInterval(1000); 0050 m_loadCompressor->setSingleShot(true); 0051 connect(m_loadCompressor, &QTimer::timeout, this, &KCMKScreen::load); 0052 0053 m_orientationSensor = new OrientationSensor(this); 0054 connect(m_orientationSensor, &OrientationSensor::availableChanged, this, &KCMKScreen::orientationSensorAvailableChanged); 0055 0056 connect(KScreen::ConfigMonitor::instance(), &KScreen::ConfigMonitor::configurationChanged, this, &KCMKScreen::updateFromBackend); 0057 0058 registerSettings(GlobalScaleSettings::self()); 0059 connect(GlobalScaleSettings::self(), &GlobalScaleSettings::scaleFactorChanged, this, &KCMKScreen::globalScaleChanged); 0060 0061 registerSettings(KWinCompositingSetting::self()); 0062 connect(KWinCompositingSetting::self(), &KWinCompositingSetting::allowTearingChanged, this, &KCMKScreen::tearingAllowedChanged); 0063 } 0064 0065 void KCMKScreen::configReady(ConfigOperation *op) 0066 { 0067 qCDebug(KSCREEN_KCM) << "Reading in config now."; 0068 if (op->hasError()) { 0069 m_configHandler.reset(); 0070 m_configNeedsSave = false; 0071 settingsChanged(); 0072 Q_EMIT backendError(); 0073 return; 0074 } 0075 0076 KScreen::ConfigPtr config = qobject_cast<GetConfigOperation *>(op)->config(); 0077 const bool autoRotationSupported = config->supportedFeatures() & (KScreen::Config::Feature::AutoRotation | KScreen::Config::Feature::TabletMode); 0078 m_orientationSensor->setEnabled(autoRotationSupported); 0079 0080 m_configHandler->setConfig(config); 0081 setBackendReady(true); 0082 checkConfig(); 0083 Q_EMIT perOutputScalingChanged(); 0084 Q_EMIT xwaylandClientsScaleSupportedChanged(); 0085 Q_EMIT tearingSupportedChanged(); 0086 Q_EMIT primaryOutputSupportedChanged(); 0087 Q_EMIT outputReplicationSupportedChanged(); 0088 Q_EMIT tabletModeAvailableChanged(); 0089 Q_EMIT autoRotationSupportedChanged(); 0090 } 0091 0092 void KCMKScreen::save() 0093 { 0094 doSave(); 0095 } 0096 0097 void KCMKScreen::revertSettings() 0098 { 0099 if (!m_configHandler || !m_configHandler->config()) { 0100 return; 0101 } 0102 if (!m_settingsReverted) { 0103 m_configHandler->revertConfig(); 0104 m_settingsReverted = true; 0105 doSave(); 0106 load(); // reload the configuration 0107 Q_EMIT settingsReverted(); 0108 m_stopUpdatesFromBackend = false; 0109 } 0110 } 0111 0112 void KCMKScreen::requestReboot() 0113 { 0114 QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.LogoutPrompt"), 0115 QStringLiteral("/LogoutPrompt"), 0116 QStringLiteral("org.kde.LogoutPrompt"), 0117 QStringLiteral("promptReboot")); 0118 QDBusConnection::sessionBus().asyncCall(msg); 0119 } 0120 0121 void KCMKScreen::setStopUpdatesFromBackend(bool value) 0122 { 0123 m_stopUpdatesFromBackend = value; 0124 } 0125 0126 void KCMKScreen::updateFromBackend() 0127 { 0128 if (needsSave() || m_stopUpdatesFromBackend) { 0129 return; 0130 } 0131 0132 m_loadCompressor->start(); 0133 } 0134 0135 void KCMKScreen::doSave() 0136 { 0137 if (!m_configHandler || !m_configHandler->config()) { 0138 Q_EMIT errorOnSave(); 0139 return; 0140 } 0141 0142 const auto outputs = m_configHandler->config()->outputs(); 0143 for (const KScreen::OutputPtr &output : outputs) { 0144 KScreen::ModePtr mode = output->currentMode(); 0145 qCDebug(KSCREEN_KCM) << output->name() << output->id() << output.data() << "\n" 0146 << "\tConnected:" << output->isConnected() << "\n" 0147 << "\tEnabled:" << output->isEnabled() << "\n" 0148 << "\tPriority:" << output->priority() << "\n" 0149 << "\tRotation:" << output->rotation() << "\n" 0150 << "\tMode:" << (mode ? mode->name() : QStringLiteral("unknown")) << "@" << (mode ? mode->refreshRate() : 0.0) << "Hz" 0151 << "\n" 0152 << "\tPosition:" << output->pos().x() << "x" << output->pos().y() << "\n" 0153 << "\tScale:" << (perOutputScaling() ? QString::number(output->scale()) : QStringLiteral("global")) << "\n" 0154 << "\tReplicates:" << (output->replicationSource() == 0 ? "no" : "yes"); 0155 } 0156 0157 auto config = m_configHandler->config(); 0158 0159 if (!Config::canBeApplied(config)) { 0160 Q_EMIT errorOnSave(); 0161 m_configHandler->checkNeedsSave(); 0162 return; 0163 } 0164 0165 const bool globalScaleChanged = GlobalScaleSettings::self()->isSaveNeeded(); 0166 KQuickManagedConfigModule::save(); 0167 if (globalScaleChanged) { 0168 exportGlobalScale(); 0169 } 0170 0171 m_configHandler->writeControl(); 0172 0173 // Store the current config, apply settings. Block until operation is 0174 // completed, otherwise ConfigModule might terminate before we get to 0175 // execute the Operation. 0176 auto *op = new SetConfigOperation(config); 0177 m_stopUpdatesFromBackend = true; 0178 op->exec(); 0179 0180 // exec() opens a nested eventloop that may have unset m_configHandler if (e.g.) 0181 // outputs changed during saving. https://bugs.kde.org/show_bug.cgi?id=466960 0182 if (!m_configHandler || !m_configHandler->config()) { 0183 Q_EMIT errorOnSave(); 0184 return; 0185 } 0186 0187 const auto updateInitialData = [this]() { 0188 if (!m_configHandler || !m_configHandler->config()) { 0189 return; 0190 } 0191 m_configHandler->updateInitialData(); 0192 0193 if (!m_settingsReverted && m_configHandler->shouldTestNewSettings()) { 0194 Q_EMIT showRevertWarning(); 0195 } else { 0196 m_settingsReverted = false; 0197 m_stopUpdatesFromBackend = false; 0198 } 0199 }; 0200 0201 if (m_configHandler->config()->supportedFeatures() & (KScreen::Config::Feature::SynchronousOutputChanges)) { 0202 updateInitialData(); 0203 } else { 0204 // The 1000ms is a legacy value tested to work for randr having 0205 // enough time to change configuration. 0206 QTimer::singleShot(1000, this, updateInitialData); 0207 } 0208 } 0209 0210 bool KCMKScreen::backendReady() const 0211 { 0212 return m_backendReady; 0213 } 0214 0215 void KCMKScreen::setBackendReady(bool ready) 0216 { 0217 if (m_backendReady == ready) { 0218 return; 0219 } 0220 m_backendReady = ready; 0221 Q_EMIT backendReadyChanged(); 0222 } 0223 0224 OutputModel *KCMKScreen::outputModel() const 0225 { 0226 if (!m_configHandler) { 0227 return nullptr; 0228 } 0229 return m_configHandler->outputModel(); 0230 } 0231 0232 void KCMKScreen::identifyOutputs() 0233 { 0234 const QString name = QStringLiteral("org.kde.KWin"); 0235 const QString interface = QStringLiteral("org.kde.KWin.Effect.OutputLocator1"); 0236 const QString path = QStringLiteral("/org/kde/KWin/Effect/OutputLocator1"); 0237 auto message = QDBusMessage::createMethodCall(name, path, interface, QStringLiteral("show")); 0238 QDBusConnection::sessionBus().send(message); 0239 } 0240 0241 QSize KCMKScreen::normalizeScreen() const 0242 { 0243 if (!m_configHandler) { 0244 return QSize(); 0245 } 0246 return m_configHandler->normalizeScreen(); 0247 } 0248 0249 bool KCMKScreen::screenNormalized() const 0250 { 0251 return m_screenNormalized; 0252 } 0253 0254 bool KCMKScreen::perOutputScaling() const 0255 { 0256 if (!m_configHandler || !m_configHandler->config()) { 0257 return false; 0258 } 0259 return m_configHandler->config()->supportedFeatures().testFlag(Config::Feature::PerOutputScaling); 0260 } 0261 0262 bool KCMKScreen::primaryOutputSupported() const 0263 { 0264 if (!m_configHandler || !m_configHandler->config()) { 0265 return false; 0266 } 0267 return m_configHandler->config()->supportedFeatures().testFlag(Config::Feature::PrimaryDisplay); 0268 } 0269 0270 bool KCMKScreen::outputReplicationSupported() const 0271 { 0272 if (!m_configHandler || !m_configHandler->config()) { 0273 return false; 0274 } 0275 return m_configHandler->config()->supportedFeatures().testFlag(Config::Feature::OutputReplication); 0276 } 0277 0278 bool KCMKScreen::autoRotationSupported() const 0279 { 0280 if (!m_configHandler || !m_configHandler->config()) { 0281 return false; 0282 } 0283 return m_configHandler->config()->supportedFeatures() & (KScreen::Config::Feature::AutoRotation | KScreen::Config::Feature::TabletMode); 0284 } 0285 0286 bool KCMKScreen::orientationSensorAvailable() const 0287 { 0288 return m_orientationSensor->available(); 0289 } 0290 0291 bool KCMKScreen::tabletModeAvailable() const 0292 { 0293 if (!m_configHandler || !m_configHandler->config()) { 0294 return false; 0295 } 0296 return m_configHandler->config()->tabletModeAvailable(); 0297 } 0298 0299 void KCMKScreen::setScreenNormalized(bool normalized) 0300 { 0301 if (m_screenNormalized == normalized) { 0302 return; 0303 } 0304 m_screenNormalized = normalized; 0305 Q_EMIT screenNormalizedChanged(); 0306 } 0307 0308 void KCMKScreen::defaults() 0309 { 0310 qCDebug(KSCREEN_KCM) << "Applying defaults."; 0311 load(); 0312 } 0313 0314 void KCMKScreen::load() 0315 { 0316 qCDebug(KSCREEN_KCM) << "About to read in config."; 0317 0318 KQuickManagedConfigModule::load(); 0319 0320 setBackendReady(false); 0321 m_configNeedsSave = false; 0322 settingsChanged(); 0323 if (!screenNormalized()) { 0324 Q_EMIT screenNormalizedChanged(); 0325 } 0326 0327 // Don't pull away the outputModel under QML's feet 0328 // signal its disappearance first before deleting and replacing it. 0329 // We take the m_config pointer so outputModel() will return null, 0330 // gracefully cleaning up the QML side and only then we will delete it. 0331 auto *oldConfig = m_configHandler.release(); 0332 if (oldConfig) { 0333 emit outputModelChanged(); 0334 delete oldConfig; 0335 } 0336 0337 m_configHandler.reset(new ConfigHandler(this)); 0338 Q_EMIT perOutputScalingChanged(); 0339 Q_EMIT xwaylandClientsScaleSupportedChanged(); 0340 Q_EMIT tearingSupportedChanged(); 0341 Q_EMIT tearingAllowedChanged(); 0342 connect(m_configHandler.get(), &ConfigHandler::outputModelChanged, this, &KCMKScreen::outputModelChanged); 0343 connect(m_configHandler.get(), &ConfigHandler::outputConnect, this, [this](bool connected) { 0344 Q_EMIT outputConnect(connected); 0345 setBackendReady(false); 0346 0347 // Reload settings delayed such that daemon can update output values. 0348 m_loadCompressor->start(); 0349 }); 0350 connect(m_configHandler.get(), &ConfigHandler::screenNormalizationUpdate, this, &KCMKScreen::setScreenNormalized); 0351 0352 // This is a queued connection so that we can fire the event from 0353 // within the save() call in case it failed. 0354 connect(m_configHandler.get(), &ConfigHandler::needsSaveChecked, this, &KCMKScreen::continueNeedsSaveCheck, Qt::QueuedConnection); 0355 0356 connect(m_configHandler.get(), &ConfigHandler::changed, this, &KCMKScreen::changed); 0357 0358 connect(new GetConfigOperation(), &GetConfigOperation::finished, this, &KCMKScreen::configReady); 0359 0360 Q_EMIT changed(); 0361 } 0362 0363 void KCMKScreen::checkConfig() 0364 { 0365 if (!m_configHandler || !m_configHandler->config()) { 0366 return; 0367 } 0368 0369 const auto outputs = m_configHandler->config()->outputs(); 0370 std::vector<OutputPtr> enabledOutputs; 0371 std::copy_if(outputs.cbegin(), outputs.cend(), std::back_inserter(enabledOutputs), std::mem_fn(&Output::isEnabled)); 0372 if (enabledOutputs.empty()) { 0373 Q_EMIT invalidConfig(NoEnabledOutputs); 0374 m_configNeedsSave = false; 0375 } 0376 auto rectsTouch = [](const QRect &rect, const QRect &other) { 0377 return rect.left() <= other.left() + other.width() && other.left() <= rect.left() + rect.width() && rect.top() <= other.top() + other.height() 0378 && other.top() <= rect.top() + rect.height(); 0379 }; 0380 auto doesNotTouchAnyOther = [&enabledOutputs, &rectsTouch](const OutputPtr &output) { 0381 return std::none_of(enabledOutputs.cbegin(), enabledOutputs.cend(), [&output, &rectsTouch](const OutputPtr &other) { 0382 return other != output && rectsTouch(output->geometry(), other->geometry()); 0383 }); 0384 }; 0385 if (enabledOutputs.size() > 1 && std::any_of(enabledOutputs.cbegin(), enabledOutputs.cend(), doesNotTouchAnyOther)) { 0386 Q_EMIT invalidConfig(ConfigHasGaps); 0387 m_configNeedsSave = false; 0388 } 0389 } 0390 0391 void KCMKScreen::continueNeedsSaveCheck(bool needs) 0392 { 0393 m_configNeedsSave = needs; 0394 0395 if (needs) { 0396 checkConfig(); 0397 } 0398 0399 settingsChanged(); 0400 } 0401 0402 bool KCMKScreen::isSaveNeeded() const 0403 { 0404 return m_configNeedsSave; 0405 } 0406 0407 void KCMKScreen::exportGlobalScale() 0408 { 0409 // Write env var to be used by session startup scripts to populate the QT_SCREEN_SCALE_FACTORS 0410 // env var. 0411 // We use QT_SCREEN_SCALE_FACTORS as opposed to QT_SCALE_FACTOR as we need to use one that will 0412 // NOT scale fonts according to the scale. 0413 // Scaling the fonts makes sense if you don't also set a font DPI, but we NEED to set a font 0414 // DPI for both PlasmaShell which does it's own thing, and for KDE4/GTK2 applications. 0415 QString screenFactors; 0416 const auto outputs = m_configHandler->config()->outputs(); 0417 for (const auto &output : outputs) { 0418 screenFactors.append(output->name() + QLatin1Char('=') + QString::number(globalScale()) + QLatin1Char(';')); 0419 } 0420 auto config = KSharedConfig::openConfig("kdeglobals"); 0421 config->group("KScreen").writeEntry("ScreenScaleFactors", screenFactors); 0422 config->sync(); 0423 0424 KConfig fontConfig(QStringLiteral("kcmfonts")); 0425 auto fontConfigGroup = fontConfig.group("General"); 0426 0427 if (qFuzzyCompare(globalScale(), 1.0)) { 0428 // if dpi is the default (96) remove the entry rather than setting it 0429 QProcess queryProc; 0430 queryProc.start(QStringLiteral("xrdb"), {QStringLiteral("-query")}); 0431 if (queryProc.waitForFinished()) { 0432 QByteArray db = queryProc.readAllStandardOutput(); 0433 int idx1 = 0; 0434 while (idx1 < db.size()) { 0435 int idx2 = db.indexOf('\n', idx1); 0436 if (idx2 == -1) { 0437 idx2 = db.size() - 1; 0438 } 0439 const auto entry = QByteArray::fromRawData(db.constData() + idx1, idx2 - idx1 + 1); 0440 if (entry.startsWith("Xft.dpi:")) { 0441 db.remove(idx1, entry.size()); 0442 } else { 0443 idx1 = idx2 + 1; 0444 } 0445 } 0446 0447 QProcess loadProc; 0448 loadProc.start(QStringLiteral("xrdb"), {QStringLiteral("-quiet"), QStringLiteral("-load"), QStringLiteral("-nocpp")}); 0449 if (loadProc.waitForStarted()) { 0450 loadProc.write(db); 0451 loadProc.closeWriteChannel(); 0452 loadProc.waitForFinished(); 0453 } 0454 } 0455 fontConfigGroup.writeEntry("forceFontDPI", 0, KConfig::Notify); 0456 } else { 0457 const int scaleDpi = qRound(globalScale() * 96.0); 0458 QProcess proc; 0459 proc.start(QStringLiteral("xrdb"), {QStringLiteral("-quiet"), QStringLiteral("-merge"), QStringLiteral("-nocpp")}); 0460 if (proc.waitForStarted()) { 0461 proc.write(QByteArray("Xft.dpi: ") + QByteArray::number(scaleDpi)); 0462 proc.closeWriteChannel(); 0463 proc.waitForFinished(); 0464 } 0465 fontConfigGroup.writeEntry("forceFontDPI", scaleDpi, KConfig::Notify); 0466 } 0467 0468 Q_EMIT globalScaleWritten(); 0469 } 0470 0471 qreal KCMKScreen::globalScale() const 0472 { 0473 return GlobalScaleSettings::self()->scaleFactor(); 0474 } 0475 0476 void KCMKScreen::setGlobalScale(qreal scale) 0477 { 0478 GlobalScaleSettings::self()->setScaleFactor(scale); 0479 Q_EMIT changed(); 0480 } 0481 0482 bool KCMKScreen::xwaylandClientsScale() const 0483 { 0484 return GlobalScaleSettings::self()->xwaylandClientsScale(); 0485 } 0486 0487 void KCMKScreen::setXwaylandClientsScale(bool scale) 0488 { 0489 GlobalScaleSettings::self()->setXwaylandClientsScale(scale); 0490 Q_EMIT changed(); 0491 } 0492 0493 bool KCMKScreen::xwaylandClientsScaleSupported() const 0494 { 0495 if (!m_configHandler || !m_configHandler->config()) { 0496 return false; 0497 } 0498 return m_configHandler->config()->supportedFeatures().testFlag(Config::Feature::XwaylandScales); 0499 } 0500 0501 void KCMKScreen::setAllowTearing(bool allow) 0502 { 0503 KWinCompositingSetting::self()->setAllowTearing(allow); 0504 Q_EMIT changed(); 0505 } 0506 0507 bool KCMKScreen::allowTearing() const 0508 { 0509 return KWinCompositingSetting::self()->allowTearing(); 0510 } 0511 0512 bool KCMKScreen::tearingSupported() const 0513 { 0514 if (!m_configHandler || !m_configHandler->config()) { 0515 return false; 0516 } 0517 // == is Wayland 0518 return m_configHandler->config()->supportedFeatures().testFlag(Config::Feature::XwaylandScales); 0519 } 0520 0521 #include "kcm.moc" 0522 0523 #include "moc_kcm.cpp"