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