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"