diff --git a/common/control.cpp b/common/control.cpp index 7e6e767..fac17de 100644 --- a/common/control.cpp +++ b/common/control.cpp @@ -1,240 +1,258 @@ -/******************************************************************** +/******************************************************************** Copyright 2019 Roman Gilg This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "control.h" #include "globals.h" #include #include #include #include #include QString Control::s_dirName = QStringLiteral("control/"); Control::Control(QObject *parent) : QObject(parent) { } +bool Control::writeFile() +{ + const QString path = filePath(); + const auto infoMap = constInfo(); + + if (infoMap.isEmpty()) { + // Nothing to write. Default control. Remove file if it exists. + QFile::remove(path); + return true; + } + if (!QDir().mkpath(dirPath())) { + // TODO: error message + return false; + } + + // write updated data to file + QFile file(path); + if (!file.open(QIODevice::WriteOnly)) { + // TODO: logging category? +// qCWarning(KSCREEN_COMMON) << "Failed to open config control file for writing! " << file.errorString(); + return false; + } + file.write(QJsonDocument::fromVariant(infoMap).toJson()); +// qCDebug(KSCREEN_COMMON) << "Control saved on: " << file.fileName(); + return true; +} + QString Control::dirPath() const { return Globals::dirPath() % s_dirName; } +void Control::readFile() +{ + QFile file(filePath()); + if (file.open(QIODevice::ReadOnly)) { + // This might not be reached, bus this is ok. The control file will + // eventually be created on first write later on. + QJsonDocument parser; + m_info = parser.fromJson(file.readAll()).toVariant().toMap(); + } +} + QString Control::filePathFromHash(const QString &hash) const { return dirPath() % hash; } +QVariantMap& Control::info() +{ + return m_info; +} + +const QVariantMap& Control::constInfo() const +{ + return m_info; +} + Control::OutputRetention Control::convertVariantToOutputRetention(QVariant variant) { if (variant.canConvert()) { const auto retention = variant.toInt(); if (retention == (int)OutputRetention::Global) { return OutputRetention::Global; } if (retention == (int)OutputRetention::Individual) { return OutputRetention::Individual; } } return OutputRetention::Undefined; } ControlConfig::ControlConfig(KScreen::ConfigPtr config, QObject *parent) : Control(parent) , m_config(config) { // qDebug() << "Looking for control file:" << config->connectedOutputsHash(); - QFile file(filePathFromHash(config->connectedOutputsHash())); - if (file.open(QIODevice::ReadOnly)) { - // This might not be reached, bus this is ok. The control file will - // eventually be created on first write later on. - QJsonDocument parser; - m_info = parser.fromJson(file.readAll()).toVariant().toMap(); - } + readFile(); // TODO: use a file watcher in case of changes to the control file while // object exists? // As global outputs are indexed by a hash of their edid, which is not unique, // to be able to tell apart multiple identical outputs, these need special treatment { QStringList allIds; const auto outputs = config->outputs(); allIds.reserve(outputs.count()); for (const KScreen::OutputPtr &output : outputs) { const auto outputId = output->hashMd5(); if (allIds.contains(outputId) && !m_duplicateOutputIds.contains(outputId)) { m_duplicateOutputIds << outputId; } allIds << outputId; } } // TODO: this is same in Output::readInOutputs of the daemon. Combine? // TODO: connect to outputs added/removed signals and reevaluate duplicate ids // in case of such a change while object exists? } QString ControlConfig::dirPath() const { return Control::dirPath() % QStringLiteral("configs/"); } QString ControlConfig::filePath() const { if (!m_config) { return QString(); } return filePathFromHash(m_config->connectedOutputsHash()); } bool ControlConfig::infoIsOutput(const QVariantMap &info, const QString &outputId, const QString &outputName) const { const QString outputIdInfo = info[QStringLiteral("id")].toString(); if (outputIdInfo.isEmpty()) { return false; } if (outputId != outputIdInfo) { return false; } if (!outputName.isEmpty() && m_duplicateOutputIds.contains(outputId)) { // We may have identical outputs connected, these will have the same id in the config // in order to find the right one, also check the output's name (usually the connector) const auto metadata = info[QStringLiteral("metadata")].toMap(); const auto outputNameInfo = metadata[QStringLiteral("name")].toString(); if (outputName != outputNameInfo) { // was a duplicate id, but info not for this output return false; } } return true; } Control::OutputRetention ControlConfig::getOutputRetention(const KScreen::OutputPtr &output) const { return getOutputRetention(output->hashMd5(), output->name()); } Control::OutputRetention ControlConfig::getOutputRetention(const QString &outputId, const QString &outputName) const { const QVariantList outputsInfo = getOutputs(); for (const auto variantInfo : outputsInfo) { const QVariantMap info = variantInfo.toMap(); if (!infoIsOutput(info, outputId, outputName)) { continue; } return convertVariantToOutputRetention(info[QStringLiteral("retention")]); } // info for output not found return OutputRetention::Undefined; } static QVariantMap metadata(const QString &outputName) { QVariantMap metadata; metadata[QStringLiteral("name")] = outputName; return metadata; } void ControlConfig::setOutputRetention(const KScreen::OutputPtr &output, OutputRetention value) { setOutputRetention(output->hashMd5(), output->name(), value); } void ControlConfig::setOutputRetention(const QString &outputId, const QString &outputName, OutputRetention value) { QList::iterator it; QVariantList outputsInfo = getOutputs(); for (it = outputsInfo.begin(); it != outputsInfo.end(); ++it) { QVariantMap outputInfo = (*it).toMap(); if (!infoIsOutput(outputInfo, outputId, outputName)) { continue; } outputInfo[QStringLiteral("retention")] = (int)value; *it = outputInfo; setOutputs(outputsInfo); return; } // no entry yet, create one QVariantMap outputInfo; outputInfo[QStringLiteral("id")] = outputId; outputInfo[QStringLiteral("metadata")] = metadata(outputName); outputInfo[QStringLiteral("retention")] = (int)value; outputsInfo << outputInfo; setOutputs(outputsInfo); } -bool ControlConfig::writeFile() -{ - const QString path = filePath(); - - if (m_info.isEmpty()) { - // Nothing to write. Default control. Remove file if it exists. - QFile::remove(path); - return true; - } - if (!QDir().mkpath(dirPath())) { - // TODO: error message - return false; - } - - // write updated data to file - QFile file(path); - if (!file.open(QIODevice::WriteOnly)) { - // TODO: logging category? -// qCWarning(KSCREEN_COMMON) << "Failed to open config control file for writing! " << file.errorString(); - return false; - } - file.write(QJsonDocument::fromVariant(m_info).toJson()); -// qCDebug(KSCREEN_COMMON) << "Config control saved on: " << file.fileName(); - return true; -} - QVariantList ControlConfig::getOutputs() const { - return m_info[QStringLiteral("outputs")].toList(); + return constInfo()[QStringLiteral("outputs")].toList(); } void ControlConfig::setOutputs(QVariantList outputsInfo) { - m_info[QStringLiteral("outputs")] = outputsInfo; + auto &infoMap = info(); + infoMap[QStringLiteral("outputs")] = outputsInfo; } ControlOutput::ControlOutput(KScreen::OutputPtr output, QObject *parent) : Control(parent) , m_output(output) { + readFile(); } QString ControlOutput::dirPath() const { return Control::dirPath() % QStringLiteral("outputs/"); } QString ControlOutput::filePath() const { if (!m_output) { return QString(); } return filePathFromHash(m_output->hashMd5()); } diff --git a/common/control.h b/common/control.h index 14ad374..0b7896a 100644 --- a/common/control.h +++ b/common/control.h @@ -1,93 +1,96 @@ /******************************************************************** Copyright 2019 Roman Gilg This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #ifndef COMMON_CONTROL_H #define COMMON_CONTROL_H #include #include #include class Control : public QObject { Q_OBJECT public: enum class OutputRetention { Undefined = -1, Global = 0, Individual = 1, }; Q_ENUM(OutputRetention) explicit Control(QObject *parent = nullptr); ~Control() override = default; + bool writeFile(); + protected: virtual QString dirPath() const; virtual QString filePath() const = 0; QString filePathFromHash(const QString &hash) const; + void readFile(); + QVariantMap& info(); + const QVariantMap& constInfo() const; static OutputRetention convertVariantToOutputRetention(QVariant variant); private: static QString s_dirName; + QVariantMap m_info; }; class ControlConfig : public Control { Q_OBJECT public: explicit ControlConfig(KScreen::ConfigPtr config, QObject *parent = nullptr); OutputRetention getOutputRetention(const KScreen::OutputPtr &output) const; OutputRetention getOutputRetention(const QString &outputId, const QString &outputName) const; void setOutputRetention(const KScreen::OutputPtr &output, OutputRetention value); void setOutputRetention(const QString &outputId, const QString &outputName, OutputRetention value); - bool writeFile(); - QString dirPath() const override; QString filePath() const override; private: QVariantList getOutputs() const; void setOutputs(QVariantList outputsInfo); bool infoIsOutput(const QVariantMap &info, const QString &outputId, const QString &outputName) const; KScreen::ConfigPtr m_config; - QVariantMap m_info; QStringList m_duplicateOutputIds; }; class ControlOutput : public Control { Q_OBJECT public: explicit ControlOutput(KScreen::OutputPtr output, QObject *parent = nullptr); // TODO: scale auto value QString dirPath() const override; QString filePath() const override; private: KScreen::OutputPtr m_output; }; #endif diff --git a/kcm/kcm.cpp b/kcm/kcm.cpp index 0f2237d..d3bd561 100644 --- a/kcm/kcm.cpp +++ b/kcm/kcm.cpp @@ -1,413 +1,413 @@ /******************************************************************** Copyright © 2019 Roman Gilg This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "kcm.h" #include "config_handler.h" #include "kcm_screen_debug.h" #include "output_identifier.h" #include "output_model.h" #include "../common/control.h" #include #include #include #include #include #include #include #include #include #include #include #include K_PLUGIN_FACTORY_WITH_JSON(KCMDisplayConfigurationFactory, "kcm_kscreen.json", registerPlugin();) using namespace KScreen; KCMKScreen::KCMKScreen(QObject *parent, const QVariantList &args) : KQuickAddons::ConfigModule(parent, args) { qmlRegisterType(); qmlRegisterType("org.kde.private.kcm.kscreen", 1, 0, "Output"); qmlRegisterUncreatableType("org.kde.private.kcm.kscreen", 1, 0, "Control", QStringLiteral("Provides only the OutputRetention enum class")); Log::instance(); KAboutData *about = new KAboutData(QStringLiteral("kcm_kscreen"), i18n("Display Configuration"), QStringLiteral(KSCREEN_VERSION), i18n("Manage and configure monitors and displays"), KAboutLicense::GPL, i18n("Copyright © 2019 Roman Gilg")); about->addAuthor(i18n("Roman Gilg"), i18n("Maintainer"), QStringLiteral("subdiff@gmail.com")); setAboutData(about); setButtons(Apply); m_loadCompressor = new QTimer(this); m_loadCompressor->setInterval(1000); m_loadCompressor->setSingleShot(true); connect (m_loadCompressor, &QTimer::timeout, this, &KCMKScreen::load); } void KCMKScreen::configReady(ConfigOperation *op) { qCDebug(KSCREEN_KCM) << "Reading in config now."; if (op->hasError()) { m_config.reset(); Q_EMIT backendError(); return; } m_config->setConfig(qobject_cast(op)->config()); setBackendReady(true); Q_EMIT perOutputScalingChanged(); Q_EMIT primaryOutputSupportedChanged(); Q_EMIT outputReplicationSupportedChanged(); } void KCMKScreen::forceSave() { doSave(true); } void KCMKScreen::save() { doSave(false); } void KCMKScreen::doSave(bool force) { if (!m_config) { Q_EMIT errorOnSave(); return; } auto config = m_config->config(); bool atLeastOneEnabledOutput = false; for (const KScreen::OutputPtr &output : config->outputs()) { KScreen::ModePtr mode = output->currentMode(); atLeastOneEnabledOutput |= output->isEnabled(); qCDebug(KSCREEN_KCM) << output->name() << output->id() << output.data() << "\n" << " Connected:" << output->isConnected() << "\n" << " Enabled:" << output->isEnabled() << "\n" << " Primary:" << output->isPrimary() << "\n" << " Rotation:" << output->rotation() << "\n" << " Mode:" << (mode ? mode->name() : QStringLiteral("unknown")) << "@" << (mode ? mode->refreshRate() : 0.0) << "Hz" << "\n" << " Position:" << output->pos().x() << "x" << output->pos().y() << "\n" << " Scale:" << (perOutputScaling() ? QString::number(output->scale()) : QStringLiteral("global")) << "\n" << " Replicates:" << (output->replicationSource() == 0 ? "no" : "yes"); } if (!atLeastOneEnabledOutput && !force) { Q_EMIT dangerousSave(); m_config->checkNeedsSave(); return; } if (!Config::canBeApplied(config)) { Q_EMIT errorOnSave(); m_config->checkNeedsSave(); return; } if (!perOutputScaling()) { writeGlobalScale(); } m_config->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); op->exec(); // The 1000ms is a legacy value tested to work for randr having // enough time to change configuration. QTimer::singleShot(1000, this, [this] () { if (!m_config) { setNeedsSave(false); return; } m_config->updateInitialConfig(); } ); } bool KCMKScreen::backendReady() const { return m_backendReady; } void KCMKScreen::setBackendReady(bool ready) { if (m_backendReady == ready) { return; } m_backendReady = ready; Q_EMIT backendReadyChanged(); } OutputModel* KCMKScreen::outputModel() const { if (!m_config) { return nullptr; } return m_config->outputModel(); } void KCMKScreen::identifyOutputs() { if (!m_config || !m_config->config() || m_outputIdentifier) { return; } m_outputIdentifier.reset(new OutputIdentifier(m_config->config(), this)); connect(m_outputIdentifier.get(), &OutputIdentifier::identifiersFinished, this, [this]() { m_outputIdentifier.reset(); }); } QSize KCMKScreen::normalizeScreen() const { if (!m_config) { return QSize(); } return m_config->normalizeScreen(); } bool KCMKScreen::screenNormalized() const { return m_screenNormalized; } bool KCMKScreen::perOutputScaling() const { if (!m_config || !m_config->config()) { return false; } return m_config->config()->supportedFeatures().testFlag(Config::Feature:: PerOutputScaling); } bool KCMKScreen::primaryOutputSupported() const { if (!m_config || !m_config->config()) { return false; } return m_config->config()->supportedFeatures().testFlag(Config::Feature:: PrimaryDisplay); } bool KCMKScreen::outputReplicationSupported() const { if (!m_config || !m_config->config()) { return false; } return m_config->config()->supportedFeatures().testFlag(Config::Feature:: OutputReplication); } 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."; setBackendReady(false); setNeedsSave(false); if (!screenNormalized()) { Q_EMIT screenNormalizedChanged(); } fetchGlobalScale(); // 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_config.release(); if (oldConfig) { emit outputModelChanged(); delete oldConfig; } m_config.reset(new ConfigHandler(this)); Q_EMIT perOutputScalingChanged(); connect (m_config.get(), &ConfigHandler::outputModelChanged, this, &KCMKScreen::outputModelChanged); connect (m_config.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_config.get(), &ConfigHandler::screenNormalizationUpdate, this, &KCMKScreen::setScreenNormalized); connect (m_config.get(), &ConfigHandler::retentionChanged, this, &KCMKScreen::outputRetentionChanged); // This is a queued connection so that we can fire the event from // within the save() call in case it failed. connect (m_config.get(), &ConfigHandler::needsSaveChecked, this, &KCMKScreen::continueNeedsSaveCheck, Qt::QueuedConnection); connect (m_config.get(), &ConfigHandler::changed, this, &KCMKScreen::changed); connect(new GetConfigOperation(), &GetConfigOperation::finished, this, &KCMKScreen::configReady); Q_EMIT changed(); } void KCMKScreen::continueNeedsSaveCheck(bool needs) { if (needs || m_globalScale != m_initialGlobalScale) { setNeedsSave(true); } else { setNeedsSave(false); } } void KCMKScreen::fetchGlobalScale() { const auto config = KSharedConfig::openConfig(QStringLiteral("kdeglobals")); const qreal scale = config->group("KScreen").readEntry("ScaleFactor", 1.0); m_initialGlobalScale = scale; setGlobalScale(scale); } void KCMKScreen::writeGlobalScale() { if (qFuzzyCompare(m_initialGlobalScale, m_globalScale)) { return; } auto config = KSharedConfig::openConfig(QStringLiteral("kdeglobals")); config->group("KScreen").writeEntry("ScaleFactor", m_globalScale); // 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; for (const KScreen::OutputPtr output : m_config->config()->outputs()) { screenFactors.append(output->name() + QLatin1Char('=') + QString::number(m_globalScale) + QLatin1Char(';')); } config->group("KScreen").writeEntry("ScreenScaleFactors", screenFactors); KConfig fontConfig(QStringLiteral("kcmfonts")); auto fontConfigGroup = fontConfig.group("General"); if (qFuzzyCompare(m_globalScale, 1.0)) { //if dpi is the default (96) remove the entry rather than setting it QProcess proc; proc.start(QStringLiteral("xrdb -quiet -remove -nocpp")); if (proc.waitForStarted()) { proc.write(QByteArray("Xft.dpi\n")); proc.closeWriteChannel(); proc.waitForFinished(); } fontConfigGroup.writeEntry("forceFontDPI", 0); } else { const int scaleDpi = qRound(m_globalScale * 96.0); QProcess proc; proc.start(QStringLiteral("xrdb -quiet -merge -nocpp")); if (proc.waitForStarted()) { proc.write(QByteArray("Xft.dpi: " + QString::number(scaleDpi).toLatin1())); proc.closeWriteChannel(); proc.waitForFinished(); } fontConfigGroup.writeEntry("forceFontDPI", scaleDpi); } m_initialGlobalScale = m_globalScale; Q_EMIT globalScaleWritten(); } qreal KCMKScreen::globalScale() const { return m_globalScale; } void KCMKScreen::setGlobalScale(qreal scale) { if (qFuzzyCompare(m_globalScale, scale)) { return; } m_globalScale = scale; if (m_config) { m_config->checkNeedsSave(); } else { continueNeedsSaveCheck(false); } Q_EMIT changed(); Q_EMIT globalScaleChanged(); } int KCMKScreen::outputRetention() const { if (!m_config) { return -1; } return m_config->retention(); } void KCMKScreen::setOutputRetention(int retention) { if (!m_config) { return; } - return m_config->setRetention(retention); + m_config->setRetention(retention); } #include "kcm.moc"