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"