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"