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