/* SPDX-FileCopyrightText: 2019 Roman Gilg SPDX-License-Identifier: GPL-2.0-or-later */ #include "kcm.h" #include "../common/control.h" #include "../common/orientation_sensor.h" #include "config_handler.h" #include "globalscalesettings.h" #include "kcm_screen_debug.h" #include "kwincompositing_setting.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include K_PLUGIN_CLASS_WITH_JSON(KCMKScreen, "kcm_kscreen.json") using namespace KScreen; class ScreenSortProxyModel : public QSortFilterProxyModel { Q_OBJECT public: ScreenSortProxyModel(QObject *parent) : QSortFilterProxyModel(parent) { } protected: bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override; }; KCMKScreen::KCMKScreen(QObject *parent, const KPluginMetaData &data) : KQuickManagedConfigModule(parent, data) { qmlRegisterAnonymousType("org.kde.private.kcm.screen", 1); qmlRegisterType("org.kde.private.kcm.kscreen", 1, 0, "Output"); qmlRegisterUncreatableType("org.kde.private.kcm.kscreen", 1, 0, "KCMKScreen", QStringLiteral("For InvalidConfig enum")); Log::instance(); setButtons(Apply); m_outputProxyModel = new ScreenSortProxyModel(this); m_outputProxyModel->sort(0); m_loadCompressor = new QTimer(this); m_loadCompressor->setInterval(1000); m_loadCompressor->setSingleShot(true); connect(m_loadCompressor, &QTimer::timeout, this, &KCMKScreen::load); m_orientationSensor = new OrientationSensor(this); connect(m_orientationSensor, &OrientationSensor::availableChanged, this, &KCMKScreen::orientationSensorAvailableChanged); connect(KScreen::ConfigMonitor::instance(), &KScreen::ConfigMonitor::configurationChanged, this, &KCMKScreen::updateFromBackend); registerSettings(GlobalScaleSettings::self()); connect(GlobalScaleSettings::self(), &GlobalScaleSettings::scaleFactorChanged, this, &KCMKScreen::globalScaleChanged); registerSettings(KWinCompositingSetting::self()); connect(KWinCompositingSetting::self(), &KWinCompositingSetting::allowTearingChanged, this, &KCMKScreen::tearingAllowedChanged); } void KCMKScreen::configReady(ConfigOperation *op) { qCDebug(KSCREEN_KCM) << "Reading in config now."; if (op->hasError()) { m_configHandler.reset(); m_configNeedsSave = false; settingsChanged(); Q_EMIT backendError(); return; } KScreen::ConfigPtr config = qobject_cast(op)->config(); const bool autoRotationSupported = config->supportedFeatures() & (KScreen::Config::Feature::AutoRotation | KScreen::Config::Feature::TabletMode); m_orientationSensor->setEnabled(autoRotationSupported); m_configHandler->setConfig(config); Q_EMIT multipleScreensAvailableChanged(); setBackendReady(true); checkConfig(); Q_EMIT perOutputScalingChanged(); Q_EMIT xwaylandClientsScaleSupportedChanged(); Q_EMIT tearingSupportedChanged(); Q_EMIT primaryOutputSupportedChanged(); Q_EMIT outputReplicationSupportedChanged(); Q_EMIT tabletModeAvailableChanged(); Q_EMIT autoRotationSupportedChanged(); } void KCMKScreen::save() { doSave(); } void KCMKScreen::revertSettings() { if (!m_configHandler || !m_configHandler->config()) { return; } if (!m_settingsReverted) { m_configHandler->revertConfig(); m_settingsReverted = true; doSave(); load(); // reload the configuration Q_EMIT settingsReverted(); m_stopUpdatesFromBackend = false; } } void KCMKScreen::requestReboot() { QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.LogoutPrompt"), QStringLiteral("/LogoutPrompt"), QStringLiteral("org.kde.LogoutPrompt"), QStringLiteral("promptReboot")); QDBusConnection::sessionBus().asyncCall(msg); } void KCMKScreen::setStopUpdatesFromBackend(bool value) { m_stopUpdatesFromBackend = value; } void KCMKScreen::updateFromBackend() { if (needsSave() || m_stopUpdatesFromBackend) { return; } m_loadCompressor->start(); } void KCMKScreen::doSave() { if (!m_configHandler || !m_configHandler->config()) { Q_EMIT errorOnSave(); return; } const auto outputs = m_configHandler->config()->outputs(); for (const KScreen::OutputPtr &output : outputs) { KScreen::ModePtr mode = output->currentMode(); qCDebug(KSCREEN_KCM) << output->name() << output->id() << output.data() << "\n" << "\tConnected:" << output->isConnected() << "\n" << "\tEnabled:" << output->isEnabled() << "\n" << "\tPriority:" << output->priority() << "\n" << "\tRotation:" << output->rotation() << "\n" << "\tMode:" << (mode ? mode->name() : QStringLiteral("unknown")) << "@" << (mode ? mode->refreshRate() : 0.0) << "Hz" << "\n" << "\tPosition:" << output->pos().x() << "x" << output->pos().y() << "\n" << "\tScale:" << (perOutputScaling() ? QString::number(output->scale()) : QStringLiteral("global")) << "\n" << "\tReplicates:" << (output->replicationSource() == 0 ? "no" : "yes"); } auto config = m_configHandler->config(); if (!Config::canBeApplied(config)) { Q_EMIT errorOnSave(); m_configHandler->checkNeedsSave(); return; } const bool globalScaleChanged = GlobalScaleSettings::self()->isSaveNeeded(); KQuickManagedConfigModule::save(); if (globalScaleChanged) { exportGlobalScale(); } m_configHandler->writeControl(); // Store the current config, apply settings. Block until operation is // completed, otherwise ConfigModule might terminate before we get to // execute the Operation. auto *op = new SetConfigOperation(config); m_stopUpdatesFromBackend = true; op->exec(); // exec() opens a nested eventloop that may have unset m_configHandler if (e.g.) // outputs changed during saving. https://bugs.kde.org/show_bug.cgi?id=466960 if (!m_configHandler || !m_configHandler->config()) { Q_EMIT errorOnSave(); return; } const auto updateInitialData = [this]() { if (!m_configHandler || !m_configHandler->config()) { return; } m_configHandler->updateInitialData(); if (!m_settingsReverted && m_configHandler->shouldTestNewSettings()) { Q_EMIT showRevertWarning(); } else { m_settingsReverted = false; m_stopUpdatesFromBackend = false; } }; if (m_configHandler->config()->supportedFeatures() & (KScreen::Config::Feature::SynchronousOutputChanges)) { updateInitialData(); } else { // The 1000ms is a legacy value tested to work for randr having // enough time to change configuration. QTimer::singleShot(1000, this, updateInitialData); } } bool KCMKScreen::backendReady() const { return m_backendReady; } void KCMKScreen::setBackendReady(bool ready) { if (m_backendReady == ready) { return; } m_backendReady = ready; Q_EMIT backendReadyChanged(); } QAbstractItemModel *KCMKScreen::outputModel() const { return m_outputProxyModel; } void KCMKScreen::identifyOutputs() { const QString name = QStringLiteral("org.kde.KWin"); const QString interface = QStringLiteral("org.kde.KWin.Effect.OutputLocator1"); const QString path = QStringLiteral("/org/kde/KWin/Effect/OutputLocator1"); auto message = QDBusMessage::createMethodCall(name, path, interface, QStringLiteral("show")); QDBusConnection::sessionBus().send(message); } QSize KCMKScreen::normalizeScreen() const { if (!m_configHandler) { return QSize(); } return m_configHandler->normalizeScreen(); } bool KCMKScreen::screenNormalized() const { return m_screenNormalized; } bool KCMKScreen::perOutputScaling() const { if (!m_configHandler || !m_configHandler->config()) { return false; } return m_configHandler->config()->supportedFeatures().testFlag(Config::Feature::PerOutputScaling); } bool KCMKScreen::primaryOutputSupported() const { if (!m_configHandler || !m_configHandler->config()) { return false; } return m_configHandler->config()->supportedFeatures().testFlag(Config::Feature::PrimaryDisplay); } bool KCMKScreen::outputReplicationSupported() const { if (!m_configHandler || !m_configHandler->config()) { return false; } return m_configHandler->config()->supportedFeatures().testFlag(Config::Feature::OutputReplication); } bool KCMKScreen::autoRotationSupported() const { if (!m_configHandler || !m_configHandler->config()) { return false; } return m_configHandler->config()->supportedFeatures() & (KScreen::Config::Feature::AutoRotation | KScreen::Config::Feature::TabletMode); } bool KCMKScreen::orientationSensorAvailable() const { return m_orientationSensor->available(); } bool KCMKScreen::tabletModeAvailable() const { if (!m_configHandler || !m_configHandler->config()) { return false; } return m_configHandler->config()->tabletModeAvailable(); } void KCMKScreen::setScreenNormalized(bool normalized) { if (m_screenNormalized == normalized) { return; } m_screenNormalized = normalized; Q_EMIT screenNormalizedChanged(); } void KCMKScreen::defaults() { qCDebug(KSCREEN_KCM) << "Applying defaults."; load(); } void KCMKScreen::load() { qCDebug(KSCREEN_KCM) << "About to read in config."; KQuickManagedConfigModule::load(); setBackendReady(false); m_configNeedsSave = false; settingsChanged(); if (!screenNormalized()) { Q_EMIT screenNormalizedChanged(); } // Don't pull away the outputModel under QML's feet // signal its disappearance first before deleting and replacing it. // We take the m_config pointer so outputModel() will return null, // gracefully cleaning up the QML side and only then we will delete it. auto *oldConfig = m_configHandler.release(); if (oldConfig) { m_outputProxyModel->setSourceModel(nullptr); delete oldConfig; } m_configHandler.reset(new ConfigHandler(this)); m_outputProxyModel->setSourceModel(m_configHandler->outputModel()); Q_EMIT perOutputScalingChanged(); Q_EMIT xwaylandClientsScaleSupportedChanged(); Q_EMIT tearingSupportedChanged(); Q_EMIT tearingAllowedChanged(); connect(m_configHandler.get(), &ConfigHandler::outputModelChanged, this, [this]() { m_outputProxyModel->setSourceModel(m_configHandler->outputModel()); }); connect(m_configHandler.get(), &ConfigHandler::outputConnect, this, [this](bool connected) { Q_EMIT outputConnect(connected); setBackendReady(false); // Reload settings delayed such that daemon can update output values. m_loadCompressor->start(); }); connect(m_configHandler.get(), &ConfigHandler::screenNormalizationUpdate, this, &KCMKScreen::setScreenNormalized); // This is a queued connection so that we can fire the event from // within the save() call in case it failed. connect(m_configHandler.get(), &ConfigHandler::needsSaveChecked, this, &KCMKScreen::continueNeedsSaveCheck, Qt::QueuedConnection); connect(m_configHandler.get(), &ConfigHandler::changed, this, &KCMKScreen::changed); connect(new GetConfigOperation(), &GetConfigOperation::finished, this, &KCMKScreen::configReady); Q_EMIT changed(); } void KCMKScreen::checkConfig() { if (!m_configHandler || !m_configHandler->config()) { return; } const auto outputs = m_configHandler->config()->outputs(); std::vector enabledOutputs; std::copy_if(outputs.cbegin(), outputs.cend(), std::back_inserter(enabledOutputs), std::mem_fn(&Output::isEnabled)); if (enabledOutputs.empty()) { Q_EMIT invalidConfig(NoEnabledOutputs); m_configNeedsSave = false; } auto rectsTouch = [](const QRect &rect, const QRect &other) { return rect.left() <= other.left() + other.width() && other.left() <= rect.left() + rect.width() && rect.top() <= other.top() + other.height() && other.top() <= rect.top() + rect.height(); }; auto doesNotTouchAnyOther = [&enabledOutputs, &rectsTouch](const OutputPtr &output) { return std::none_of(enabledOutputs.cbegin(), enabledOutputs.cend(), [&output, &rectsTouch](const OutputPtr &other) { return other != output && rectsTouch(output->geometry(), other->geometry()); }); }; if (enabledOutputs.size() > 1 && std::any_of(enabledOutputs.cbegin(), enabledOutputs.cend(), doesNotTouchAnyOther)) { Q_EMIT invalidConfig(ConfigHasGaps); m_configNeedsSave = false; } } void KCMKScreen::continueNeedsSaveCheck(bool needs) { m_configNeedsSave = needs; if (needs) { checkConfig(); } settingsChanged(); } bool KCMKScreen::isSaveNeeded() const { return m_configNeedsSave; } void KCMKScreen::exportGlobalScale() { // Write env var to be used by session startup scripts to populate the QT_SCREEN_SCALE_FACTORS // env var. // We use QT_SCREEN_SCALE_FACTORS as opposed to QT_SCALE_FACTOR as we need to use one that will // NOT scale fonts according to the scale. // Scaling the fonts makes sense if you don't also set a font DPI, but we NEED to set a font // DPI for both PlasmaShell which does it's own thing, and for KDE4/GTK2 applications. QString screenFactors; const auto outputs = m_configHandler->config()->outputs(); for (const auto &output : outputs) { screenFactors.append(output->name() + QLatin1Char('=') + QString::number(globalScale()) + QLatin1Char(';')); } auto config = KSharedConfig::openConfig("kdeglobals"); config->group("KScreen").writeEntry("ScreenScaleFactors", screenFactors); config->sync(); KConfig fontConfig(QStringLiteral("kcmfonts")); auto fontConfigGroup = fontConfig.group("General"); if (qFuzzyCompare(globalScale(), 1.0)) { // if dpi is the default (96) remove the entry rather than setting it QProcess queryProc; queryProc.start(QStringLiteral("xrdb"), {QStringLiteral("-query")}); if (queryProc.waitForFinished()) { QByteArray db = queryProc.readAllStandardOutput(); int idx1 = 0; while (idx1 < db.size()) { int idx2 = db.indexOf('\n', idx1); if (idx2 == -1) { idx2 = db.size() - 1; } const auto entry = QByteArray::fromRawData(db.constData() + idx1, idx2 - idx1 + 1); if (entry.startsWith("Xft.dpi:")) { db.remove(idx1, entry.size()); } else { idx1 = idx2 + 1; } } QProcess loadProc; loadProc.start(QStringLiteral("xrdb"), {QStringLiteral("-quiet"), QStringLiteral("-load"), QStringLiteral("-nocpp")}); if (loadProc.waitForStarted()) { loadProc.write(db); loadProc.closeWriteChannel(); loadProc.waitForFinished(); } } fontConfigGroup.writeEntry("forceFontDPI", 0, KConfig::Notify); } else { const int scaleDpi = qRound(globalScale() * 96.0); QProcess proc; proc.start(QStringLiteral("xrdb"), {QStringLiteral("-quiet"), QStringLiteral("-merge"), QStringLiteral("-nocpp")}); if (proc.waitForStarted()) { proc.write(QByteArray("Xft.dpi: ") + QByteArray::number(scaleDpi)); proc.closeWriteChannel(); proc.waitForFinished(); } fontConfigGroup.writeEntry("forceFontDPI", scaleDpi, KConfig::Notify); } Q_EMIT globalScaleWritten(); } qreal KCMKScreen::globalScale() const { return GlobalScaleSettings::self()->scaleFactor(); } void KCMKScreen::setGlobalScale(qreal scale) { GlobalScaleSettings::self()->setScaleFactor(scale); Q_EMIT changed(); } bool KCMKScreen::xwaylandClientsScale() const { return GlobalScaleSettings::self()->xwaylandClientsScale(); } void KCMKScreen::setXwaylandClientsScale(bool scale) { GlobalScaleSettings::self()->setXwaylandClientsScale(scale); Q_EMIT changed(); } bool KCMKScreen::xwaylandClientsScaleSupported() const { if (!m_configHandler || !m_configHandler->config()) { return false; } return m_configHandler->config()->supportedFeatures().testFlag(Config::Feature::XwaylandScales); } void KCMKScreen::setAllowTearing(bool allow) { KWinCompositingSetting::self()->setAllowTearing(allow); Q_EMIT changed(); } bool KCMKScreen::allowTearing() const { return KWinCompositingSetting::self()->allowTearing(); } bool KCMKScreen::tearingSupported() const { if (!m_configHandler || !m_configHandler->config()) { return false; } // == is Wayland return m_configHandler->config()->supportedFeatures().testFlag(Config::Feature::XwaylandScales); } bool KCMKScreen::multipleScreensAvailable() const { return m_outputProxyModel->rowCount() > 1; } bool ScreenSortProxyModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const { const bool leftEnabled = source_left.data(OutputModel::EnabledRole).toBool(); const bool rightEnabled = source_right.data(OutputModel::EnabledRole).toBool(); if (leftEnabled != rightEnabled) { return leftEnabled; } return QSortFilterProxyModel::lessThan(source_left, source_right); } #include "kcm.moc" #include "moc_kcm.cpp"